diff --git a/src/corrodias/minecraft/landgenerator/Main.java b/src/corrodias/minecraft/landgenerator/Main.java new file mode 100644 index 0000000..bb11ff0 --- /dev/null +++ b/src/corrodias/minecraft/landgenerator/Main.java @@ -0,0 +1,535 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package corrodias.minecraft.landgenerator; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jnbt.CompoundTag; +import org.jnbt.IntTag; +import org.jnbt.NBTInputStream; +import org.jnbt.NBTOutputStream; +import org.jnbt.Tag; + +/** + * + * @author Corrodias + */ +public class Main { + + private static final String separator = System.getProperty("file.separator"); + //private static final String classpath = System.getProperty("java.class.path"); + //private static final String javaPath = System.getProperty("java.home") + separator + "bin" + separator + "java"; + private static final String VERSION = "1.2.0"; + private int increment = 300; + private ProcessBuilder minecraft = null; + private String javaLine = null; + private String serverPath = null; + private String worldPath = null; + private int xRange = 0; + private int yRange = 0; + private Integer xOffset = null; + private Integer yOffset = null; + private boolean verbose = false; + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + (new Main()).run(args); + } + + private void run(String[] args) { + System.out.println("Minecraft Land Generator version " + VERSION); + System.out.println("Uses a Minecraft server to generate square land of a specified size."); + System.out.println(""); + + // ===================================================================== + // INSTRUCTIONS + // ===================================================================== + + if (args.length == 0 || args[0].equals("-version") || args[0].equals("-help") || args[0].equals("/?")) { + System.out.println("Usage: java -jar MinecraftLandGenerator.jar x y [serverpath] [switches]"); + System.out.println(""); + System.out.println("Arguments:"); + System.out.println(" x : X range to generate"); + System.out.println(" y : Y range to generate"); + System.out.println(" serverpath : the path to the directory in which the server runs (takes precedence over the config file setting)"); + System.out.println(""); + System.out.println("Switches:"); + System.out.println(" -verbose : causes the application to output the server's messages to the console"); + System.out.println(" -v : same as -verbose"); + System.out.println(" -i# : override the iteration spawn offset increment (default 300) (example: -i100)"); + System.out.println(" -x# : set the X offset to generate land around (example: -x0)"); + System.out.println(" -y# : set the X offset to generate land around (example: -y0)"); + System.out.println(""); + System.out.println("Other options:"); + System.out.println(" java -jar MinecraftLandGenerator.jar -printspawn"); + System.out.println(" java -jar MinecraftLandGenerator.jar -ps"); + System.out.println(" Outputs the current world's spawn point coordinates."); + System.out.println(""); + System.out.println(" java -jar MinecraftLandGenerator.jar -conf"); + System.out.println(" Generates a MinecraftLandGenerator.conf file."); + System.out.println(""); + System.out.println(" java -jar MinecraftLandGenerator.jar -version"); + System.out.println(" java -jar MinecraftLandGenerator.jar -help"); + System.out.println(" java -jar MinecraftLandGenerator.jar /?"); + System.out.println(" Prints this message."); + System.out.println(""); + System.out.println("When launched with the -conf switch, this application creates a MinecraftLandGenerator.conf file that contains configuration options."); + System.out.println("If this file does not exist or does not contain all required properties, the application will not run."); + System.out.println(""); + System.out.println("MinecraftLandGenerator.conf properties:"); + System.out.println(" java : the command line to use to launch the server"); + System.out.println(" serverpath : the path to the directory in which the server runs (can be overridden by the serverpath argument)"); + return; + } + + // ===================================================================== + // STARTUP AND CONFIG + // ===================================================================== + + // the arguments are apparently okay so far. parse the conf file. + if (args[0].equalsIgnoreCase("-conf")) { + try { + File config = new File("MinecraftLandGenerator.conf"); + BufferedWriter out = new BufferedWriter(new FileWriter(config)); + out.write("java=java -Xms1024m -Xmx1024m -jar minecraft_server.jar nogui"); + out.newLine(); + out.write("serverpath=."); + out.newLine(); + out.close(); + System.out.println("MinecraftLandGenerator.conf file created."); + return; + } catch (IOException ex) { + System.err.println("Could not create MinecraftLandGenerator.conf."); + return; + } + } else if (args[0].equalsIgnoreCase("-ps") || args[0].equalsIgnoreCase("-printspawn")) { + // okay, sorry, this is an ugly hack, but it's just a last-minute feature. + printSpawn(); + return; + } else if (args.length == 1) { + System.out.println("For help, use java -jar MinecraftLandGenerator.jar -help"); + return; + } + + try { + File config = new File("MinecraftLandGenerator.conf"); + BufferedReader in = new BufferedReader(new FileReader(config)); + String line; + while ((line = in.readLine()) != null) { + int pos = line.indexOf('='); + if (pos != -1) { + if (line.substring(0, pos).toLowerCase().equals("serverpath")) { + serverPath = line.substring(pos + 1); + } else if (line.substring(0, pos).toLowerCase().equals("java")) { + javaLine = line.substring(pos + 1); + } + } + } + in.close(); + + if (serverPath == null || javaLine == null) { + System.err.println("MinecraftLandGenerator.conf does not contain all requird properties. Please recreate it by running this application with no arguments."); + return; + } + } catch (FileNotFoundException ex) { + System.out.println("Could not find MinecraftLandGenerator.conf. It is recommended that you run the application with the -conf option to create it."); + return; + } catch (IOException ex) { + System.err.println("Could not read MinecraftLandGenerator.conf"); + return; + } + + // ARGUMENTS + try { + xRange = Integer.parseInt(args[0]); + yRange = Integer.parseInt(args[1]); + } catch (NumberFormatException ex) { + System.err.println("Invalid X or Y argument."); + return; + } + + // This is embarrassing. Don't look. + try { + for (int i = 0; i < args.length - 2; i++) { + String nextSwitch = args[i + 2].toLowerCase(); + if (nextSwitch.equals("-verbose") || nextSwitch.equals("-v")) { + verbose = true; + } else if (nextSwitch.startsWith("-i")) { + increment = Integer.parseInt(args[i + 2].substring(2)); + } else if (nextSwitch.startsWith("-x")) { + xOffset = Integer.valueOf(args[i + 2].substring(2)); + } else if (nextSwitch.startsWith("-y")) { + yOffset = Integer.valueOf(args[i + 2].substring(2)); + } else { + serverPath = args[i + 2]; + } + } + } catch (NumberFormatException ex) { + System.err.println("Invalid -i switch value."); + return; + } + + { + // verify that we ended up with a good server path, either from the file or from an argument. + File file = new File(serverPath); + if (!file.exists() || !file.isDirectory()) { + System.err.println("The server directory is invalid: " + serverPath); + return; + } + } + + try { + // read the name of the current world from the server.properties file + BufferedReader props = new BufferedReader(new FileReader(new File(serverPath + separator + "server.properties"))); + String line; + while ((line = props.readLine()) != null) { + int pos = line.indexOf('='); + if (pos != -1) { + if (line.substring(0, pos).toLowerCase().equals("level-name")) { + worldPath = serverPath + separator + line.substring(pos + 1); + } + } + } + + } catch (FileNotFoundException ex) { + System.err.println("Could not open " + serverPath + separator + "server.properties"); + return; + } catch (IOException ex) { + Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + return; + } + + { + File backupLevel = new File(worldPath + separator + "level_backup.dat"); + if (backupLevel.exists()) { + System.err.println("There is a level_backup.dat file left over from a previous attempt that failed. You should go determine whether to keep the current level.dat" + + " or restore the backup."); + return; + } + } + + // ===================================================================== + // PROCESSING + // ===================================================================== + + System.out.println("Processing world \"" + worldPath + "\", in " + increment + " block increments, with: " + javaLine); + System.out.println(""); + + // prepare our two ProcessBuilders + //minecraft = new ProcessBuilder(javaLine, "-Xms1024m", "-Xmx1024m", "-jar", jarFile, "nogui"); + minecraft = new ProcessBuilder(javaLine.split("\\s")); // is this always going to work? i don't know. + minecraft.directory(new File(serverPath)); + minecraft.redirectErrorStream(true); + + try { + System.out.println("Launching server once to make sure there is a world."); + runMinecraft(minecraft, verbose); + System.out.println(""); + + File serverLevel = new File(worldPath + separator + "level.dat"); + File backupLevel = new File(worldPath + separator + "level_backup.dat"); + + System.out.println("Backing up level.dat to level_backup.dat."); + copyFile(serverLevel, backupLevel); + System.out.println(""); + + Integer[] spawn = getSpawn(serverLevel); + System.out.println("Spawn point detected: [" + spawn[0] + ", " + spawn[2] + "]"); + { + boolean overridden = false; + if (xOffset == null) { + xOffset = spawn[0]; + } else { + overridden = true; + } + if (yOffset == null) { + yOffset = spawn[2]; + } else { + overridden = true; + } + if (overridden) { + System.out.println("Centering land generation on [" + xOffset + ", " + yOffset + "] due to switches."); + } + } + System.out.println(""); + + int totalIterations = (xRange / increment + 1) * (yRange / increment + 1); + int currentIteration = 0; + + long differenceTime = System.currentTimeMillis(); + Long[] timeTracking = new Long[]{differenceTime, differenceTime, differenceTime, differenceTime}; + for (int currentX = 0 - xRange / 2; currentX <= xRange / 2; currentX += increment) { + for (int currentY = 0 - yRange / 2; currentY <= yRange / 2; currentY += increment) { + currentIteration++; + System.out.println("Setting spawn to [" + Integer.toString(currentX + xOffset) + ", " + Integer.toString(currentY + yOffset) + "] (" + currentIteration + "/" + totalIterations + ")"); + + // Time Remaining estimate + timeTracking[0] = timeTracking[1]; + timeTracking[1] = timeTracking[2]; + timeTracking[2] = timeTracking[3]; + timeTracking[3] = System.currentTimeMillis(); + if (currentIteration >= 4) { + differenceTime = (timeTracking[3] - timeTracking[0]) / 3; // well, this is what it boils down to + differenceTime *= 1 + (totalIterations - currentIteration); + System.out.println(String.format("Estimated time remaining: %dh%dm%ds", + differenceTime / (1000 * 60 * 60), (differenceTime % (1000 * 60 * 60)) / (1000 * 60), ((differenceTime % (1000 * 60 * 60)) % (1000 * 60)) / 1000)); + } + + // Set the spawn point + setSpawn(serverLevel, currentX + xOffset, 128, currentY + yOffset); + + // Launch the server + runMinecraft(minecraft, verbose); + System.out.println(""); + } + } + + System.out.println("Finished generating chunks."); + copyFile(backupLevel, serverLevel); + backupLevel.delete(); + System.out.println("Restored original level.dat."); + } catch (IOException ex) { + Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + } + } + + protected static Integer[] getSpawn(File level) throws IOException { + try { + NBTInputStream input = new NBTInputStream(new FileInputStream(level)); + CompoundTag originalTopLevelTag = (CompoundTag) input.readTag(); + input.close(); + + Map originalData = ((CompoundTag) originalTopLevelTag.getValue().get("Data")).getValue(); + // This is our map of data. It is an unmodifiable map, for some reason, so we have to make a copy. + Map newData = new LinkedHashMap(originalData); + // .get() a couple of values, just to make sure we're dealing with a valid level file, here. Good for debugging, too. + IntTag spawnX = (IntTag) newData.get("SpawnX"); + IntTag spawnY = (IntTag) newData.get("SpawnY"); + IntTag spawnZ = (IntTag) newData.get("SpawnZ"); + + Integer[] ret = new Integer[]{spawnX.getValue(), spawnY.getValue(), spawnZ.getValue()}; + return ret; + } catch (ClassCastException ex) { + throw new IOException("Invalid level format."); + } catch (NullPointerException ex) { + throw new IOException("Invalid level format."); + } + } + + /** + * Changes the spawn point in the given Alpha level to the given coordinates. + * Note that, in Minecraft levels, the Y coordinate is height, while Z is + * what may normally be thought of as Y. + * @param level the level file to change the spawn point in + * @param x the new X value + * @param z the new Y value + * @param y the new Z value + * @throws IOException if there are any problems reading/writing the file + */ + protected static void setSpawn(File level, Integer x, Integer y, Integer z) throws IOException { + try { + NBTInputStream input = new NBTInputStream(new FileInputStream(level)); + CompoundTag originalTopLevelTag = (CompoundTag) input.readTag(); + input.close(); + +// +// Structure: +// +//TAG_Compound("Data"): World data. +// * TAG_Long("Time"): Stores the current "time of day" in ticks. There are 20 ticks per real-life second, and 24000 ticks per Minecraft day, making the day length 20 minutes. 0 appears to be sunrise, 12000 sunset and 24000 sunrise again. +// * TAG_Long("LastPlayed"): Stores the Unix time stamp (in milliseconds) when the player saved the game. +// * TAG_Compound("Player"): Player entity information. See Entity Format and Mob Entity Format for details. Has additional elements: +// o TAG_List("Inventory"): Each TAG_Compound in this list defines an item the player is carrying, holding, or wearing as armor. +// + TAG_Compound: Inventory item data +// # TAG_Short("id"): Item or Block ID. +// # TAG_Short("Damage"): The amount of wear each item has suffered. 0 means undamaged. When the Damage exceeds the item's durability, it breaks and disappears. Only tools and armor accumulate damage normally. +// # TAG_Byte("Count"): Number of items stacked in this inventory slot. Any item can be stacked, including tools, armor, and vehicles. Range is 1-255. Values above 127 are not displayed in-game. +// # TAG_Byte("Slot"): Indicates which inventory slot this item is in. +// o TAG_Int("Score"): Current score, doesn't appear to be implemented yet. Always 0. +// * TAG_Int("SpawnX"): X coordinate of the player's spawn position. Default is 0. +// * TAG_Int("SpawnY"): Y coordinate of the player's spawn position. Default is 64. +// * TAG_Int("SpawnZ"): Z coordinate of the player's spawn position. Default is 0. +// * TAG_Byte("SnowCovered"): 1 enables, 0 disables, see Winter Mode +// * TAG_Long("SizeOnDisk"): Estimated size of the entire world in bytes. +// * TAG_Long("RandomSeed"): Random number providing the Random Seed for the terrain. +// + + Map originalData = ((CompoundTag) originalTopLevelTag.getValue().get("Data")).getValue(); + // This is our map of data. It is an unmodifiable map, for some reason, so we have to make a copy. + Map newData = new LinkedHashMap(originalData); + // .get() a couple of values, just to make sure we're dealing with a valid level file, here. Good for debugging, too. + IntTag spawnX = (IntTag) newData.get("SpawnX"); + IntTag spawnY = (IntTag) newData.get("SpawnY"); + IntTag spawnZ = (IntTag) newData.get("SpawnZ"); + newData.put("SpawnX", new IntTag("SpawnX", x)); + newData.put("SpawnY", new IntTag("SpawnY", y)); + newData.put("SpawnZ", new IntTag("SpawnZ", z)); + + // Again, we can't modify the data map in the old Tag, so we have to make a new one. + CompoundTag newDataTag = new CompoundTag("Data", newData); + Map newTopLevelMap = new HashMap(1); + newTopLevelMap.put("Data", newDataTag); + CompoundTag newTopLevelTag = new CompoundTag("", newTopLevelMap); + + NBTOutputStream output = new NBTOutputStream(new FileOutputStream(level)); + output.writeTag(newTopLevelTag); + output.close(); + } catch (ClassCastException ex) { + throw new IOException("Invalid level format."); + } catch (NullPointerException ex) { + throw new IOException("Invalid level format."); + } + } + + /** + * Starts the process in the given ProcessBuilder, monitors its output for a + * "[INFO] Done!" message, and sends it a "stop\r\n" message. One message is printed + * to the console before launching and one is printed to the console when the + * Done! message is detected. If "verbose" is true, the process's output will + * also be printed to the console. + * @param minecraft + * @param verbose + * @throws IOException + */ + protected static void runMinecraft(ProcessBuilder minecraft, boolean verbose) throws IOException { + System.out.println("Starting server."); + Process process = minecraft.start(); + + // monitor output and print to console where required. + // STOP the server when it's done. + BufferedReader pOut = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = pOut.readLine()) != null) { + if (verbose) { + System.out.println(line); + } + if (line.contains("[INFO] Done!")) { + System.out.println("Stopping server."); + byte[] stop = {'s', 't', 'o', 'p', '\r', '\n'}; + OutputStream outputStream = process.getOutputStream(); + outputStream.write(stop); + outputStream.flush(); + } + } + // readLine() returns null when the process exits + } + + // I'd love to use nio, but it requires Java 7. + // I could use Apache Commons, but i don't want to include a library for one little thing. + // Copies src file to dst file. + // If the dst file does not exist, it is created + public static void copyFile(File src, File dst) throws IOException { + InputStream in = new FileInputStream(src); + OutputStream out = new FileOutputStream(dst); + + // Transfer bytes from in to out + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) >= 0) { + if (len > 0) { + out.write(buf, 0, len); + } + } + in.close(); + out.flush(); + out.close(); + } + + private boolean printSpawn() { + // ugh, sorry, this is an ugly hack, but it's a last-minute feature. + // this is a lot of duplicated code. + + try { + File config = new File("MinecraftLandGenerator.conf"); + BufferedReader in = new BufferedReader(new FileReader(config)); + String line; + while ((line = in.readLine()) != null) { + int pos = line.indexOf('='); + if (pos != -1) { + if (line.substring(0, pos).toLowerCase().equals("serverpath")) { + serverPath = line.substring(pos + 1); + } else if (line.substring(0, pos).toLowerCase().equals("java")) { + javaLine = line.substring(pos + 1); + } + } + } + in.close(); + + if (serverPath == null || javaLine == null) { + System.err.println("MinecraftLandGenerator.conf does not contain all requird properties. Please recreate it by running this application with no arguments."); + return false; + } + } catch (FileNotFoundException ex) { + System.out.println("Could not find MinecraftLandGenerator.conf. It is recommended that you run the application with the -conf option to create it."); + return false; + } catch (IOException ex) { + System.err.println("Could not read MinecraftLandGenerator.conf"); + return false; + } + + { + // verify that we ended up with a good server path, either from the file or from an argument. + File file = new File(serverPath); + if (!file.exists() || !file.isDirectory()) { + System.err.println("The server directory is invalid: " + serverPath); + return false; + } + } + + try { + // read the name of the current world from the server.properties file + BufferedReader props = new BufferedReader(new FileReader(new File(serverPath + separator + "server.properties"))); + String line; + while ((line = props.readLine()) != null) { + int pos = line.indexOf('='); + if (pos != -1) { + if (line.substring(0, pos).toLowerCase().equals("level-name")) { + worldPath = serverPath + separator + line.substring(pos + 1); + } + } + } + + } catch (FileNotFoundException ex) { + System.err.println("Could not open " + serverPath + separator + "server.properties"); + return false; + } catch (IOException ex) { + Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); + return false; + } + + File level = new File(worldPath + separator + "level.dat"); + if (!level.exists() || !level.isFile()) { + System.err.println("The currently-configured world does not exist. Please launch the server once, first."); + return false; + } + + try { + Integer[] spawn = getSpawn(level); + System.out.println("The current spawn point is: [" + spawn[0] + ", " + spawn[2] + "]"); + return true; + } catch (IOException ex) { + System.err.println("Error while reading " + level.getPath()); + return false; + } + } +}