/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.pepsoft.worldpainter.merging;
import org.jnbt.CompoundTag;
import org.jnbt.NBTInputStream;
import org.jnbt.NBTOutputStream;
import org.jnbt.Tag;
import org.pepsoft.minecraft.*;
import org.pepsoft.util.FileUtils;
import org.pepsoft.util.ParallelProgressManager;
import org.pepsoft.util.ProgressReceiver;
import org.pepsoft.util.SubProgressReceiver;
import org.pepsoft.worldpainter.*;
import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.exporting.*;
import org.pepsoft.worldpainter.history.HistoryEntry;
import org.pepsoft.worldpainter.layers.*;
import org.pepsoft.worldpainter.util.FileInUseException;
import org.pepsoft.worldpainter.vo.EventVO;
import java.awt.*;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.pepsoft.minecraft.Block.BLOCKS;
import static org.pepsoft.minecraft.Constants.*;
import static org.pepsoft.worldpainter.Constants.*;
/**
*
* @author pepijn
*/
public class WorldMerger extends WorldExporter {
public WorldMerger(World2 world, File levelDatFile) {
super(world);
if (levelDatFile == null) {
throw new NullPointerException();
}
if (! levelDatFile.isFile()) {
throw new IllegalArgumentException(levelDatFile + " does not exist or is not a regular file");
}
this.levelDatFile = levelDatFile;
}
public File getLevelDatFile() {
return levelDatFile;
}
public boolean isReplaceChunks() {
return replaceChunks;
}
public void setReplaceChunks(boolean replaceChunks) {
this.replaceChunks = replaceChunks;
}
/**
* Whether to merge the part of the map <em>above</em> the surface.
*/
public boolean isMergeOverworld() {
return mergeOverworld;
}
public void setMergeOverworld(final boolean mergeOverworld) {
this.mergeOverworld = mergeOverworld;
}
/**
* Whether to merge the part of the map <em>below</em> the surface.
*/
public boolean isMergeUnderworld() {
return mergeUnderworld;
}
public void setMergeUnderworld(final boolean mergeUnderworld) {
this.mergeUnderworld = mergeUnderworld;
}
public int getSurfaceMergeDepth() {
return surfaceMergeDepth;
}
public void setSurfaceMergeDepth(int surfaceMergeDepth) {
this.surfaceMergeDepth = surfaceMergeDepth;
}
/**
* Whether to clear any existing trees (wood and leaf blocks, as well as
* vines, cocoa plants and saplings) above the surface.
*/
public boolean isClearTrees() {
return clearTrees;
}
public void setClearTrees(final boolean clearTrees) {
this.clearTrees = clearTrees;
}
/**
* Whether to remove any existing resource blocks (diamonds, coal, iron ore,
* emeralds, redstone ore, gold ore; by changing them to stone blocks)
* below the surface.
*/
public boolean isClearResources() {
return clearResources;
}
public void setClearResources(final boolean clearResources) {
this.clearResources = clearResources;
}
/**
* Whether to remove any existing caves (by changing all air blocks to the
* most prevalent surrounding block, or stone if it is mostly surrounded by
* air) below the surface. Note that all hollow spaces are filled in,
* including dungeons, abandoned mineshafts and strongholds, but any man-
* made blocks are not removed.
*/
public boolean isFillCaves() {
return fillCaves;
}
public void setFillCaves(final boolean fillCaves) {
this.fillCaves = fillCaves;
}
/**
* Whether to remove any above ground vegetation (tall grass, flowers,
* mushrooms, crops, etc.) other than trees.
*/
public boolean isClearVegetation() {
return clearVegetation;
}
public void setClearVegetation(boolean clearVegetation) {
this.clearVegetation = clearVegetation;
}
/**
* Whether to remove all man-made blocks above ground (by replacing them
* with air).
*/
public boolean isClearManMadeAboveGround() {
return clearManMadeAboveGround;
}
public void setClearManMadeAboveGround(boolean clearManMadeAboveGround) {
this.clearManMadeAboveGround = clearManMadeAboveGround;
}
/**
* Whether to remove all man-made blocks below ground (by replacing them
* with whatever natural block it is surrounded by most, including air).
*/
public boolean isClearManMadeBelowGround() {
return clearManMadeBelowGround;
}
public void setClearManMadeBelowGround(boolean clearManMadeBelowGround) {
this.clearManMadeBelowGround = clearManMadeBelowGround;
}
public void merge(File backupDir, ProgressReceiver progressReceiver) throws IOException, ProgressReceiver.OperationCancelled {
logger.info("Merging world " + world.getName() + " with map at " + levelDatFile.getParentFile());
// Read existing level.dat file
Level level = Level.load(levelDatFile);
// Sanity checks
int existingMaxHeight = level.getMaxHeight();
if (existingMaxHeight != world.getMaxHeight()) {
throw new IllegalArgumentException("Level has different max height (" + existingMaxHeight + ") than WorldPainter world (" + world.getMaxHeight() + ")");
}
int version = level.getVersion();
if ((version != SUPPORTED_VERSION_1) && (version != SUPPORTED_VERSION_2)) {
throw new IllegalArgumentException("Not a supported version: 0x" + Integer.toHexString(version));
}
// Dimension sanity checks
for (Dimension dimension: world.getDimensions()) {
if (existingMaxHeight != dimension.getMaxHeight()) {
throw new IllegalArgumentException("Dimension " + dimension.getDim() + " has different max height (" + dimension.getMaxHeight() + ") than existing level (" + existingMaxHeight + ")");
}
}
File worldDir = levelDatFile.getParentFile();
// Record start of export
long start = System.currentTimeMillis();
// Backup existing level
if (! worldDir.renameTo(backupDir)) {
throw new FileInUseException("Could not move " + worldDir + " to " + backupDir);
}
if (! worldDir.mkdirs()) {
throw new IOException("Could not create " + worldDir);
}
// Set the world to the same Minecraft version as the existing map, in
// case it has changed. This affects the type of chunks created in the
// first pass
world.setVersion(version);
// Modify it if necessary and write it to the the new level
if ((selectedDimensions == null) || selectedDimensions.contains(DIM_NORMAL)) {
Dimension surfaceDimension = world.getDimension(DIM_NORMAL);
level.setSeed(surfaceDimension.getMinecraftSeed());
Point spawnPoint = world.getSpawnPoint();
level.setSpawnX(spawnPoint.x);
level.setSpawnY(Math.max(surfaceDimension.getIntHeightAt(spawnPoint), surfaceDimension.getWaterLevelAt(spawnPoint)));
level.setSpawnZ(spawnPoint.y);
}
// 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);
// Copy everything that we are not going to generate
File[] files = backupDir.listFiles();
//noinspection ConstantConditions // Cannot happen because we previously loaded level.dat from it
for (File file: files) {
if ((! file.getName().equalsIgnoreCase("level.dat"))
&& (! file.getName().equalsIgnoreCase("level.dat_old"))
&& (! file.getName().equalsIgnoreCase("session.lock"))
&& (((selectedDimensions != null) && (! selectedDimensions.contains(DIM_NORMAL))) || (! file.getName().equalsIgnoreCase("region")))
&& (! file.getName().equalsIgnoreCase("maxheight.txt"))
&& (! file.getName().equalsIgnoreCase("Height.txt"))
&& (((selectedDimensions != null) && (! selectedDimensions.contains(DIM_NETHER))) || (! file.getName().equalsIgnoreCase("DIM-1")))
&& (((selectedDimensions != null) && (! selectedDimensions.contains(DIM_END))) || (! file.getName().equalsIgnoreCase("DIM1")))) {
if (file.isFile()) {
FileUtils.copyFileToDir(file, worldDir);
} else if (file.isDirectory()) {
FileUtils.copyDir(file, new File(worldDir, file.getName()));
} else {
logger.warn("Not copying " + file + "; not a regular file or directory");
}
}
}
if ((selectedDimensions == null) ? (world.getDimension(DIM_NORMAL) != null) : selectedDimensions.contains(DIM_NORMAL)) {
mergeDimension(worldDir, backupDir, world.getDimension(DIM_NORMAL), version, progressReceiver);
}
if ((selectedDimensions == null) ? (world.getDimension(DIM_NETHER) != null) : selectedDimensions.contains(DIM_NETHER)) {
mergeDimension(worldDir, backupDir, world.getDimension(DIM_NETHER), version, progressReceiver);
}
if ((selectedDimensions == null) ? (world.getDimension(DIM_END) != null) : selectedDimensions.contains(DIM_END)) {
mergeDimension(worldDir, backupDir, world.getDimension(DIM_END), version, 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 merge in the world history
if (selectedDimensions == null) {
world.addHistoryEntry(HistoryEntry.WORLD_MERGED_FULL, level.getName(), worldDir);
} else {
String dimNames = selectedDimensions.stream().map(dim -> {
switch (dim) {
case DIM_NORMAL:
return "Surface";
case DIM_NETHER:
return "Nether";
case DIM_END:
return "End";
default:
return Integer.toString(dim);
}
}).collect(Collectors.joining(", "));
world.addHistoryEntry(HistoryEntry.WORLD_MERGED_PARTIAL, level.getName(), worldDir, dimNames);
}
if (! levelDatFile.equals(world.getMergedWith())) {
world.setMergedWith(levelDatFile);
}
// Log an event
Configuration config = Configuration.getInstance();
if (config != null) {
EventVO event = new EventVO(EVENT_KEY_ACTION_MERGE_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());
}
if ((selectedDimensions == null) || selectedDimensions.contains(DIM_NORMAL)) {
Dimension surfaceDimension = world.getDimension(0);
event.setAttribute(ATTRIBUTE_KEY_TILES, surfaceDimension.getTiles().size());
logLayers(surfaceDimension, event, "");
}
if (world.getImportedFrom() == null) {
event.setAttribute(ATTRIBUTE_KEY_IMPORTED_WORLD, false);
}
config.logEvent(event);
}
}
public String getWarnings() {
return warnings;
}
private void mergeDimension(final File worldDir, File backupWorldDir, final Dimension dimension, final int version, ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled, IOException {
if (progressReceiver != null) {
progressReceiver.setMessage("merging " + dimension.getName() + " dimension");
}
final File dimensionDir, backupDimensionDir;
switch (dimension.getDim()) {
case org.pepsoft.worldpainter.Constants.DIM_NORMAL:
dimensionDir = worldDir;
backupDimensionDir = backupWorldDir;
break;
case org.pepsoft.worldpainter.Constants.DIM_NETHER:
dimensionDir = new File(worldDir, "DIM-1");
backupDimensionDir = new File(backupWorldDir, "DIM-1");
break;
case org.pepsoft.worldpainter.Constants.DIM_END:
dimensionDir = new File(worldDir, "DIM1");
backupDimensionDir = new File(backupWorldDir, "DIM1");
break;
default:
throw new IllegalArgumentException("Dimension " + dimension.getDim() + " not supported");
}
File regionDir = new File(dimensionDir, "region");
if (! regionDir.exists()) {
regionDir.mkdirs();
}
dimension.rememberChanges();
try {
// 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) {
// 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);
// 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);
}
}
// Sort tiles into regions
int lowestRegionX = Integer.MAX_VALUE, highestRegionX = Integer.MIN_VALUE, lowestRegionZ = Integer.MAX_VALUE, highestRegionZ = Integer.MIN_VALUE;
Map<Point, Map<Point, Tile>> tilesByRegion = new HashMap<>();
final boolean tileSelection = selectedTiles != null;
if (tileSelection) {
// Sanity check
assert selectedDimensions.size() == 1;
assert selectedDimensions.contains(dimension.getDim());
for (Point tileCoords: selectedTiles) {
Tile tile = dimension.getTile(tileCoords);
boolean nonReadOnlyChunksFound = false;
outerLoop: for (int chunkX = 0; chunkX < TILE_SIZE; chunkX += 16) {
for (int chunkY = 0; chunkY < TILE_SIZE; chunkY += 16) {
if (! tile.getBitLayerValue(ReadOnly.INSTANCE, chunkX, chunkY)) {
nonReadOnlyChunksFound = true;
break outerLoop;
}
}
}
if (! nonReadOnlyChunksFound) {
// All chunks in this tile are marked read-only, so we can
// skip the entire tile. If all tiles in the region have
// only read-only chunks, the entire region does not have to
// be merged
continue;
}
int regionX = tileCoords.x >> 2;
int regionZ = tileCoords.y >> 2;
Point regionCoords = new Point(regionX, regionZ);
Map<Point, Tile> tilesForRegion = tilesByRegion.computeIfAbsent(regionCoords, k -> new HashMap<>());
tilesForRegion.put(tileCoords, tile);
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()) {
boolean nonReadOnlyChunksFound = false;
outerLoop: for (int chunkX = 0; chunkX < TILE_SIZE; chunkX += 16) {
for (int chunkY = 0; chunkY < TILE_SIZE; chunkY += 16) {
if (! tile.getBitLayerValue(ReadOnly.INSTANCE, chunkX, chunkY)) {
nonReadOnlyChunksFound = true;
break outerLoop;
}
}
}
if (! nonReadOnlyChunksFound) {
// All chunks in this tile are marked read-only, so we can
// skip the entire tile. If all tiles in the region have
// only read-only chunks, the entire region does not have to
// be merged
continue;
}
int regionX = tile.getX() >> 2;
int regionZ = tile.getY() >> 2;
Point regionCoords = new Point(regionX, regionZ);
Map<Point, Tile> tilesForRegion = tilesByRegion.computeIfAbsent(regionCoords, k -> new HashMap<>());
tilesForRegion.put(new Point(tile.getX(), tile.getY()), tile);
if (regionX < lowestRegionX) {
lowestRegionX = regionX;
}
if (regionX > highestRegionX) {
highestRegionX = regionX;
}
if (regionZ < lowestRegionZ) {
lowestRegionZ = regionZ;
}
if (regionZ > highestRegionZ) {
highestRegionZ = regionZ;
}
}
}
// Read the region coordinates of the existing map
final File backupRegionDir = new File(backupDimensionDir, "region");
final Pattern regionFilePattern = (version == SUPPORTED_VERSION_2)
? Pattern.compile("r\\.-?\\d+\\.-?\\d+\\.mca")
: Pattern.compile("r\\.-?\\d+\\.-?\\d+\\.mcr");
File[] existingRegionFiles = backupRegionDir.listFiles((dir, name) -> regionFilePattern.matcher(name).matches());
Map<Point, File> existingRegions = new HashMap<>();
for (File file: existingRegionFiles) {
String[] parts = file.getName().split("\\.");
int regionX = Integer.parseInt(parts[1]);
int regionZ = Integer.parseInt(parts[2]);
existingRegions.put(new Point(regionX, regionZ), file);
if (regionX < lowestRegionX) {
lowestRegionX = regionX;
}
if (regionX > highestRegionX) {
highestRegionX = regionX;
}
if (regionZ < lowestRegionZ) {
lowestRegionZ = regionZ;
}
if (regionZ > highestRegionZ) {
highestRegionZ = regionZ;
}
}
final Set<Point> allRegionCoords = new HashSet<>();
allRegionCoords.addAll(tilesByRegion.keySet());
allRegionCoords.addAll(existingRegions.keySet());
// 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<>(allRegionCoords.size());
if (lowestRegionZ == highestRegionZ) {
// No point in sorting it
sortedRegions.addAll(allRegionCoords);
} else {
for (int x = lowestRegionX; x <= highestRegionX; x++) {
for (int z = lowestRegionZ; z <= (lowestRegionZ + 1); z++) {
Point regionCoords = new Point(x, z);
if (allRegionCoords.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 (allRegionCoords.contains(regionCoords)) {
sortedRegions.add(regionCoords);
}
}
}
}
// Merge each individual region
final WorldPainterChunkFactory chunkFactory = new WorldPainterChunkFactory(dimension, exporters, world.getVersion(), world.getMaxHeight());
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")), tilesByRegion.size()), 1);
} else {
threads = Math.max(Math.min(Math.min(maxThreadsByMem, runtime.availableProcessors()), allRegionCoords.size()), 1);
}
logger.info("Using " + threads + " thread(s) for merge (cores: " + runtime.availableProcessors() + ", available memory: " + (maxMemoryAvailable / 1048576L) + " MB)");
final Map<Point, List<Fixup> >fixups = new HashMap<>();
final Set<Point> exportedRegions = new HashSet<>();
ExecutorService executor = Executors.newFixedThreadPool(threads, new ThreadFactory() {
@Override
public synchronized Thread newThread(Runnable r) {
Thread thread = new Thread(threadGroup, r, "Merger-" + nextID++);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
}
private final ThreadGroup threadGroup = new ThreadGroup("Mergers");
private int nextID = 1;
});
final ParallelProgressManager parallelProgressManager = (progressReceiver != null) ? new ParallelProgressManager(progressReceiver, allRegionCoords.size()) : null;
try {
// Merge each individual region
for (final Point regionCoords: sortedRegions) {
if (existingRegions.containsKey(regionCoords)) {
if (tilesByRegion.containsKey(regionCoords)) {
// Region exists in new and existing maps; merge it
final Map<Point, Tile> tiles = tilesByRegion.get(regionCoords);
executor.execute(() -> {
ProgressReceiver progressReceiver1 = (parallelProgressManager != null) ? parallelProgressManager.createProgressReceiver() : null;
if (progressReceiver1 != null) {
try {
progressReceiver1.checkForCancellation();
} catch (ProgressReceiver.OperationCancelled e) {
return;
}
}
try {
List<Fixup> regionFixups = new ArrayList<>();
WorldRegion minecraftWorld = new WorldRegion(regionCoords.x, regionCoords.y, dimension.getMaxHeight(), version);
try {
String regionWarnings = mergeRegion(minecraftWorld, backupRegionDir, dimension, regionCoords, tiles, tileSelection, exporters, chunkFactory, regionFixups, (progressReceiver1 != null) ? new SubProgressReceiver(progressReceiver1, 0.0f, 0.9f) : null);
if (regionWarnings != null) {
if (warnings == null) {
warnings = regionWarnings;
} else {
warnings = warnings + regionWarnings;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Merged region " + regionCoords.x + "," + regionCoords.y);
}
} finally {
minecraftWorld.save(dimensionDir);
}
synchronized (fixups) {
if (! regionFixups.isEmpty()) {
fixups.put(new Point(regionCoords.x, regionCoords.y), regionFixups);
}
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(allRegionCoords, 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);
}
}
});
} else {
// Region only exists in existing world. Copy it to the new
// world
ProgressReceiver subProgressReceiver = (parallelProgressManager != null) ? parallelProgressManager.createProgressReceiver() : null;
if (subProgressReceiver != null) {
subProgressReceiver.setMessage("Copying region " + regionCoords.x + "," + regionCoords.y + " unchanged");
}
FileUtils.copyFileToDir(existingRegions.get(regionCoords), regionDir, subProgressReceiver);
synchronized (fixups) {
exportedRegions.add(regionCoords);
}
if (logger.isDebugEnabled()) {
logger.debug("Copied region " + regionCoords.x + "," + regionCoords.y);
}
}
} else {
// Region only exists in new world. Create it as new
executor.execute(() -> {
ProgressReceiver progressReceiver1 = (parallelProgressManager != null) ? parallelProgressManager.createProgressReceiver() : null;
if (progressReceiver1 != null) {
try {
progressReceiver1.checkForCancellation();
} catch (ProgressReceiver.OperationCancelled e) {
return;
}
}
try {
WorldRegion minecraftWorld = new WorldRegion(regionCoords.x, regionCoords.y, dimension.getMaxHeight(), version);
ExportResults exportResults = null;
try {
exportResults = exportRegion(minecraftWorld, dimension, null, regionCoords, tileSelection, exporters, null, chunkFactory, null, (progressReceiver1 != null) ? new SubProgressReceiver(progressReceiver1, 0.9f, 0.1f) : null);
if (logger.isDebugEnabled()) {
logger.debug("Generated region " + regionCoords.x + "," + regionCoords.y);
}
} 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(allRegionCoords, 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(1000, 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 != null) ? new SubProgressReceiver(progressReceiver, 0.9f, 0.1f) : null, fixups);
}
}
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();
}
}
}
private String mergeRegion(MinecraftWorld minecraftWorld, File oldRegionDir, Dimension dimension, Point regionCoords, Map<Point, Tile> tiles, boolean tileSelection, Map<Layer, LayerExporter> exporters, ChunkFactory chunkFactory, List<Fixup> fixups, ProgressReceiver progressReceiver) throws IOException, ProgressReceiver.OperationCancelled {
if (progressReceiver != null) {
progressReceiver.setMessage("Merging region " + regionCoords.x + "," + regionCoords.y + " of " + dimension.getName());
}
Set<Layer> allLayers = 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();
allLayers.addAll(minimumLayers);
List<Layer> secondaryPassLayers = new ArrayList<>();
for (Layer layer: allLayers) {
LayerExporter exporter = layer.getExporter();
if (exporter instanceof SecondPassLayerExporter) {
secondaryPassLayers.add(layer);
}
}
Collections.sort(secondaryPassLayers);
// First pass. Create terrain and apply layers which don't need access
// to neighbouring chunks
long t1 = System.currentTimeMillis();
String warnings;
if (firstPass(minecraftWorld, dimension, regionCoords, tiles, tileSelection, exporters, chunkFactory, false, (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.0f, 0.3f) : null).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.3f, 0.1f) : null);
if ((myFixups != null) && (! myFixups.isEmpty())) {
synchronized (fixups) {
fixups.addAll(myFixups);
}
}
// Merge chunks
long t3 = System.currentTimeMillis();
warnings = thirdPass(minecraftWorld, oldRegionDir, dimension, regionCoords, (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.4f, 0.25f) : null);
// Post processing. Fix covered grass blocks, things like that
long t4 = System.currentTimeMillis();
PostProcessor.postProcess(minecraftWorld, new Rectangle(regionCoords.x << 9, regionCoords.y << 9, 512, 512), (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.65f, 0.1f) : null);
// Third pass. Calculate lighting
long t5 = System.currentTimeMillis();
lightingPass(minecraftWorld, regionCoords, (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.75f, 0.25f) : null);
long t6 = System.currentTimeMillis();
if ("true".equalsIgnoreCase(System.getProperty("org.pepsoft.worldpainter.devMode"))) {
String timingMessage = (t2 - t1) + ", " + (t3 - t2) + ", " + (t4 - t3) + ", " + (t5 - t4) + ", " + (t6 - t5) + ", " + (t6 - t1);
// System.out.println("Merge timing: " + timingMessage);
synchronized (TIMING_FILE_LOCK) {
try (PrintWriter out = new PrintWriter(new FileOutputStream("mergetimings.csv", true))) {
out.println(timingMessage);
}
}
}
} else {
// First pass produced no chunks; copy all chunks from the existing
// region
warnings = copyAllChunks(minecraftWorld, oldRegionDir, dimension, regionCoords, (progressReceiver != null) ? new SubProgressReceiver(progressReceiver, 0.3f, 0.7f) : null);
}
return warnings;
}
/**
* Merge only the biomes, leave everything else the same.
*/
public void mergeBiomes(File backupDir, ProgressReceiver progressReceiver) throws IOException, ProgressReceiver.OperationCancelled {
// Read existing level.dat file
Level level = Level.load(levelDatFile);
// Sanity checks
int existingMaxHeight = level.getMaxHeight();
if (existingMaxHeight != world.getMaxHeight()) {
throw new IllegalArgumentException("Level has different max height (" + existingMaxHeight + ") than WorldPainter world (" + world.getMaxHeight() + ")");
}
int version = level.getVersion();
if (version != SUPPORTED_VERSION_2) {
throw new IllegalArgumentException("Not a supported version: 0x" + Integer.toHexString(version));
}
// Dimension sanity checks
Dimension dimension = world.getDimension(0);
if (existingMaxHeight != dimension.getMaxHeight()) {
throw new IllegalArgumentException("Dimension " + dimension.getDim() + " has different max height (" + dimension.getMaxHeight() + ") than existing level (" + existingMaxHeight + ")");
}
File worldDir = levelDatFile.getParentFile();
// Backup existing level
if (! worldDir.renameTo(backupDir)) {
throw new FileInUseException("Could not move " + worldDir + " to " + backupDir);
}
if (! worldDir.mkdirs()) {
throw new IOException("Could not create " + worldDir);
}
// Set the world to the same Minecraft version as the existing map, in
// case it has changed. This affects the type of chunks created in the
// first pass
world.setVersion(version);
// Modify it if necessary and write it to the the new level
level.setSeed(dimension.getMinecraftSeed());
// Copy everything that we are not going to generate (this includes the
// Nether and End dimensions)
File[] files = backupDir.listFiles();
//noinspection ConstantConditions // Cannot happen because we previously loaded level.dat from it
for (File file: files) {
if ((! file.getName().equalsIgnoreCase("level.dat"))
&& (! file.getName().equalsIgnoreCase("level.dat_old"))
&& (! file.getName().equalsIgnoreCase("session.lock"))
&& (! file.getName().equalsIgnoreCase("region"))
&& (! file.getName().equalsIgnoreCase("maxheight.txt"))
&& (! file.getName().equalsIgnoreCase("Height.txt"))) {
if (file.isFile()) {
FileUtils.copyFileToDir(file, worldDir);
} else if (file.isDirectory()) {
FileUtils.copyDir(file, new File(worldDir, file.getName()));
} else {
logger.warn("Not copying " + file + "; not a regular file or directory");
}
}
}
level.save(worldDir);
// Process all chunks and copy just the biomes
if (progressReceiver != null) {
progressReceiver.setMessage("Merging biomes");
}
// Find all the region files of the existing level
File oldRegionDir = new File(backupDir, "region");
final Pattern regionFilePattern = Pattern.compile("r\\.-?\\d+\\.-?\\d+\\.mca");
File[] oldRegionFiles = oldRegionDir.listFiles((dir, name) -> regionFilePattern.matcher(name).matches());
// Process each region file, copying every chunk unmodified, except
// for the biomes
@SuppressWarnings("ConstantConditions") // Can only happen for corrupted maps
int totalChunkCount = oldRegionFiles.length * 32 * 32, chunkCount = 0;
File newRegionDir = new File(worldDir, "region");
newRegionDir.mkdirs();
for (File file: oldRegionFiles) {
try (RegionFile oldRegion = new RegionFile(file)) {
String[] parts = file.getName().split("\\.");
int regionX = Integer.parseInt(parts[1]);
int regionZ = Integer.parseInt(parts[2]);
File newRegionFile = new File(newRegionDir, "r." + regionX + "." + regionZ + ".mca");
try (RegionFile newRegion = new RegionFile(newRegionFile)) {
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
if (oldRegion.containsChunk(x, z)) {
ChunkImpl2 chunk;
try (NBTInputStream in = new NBTInputStream(oldRegion.getChunkDataInputStream(x, z))) {
CompoundTag tag = (CompoundTag) in.readTag();
chunk = new ChunkImpl2(tag, level.getMaxHeight());
}
int chunkX = chunk.getxPos(), chunkZ = chunk.getzPos();
for (int xx = 0; xx < 16; xx++) {
for (int zz = 0; zz < 16; zz++) {
chunk.setBiome(xx, zz, dimension.getLayerValueAt(Biome.INSTANCE, (chunkX << 4) | xx, (chunkZ << 4) | zz));
}
}
try (NBTOutputStream out = new NBTOutputStream(newRegion.getChunkDataOutputStream(x, z))) {
out.writeTag(chunk.toNBT());
}
}
chunkCount++;
if (progressReceiver != null) {
progressReceiver.setProgress((float) chunkCount / totalChunkCount);
}
}
}
}
}
}
}
private String thirdPass(MinecraftWorld minecraftWorld, File oldRegionDir, Dimension dimension, Point regionCoords, ProgressReceiver progressReceiver) throws IOException, ProgressReceiver.OperationCancelled {
if (progressReceiver != null) {
progressReceiver.setMessage("Merging existing blocks with new");
}
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 version = dimension.getWorld().getVersion();
int maxHeight = dimension.getMaxHeight();
Map<Point, RegionFile> regionFiles = new HashMap<>();
Set<Point> damagedRegions = new HashSet<>();
StringBuilder reportBuilder = new StringBuilder();
try {
int chunkNo = 0;
for (int chunkX = lowestChunkX; chunkX <= highestChunkX; chunkX++) {
for (int chunkY = lowestChunkY; chunkY <= highestChunkY; chunkY++) {
chunkNo++;
if (progressReceiver != null) {
progressReceiver.setProgress((float) chunkNo / 1156);
}
Chunk newChunk;
if (dimension.getTile(chunkX >> 3, chunkY >> 3) == null) {
// The tile for this chunk does not exist in the new
// world, so the chunk from the existing world should
// be copied
newChunk = null;
} else {
newChunk = minecraftWorld.getChunk(chunkX, chunkY);
}
if (replaceChunks && (newChunk != null)) {
// The chunk exists in the new world, and replace all
// chunks has been requested, so leave the new chunk
// as is
continue;
}
int regionX = chunkX >> 5;
int regionY = chunkY >> 5;
Point coords = new Point(regionX, regionY);
if (damagedRegions.contains(coords)) {
// We can't read this region, which we have already
// reported and logged earlier
continue;
}
RegionFile regionFile = regionFiles.get(coords);
if (regionFile == null) {
File file = new File(oldRegionDir, "r." + regionX + "." + regionY + ((version == SUPPORTED_VERSION_2) ? ".mca" : ".mcr"));
try {
regionFile = new RegionFile(file);
regionFiles.put(coords, regionFile);
} catch (IOException e) {
reportBuilder.append("I/O error while opening region file " + file + " (message: \"" + e.getMessage() + "\"); skipping region" + EOL);
logger.error("I/O error while opening region file " + file + "; skipping region", e);
damagedRegions.add(coords);
continue;
}
}
int chunkXInRegion = chunkX & 0x1f;
int chunkYInRegion = chunkY & 0x1f;
if (regionFile.containsChunk(chunkXInRegion, chunkYInRegion)) {
Tag tag;
try {
DataInputStream chunkData = regionFile.getChunkDataInputStream(chunkXInRegion, chunkYInRegion);
if (chunkData == null) {
// This should never happen, since we checked with
// isChunkPresent(), but in practice it does. Perhaps
// corrupted data?
reportBuilder.append("Missing chunk data in existing map for chunk " + chunkXInRegion + ", " + chunkYInRegion + " in " + regionFile + "; skipping chunk" + EOL);
logger.warn("Missing chunk data in existing map for chunk " + chunkXInRegion + ", " + chunkYInRegion + " in " + regionFile + "; skipping chunk");
continue;
}
try (NBTInputStream in = new NBTInputStream(chunkData)) {
tag = in.readTag();
}
} catch (IOException e) {
reportBuilder.append("I/O error while reading chunk in existing map " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + " (message: \"" + e.getMessage() + "\"); skipping chunk" + EOL);
logger.error("I/O error while reading chunk in existing map " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + "; skipping chunk", e);
continue;
} catch (IllegalArgumentException e) {
reportBuilder.append("Illegal argument exception while reading chunk in existing map " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + " (message: \"" + e.getMessage() + "\"); skipping chunk" + EOL);
logger.error("Illegal argument exception while reading chunk in existing map " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + "; skipping chunk", e);
continue;
}
Chunk existingChunk = (version == SUPPORTED_VERSION_2)
? new ChunkImpl2((CompoundTag) tag, maxHeight)
: new ChunkImpl((CompoundTag) tag, maxHeight);
if (newChunk != null) {
// Chunk exists in existing and new world; merge it
// Do any necessary processing of the existing chunk
// (clearing trees, etc.) No need to check for
// read-only; if the chunk was read-only it
// wouldn't exist in the new map and we wouldn't
// be here
processExistingChunk(existingChunk);
try {
newChunk = mergeChunk(existingChunk, newChunk, dimension);
minecraftWorld.addChunk(newChunk);
} catch (NullPointerException e) {
reportBuilder.append("Null pointer exception while reading chunk in existing map " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + "; skipping chunk" + EOL);
logger.error("Null pointer exception while reading chunk in existing map " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + "; skipping chunk", e);
continue;
} catch (ArrayIndexOutOfBoundsException e) {
reportBuilder.append("Array index out of bounds while reading chunk in existing map " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + " (message: \"" + e.getMessage() + "\"); skipping chunk" + EOL);
logger.error("Array index out of bounds while reading chunk in existing map " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + "; skipping chunk", e);
continue;
}
} else {
// Chunk exists in existing world, but not in new
// one, copy old to new
minecraftWorld.addChunk(existingChunk);
}
}
}
}
} finally {
for (RegionFile regionFile: regionFiles.values()) {
regionFile.close();
}
}
if (progressReceiver != null) {
progressReceiver.setProgress(1.0f);
}
return reportBuilder.length() != 0 ? reportBuilder.toString() : null;
}
private void processExistingChunk(final Chunk existingChunk) {
if (! (clearTrees || fillCaves || clearResources || clearVegetation || clearManMadeAboveGround || clearManMadeBelowGround)) {
return;
}
int maxZ = world.getMaxHeight() - 1;
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 16; y++) {
boolean aboveGround = true;
for (int z = maxZ; z >= 0; z--) {
Block existingBlock = BLOCKS[existingChunk.getBlockType(x, z, y)];
if (aboveGround) {
if ((clearTrees && existingBlock.treeRelated)
|| (clearVegetation && existingBlock.vegetation)
|| (clearManMadeAboveGround && (! existingBlock.natural))) {
setToAir(existingChunk, x, y, z);
} else if (existingBlock.terrain) {
aboveGround = false;
}
} else {
// Separate if-statements so that if both are enabled,
// man made blocks are correctly removed and then filled
// in
if (clearManMadeBelowGround && (! existingBlock.natural)) {
final Material newMaterial = findMostPrevalentSolidSurroundingMaterial(existingChunk, x, y, z);
if (newMaterial == Material.AIR) {
setToAir(existingChunk, x, y, z);
} else {
existingChunk.setMaterial(x, z, y, newMaterial);
existingChunk.setSkyLightLevel(x, z, y, 0);
existingChunk.setBlockLightLevel(x, z, y, 0);
}
existingBlock = BLOCKS[existingChunk.getBlockType(x, z, y)];
}
if (fillCaves && existingBlock.veryInsubstantial) {
final Material newMaterial = findMostPrevalentSolidSurroundingMaterial(existingChunk, x, y, z);
if (newMaterial == Material.AIR) {
existingChunk.setMaterial(x, z, y, Material.STONE);
} else {
existingChunk.setMaterial(x, z, y, newMaterial);
}
existingChunk.setSkyLightLevel(x, z, y, 0);
existingChunk.setBlockLightLevel(x, z, y, 0);
} else if (clearResources && existingBlock.resource) {
if (existingBlock.id == BLK_QUARTZ_ORE) {
existingChunk.setMaterial(x, z, y, Material.NETHERRACK);
} else {
existingChunk.setMaterial(x, z, y, Material.STONE);
}
}
}
}
}
}
}
private void setToAir(final Chunk chunk, final int x, final int y, final int z) {
chunk.setMaterial(x, z, y, Material.AIR);
// Note that these lighting calculations aren't strictly necessary since
// the lighting will be fully recalculated later on, but it doesn't hurt
// and it might improve performance and/or fill in gaps in the logic
int maxZ = world.getMaxHeight() - 1;
int skyLightLevelAbove = (z < maxZ) ? chunk.getSkyLightLevel(x, z + 1, y) : 15;
int skyLightLevelBelow = (z > 0) ? chunk.getSkyLightLevel(x, z - 1, y) : 0;
int blockLightLevelAbove = (z < maxZ) ? chunk.getSkyLightLevel(x, z + 1, y) : 0;
int blockLightLevelBelow = (z > 0) ? chunk.getBlockLightLevel(x, z - 1, y) : 0;
if (skyLightLevelAbove == 15) {
// Propagate full daylight down
chunk.setSkyLightLevel(x, z, y, 15);
} else {
chunk.setSkyLightLevel(x, z, y, Math.max(Math.max(skyLightLevelAbove, skyLightLevelBelow) - 1, 0));
}
chunk.setBlockLightLevel(x, z, y, Math.max(Math.max(blockLightLevelAbove, blockLightLevelBelow) - 1, 0));
}
/**
* Finds the most prevalent natural, non-ore, solid block type surrounding
* a particular block (inside the same chunk).
*/
private Material findMostPrevalentSolidSurroundingMaterial(Chunk existingChunk, int x, int y, int z) {
byte[] histogram = histogramRef.get();
Arrays.fill(histogram, (byte) 0);
int highestMaterialIndex = -1;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
for (int dz = -1; dz <= 1; dz++) {
if ((dx == 0) && (dy == 0) && (dz == 0)) {
continue;
}
int xx = x + dx, yy = y + dy, zz = z + dz;
if ((xx < 0) || (xx > 15) || (yy < 0) || (yy > 15) || (zz < 0) || (zz >= existingChunk.getMaxHeight())) {
continue;
}
Material material = existingChunk.getMaterial(xx, zz, yy);
int blockType = material.blockType;
if (SOLID_BLOCKS.get(blockType)) {
int index = (blockType << 4) | material.data;
histogram[index]++;
if (histogram[index] > highestMaterialIndex) {
highestMaterialIndex = blockType;
}
}
}
}
}
if (highestMaterialIndex > -1) {
return Material.get(highestMaterialIndex >> 4, highestMaterialIndex & 0xf);
} else {
return Material.AIR;
}
}
private String copyAllChunks(MinecraftWorld minecraftWorld, File oldRegionDir, Dimension dimension, Point regionCoords, ProgressReceiver progressReceiver) throws IOException, ProgressReceiver.OperationCancelled {
if (progressReceiver != null) {
progressReceiver.setMessage("Copying chunks unchanged");
}
int lowestChunkX = regionCoords.x << 5;
int highestChunkX = (regionCoords.x << 5) + 31;
int lowestChunkY = regionCoords.y << 5;
int highestChunkY = (regionCoords.y << 5) + 31;
int version = dimension.getWorld().getVersion();
int maxHeight = dimension.getMaxHeight();
Map<Point, RegionFile> regionFiles = new HashMap<>();
Set<Point> damagedRegions = new HashSet<>();
StringBuilder reportBuilder = new StringBuilder();
try {
int chunkNo = 0;
for (int chunkX = lowestChunkX; chunkX <= highestChunkX; chunkX++) {
for (int chunkY = lowestChunkY; chunkY <= highestChunkY; chunkY++) {
chunkNo++;
if (progressReceiver != null) {
progressReceiver.setProgress((float) chunkNo / 1024);
}
int regionX = chunkX >> 5;
int regionY = chunkY >> 5;
Point coords = new Point(regionX, regionY);
if (damagedRegions.contains(coords)) {
// We can't read this region, which we have already
// reported and logged earlier
continue;
}
RegionFile regionFile = regionFiles.get(coords);
if (regionFile == null) {
File file = new File(oldRegionDir, "r." + regionX + "." + regionY + ((version == SUPPORTED_VERSION_2) ? ".mca" : ".mcr"));
try {
regionFile = new RegionFile(file);
regionFiles.put(coords, regionFile);
} catch (IOException e) {
reportBuilder.append("I/O error while opening region file " + file + " (message: \"" + e.getMessage() + "\"); skipping region" + EOL);
logger.error("I/O error while opening region file " + file + "; skipping region", e);
damagedRegions.add(coords);
continue;
}
}
int chunkXInRegion = chunkX & 0x1f;
int chunkYInRegion = chunkY & 0x1f;
if (regionFile.containsChunk(chunkXInRegion, chunkYInRegion)) {
Tag tag;
try {
InputStream chunkData = regionFile.getChunkDataInputStream(chunkXInRegion, chunkYInRegion);
if (chunkData == null) {
// This should never happen, since we checked
// with isChunkPresent(), but in practice it
// does. Perhaps corrupted data?
reportBuilder.append("Missing chunk data for chunk " + chunkXInRegion + ", " + chunkYInRegion + " in " + regionFile + "; skipping chunk" + EOL);
logger.warn("Missing chunk data for chunk " + chunkXInRegion + ", " + chunkYInRegion + " in " + regionFile + "; skipping chunk");
continue;
}
try (NBTInputStream in = new NBTInputStream(chunkData)) {
tag = in.readTag();
}
} catch (IOException e) {
reportBuilder.append("I/O error while reading chunk " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + " (message: \"" + e.getMessage() + "\"); skipping chunk" + EOL);
logger.error("I/O error while reading chunk " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + "; skipping chunk", e);
continue;
} catch (IllegalArgumentException e) {
reportBuilder.append("Illegal argument exception while reading chunk " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + " (message: \"" + e.getMessage() + "\"); skipping chunk" + EOL);
logger.error("Illegal argument exception while reading chunk " + chunkXInRegion + ", " + chunkYInRegion + " from file " + regionFile + "; skipping chunk", e);
continue;
}
Chunk existingChunk = (version == SUPPORTED_VERSION_1)
? new ChunkImpl((CompoundTag) tag, maxHeight)
: new ChunkImpl2((CompoundTag) tag, maxHeight);
minecraftWorld.addChunk(existingChunk);
}
}
}
} finally {
for (RegionFile regionFile: regionFiles.values()) {
regionFile.close();
}
}
if (progressReceiver != null) {
progressReceiver.setProgress(1.0f);
}
return reportBuilder.length() != 0 ? reportBuilder.toString() : null;
}
private Chunk mergeChunk(Chunk existingChunk, Chunk newChunk, Dimension dimension) {
int maxY = existingChunk.getMaxHeight() - 1;
int chunkX = existingChunk.getxPos() << 4, chunkZ = existingChunk.getzPos() << 4;
List<Entity> newChunkEntities = newChunk.getEntities();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (dimension.getBitLayerValueAt(org.pepsoft.worldpainter.layers.Void.INSTANCE, chunkX | x, chunkZ | z)) {
// Void. Just empty the entire column
for (int y = 0; y <= maxY; y++) {
newChunk.setMaterial(x, y, z, Material.AIR);
newChunk.setBlockLightLevel(x, y, z, 0);
newChunk.setSkyLightLevel(x, y, z, 15);
}
} else {
final int newHeight = dimension.getIntHeightAt(chunkX | x, chunkZ | z);
final boolean frost = dimension.getBitLayerValueAt(Frost.INSTANCE, chunkX | x, chunkZ | z);
int oldHeight = 0;
for (int y = maxY; y >= 0; y--) {
int oldBlockType = existingChunk.getBlockType(x, y, z);
if (BLOCKS[oldBlockType].terrain) {
// Terrain found
oldHeight = y;
break;
}
}
final int dy = newHeight - oldHeight;
if (dy > 0) {
// Terrain has been raised
// Copy or merge underground portion from existing chunk
final int mergeLimit = Math.min(newHeight - surfaceMergeDepth, oldHeight);
for (int y = 0; y <= mergeLimit; y++) {
mergeUndergroundBlock(existingChunk, newChunk, x, y, z);
}
// Merge surface layer blocks
for (int y = mergeLimit + 1; y <= newHeight; y++) {
mergeSurfaceBlock(existingChunk, newChunk, x, y, z, y < oldHeight);
}
// Merge above ground portion from existing chunk, raised by
// the appropriate amount
for (int y = newHeight + 1; y <= maxY; y++) {
mergeAboveGroundBlock(existingChunk, newChunk, x, y, z, dy, frost);
}
newChunk.setHeight(x, z, Math.min(existingChunk.getHeight(x, z) + dy, maxY));
} else if (dy < 0) {
// Terrain has been lowered
// Copy underground portion from existing chunk
final int mergeLimit = newHeight - surfaceMergeDepth;
for (int y = 0; y <= mergeLimit; y++) {
mergeUndergroundBlock(existingChunk, newChunk, x, y, z);
}
// If the new ground height block is insubstantial in the
// existing chunk, and there is nothing substantial on the
// block in the new or existing chunks, remove it, so as not
// to create a weird one block layer of blocks over newly
// opened up voids such as caves, chasms, abandoned mines,
// etc.
final int mergeStartHeight = newHeight + 1;
final int existingBlockType = existingChunk.getBlockType(x, newHeight, z);
if ((existingBlockType == BLK_AIR) || BLOCKS[existingBlockType].insubstantial) {
int existingBlockAboveType = (newHeight < maxY) ? existingChunk.getBlockType(x, newHeight + 1, z) : BLK_AIR;
int newBlockAboveType = (((newHeight - dy) >= -1) && ((newHeight - dy) < maxY)) ? newChunk.getBlockType(x, newHeight + 1 - dy, z) : BLK_AIR;
if (((newBlockAboveType == BLK_AIR) || BLOCKS[newBlockAboveType].insubstantial) && ((existingBlockAboveType == BLK_AIR) || BLOCKS[existingBlockAboveType].insubstantial)) {
newChunk.setBlockType(x, newHeight, z, BLK_AIR);
newChunk.setDataValue(x, newHeight, z, 0);
newChunk.setSkyLightLevel(x, newHeight, z, 0);
newChunk.setBlockLightLevel(x, newHeight, z, 0);
}
}
// Copy above ground portion from existing chunk, lowered by
// the appropriate amount
for (int y = mergeStartHeight; y <= (maxY + dy); y++) {
mergeAboveGroundBlock(existingChunk, newChunk, x, y, z, dy, frost);
}
newChunk.setHeight(x, z, Math.min(existingChunk.getHeight(x, z) + dy, maxY));
} else {
// Terrain height has not changed. Copy everything from the
// existing chunk, except the top layer of the terrain.
final int mergeLimit = newHeight - surfaceMergeDepth;
for (int y = 0; y <= mergeLimit; y++) {
mergeUndergroundBlock(existingChunk, newChunk, x, y, z);
}
for (int y = newHeight + 1; y <= maxY; y++) {
mergeAboveGroundBlock(existingChunk, newChunk, x, y, z, 0, frost);
}
}
// Tilled earth is imported as dirt, so make sure to leave
// it intact
if ((newChunk.getBlockType(x, newHeight, z) == BLK_DIRT) && (existingChunk.getBlockType(x, oldHeight, z) == BLK_TILLED_DIRT)) {
newChunk.setMaterial(x, newHeight, z, existingChunk.getMaterial(x, oldHeight, z));
}
// Frosted ice is imported as water, so make sure to leave
// it intact
if ((newChunk.getBlockType(x, newHeight, z) == BLK_STATIONARY_WATER) && (existingChunk.getBlockType(x, oldHeight, z) == BLK_FROSTED_ICE)) {
newChunk.setMaterial(x, newHeight, z, existingChunk.getMaterial(x, oldHeight, z));
}
final int blockX = chunkX + x, blockZ = chunkZ + z;
for (Entity entity: existingChunk.getEntities()) {
final double[] pos = entity.getPos();
if ((pos[0] >= blockX) && (pos[0] < blockX + 1) && (pos[2] >= blockZ) && (pos[2] < blockZ + 1)) {
if (pos[1] > oldHeight) {
pos[1] = pos[1] + dy;
if (pos[1] > maxY + 2) {
pos[1] = maxY + 2;
}
entity.setPos(pos);
}
newChunkEntities.add(entity);
}
}
}
}
}
for (Entity entity: existingChunk.getEntities()) {
final double[] pos = entity.getPos();
if ((pos[0] < chunkX) || (pos[0] >= chunkX + 16) || (pos[2] < chunkZ) || (pos[2] >= chunkZ + 16)) {
// The entity has wandered outside of the chunk, we
// don't have the information to determine how much to
// adjust its vertical position; just copy it, since in
// practice most chunks will not have changed height
// anyway, so at least in those cases the result will be
// correct
newChunkEntities.add(entity);
}
}
newChunk.setInhabitedTime(existingChunk.getInhabitedTime());
return newChunk;
}
/**
* Merge one surface layer block.
*
* @param existingChunk The chunk from the existing map.
* @param newChunk The newly generated chunk.
* @param x The X coordinate of the block to merge.
* @param y The Y coordinate of the block to merge.
* @param z The Z coordinate of the block to merge.
* @param preserveCaves Whether empty blocks from the existing chunk should be preserved.
*/
private void mergeSurfaceBlock(final Chunk existingChunk, final Chunk newChunk, final int x, final int y, final int z, final boolean preserveCaves) {
final int existingBlockType = existingChunk.getBlockType(x, y, z);
if (preserveCaves && (BLOCKS[existingBlockType].veryInsubstantial || (! BLOCKS[existingBlockType].natural))) {
newChunk.setBlockType(x, y, z, existingBlockType);
newChunk.setDataValue(x, y, z, existingChunk.getDataValue(x, y, z));
newChunk.setSkyLightLevel(x, y, z, existingChunk.getSkyLightLevel(x, y, z));
newChunk.setBlockLightLevel(x, y, z, existingChunk.getBlockLightLevel(x, y, z));
if (BLOCKS[existingBlockType].tileEntity) {
copyEntityTileData(existingChunk, newChunk, x, y, z, 0);
}
}
}
/**
* Merge one underground block.
*
* @param existingChunk The chunk from the existing map.
* @param newChunk The newly generated chunk.
* @param x The X coordinate of the block to merge.
* @param y The Y coordinate of the block to merge.
* @param z The Z coordinate of the block to merge.
*/
private void mergeUndergroundBlock(final Chunk existingChunk, final Chunk newChunk, final int x, final int y, final int z) {
if (mergeUnderworld) {
final int existingBlockType = existingChunk.getBlockType(x, y, z);
if (UNDERGROUND_MERGE_MATRIX[BLOCKS[newChunk.getBlockType(x, y, z)].category][BLOCKS[existingBlockType].category]) {
newChunk.setBlockType(x, y, z, existingBlockType);
newChunk.setDataValue(x, y, z, existingChunk.getDataValue(x, y, z));
newChunk.setSkyLightLevel(x, y, z, existingChunk.getSkyLightLevel(x, y, z));
newChunk.setBlockLightLevel(x, y, z, existingChunk.getBlockLightLevel(x, y, z));
if (BLOCKS[existingBlockType].tileEntity) {
copyEntityTileData(existingChunk, newChunk, x, y, z, 0);
}
}
} else {
final int existingBlockType = existingChunk.getBlockType(x, y, z);
newChunk.setBlockType(x, y, z, existingBlockType);
newChunk.setDataValue(x, y, z, existingChunk.getDataValue(x, y, z));
newChunk.setSkyLightLevel(x, y, z, existingChunk.getSkyLightLevel(x, y, z));
newChunk.setBlockLightLevel(x, y, z, existingChunk.getBlockLightLevel(x, y, z));
if (BLOCKS[existingBlockType].tileEntity) {
copyEntityTileData(existingChunk, newChunk, x, y, z, 0);
}
}
}
/**
* Merge one above ground block. Supports a changed surface height by
* specifying a delta between the Y coordinate of the block to merge in the
* existing and new chunks.
*
* <p>Coordinates are in Minecraft coordinate system.
*
* @param existingChunk The chunk from the existing map.
* @param newChunk The newly generated chunk.
* @param x The X coordinate of the block to merge.
* @param y The Y coordinate of the block to merge, in the new chunk.
* @param z The Z coordinate of the block to merge.
* @param dy The difference between the Y coordinate in the new chunk and
* the Y coordinate of the corresponding block in the existing chunk.
* @param frost Whether the {@link Frost} layer was applied at the specified
* x,z coordinates in the new map.
*/
private void mergeAboveGroundBlock(final Chunk existingChunk, final Chunk newChunk, final int x, final int y, final int z, final int dy, final boolean frost) {
final int existingBlockType = existingChunk.getBlockType(x, y - dy, z);
final int newBlockType = newChunk.getBlockType(x, y, z);
if (((existingBlockType == BLK_AIR) // replace *all* fluids (and ice) from the existing map with fluids (or lack thereof) from the new map
|| (existingBlockType == BLK_ICE)
|| (existingBlockType == BLK_WATER)
|| (existingBlockType == BLK_STATIONARY_WATER)
|| (existingBlockType == BLK_LAVA)
|| (existingBlockType == BLK_STATIONARY_LAVA))
|| ((BLOCKS[existingBlockType].insubstantial // the existing block is insubstantial and the new block is not
&& (newBlockType != BLK_AIR)
&& (! BLOCKS[newBlockType].insubstantial))
&& (! (frost // the existing block is not snow or the Frost layer has not been applied to the current column or the new block is solid
&& (existingBlockType == BLK_SNOW)
&& BLOCKS[newBlockType].insubstantial)))
// the Frost layer has not been applied and the existing block is snow
|| ((! frost)
&& (existingBlockType == BLK_SNOW))
// the existing block is insubstantial and the new block is a fluid which would burn it or wash it away
|| (((newBlockType == BLK_WATER) || (newBlockType == BLK_STATIONARY_WATER) || (newBlockType == BLK_LAVA) || (newBlockType == BLK_STATIONARY_LAVA))
&& BLOCKS[existingBlockType].insubstantial)) {
// Do nothing
} else {
newChunk.setBlockType(x, y, z, existingBlockType);
if ((existingBlockType == BLK_SNOW) && (newBlockType == BLK_SNOW)) {
// If both the existing and new blocks are snow, use the highest snow level of the two, to leave smooth snow in the existing map intact
newChunk.setDataValue(x, y, z, Math.max(existingChunk.getDataValue(x, y - dy, z), newChunk.getDataValue(x, y, z)));
} else {
newChunk.setDataValue(x, y, z, existingChunk.getDataValue(x, y - dy, z));
}
newChunk.setSkyLightLevel(x, y, z, existingChunk.getSkyLightLevel( x, y - dy, z));
newChunk.setBlockLightLevel(x, y, z, existingChunk.getBlockLightLevel(x, y - dy, z));
if (BLOCKS[existingBlockType].tileEntity) {
copyEntityTileData(existingChunk, newChunk, x, y, z, dy);
}
}
}
// Coordinates are in Minecraft coordinate system
private void copyEntityTileData(Chunk fromChunk, Chunk toChunk, int x, int y, int z, int dy) {
int existingBlockDX = fromChunk.getxPos() << 4, existingBlockDZ = fromChunk.getzPos() << 4;
List<TileEntity> fromEntities = fromChunk.getTileEntities();
for (TileEntity entity: fromEntities) {
if ((entity.getY() == (y - dy)) && ((entity.getX() - existingBlockDX) == x) && ((entity.getZ() - existingBlockDZ) == z)) {
entity.setY(y);
toChunk.getTileEntities().add(entity);
return;
}
}
}
private final File levelDatFile;
private final ThreadLocal<byte[]> histogramRef = ThreadLocal.withInitial(() -> new byte[65536]);
private boolean replaceChunks, mergeOverworld, mergeUnderworld, clearTrees,
clearResources, fillCaves, clearVegetation,
clearManMadeAboveGround, clearManMadeBelowGround;
private String warnings;
private int surfaceMergeDepth = 1;
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(WorldMerger.class);
private static final Object TIMING_FILE_LOCK = new Object();
private static final String EOL = System.getProperty("line.separator");
private static final BitSet SOLID_BLOCKS = new BitSet();
// true means copy existing block Existing map: Air: Fluid: Insub: Manmd: Resrc: Solid:
private static final boolean[][] UNDERGROUND_MERGE_MATRIX = {{false, false, true , true , false, false}, // Air in new map
{false, false, false, true , false, false}, // Fluids in new map
{false, false, false, true , false, false}, // Insubstantial in new map
{false, false, false, false, false, false}, // Man-made in new map
{true, true, true, true, false, false}, // Resource in new map
{true, true, true, true, true , false}}; // Natural solid in new map
static {
for (Block block: BLOCKS) {
if (block.natural
&& (! block.resource)
&& block.opaque) {
SOLID_BLOCKS.set(block.id);
}
}
}
}