From f7cef3fdf8a07cc745950cca0d9e403e857f23bd Mon Sep 17 00:00:00 2001 From: Piegames <14054505+piegamesde@users.noreply.github.com> Date: Thu, 15 Nov 2018 13:38:56 +0100 Subject: [PATCH] Documentation JavaDoc and CLI help strings. --- .../MinecraftLandGenerator/BackupHandler.java | 24 ++++++ ...eMain.java => MinecraftLandGenerator.java} | 79 ++++++++++++------- .../MinecraftLandGenerator/Server.java | 60 +++++++++++--- .../MinecraftLandGenerator/World.java | 47 +++++++++-- 4 files changed, 166 insertions(+), 44 deletions(-) rename src/main/java/morlok8k/MinecraftLandGenerator/{CommandLineMain.java => MinecraftLandGenerator.java} (62%) diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/BackupHandler.java b/src/main/java/morlok8k/MinecraftLandGenerator/BackupHandler.java index dd4fdfa..116ef26 100644 --- a/src/main/java/morlok8k/MinecraftLandGenerator/BackupHandler.java +++ b/src/main/java/morlok8k/MinecraftLandGenerator/BackupHandler.java @@ -9,6 +9,13 @@ import java.nio.file.StandardCopyOption; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +/** + * Helper class to back up files that shouldn't be modified and to restore them to their previous state. Backup files + * have the same name as the original file, but extended by a ".bak". Existing backup files are never ever overwritten due + * to potential data loss. + * + * @author piegames + */ public class BackupHandler { private static Log log = LogFactory.getLog(BackupHandler.class); @@ -16,12 +23,24 @@ public class BackupHandler { public final Path file, backup; protected volatile boolean hasBackup; + /** + * @param file + * The file to create a backup from + * @throws FileAlreadyExistsException + * if the backup file already exists + * @author piegames + */ 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()); } + /** + * Creates a backup of the file if it hasn't already been done yet and the original file actually exists. + * + * @author piegames + */ public synchronized void backup() throws IOException { if (!hasBackup && !Files.exists(backup)) { log.debug("Creating backup " + file + " --> " + backup); @@ -30,6 +49,11 @@ public class BackupHandler { } } + /** + * Restore the backup file to the original and delete it. + * + * @author piegames + */ public synchronized void restore() throws IOException { log.debug("Restoring backup " + file + " <-- " + backup); if (Files.exists(backup)) diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/CommandLineMain.java b/src/main/java/morlok8k/MinecraftLandGenerator/MinecraftLandGenerator.java similarity index 62% rename from src/main/java/morlok8k/MinecraftLandGenerator/CommandLineMain.java rename to src/main/java/morlok8k/MinecraftLandGenerator/MinecraftLandGenerator.java index 25f9cdf..fb40c1c 100644 --- a/src/main/java/morlok8k/MinecraftLandGenerator/CommandLineMain.java +++ b/src/main/java/morlok8k/MinecraftLandGenerator/MinecraftLandGenerator.java @@ -14,9 +14,9 @@ import org.apache.logging.log4j.Level; 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.MinecraftLandGenerator.AutoSpawnpoints; +import morlok8k.MinecraftLandGenerator.MinecraftLandGenerator.ForceloadChunks; +import morlok8k.MinecraftLandGenerator.MinecraftLandGenerator.ManualSpawnpoints; import morlok8k.MinecraftLandGenerator.World.Dimension; import picocli.CommandLine; import picocli.CommandLine.Command; @@ -29,11 +29,14 @@ import picocli.CommandLine.Parameters; import picocli.CommandLine.ParentCommand; import picocli.CommandLine.RunAll; -@Command(name = "MinecraftLandGenerator", subcommands = { HelpCommand.class, - ManualSpawnpoints.class, AutoSpawnpoints.class, ForceloadChunks.class }) -public class CommandLineMain implements Runnable { +/** @author piegames, sommerlilie */ +@Command(name = "MinecraftLandGenerator", + subcommands = { HelpCommand.class, ManualSpawnpoints.class, AutoSpawnpoints.class, + ForceloadChunks.class }, + description = "Generate Minecraft worlds by tricking the server to do it for you. You need to provide a server jar file for this to work.") +public class MinecraftLandGenerator implements Runnable { - private static Log log = LogFactory.getLog(CommandLineMain.class); + private static Log log = LogFactory.getLog(MinecraftLandGenerator.class); @Option(names = { "-v", "--verbose" }, description = "Be verbose.") private boolean verbose = false; @@ -43,7 +46,7 @@ public class CommandLineMain implements Runnable { private boolean debugServer = false; @Option(names = { "-s", "--serverFile" }, description = "Path to the server's jar file.", - defaultValue = "server.jar", showDefaultValue = Visibility.ALWAYS) + required = true, defaultValue = "server.jar", showDefaultValue = Visibility.ALWAYS) private Path serverFile; @Option(names = { "-w", "--worldPath" }, @@ -51,7 +54,7 @@ public class CommandLineMain implements Runnable { private Path worldPath; @Option(names = { "--java-cmd" }, - description = "Java command to launch the server. Defaults to `java -jar`.") + description = "Java command to launch the server. Defaults to [java, -jar]. Use this to specify JVM options (like more RAM etc.) or to enforce the usage of a specific java version.") private String[] javaOpts; @Override @@ -61,20 +64,30 @@ public class CommandLineMain implements Runnable { } } + /** + * Mixin that provides positional arguments describing a rectangle. + * + * @see Mixin + * @author piegames + */ protected static class RectangleMixin { - @Parameters(index = "0", description = "X-coordinate") + @Parameters(index = "0", paramLabel = "START_X", + description = "X-coordinate of the first chunk to be generated (in chunk coordinates)") int x; - @Parameters(index = "1", description = "Z-coordinate") + @Parameters(index = "1", paramLabel = "START_Z", + description = "Z-coordinate of the first chunk to be generated (in chunk coordinates)") int z; - @Parameters(index = "2", description = "Width") + @Parameters(index = "2", paramLabel = "WIDTH", + description = "Amount of chunks to generate from the starting chunk to the east") int w; - @Parameters(index = "3", description = "Height") + @Parameters(index = "3", paramLabel = "HEIGHT", + description = "Amount of chunks to generate from the starting chunk to the south") int h; } protected static abstract class CommandLineHelper implements Runnable { @ParentCommand - protected CommandLineMain parent; + protected MinecraftLandGenerator parent; protected Server server; protected World world; @@ -82,7 +95,7 @@ public class CommandLineMain implements Runnable { @Override public final void run() { try { - server = new Server(parent.serverFile, parent.debugServer, parent.javaOpts); + server = new Server(parent.serverFile, parent.javaOpts); } catch (FileAlreadyExistsException e1) { log.fatal( "Server backup file already exists. Please delete or restore it and then start again", @@ -95,7 +108,7 @@ public class CommandLineMain implements Runnable { return; } try { - world = server.initWorld(parent.worldPath); + world = server.initWorld(parent.worldPath, parent.debugServer); } catch (IOException | InterruptedException e) { log.fatal("Could not initialize world", e); return; @@ -119,15 +132,17 @@ public class CommandLineMain implements Runnable { } - @Command(name = "auto-spawnpoints") + @Command(name = "auto-spawnpoints", + description = "Automatically generate spawnpoints that exactly fill up a rectangular area and generate the world around them.") public static class AutoSpawnpoints extends CommandLineHelper { @Mixin private RectangleMixin bounds; - @Option(names = "-i", description = "override the iteration spawn offset increment", + @Option(names = "-i", + description = "Maximum number of chunks between two spawn points, horizontally or vertically. Since Minecraft by default generates 25 chunks around each spawn point, this should be 25 for most servers. This value should be unpair.", defaultValue = "25", showDefaultValue = CommandLine.Help.Visibility.ALWAYS, - hidden = true) + hidden = false) private int increment = 25; @Override @@ -141,7 +156,7 @@ public class CommandLineMain implements Runnable { log.info("Processing " + i + "/" + spawnpoints.size() + ", spawn point " + spawn); world.setSpawn(spawn); - server.runMinecraft(); + server.runMinecraft(parent.debugServer); } catch (IOException | InterruptedException e) { log.warn("Could not process spawn point " + spawn + " this part of the world won't be generated", e); @@ -151,10 +166,12 @@ public class CommandLineMain implements Runnable { } - @Command(name = "manual-spawnpoints") + @Command(name = "manual-spawnpoints", + description = "Provide a list of spawnpoints and a Minecraft server will be started on each one to generate the chunks around it.") public static class ManualSpawnpoints extends CommandLineHelper { - @Parameters(index = "0..*") + @Parameters(index = "0..*", paramLabel = "SPAWNPOINTS", + description = "Provide a lsit of spawnpoints. All chunks in a 25x25 area aroud each spawn point will be generated. Usage: x1,y1 x2,y2 x3,y3 ...") private Vector2i[] spawnpoints; @Override @@ -167,7 +184,7 @@ public class CommandLineMain implements Runnable { log.info("Processing " + i + "/" + spawnpoints.length + ", spawn point " + spawn); world.setSpawn(spawn); - server.runMinecraft(); + server.runMinecraft(parent.debugServer); } catch (IOException | InterruptedException e) { log.warn("Could not process spawn point " + spawn + " this part of the world won't be generated", e); @@ -176,15 +193,21 @@ public class CommandLineMain implements Runnable { } } - @Command(name = "forceload-chunks") + @Command(name = "forceload-chunks", + description = "Force the chunks in a given rectangular area to be permanently loaded. If the do not exist, the server will conveniently generate them for us.") public static class ForceloadChunks extends CommandLineHelper { @Mixin private RectangleMixin bounds; - @Option(names = "--dimension") + @Option(names = "--dimension", description = "The dimension to generate the chunks in.", + defaultValue = "OVERWORLD", showDefaultValue = Visibility.ALWAYS) private Dimension dimension; - @Option(names = "--max-loaded", defaultValue = "16384") + @Option(names = "--max-loaded", defaultValue = "16384", + showDefaultValue = Visibility.ALWAYS, + description = "The maximum amount of chunks to force-load at once. Increasing this number will result in more RAM consumption and thus " + + "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; @Override @@ -205,7 +228,7 @@ public class CommandLineMain implements Runnable { + " chunks"); try { world.setLoadedChunks(batch, dimension); - server.runMinecraft(); + server.runMinecraft(parent.debugServer); } catch (IOException | InterruptedException e) { log.error("Could not force-load chunks", e); } @@ -217,7 +240,7 @@ public class CommandLineMain implements Runnable { /* Without this, JOML will print vectors out in scientific notation which isn't the most human readable thing in the world */ System.setProperty("joml.format", "false"); - CommandLine cli = new CommandLine(new CommandLineMain()); + CommandLine cli = new CommandLine(new MinecraftLandGenerator()); cli.registerConverter(Vector2i.class, new ITypeConverter() { @Override diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/Server.java b/src/main/java/morlok8k/MinecraftLandGenerator/Server.java index f485343..362c10a 100644 --- a/src/main/java/morlok8k/MinecraftLandGenerator/Server.java +++ b/src/main/java/morlok8k/MinecraftLandGenerator/Server.java @@ -26,8 +26,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** + * Start and manipulate a Minecraft server. * - * @author morlok8k + * @author morlok8k, piegames */ public class Server { @@ -35,12 +36,21 @@ public class Server { protected final ProcessBuilder builder; protected final Path workDir; - protected final boolean debugServer; protected BackupHandler serverProperties; - public Server(Path serverFile, boolean debugServer, String[] javaOpts) + /** + * @param serverFile + * the path to the server.jar. The folder it is in will be used as server work directory. + * @param javaOpts + * the command line to start the server. To actually launch it, the path to the server file and "nogui" will be appended. + * The default value is ["java", "-jar"]. Use this to specify JVM options (like more RAM etc.) or to enforce the usage of a specific java version. + * @throws FileAlreadyExistsException + * if there is an old backup of the server.properties + * @throws NoSuchFileException + * if the server jar file does not exist + */ + public Server(Path serverFile, String[] javaOpts) throws FileAlreadyExistsException, NoSuchFileException { - this.debugServer = debugServer; if (!Files.exists(serverFile)) throw new NoSuchFileException(serverFile.toString()); workDir = serverFile.getParent(); serverProperties = new BackupHandler(workDir.resolve("server.properties")); @@ -54,14 +64,32 @@ public class Server { builder.directory(workDir.toFile()); } - public World initWorld(Path worldPath) throws IOException, InterruptedException { + /** + * Initialize the world. This will do the following: + * + * + * @param worldPath + * the path to the world to generate. May be {@code null} + * @see #runMinecraft(boolean) + * @throws NoSuchFileException + * if we fail to get a valid world folder using multiple methods + * @return A {@link World} object representing the world that will be loaded from the server by {@link #runMinecraft(boolean)} + * @author piegames + */ + public World initWorld(Path worldPath, boolean debugServer) + throws IOException, InterruptedException { if (worldPath != null) setWorld(worldPath); else worldPath = getWorld(); if (worldPath == null || !Files.exists(worldPath)) { log.warn( "No world was specified or the world at the given path does not exist. Starting the server once to create one..."); - runMinecraft(); + runMinecraft(debugServer); worldPath = getWorld(); if (!worldPath.isAbsolute()) worldPath = workDir.resolve(worldPath); } @@ -71,6 +99,11 @@ public class Server { return new World(worldPath); } + /** + * Set the world's path in the server.properties, creating it if it does not exist yet. + * + * @author piegames + */ public void setWorld(Path worldPath) throws IOException { Path propsFile = workDir.resolve("server.properties"); if (!Files.exists(propsFile)) { @@ -85,6 +118,12 @@ public class Server { } } + /** + * Retrieve the world currently specified in the {@code server.properties} file. Returns {@code null} if the file does not exist or if the world is not specified in the file. The returned path + * will be made absolute by resolving it against the server folder. + * + * @author piegames + */ public Path getWorld() throws IOException { Path propsFile = workDir.resolve("server.properties"); Properties props = new Properties(); @@ -97,19 +136,22 @@ public class Server { return p; } + /** Reset all changes made to the server. Currently, this is only the {@code server.properties} */ public void resetChanges() throws IOException { serverProperties.restore(); } /** - * Starts the process in the given ProcessBuilder, monitors its output for a "Done" message, and sends it a "stop" message. - * If "verbose" is true, the process's output will also be printed to the console. + * Start the Minecraft server using the current settings, wait until it finished loading the world and stop it. Communication with the server is done via standard IO streams and commands. * + * @param debugServer + * if set to true, all output from the server will be redirected to {@link System#out} for debugging purposes. * @throws IOException * @throws InterruptedException + * if the thread gets interrupted while waiting for the server process to finish. This should never happen. * @author Corrodias, Morlok8k, piegames */ - public void runMinecraft() throws IOException, InterruptedException { + public void runMinecraft(boolean debugServer) throws IOException, InterruptedException { log.debug("Setting EULA"); Files.write(workDir.resolve("eula.txt"), "eula=true".getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); diff --git a/src/main/java/morlok8k/MinecraftLandGenerator/World.java b/src/main/java/morlok8k/MinecraftLandGenerator/World.java index b61a031..96659d3 100644 --- a/src/main/java/morlok8k/MinecraftLandGenerator/World.java +++ b/src/main/java/morlok8k/MinecraftLandGenerator/World.java @@ -27,8 +27,18 @@ import com.flowpowered.nbt.Tag; import com.flowpowered.nbt.stream.NBTInputStream; import com.flowpowered.nbt.stream.NBTOutputStream; +/** + * Represents a world folder and provide some methods to modify it. All modifications can be reverted using {@link #resetChanges()}. + * + * @author piegames + */ public class World { + /** + * A simple enumeration representing the different Minecraft dimensions + * + * @author piegames + */ public static enum Dimension { OVERWORLD("."), NETHER("DIM-1"), END("DIM1"); public final Path path; @@ -55,6 +65,11 @@ public class World { chunks = new BackupHandler[] { chunksOverworld, chunksNether, chunksEnd }; } + /** + * Reset all changes made to the world. + * + * @see BackupHandler#restore() + */ public void resetChanges() throws IOException { level.restore(); chunksOverworld.restore(); @@ -62,10 +77,15 @@ public class World { chunksEnd.restore(); } - public void setSpawn(Vector2i chunkSpawn) throws IOException { - setSpawn(new Vector3i(chunkSpawn.x << 4 | 7, 64, chunkSpawn.y << 4 | 8)); - } - + /** + * Set the list of force-loaded chunks for a given dimension. + * + * @param chunks + * The chunks to load, using chunk coordinates + * @param the + * dimension to load them in + * @author piegames + */ public void setLoadedChunks(List chunks, Dimension dimension) throws IOException { BackupHandler handler = this.chunks[dimension.ordinal()]; handler.backup(); @@ -80,6 +100,17 @@ public class World { } } + /** + * Set the spawnpoint of the world somewhere in this chunk. + * + * @param chunkSpawn + * the new spawn point of the world in chunk coordinates + * @author piegames + */ + public void setSpawn(Vector2i chunkSpawn) throws IOException { + setSpawn(new Vector3i(chunkSpawn.x << 4 | 7, 64, chunkSpawn.y << 4 | 8)); + } + /** * Changes the spawn point in the given Alpha/Beta level to the given coordinates.
* Note that, in Minecraft levels, the Y coordinate is height.
@@ -130,10 +161,12 @@ public class World { } /** + * Generate a grid of spawn points that fully cover a given rectangular area. The area is specified in chunk coordinates. + * * @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!) + * Maximum number of chunks between two spawn points, horizontally or vertically. Since Minecraft by default generates 25 chunks around each spawn point, this should be 25 for most + * servers. This value should be unpair. + * @author piegames */ public static List generateSpawnpoints(int startX, int startZ, int width, int height, int increment) {