diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/MinecraftLandGenerator.java b/src/main/java/morlok8k/MinecraftLandGenerator/MinecraftLandGenerator.java index fb40c1c..4c82208 100644 --- a/src/main/java/morlok8k/MinecraftLandGenerator/MinecraftLandGenerator.java +++ b/src/main/java/morlok8k/MinecraftLandGenerator/MinecraftLandGenerator.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -209,6 +210,9 @@ public class MinecraftLandGenerator implements Runnable { + "in slower generation. Smaller values will result in starting the server more often, which has some overhead and takes time. " + "Set this as high as possible without totally filling up your RAM.") private int maxLoaded; + @Option(names = { "--lazy", "-l" }, + description = "Scan the world for existing chunks and skip them.") + private boolean lazy; @Override protected void runGenerate() { @@ -216,6 +220,21 @@ public class MinecraftLandGenerator implements Runnable { for (int x = bounds.x; x < bounds.x + bounds.w; x++) for (int z = bounds.z; z < bounds.z + bounds.h; z++) loadedChunks.add(new Vector2i(x, z)); + if (lazy) { + log.info("Searching for existing chunks to be skipped"); + /* + * All chunks to load -> The set of region files containing them -> (The chunks in those region files -> to world coordinates) + * -> to list -> remove from original list + */ + int size = loadedChunks.size(); + loadedChunks.removeAll( + loadedChunks.stream().map(v -> new Vector2i(v.x >> 5, v.y >> 5)).distinct() + .flatMap(v -> world.availableChunks(v, dimension) + .map(w -> new Vector2i((v.x << 5) | w.x, (v.y << 5) | w.y))) + .collect(Collectors.toList())); + log.debug("Removed " + (size - loadedChunks.size()) + + " chunks that are already present"); + } log.info("Generating world"); if (loadedChunks.size() < 5000) log.debug("Chunks to generate: " + loadedChunks); @@ -224,7 +243,7 @@ public class MinecraftLandGenerator implements Runnable { for (int i = 0; i < stepCount; i++) { List batch = loadedChunks.subList(i * maxLoaded, Math.min((i + 1) * maxLoaded, loadedChunks.size() - 1)); - log.info("Generating batch " + i + " / " + stepCount + " with " + batch.size() + log.info("Generating batch " + (i + 1) + " / " + stepCount + " with " + batch.size() + " chunks"); try { world.setLoadedChunks(batch, dimension); diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/World.java b/src/main/java/morlok8k/MinecraftLandGenerator/World.java index 68e36f9..fa3b74c 100644 --- a/src/main/java/morlok8k/MinecraftLandGenerator/World.java +++ b/src/main/java/morlok8k/MinecraftLandGenerator/World.java @@ -1,12 +1,16 @@ package morlok8k.MinecraftLandGenerator; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -14,6 +18,7 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -198,4 +203,47 @@ public class World { return IntStream.rangeClosed(0, (int) stepCount).mapToObj(i -> start + (int) (realStep * i)) .collect(Collectors.toList()); } + + /** + * List all available chunks in a region file. + * + * @param regionX + * the x coordinate of the region file in the world + * @param regionZ + * the z coordinate of the region file in the world + * @param dimension + * the dimension of the region file in the world + * @return a stream of all chunks in the given region file, in coordinates relative to the region file's origin (not in world coordinates) + * @see #availableChunks(Vector2i, Dimension) + */ + public List availableChunks(int regionX, int regionZ, Dimension dimension) { + return availableChunks(new Vector2i(regionX, regionZ), dimension) + .collect(Collectors.toList()); + } + + /** + * List all available chunks in a region file. + * + * @param regionCoords + * the coordinates of the region file + * @param dimension + * the dimension of the region file in the world + * @return a stream of all chunks in the given region file, in coordinates relative to the region file's origin (not in world coordinates) + * @see #availableChunks(int, int, Dimension) + */ + public Stream availableChunks(Vector2i regionCoords, Dimension dimension) { + Path path = world.resolve(dimension.path).resolve("region") + .resolve("r." + regionCoords.x + "." + regionCoords.y + ".mca"); + if (!Files.exists(path)) return Collections. emptyList().stream(); + try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { + ByteBuffer buffer = ByteBuffer.allocate(4096); + channel.read(buffer); + buffer.flip(); + return IntStream.range(0, buffer.capacity()).filter(i -> buffer.get(i) != 0) + .mapToObj(i -> new Vector2i(i & 31, i >> 5)); + } catch (IOException e) { + log.warn("Could not open region file " + path + ", assuming it contains no chunks", e); + return Collections. emptyList().stream(); + } + } }