More library friendly + forceload generator

This commit is contained in:
Piegames 2018-11-15 10:21:49 +01:00
parent d79061f722
commit 1a212bc9aa
4 changed files with 274 additions and 141 deletions

View File

@ -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;
}
}

View File

@ -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<Vector2i> 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<Vector2i> 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<Vector2i> 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<Vector2i> 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<Vector2i> 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<Integer> xPoints =
generateLinearSpawnpoints(startX + margin, width - increment, increment);
log.debug("X grid: " + xPoints);
List<Integer> zPoints =
generateLinearSpawnpoints(startZ + margin, height - increment, increment);
log.debug("Z grid: " + zPoints);
List<Vector2i> 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<Integer> 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());
}
}

View File

@ -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<String> 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<Vector2i> 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();

View File

@ -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<Vector2i> 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.<br>
* Note that, in Minecraft levels, the Y coordinate is height.<br>
@ -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<Vector2i> 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<Integer> xPoints =
generateLinearSpawnpoints(startX + margin, width - increment, increment);
log.debug("X grid: " + xPoints);
List<Integer> zPoints =
generateLinearSpawnpoints(startZ + margin, height - increment, increment);
log.debug("Z grid: " + zPoints);
List<Vector2i> 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<Integer> 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());
}
}