diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/BackupHandler.java b/src/main/java/morlok8k/MinecraftLandGenerator/BackupHandler.java new file mode 100644 index 0000000..dd4fdfa --- /dev/null +++ b/src/main/java/morlok8k/MinecraftLandGenerator/BackupHandler.java @@ -0,0 +1,40 @@ +package morlok8k.MinecraftLandGenerator; + +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class BackupHandler { + + private static Log log = LogFactory.getLog(BackupHandler.class); + + public final Path file, backup; + protected volatile boolean hasBackup; + + public BackupHandler(Path file) throws FileAlreadyExistsException { + this.file = file; + this.backup = file.resolveSibling(file.getFileName().toString() + ".bak"); + if (Files.exists(backup)) throw new FileAlreadyExistsException(backup.toString()); + } + + public synchronized void backup() throws IOException { + if (!hasBackup && !Files.exists(backup)) { + log.debug("Creating backup " + file + " --> " + backup); + if (Files.exists(file)) Files.copy(file, backup); + hasBackup = true; + } + } + + public synchronized void restore() throws IOException { + log.debug("Restoring backup " + file + " <-- " + backup); + if (Files.exists(backup)) + Files.move(backup, file, StandardCopyOption.REPLACE_EXISTING); + else Files.deleteIfExists(file); + hasBackup = false; + } +} diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/CommandLineMain.java b/src/main/java/morlok8k/MinecraftLandGenerator/CommandLineMain.java index b7c9e1e..25f9cdf 100644 --- a/src/main/java/morlok8k/MinecraftLandGenerator/CommandLineMain.java +++ b/src/main/java/morlok8k/MinecraftLandGenerator/CommandLineMain.java @@ -1,11 +1,12 @@ package morlok8k.MinecraftLandGenerator; import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -14,19 +15,22 @@ import org.apache.logging.log4j.core.config.Configurator; import org.joml.Vector2i; import morlok8k.MinecraftLandGenerator.CommandLineMain.AutoSpawnpoints; +import morlok8k.MinecraftLandGenerator.CommandLineMain.ForceloadChunks; import morlok8k.MinecraftLandGenerator.CommandLineMain.ManualSpawnpoints; +import morlok8k.MinecraftLandGenerator.World.Dimension; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Help.Visibility; import picocli.CommandLine.HelpCommand; import picocli.CommandLine.ITypeConverter; +import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import picocli.CommandLine.ParentCommand; import picocli.CommandLine.RunAll; -@Command(name = "MinecraftLandGenerator", - subcommands = { HelpCommand.class, ManualSpawnpoints.class, AutoSpawnpoints.class }) +@Command(name = "MinecraftLandGenerator", subcommands = { HelpCommand.class, + ManualSpawnpoints.class, AutoSpawnpoints.class, ForceloadChunks.class }) public class CommandLineMain implements Runnable { private static Log log = LogFactory.getLog(CommandLineMain.class); @@ -50,104 +54,162 @@ public class CommandLineMain implements Runnable { description = "Java command to launch the server. Defaults to `java -jar`.") private String[] javaOpts; - @Command(name = "auto-spawnpoints") - public static class AutoSpawnpoints implements Runnable { + @Override + public void run() { + if (verbose) { + Configurator.setRootLevel(Level.DEBUG); + } + } + protected static class RectangleMixin { + @Parameters(index = "0", description = "X-coordinate") + int x; + @Parameters(index = "1", description = "Z-coordinate") + int z; + @Parameters(index = "2", description = "Width") + int w; + @Parameters(index = "3", description = "Height") + int h; + } + + protected static abstract class CommandLineHelper implements Runnable { @ParentCommand - private CommandLineMain parent; + protected CommandLineMain parent; + + protected Server server; + protected World world; + + @Override + public final void run() { + try { + server = new Server(parent.serverFile, parent.debugServer, parent.javaOpts); + } catch (FileAlreadyExistsException e1) { + log.fatal( + "Server backup file already exists. Please delete or restore it and then start again", + e1); + return; + } catch (NoSuchFileException e1) { + log.fatal( + "Server file does not exist. Please download the minecraft server and provide the path to it", + e1); + return; + } + try { + world = server.initWorld(parent.worldPath); + } catch (IOException | InterruptedException e) { + log.fatal("Could not initialize world", e); + return; + } + + runGenerate(); + + log.info("Cleaning up temporary files"); + try { + world.resetChanges(); + server.resetChanges(); + } catch (IOException e) { + log.warn( + "Could not delete backup files (server.properties.bak and level.dat.bak). Please delete them manually", + e); + } + log.info("Done."); + } + + protected abstract void runGenerate(); + + } + + @Command(name = "auto-spawnpoints") + public static class AutoSpawnpoints extends CommandLineHelper { + + @Mixin + private RectangleMixin bounds; @Option(names = "-i", description = "override the iteration spawn offset increment", defaultValue = "25", showDefaultValue = CommandLine.Help.Visibility.ALWAYS, hidden = true) private int increment = 25; - @Parameters(index = "0", description = "X-coordinate") - private int x; - @Parameters(index = "1", description = "Z-coordinate") - private int z; - @Parameters(index = "2", description = "Width") - private int w; - @Parameters(index = "3", description = "Height") - private int h; - - public AutoSpawnpoints() { - } - @Override - public void run() { - Server server = new Server(parent.debugServer, parent.javaOpts, parent.serverFile); - World world; - try { - world = server.initWorld(parent.worldPath); - } catch (IOException | InterruptedException e) { - log.error("Could not initialize world", e); - return; - } - + public void runGenerate() { log.info("Generating world"); - server.runMinecraft(world, generateSpawnpoints(x, z, w, h, increment)); - log.info("Cleaning up temporary files"); - try { - world.resetSpawn(); - server.restoreWorld(); - } catch (IOException e) { - log.warn( - "Could not delete backup files (server.properties.bak and level.dat.bak). Please delete them manually", - e); + List spawnpoints = + World.generateSpawnpoints(bounds.x, bounds.z, bounds.w, bounds.h, increment); + for (int i = 0; i < spawnpoints.size(); i++) { + Vector2i spawn = spawnpoints.get(i); + try { + log.info("Processing " + i + "/" + spawnpoints.size() + ", spawn point " + + spawn); + world.setSpawn(spawn); + server.runMinecraft(); + } catch (IOException | InterruptedException e) { + log.warn("Could not process spawn point " + spawn + + " this part of the world won't be generated", e); + } } - log.info("Done."); } } @Command(name = "manual-spawnpoints") - public static class ManualSpawnpoints implements Runnable { - - @ParentCommand - private CommandLineMain parent; + public static class ManualSpawnpoints extends CommandLineHelper { @Parameters(index = "0..*") - private Vector2i[] spawnPoints; -// @Option(names = { "-s", "--customspawn" }, description = "Customized SpawnPoints") -// private String[] customSpawnPoints; - - public ManualSpawnpoints() { - } + private Vector2i[] spawnpoints; @Override - public void run() { - Server server = new Server(parent.debugServer, parent.javaOpts, parent.serverFile); - World world; - try { - world = server.initWorld(parent.worldPath); - } catch (IOException | InterruptedException e) { - log.error("Could not initialize world", e); - return; - } - List spawnpoints = new ArrayList<>(); + public void runGenerate() { log.info("Generating world"); - server.runMinecraft(world, spawnpoints); - log.info("Cleaning up temporary files"); - try { - world.resetSpawn(); - server.restoreWorld(); - } catch (IOException e) { - log.warn( - "Could not delete backup files (server.properties.bak and level.dat.bak). Please delete them manually", - e); + log.debug("All spawn points: " + Arrays.toString(spawnpoints)); + for (int i = 0; i < spawnpoints.length; i++) { + Vector2i spawn = spawnpoints[i]; + try { + log.info("Processing " + i + "/" + spawnpoints.length + ", spawn point " + + spawn); + world.setSpawn(spawn); + server.runMinecraft(); + } catch (IOException | InterruptedException e) { + log.warn("Could not process spawn point " + spawn + + " this part of the world won't be generated", e); + } } - log.info("Done."); } } - public CommandLineMain() { + @Command(name = "forceload-chunks") + public static class ForceloadChunks extends CommandLineHelper { + @Mixin + private RectangleMixin bounds; - } + @Option(names = "--dimension") + private Dimension dimension; - @Override - public void run() { - if (verbose) { - Configurator.setRootLevel(Level.DEBUG); + @Option(names = "--max-loaded", defaultValue = "16384") + private int maxLoaded; + + @Override + protected void runGenerate() { + ArrayList loadedChunks = new ArrayList<>(); + 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)); + log.info("Generating world"); + if (loadedChunks.size() < 5000) + log.debug("Chunks to generate: " + loadedChunks); + else log.debug(loadedChunks.size() + " chunks to generate"); + int stepCount = (int) Math.ceil((double) loadedChunks.size() / maxLoaded); + 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() + + " chunks"); + try { + world.setLoadedChunks(batch, dimension); + server.runMinecraft(); + } catch (IOException | InterruptedException e) { + log.error("Could not force-load chunks", e); + } + } } } @@ -168,36 +230,4 @@ public class CommandLineMain implements Runnable { }); cli.parseWithHandler(new RunAll(), args); } - - /** - * @param increment - * Maximum number of chunks between two spawn points, horizontally or vertically - * @param margin - * The radius to each side that will be generated by the server (Not the diameter!) - */ - public static List generateSpawnpoints(int startX, int startZ, int width, int height, - int increment) { - int margin = increment / 2; - if (width < margin || height < margin) - throw new IllegalArgumentException("Width and height must both be at least " + increment - + ", but are " + width + " and " + height); - List xPoints = - generateLinearSpawnpoints(startX + margin, width - increment, increment); - log.debug("X grid: " + xPoints); - List zPoints = - generateLinearSpawnpoints(startZ + margin, height - increment, increment); - log.debug("Z grid: " + zPoints); - List spawnPoints = new ArrayList<>(xPoints.size() * zPoints.size()); - for (int x : xPoints) - for (int z : zPoints) - spawnPoints.add(new Vector2i(x, z)); - return spawnPoints; - } - - private static List generateLinearSpawnpoints(int start, int length, int maxStep) { - int stepCount = (int) Math.ceil((double) length / maxStep); - double realStep = length / stepCount; - return IntStream.rangeClosed(0, stepCount).mapToObj(i -> start + (int) (realStep * i)) - .collect(Collectors.toList()); - } } diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/Server.java b/src/main/java/morlok8k/MinecraftLandGenerator/Server.java index 3f2e152..f485343 100644 --- a/src/main/java/morlok8k/MinecraftLandGenerator/Server.java +++ b/src/main/java/morlok8k/MinecraftLandGenerator/Server.java @@ -11,11 +11,11 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; @@ -24,7 +24,6 @@ import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.joml.Vector2i; /** * @@ -37,18 +36,21 @@ public class Server { protected final ProcessBuilder builder; protected final Path workDir; protected final boolean debugServer; + protected BackupHandler serverProperties; - public Server(boolean debugServer, String[] javaOpts, Path serverFile) { + public Server(Path serverFile, boolean debugServer, String[] javaOpts) + throws FileAlreadyExistsException, NoSuchFileException { this.debugServer = debugServer; - if (!Files.exists(serverFile)) throw new IllegalArgumentException(serverFile.toString() - + " must be an existing file pointing to the minecraft server"); + if (!Files.exists(serverFile)) throw new NoSuchFileException(serverFile.toString()); + workDir = serverFile.getParent(); + serverProperties = new BackupHandler(workDir.resolve("server.properties")); + List opts = new ArrayList<>( Arrays.asList(javaOpts != null ? javaOpts : new String[] { "java", "-jar" })); opts.add(serverFile.toString()); opts.add("nogui"); builder = new ProcessBuilder(opts); builder.redirectErrorStream(true); - workDir = serverFile.getParent(); builder.directory(workDir.toFile()); } @@ -74,8 +76,8 @@ public class Server { if (!Files.exists(propsFile)) { Files.write(propsFile, "level-name=".concat(propsFile.toString()).getBytes()); } else { - /* Make a backup first*/ - Files.copy(propsFile, propsFile.resolveSibling("server.properties.bak")); + serverProperties.backup(); + Properties props = new Properties(); props.load(Files.newInputStream(propsFile)); props.put("level-name", worldPath.toString()); @@ -95,26 +97,8 @@ public class Server { return p; } - public void restoreWorld() throws IOException { - Path propsFile = workDir.resolve("server.properties"); - if (Files.exists(propsFile.resolveSibling("server.properties.bak"))) - Files.move(propsFile.resolveSibling("server.properties.bak"), propsFile, - StandardCopyOption.REPLACE_EXISTING); - } - - public void runMinecraft(World world, List spawnpoints) { - log.debug("All spawn points: " + spawnpoints); - for (int i = 0; i < spawnpoints.size(); i++) { - Vector2i spawn = spawnpoints.get(i); - try { - log.info("Processing " + i + "/" + spawnpoints.size() + ", spawn point " + spawn); - world.setSpawn(spawn); - runMinecraft(); - } catch (IOException | InterruptedException e) { - log.warn("Could not process spawn point " + spawn - + " this part of the world don't be generated", e); - } - } + public void resetChanges() throws IOException { + serverProperties.restore(); } /** @@ -135,12 +119,18 @@ public class Server { final BufferedReader pOut = new BufferedReader(new InputStreamReader(process.getInputStream())); for (String line = pOut.readLine(); line != null; line = pOut.readLine()) { + if (Thread.interrupted()) { + log.warn("Got interrupted by other process, stopping"); + process.destroy(); + break; + } line = line.trim(); if (debugServer) System.out.println(line); if (line.contains("Done")) { PrintStream out = new PrintStream(process.getOutputStream()); + out.println("forceload query"); log.info("Stopping server..."); out.println("save-all"); out.flush(); diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/World.java b/src/main/java/morlok8k/MinecraftLandGenerator/World.java index 2217f87..b61a031 100644 --- a/src/main/java/morlok8k/MinecraftLandGenerator/World.java +++ b/src/main/java/morlok8k/MinecraftLandGenerator/World.java @@ -1,13 +1,18 @@ package morlok8k.MinecraftLandGenerator; import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -17,29 +22,64 @@ import org.joml.Vector3i; import com.flowpowered.nbt.CompoundMap; import com.flowpowered.nbt.CompoundTag; import com.flowpowered.nbt.IntTag; +import com.flowpowered.nbt.LongArrayTag; import com.flowpowered.nbt.Tag; import com.flowpowered.nbt.stream.NBTInputStream; import com.flowpowered.nbt.stream.NBTOutputStream; public class World { + + public static enum Dimension { + OVERWORLD("."), NETHER("DIM-1"), END("DIM1"); + public final Path path; + + Dimension(String path) { + this.path = Paths.get(path); + } + } + private static Log log = LogFactory.getLog(World.class); public final Path world; + protected BackupHandler level, chunksOverworld, chunksNether, chunksEnd; + protected BackupHandler[] chunks; - public World(Path world) throws IOException { + public World(Path world) throws FileAlreadyExistsException { this.world = Objects.requireNonNull(world); - Files.copy(world.resolve("level.dat"), world.resolve("level.dat.bak")); + level = new BackupHandler(world.resolve("level.dat")); + chunksOverworld = new BackupHandler( + world.resolve(Dimension.OVERWORLD.path).resolve("data/chunks.dat")); + chunksNether = + new BackupHandler(world.resolve(Dimension.NETHER.path).resolve("data/chunks.dat")); + chunksEnd = new BackupHandler(world.resolve(Dimension.END.path).resolve("data/chunks.dat")); + chunks = new BackupHandler[] { chunksOverworld, chunksNether, chunksEnd }; } - public void resetSpawn() throws IOException { - if (Files.exists(world.resolve("level.dat.bak"))) Files.move(world.resolve("level.dat.bak"), - world.resolve("level.dat"), StandardCopyOption.REPLACE_EXISTING); + public void resetChanges() throws IOException { + level.restore(); + chunksOverworld.restore(); + chunksNether.restore(); + chunksEnd.restore(); } public void setSpawn(Vector2i chunkSpawn) throws IOException { setSpawn(new Vector3i(chunkSpawn.x << 4 | 7, 64, chunkSpawn.y << 4 | 8)); } + public void setLoadedChunks(List chunks, Dimension dimension) throws IOException { + BackupHandler handler = this.chunks[dimension.ordinal()]; + handler.backup(); + try (NBTOutputStream out = new NBTOutputStream(Files.newOutputStream(handler.file))) { + CompoundMap dataMap = new CompoundMap(); + dataMap.put(new LongArrayTag("Forced", + chunks.stream().mapToLong(v -> ((long) v.y << 32) | v.x).toArray())); + CompoundMap rootMap = new CompoundMap(); + rootMap.put(new CompoundTag("data", dataMap)); + out.writeTag(new CompoundTag("", rootMap)); + out.flush(); + } + } + /** * Changes the spawn point in the given Alpha/Beta level to the given coordinates.
* Note that, in Minecraft levels, the Y coordinate is height.
@@ -54,6 +94,7 @@ public class World { * @author Corrodias */ public void setSpawn(final Vector3i xyz) throws IOException { + level.backup(); log.debug("Setting spawn to " + xyz); // TODO clean this up even more try (NBTInputStream input = @@ -87,4 +128,36 @@ public class World { throw new IOException("Invalid level format."); } } + + /** + * @param increment + * Maximum number of chunks between two spawn points, horizontally or vertically + * @param margin + * The radius to each side that will be generated by the server (Not the diameter!) + */ + public static List generateSpawnpoints(int startX, int startZ, int width, int height, + int increment) { + int margin = increment / 2; + if (width < margin || height < margin) + throw new IllegalArgumentException("Width and height must both be at least " + increment + + ", but are " + width + " and " + height); + List xPoints = + generateLinearSpawnpoints(startX + margin, width - increment, increment); + log.debug("X grid: " + xPoints); + List zPoints = + generateLinearSpawnpoints(startZ + margin, height - increment, increment); + log.debug("Z grid: " + zPoints); + List spawnPoints = new ArrayList<>(xPoints.size() * zPoints.size()); + for (int x : xPoints) + for (int z : zPoints) + spawnPoints.add(new Vector2i(x, z)); + return spawnPoints; + } + + private static List generateLinearSpawnpoints(int start, int length, int maxStep) { + int stepCount = (int) Math.ceil((double) length / maxStep); + double realStep = length / stepCount; + return IntStream.rangeClosed(0, stepCount).mapToObj(i -> start + (int) (realStep * i)) + .collect(Collectors.toList()); + } }