BlueMap/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java

556 lines
19 KiB
Java

/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.cli;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.mca.MCAWorld;
import de.bluecolored.bluemap.core.render.StaticRenderSettings;
import de.bluecolored.bluemap.core.render.TileRenderer;
import de.bluecolored.bluemap.core.render.hires.HiresModelManager;
import de.bluecolored.bluemap.core.render.lowres.LowresModelManager;
import de.bluecolored.bluemap.core.resourcepack.NoSuchResourceException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.web.BlueMapWebRequestHandler;
import de.bluecolored.bluemap.core.web.WebFilesManager;
import de.bluecolored.bluemap.core.web.WebSettings;
import de.bluecolored.bluemap.core.webserver.WebServer;
import de.bluecolored.bluemap.core.world.World;
public class BlueMapCLI {
private File webroot = new File("web");
private File dataPath = new File(webroot, "data");
private File extraResourceFile = null;
private int threadCount;
private String mapId = null;
private String mapName = null;
private int highresTileSize = 32;
private int lowresTileSize = 50;
private int samplesPerHighresTile = 4;
private float highresViewDistance = 6f;
private float lowresViewDistance = 5f;
private boolean excludeFacesWithoutSunlight = true;
private float ambientOcclusion = 0.25f;
private float lighting = 0.8f;
private int sliceY = Integer.MAX_VALUE;
private int maxY = Integer.MAX_VALUE;
private int minY = 0;
private int port = 8100;
private int maxConnections = 100;
private InetAddress bindAdress = null;
public BlueMapCLI() {
threadCount = Runtime.getRuntime().availableProcessors();
}
public void renderMap(File mapPath, boolean updateOnly) throws IOException, NoSuchResourceException {
dataPath.mkdirs();
if (!mapPath.exists() || !mapPath.isDirectory()) {
throw new IOException("Save folder '" + mapPath + "' does not exist or is not a directory!");
}
Logger.global.logInfo("Reading world...");
World world = MCAWorld.load(mapPath.toPath(), UUID.randomUUID());
if (mapName == null) {
mapName = world.getName();
}
if (mapId == null) {
mapId = mapPath.getName().toLowerCase();
}
Logger.global.logInfo("Starting Render:"
+ "\n map: " + mapPath.getAbsolutePath()
+ "\n map-id: " + mapId
+ "\n map-name: " + mapName
+ "\n thread-count: " + threadCount
+ "\n data-path: " + dataPath.getAbsolutePath()
+ "\n render-all: " + !excludeFacesWithoutSunlight
+ "\n ambient-occlusion: " + ambientOcclusion
+ "\n lighting: " + lighting
+ "\n sliceY: " + (sliceY < Integer.MAX_VALUE ? sliceY : "-")
+ "\n maxY: " + (maxY < Integer.MAX_VALUE ? maxY : "-")
+ "\n minY: " + (minY > 0 ? minY : "-")
+ "\n hr-tilesize: " + highresTileSize
+ "\n lr-tilesize: " + lowresTileSize
+ "\n lr-resolution: " + samplesPerHighresTile
+ "\n hr-viewdistance: " + highresViewDistance
+ "\n lr-viewdistance: " + lowresViewDistance
);
Logger.global.logInfo("Loading Resources...");
ResourcePack resourcePack = loadResources();
Logger.global.logInfo("Initializing renderer...");
HiresModelManager hiresModelManager = new HiresModelManager(
dataPath.toPath().resolve("hires").resolve(mapId),
resourcePack,
new Vector2i(highresTileSize, highresTileSize),
ForkJoinPool.commonPool()
);
LowresModelManager lowresModelManager = new LowresModelManager(
dataPath.toPath().resolve("lowres").resolve(mapId),
new Vector2i(lowresTileSize, lowresTileSize),
new Vector2i(samplesPerHighresTile, samplesPerHighresTile)
);
TileRenderer tileRenderer = new TileRenderer(hiresModelManager, lowresModelManager, new StaticRenderSettings(
ambientOcclusion,
excludeFacesWithoutSunlight,
lighting,
maxY,
minY,
sliceY
));
File webSettingsFile = new File(dataPath, "settings.json");
Logger.global.logInfo("Writing '" + webSettingsFile.getAbsolutePath() + "'...");
WebSettings webSettings = new WebSettings(webSettingsFile);
webSettings.setName(mapName, mapId);
webSettings.setFrom(tileRenderer, mapId);
webSettings.setHiresViewDistance(highresViewDistance, mapId);
webSettings.setLowresViewDistance(lowresViewDistance, mapId);
webSettings.save();
Logger.global.logInfo("Collecting tiles to render...");
Collection<Vector2i> chunks;
if (updateOnly) {
long lastRender = webSettings.getLong(mapId, "last-render");
chunks = world.getChunkList(lastRender);
} else {
chunks = world.getChunkList();
}
Set<Vector2i> tiles = new HashSet<>();
for (Vector2i chunk : chunks) {
Vector3i minBlockPos = new Vector3i(chunk.getX() * 16, 0, chunk.getY() * 16);
tiles.add(hiresModelManager.posToTile(minBlockPos));
tiles.add(hiresModelManager.posToTile(minBlockPos.add(0, 0, 15)));
tiles.add(hiresModelManager.posToTile(minBlockPos.add(15, 0, 0)));
tiles.add(hiresModelManager.posToTile(minBlockPos.add(15, 0, 15)));
}
Logger.global.logInfo("Found " + tiles.size() + " tiles to render! (" + chunks.size() + " chunks)");
if (tiles.isEmpty()) {
Logger.global.logInfo("Render finished!");
return;
}
Logger.global.logInfo("Starting Render...");
long starttime = System.currentTimeMillis();
RenderManager renderManager = new RenderManager(world, tileRenderer, tiles, threadCount);
renderManager.start(() -> {
Logger.global.logInfo("Waiting for threads to quit...");
if (!ForkJoinPool.commonPool().awaitQuiescence(30, TimeUnit.SECONDS)) {
Logger.global.logWarning("Some save-threads are taking very long to exit (>30s), they will be ignored.");
}
try {
webSettings.set(starttime, mapId, "last-render");
webSettings.save();
} catch (IOException e) {
Logger.global.logError("Failed to update web-settings!", e);
}
Logger.global.logInfo("Render finished!");
});
}
public void updateWebFiles() throws IOException {
webroot.mkdirs();
Logger.global.logInfo("Creating webfiles in " + webroot.getAbsolutePath());
WebFilesManager webFilesManager = new WebFilesManager(webroot.toPath());
webFilesManager.updateFiles();
}
public void startWebserver() throws UnknownHostException {
if (bindAdress == null) bindAdress = InetAddress.getLocalHost();
Logger.global.logInfo("Starting webserver:"
+ "\n address: " + this.bindAdress.toString() + ""
+ "\n port: " + this.port
+ "\n max connections: " + this.maxConnections
+ "\n webroot: " + this.webroot.getAbsolutePath()
);
WebServer webserver = new WebServer(
this.port,
this.maxConnections,
this.bindAdress,
new BlueMapWebRequestHandler(this.webroot.toPath())
);
webserver.start();
}
private ResourcePack loadResources() throws IOException, NoSuchResourceException {
File defaultResourceFile;
try {
defaultResourceFile = File.createTempFile("res", ".zip");
defaultResourceFile.delete();
} catch (IOException e) {
throw new IOException("Failed to create temporary resource file!", e);
}
try {
ResourcePack.createDefaultResource(defaultResourceFile);
} catch (IOException e) {
throw new IOException("Failed to create default resources!", e);
}
List<File> resourcePacks = new ArrayList<>();
resourcePacks.add(defaultResourceFile);
if (this.extraResourceFile != null) resourcePacks.add(extraResourceFile);
ResourcePack resourcePack = new ResourcePack(resourcePacks, new File(dataPath, "textures.json"));
defaultResourceFile.delete();
return resourcePack;
}
public static void main(String[] args) throws IOException, NoSuchResourceException {
CommandLineParser parser = new DefaultParser();
try {
CommandLine cmd = parser.parse(BlueMapCLI.createOptions(), args, false);
if (cmd.hasOption("h")) {
BlueMapCLI.printHelp();
return;
}
boolean executed = false;
BlueMapCLI bluemapcli = new BlueMapCLI();
if (cmd.hasOption("o")) bluemapcli.dataPath = new File(cmd.getOptionValue("o"));
if (cmd.hasOption("r")) bluemapcli.extraResourceFile = new File(cmd.getOptionValue("r"));
if (cmd.hasOption("t")) bluemapcli.threadCount = Integer.parseInt(cmd.getOptionValue("t"));
if (cmd.hasOption("d")) bluemapcli.webroot = new File(cmd.getOptionValue("d"));
if (cmd.hasOption("i")) bluemapcli.bindAdress = InetAddress.getByName(cmd.getOptionValue("i"));
bluemapcli.port = Integer.parseInt(cmd.getOptionValue("p", Integer.toString(bluemapcli.port)));
bluemapcli.maxConnections = Integer.parseInt(cmd.getOptionValue("connections", Integer.toString(bluemapcli.maxConnections)));
bluemapcli.mapName = cmd.getOptionValue("n", bluemapcli.mapName);
bluemapcli.mapId = cmd.getOptionValue("id", bluemapcli.mapId);
bluemapcli.ambientOcclusion = Float.parseFloat(cmd.getOptionValue("ao", Float.toString(bluemapcli.ambientOcclusion)));
bluemapcli.lighting = Float.parseFloat(cmd.getOptionValue("lighting", Float.toString(bluemapcli.lighting)));
bluemapcli.sliceY = Integer.parseInt(cmd.getOptionValue("y-slice", Integer.toString(bluemapcli.sliceY)));
bluemapcli.maxY = Integer.parseInt(cmd.getOptionValue("y-max", Integer.toString(bluemapcli.maxY)));
bluemapcli.minY = Integer.parseInt(cmd.getOptionValue("y-min", Integer.toString(bluemapcli.minY)));
bluemapcli.highresTileSize = Integer.parseInt(cmd.getOptionValue("hr-tilesize", Integer.toString(bluemapcli.highresTileSize)));
bluemapcli.highresViewDistance = Float.parseFloat(cmd.getOptionValue("hr-viewdist", Float.toString(bluemapcli.highresViewDistance)));
bluemapcli.lowresTileSize = Integer.parseInt(cmd.getOptionValue("lr-tilesize", Integer.toString(bluemapcli.lowresTileSize)));
bluemapcli.samplesPerHighresTile = Integer.parseInt(cmd.getOptionValue("lr-resolution", Integer.toString(bluemapcli.samplesPerHighresTile)));
bluemapcli.lowresViewDistance = Float.parseFloat(cmd.getOptionValue("lr-viewdist", Float.toString(bluemapcli.lowresViewDistance)));
if (cmd.hasOption("c")) {
bluemapcli.updateWebFiles();
executed = true;
}
if (cmd.hasOption("s")) {
bluemapcli.startWebserver();
executed = true;
}
if (cmd.hasOption("w")) {
bluemapcli.renderMap(new File(cmd.getOptionValue("w")), !cmd.hasOption("f"));
executed = true;
}
if (executed) return;
} catch (ParseException e) {
Logger.global.logError("Failed to parse provided arguments!", e);
} catch (NumberFormatException e) {
Logger.global.logError("One argument expected a number but got the wrong format!", e);
}
BlueMapCLI.printHelp();
}
private static Options createOptions() {
Options options = new Options();
options.addOption("h", "help", false, "Displays this message");
options.addOption(
Option.builder("o")
.longOpt("out")
.hasArg()
.argName("directory-path")
.desc("Defines the render-output directory. Default is '<webroot>/data' (See option -d)")
.build()
);
options.addOption(
Option.builder("d")
.longOpt("dir")
.hasArg()
.argName("directory-path")
.desc("Defines the webroot directory. Default is './web'")
.build()
);
options.addOption("s", "webserver", false, "Starts the integrated webserver");
options.addOption(
Option.builder("c")
.longOpt("create-web")
.desc("The webfiles will be (re)created, existing web-files in the webroot will be replaced!")
.build()
);
options.addOption(
Option.builder("i")
.longOpt("ip")
.hasArg()
.argName("ip-adress")
.desc("Specifies the IP adress the webserver will use")
.build()
);
options.addOption(
Option.builder("p")
.longOpt("port")
.hasArg()
.argName("port")
.desc("Specifies the port the webserver will use. Default is 8100")
.build()
);
options.addOption(
Option.builder()
.longOpt("connections")
.hasArg()
.argName("count")
.desc("Sets the maximum count of simultaneous client-connections that the webserver will allow. Default is 100")
.build()
);
options.addOption(
Option.builder("w")
.longOpt("world")
.hasArg()
.argName("directory-path")
.desc("Defines the world-save folder that will be rendered")
.build()
);
options.addOption(
Option.builder("f")
.longOpt("force-render")
.desc("Rerenders all tiles even if there are no changes since the last render")
.build()
);
options.addOption(
Option.builder("r")
.longOpt("resource")
.hasArg()
.argName("file")
.desc("Defines the resourcepack that will be used to render the map")
.build()
);
options.addOption(
Option.builder("t")
.longOpt("threads")
.hasArg()
.argName("thread-count")
.desc("Defines the number of threads that will be used to render the map. Default is the number of system cores")
.build()
);
options.addOption(
Option.builder("I")
.longOpt("id")
.hasArg()
.argName("id")
.desc("The id of the world. Default is the name of the world-folder")
.build()
);
options.addOption(
Option.builder("n")
.longOpt("name")
.hasArg()
.argName("name")
.desc("The name of the world. Default is the world-name defined in the level.dat")
.build()
);
options.addOption(
Option.builder()
.longOpt("render-all")
.desc("Also renders blocks that are normally omitted due to a sunlight value of 0. Enabling this can cause a big performance impact in the web-viewer, but it might fix some cases where blocks are missing.")
.build()
);
options.addOption(
Option.builder("ao")
.longOpt("ambient-occlusion")
.hasArg()
.argName("value")
.desc("The strength of ambient-occlusion baked into the model (a value between 0 and 1). Default is 0.25")
.build()
);
options.addOption(
Option.builder("l")
.longOpt("lighting")
.hasArg()
.argName("value")
.desc("The max strength of shadows baked into the model (a value between 0 and 1 where 0 is fully bright (no lighting) and 1 is max lighting-contrast). Default is 0.8")
.build()
);
options.addOption(
Option.builder("ys")
.longOpt("y-slice")
.hasArg()
.argName("value")
.desc("Using this, BlueMap pretends that every Block above the defined value is AIR. Default is disabled")
.build()
);
options.addOption(
Option.builder("yM")
.longOpt("y-max")
.hasArg()
.argName("value")
.desc("Blocks above this height will not be rendered. Default is no limit")
.build()
);
options.addOption(
Option.builder("ym")
.longOpt("y-min")
.hasArg()
.argName("value")
.desc("Blocks below this height will not be rendered. Default is no limit")
.build()
);
options.addOption(
Option.builder()
.longOpt("hr-tilesize")
.hasArg()
.argName("value")
.desc("Defines the size of one map-tile in blocks. If you change this value, the lowres values might need adjustment as well! Default is 32")
.build()
);
options.addOption(
Option.builder()
.longOpt("hr-viewdist")
.hasArg()
.argName("value")
.desc("The View-Distance for hires tiles on the web-map (the value is the radius in tiles). Default is 6")
.build()
);
options.addOption(
Option.builder()
.longOpt("lr-tilesize")
.hasArg()
.argName("value")
.desc("Defines the size of one lowres-map-tile in grid-points. Default is 50")
.build()
);
options.addOption(
Option.builder()
.longOpt("lr-resolution")
.hasArg()
.argName("value")
.desc("Defines resolution of the lowres model. E.g. If the hires.tileSize is 32, a value of 4 means that every 8*8 blocks will be summarized by one point on the lowres map. Calculation: 32 / 4 = 8! You have to use values that result in an integer if you use the above calculation! Default is 4")
.build()
);
options.addOption(
Option.builder()
.longOpt("lr-viewdist")
.hasArg()
.argName("value")
.desc("The View-Distance for lowres tiles on the web-map (the value is the radius in tiles). Default is 5")
.build()
);
return options;
}
private static void printHelp() {
HelpFormatter formatter = new HelpFormatter();
String filename = "bluemapcli.jar";
try {
File file = new File(BlueMapCLI.class.getProtectionDomain()
.getCodeSource()
.getLocation()
.getPath());
if (file.isFile()) {
try {
filename = "./" + new File(".").toPath().relativize(file.toPath()).toString();
} catch (IllegalArgumentException ex) {
filename = file.getAbsolutePath();
}
}
} catch (Exception ex) {}
String command = "java -jar " + filename;
formatter.printHelp(command + " [options]", "\nOptions:", createOptions(), "\n"
+ "Examples:\n\n"
+ command + " -w ./world/\n"
+ " -> Renders the whole world to ./web/data/\n\n"
+ command + " -csi localhost\n"
+ " -> Creates all neccesary web-files in ./web/ and starts the webserver. (Open http://localhost:8100/ in your browser)"
);
}
}