482 lines
20 KiB
Java
482 lines
20 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 de.bluecolored.bluemap.common.BlueMapConfiguration;
|
|
import de.bluecolored.bluemap.common.BlueMapService;
|
|
import de.bluecolored.bluemap.common.MissingResourcesException;
|
|
import de.bluecolored.bluemap.common.config.BlueMapConfigManager;
|
|
import de.bluecolored.bluemap.common.config.ConfigurationException;
|
|
import de.bluecolored.bluemap.common.config.CoreConfig;
|
|
import de.bluecolored.bluemap.common.config.WebserverConfig;
|
|
import de.bluecolored.bluemap.common.plugin.RegionFileWatchService;
|
|
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
|
|
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
|
|
import de.bluecolored.bluemap.common.rendermanager.RenderTask;
|
|
import de.bluecolored.bluemap.common.web.*;
|
|
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
|
|
import de.bluecolored.bluemap.common.web.http.HttpServer;
|
|
import de.bluecolored.bluemap.core.BlueMap;
|
|
import de.bluecolored.bluemap.core.MinecraftVersion;
|
|
import de.bluecolored.bluemap.core.logger.Logger;
|
|
import de.bluecolored.bluemap.core.map.BmMap;
|
|
import de.bluecolored.bluemap.core.metrics.Metrics;
|
|
import de.bluecolored.bluemap.core.storage.Storage;
|
|
import de.bluecolored.bluemap.core.util.FileHelper;
|
|
import org.apache.commons.cli.*;
|
|
import org.apache.commons.lang3.time.DurationFormatUtils;
|
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.net.BindException;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.UnknownHostException;
|
|
import java.nio.file.Path;
|
|
import java.time.Instant;
|
|
import java.time.ZoneId;
|
|
import java.time.ZonedDateTime;
|
|
import java.util.*;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.function.Predicate;
|
|
import java.util.regex.Pattern;
|
|
|
|
public class BlueMapCLI {
|
|
|
|
private MinecraftVersion minecraftVersion = MinecraftVersion.LATEST_SUPPORTED;
|
|
private Path configFolder = Path.of("config");
|
|
|
|
public void renderMaps(BlueMapService blueMap, boolean watch, boolean forceRender, boolean forceGenerateWebapp,
|
|
@Nullable String mapsToRender) throws ConfigurationException, IOException, InterruptedException {
|
|
|
|
//metrics report
|
|
if (blueMap.getConfig().getCoreConfig().isMetrics()) Metrics.sendReportAsync("cli");
|
|
|
|
if (blueMap.getConfig().getWebappConfig().isEnabled())
|
|
blueMap.createOrUpdateWebApp(forceGenerateWebapp);
|
|
|
|
//try load resources
|
|
blueMap.getOrLoadResourcePack();
|
|
|
|
//create renderManager
|
|
RenderManager renderManager = new RenderManager();
|
|
|
|
//load maps
|
|
Predicate<String> mapFilter = mapId -> true;
|
|
if (mapsToRender != null) {
|
|
Set<String> mapsToRenderSet = Set.of(mapsToRender.split(","));
|
|
mapFilter = mapsToRenderSet::contains;
|
|
}
|
|
Map<String, BmMap> maps = blueMap.getOrLoadMaps(mapFilter);
|
|
|
|
//watcher
|
|
List<RegionFileWatchService> regionFileWatchServices = new ArrayList<>();
|
|
if (watch) {
|
|
for (BmMap map : maps.values()) {
|
|
try {
|
|
RegionFileWatchService watcher = new RegionFileWatchService(renderManager, map);
|
|
watcher.start();
|
|
regionFileWatchServices.add(watcher);
|
|
} catch (IOException ex) {
|
|
Logger.global.logError("Failed to create file-watcher for map: " + map.getId() +
|
|
" (This map might not automatically update)", ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
//update all maps
|
|
int totalRegions = 0;
|
|
for (BmMap map : maps.values()) {
|
|
MapUpdateTask updateTask = new MapUpdateTask(map, forceRender);
|
|
renderManager.scheduleRenderTask(updateTask);
|
|
totalRegions += updateTask.getRegions().size();
|
|
}
|
|
|
|
Logger.global.logInfo("Start updating " + maps.size() + " maps (" + totalRegions + " regions, ~" + totalRegions * 1024L + " chunks)...");
|
|
|
|
// start rendering
|
|
renderManager.start(blueMap.getConfig().getCoreConfig().resolveRenderThreadCount());
|
|
|
|
Timer timer = new Timer("BlueMap-CLI-Timer", true);
|
|
TimerTask updateInfoTask = new TimerTask() {
|
|
@Override
|
|
public void run() {
|
|
RenderTask task = renderManager.getCurrentRenderTask();
|
|
if (task == null) return;
|
|
|
|
double progress = task.estimateProgress();
|
|
long etaMs = renderManager.estimateCurrentRenderTaskTimeRemaining();
|
|
|
|
String eta = "";
|
|
if (etaMs > 0) {
|
|
String etrDurationString = DurationFormatUtils.formatDuration(etaMs, "HH:mm:ss");
|
|
eta = " (ETA: " + etrDurationString + ")";
|
|
}
|
|
Logger.global.logInfo(task.getDescription() + ": " + (Math.round(progress * 100000) / 1000.0) + "%" + eta);
|
|
}
|
|
};
|
|
timer.scheduleAtFixedRate(updateInfoTask, TimeUnit.SECONDS.toMillis(10), TimeUnit.SECONDS.toMillis(10));
|
|
|
|
TimerTask saveTask = new TimerTask() {
|
|
@Override
|
|
public void run() {
|
|
for (BmMap map : maps.values()) {
|
|
map.save();
|
|
}
|
|
}
|
|
};
|
|
timer.scheduleAtFixedRate(saveTask, TimeUnit.MINUTES.toMillis(2), TimeUnit.MINUTES.toMillis(2));
|
|
|
|
Runnable shutdown = () -> {
|
|
Logger.global.logInfo("Stopping...");
|
|
updateInfoTask.cancel();
|
|
saveTask.cancel();
|
|
renderManager.stop();
|
|
|
|
for (RegionFileWatchService watcher : regionFileWatchServices) {
|
|
watcher.close();
|
|
}
|
|
regionFileWatchServices.clear();
|
|
|
|
try {
|
|
renderManager.awaitShutdown();
|
|
} catch (InterruptedException e) {
|
|
Logger.global.logError("Unexpected interruption: ", e);
|
|
}
|
|
|
|
Logger.global.logInfo("Saving...");
|
|
saveTask.run();
|
|
|
|
Logger.global.logInfo("Stopped.");
|
|
};
|
|
|
|
Thread shutdownHook = new Thread(shutdown, "BlueMap-CLI-ShutdownHook");
|
|
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
|
|
|
// wait until done, then shutdown if not watching
|
|
renderManager.awaitIdle();
|
|
Logger.global.logInfo("Your maps are now all up-to-date!");
|
|
|
|
if (watch) {
|
|
updateInfoTask.cancel();
|
|
Logger.global.logInfo("Waiting for changes on the world-files...");
|
|
} else {
|
|
Runtime.getRuntime().removeShutdownHook(shutdownHook);
|
|
shutdown.run();
|
|
}
|
|
}
|
|
|
|
public void startWebserver(BlueMapService blueMap, boolean verbose) throws IOException, ConfigurationException, InterruptedException {
|
|
Logger.global.logInfo("Starting webserver ...");
|
|
|
|
WebserverConfig config = blueMap.getConfig().getWebserverConfig();
|
|
FileHelper.createDirectories(config.getWebroot());
|
|
|
|
RoutingRequestHandler routingRequestHandler = new RoutingRequestHandler();
|
|
|
|
// default route
|
|
routingRequestHandler.register(".*", new FileRequestHandler(config.getWebroot()));
|
|
|
|
// map route
|
|
for (var mapConfigEntry : blueMap.getConfig().getMapConfigs().entrySet()) {
|
|
Storage storage = blueMap.getOrLoadStorage(mapConfigEntry.getValue().getStorage());
|
|
|
|
routingRequestHandler.register(
|
|
"maps/" + Pattern.quote(mapConfigEntry.getKey()) + "/(.*)",
|
|
"$1",
|
|
new MapRequestHandler(mapConfigEntry.getKey(), storage)
|
|
);
|
|
}
|
|
|
|
List<Logger> webLoggerList = new ArrayList<>();
|
|
if (verbose) webLoggerList.add(Logger.stdOut(true));
|
|
if (config.getLog().getFile() != null) {
|
|
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
|
|
webLoggerList.add(Logger.file(
|
|
Path.of(String.format(config.getLog().getFile(), zdt)),
|
|
config.getLog().isAppend()
|
|
));
|
|
}
|
|
|
|
HttpRequestHandler handler = new BlueMapResponseModifier(routingRequestHandler);
|
|
handler = new LoggingRequestHandler(
|
|
handler,
|
|
config.getLog().getFormat(),
|
|
Logger.combine(webLoggerList)
|
|
);
|
|
|
|
try {
|
|
//noinspection resource
|
|
HttpServer webServer = new HttpServer(handler);
|
|
webServer.bind(new InetSocketAddress(
|
|
config.resolveIp(),
|
|
config.getPort()
|
|
));
|
|
webServer.start();
|
|
} catch (UnknownHostException ex) {
|
|
throw new ConfigurationException("BlueMap failed to resolve the ip in your webserver-config.\n" +
|
|
"Check if that is correctly configured.", ex);
|
|
} catch (BindException ex) {
|
|
throw new ConfigurationException("BlueMap failed to bind to the configured address.\n" +
|
|
"This usually happens when the configured port (" + config.getPort() + ") is already in use by some other program.", ex);
|
|
} catch (IOException ex) {
|
|
throw new ConfigurationException("BlueMap failed to initialize the webserver.\n" +
|
|
"Check your webserver-config if everything is configured correctly.\n" +
|
|
"(Make sure you DON'T use the same port for bluemap that you also use for your minecraft server)", ex);
|
|
}
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
CommandLineParser parser = new DefaultParser();
|
|
|
|
BlueMapCLI cli = new BlueMapCLI();
|
|
BlueMapService blueMap = null;
|
|
|
|
try {
|
|
CommandLine cmd = parser.parse(BlueMapCLI.createOptions(), args, false);
|
|
|
|
if (cmd.hasOption("b")) {
|
|
Logger.global.clear();
|
|
Logger.global.put(Logger.stdOut(true));
|
|
}
|
|
|
|
if (cmd.hasOption("l")) {
|
|
Logger.global.put(Logger.file(Path.of(cmd.getOptionValue("l")), cmd.hasOption("a")));
|
|
}
|
|
|
|
//help
|
|
if (cmd.hasOption("h")) {
|
|
BlueMapCLI.printHelp();
|
|
return;
|
|
}
|
|
|
|
//version
|
|
if (cmd.hasOption("V")) {
|
|
BlueMapCLI.printVersion();
|
|
return;
|
|
}
|
|
|
|
//config folder
|
|
if (cmd.hasOption("c")) {
|
|
cli.configFolder = Path.of(cmd.getOptionValue("c"));
|
|
FileHelper.createDirectories(cli.configFolder);
|
|
}
|
|
|
|
//minecraft version
|
|
if (cmd.hasOption("v")) {
|
|
String versionString = cmd.getOptionValue("v");
|
|
try {
|
|
cli.minecraftVersion = MinecraftVersion.of(versionString);
|
|
} catch (IllegalArgumentException e) {
|
|
Logger.global.logWarning("Could not determine a version from the provided version-string: '" + versionString + "'");
|
|
System.exit(1);
|
|
return;
|
|
}
|
|
}
|
|
|
|
BlueMapConfigManager configs = BlueMapConfigManager.builder()
|
|
.minecraftVersion(cli.minecraftVersion)
|
|
.configRoot(cli.configFolder)
|
|
.usePluginConfig(false)
|
|
.defaultDataFolder(Path.of("data"))
|
|
.defaultWebroot(Path.of("web"))
|
|
.build();
|
|
|
|
//apply new file-logger config
|
|
CoreConfig coreConfig = configs.getCoreConfig();
|
|
if (coreConfig.getLog().getFile() != null) {
|
|
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
|
|
Logger.global.put(Logger.file(
|
|
Path.of(String.format(coreConfig.getLog().getFile(), zdt)),
|
|
coreConfig.getLog().isAppend()
|
|
));
|
|
}
|
|
|
|
blueMap = new BlueMapService(configs);
|
|
boolean noActions = true;
|
|
|
|
if (cmd.hasOption("w")) {
|
|
noActions = false;
|
|
|
|
cli.startWebserver(blueMap, cmd.hasOption("b"));
|
|
Thread.sleep(1000); //wait a second to let the webserver start, looks nicer in the log if anything comes after that
|
|
}
|
|
|
|
if (cmd.hasOption("r")) {
|
|
noActions = false;
|
|
|
|
boolean watch = cmd.hasOption("u");
|
|
boolean force = cmd.hasOption("f");
|
|
boolean generateWebappFiles = cmd.hasOption("g");
|
|
String mapsToRender = cmd.getOptionValue("m", null);
|
|
cli.renderMaps(blueMap, watch, force, generateWebappFiles, mapsToRender);
|
|
} else {
|
|
if (cmd.hasOption("g")) {
|
|
noActions = false;
|
|
blueMap.createOrUpdateWebApp(true);
|
|
}
|
|
if (cmd.hasOption("s")) {
|
|
noActions = false;
|
|
blueMap.createOrUpdateWebApp(false);
|
|
}
|
|
}
|
|
|
|
// if nothing has been defined to do
|
|
if (noActions) {
|
|
Logger.global.logInfo("Generated default config files for you, here: " + cli.configFolder.toAbsolutePath().normalize() + "\n");
|
|
|
|
//create resourcepacks folder
|
|
FileHelper.createDirectories(cli.configFolder.resolve( "resourcepacks"));
|
|
|
|
//print help
|
|
BlueMapCLI.printHelp();
|
|
System.exit(1);
|
|
}
|
|
|
|
} catch (MissingResourcesException e) {
|
|
Logger.global.logWarning("BlueMap is missing important resources!");
|
|
Logger.global.logWarning("You must accept the required file download in order for BlueMap to work!");
|
|
if (blueMap != null) {
|
|
BlueMapConfiguration configProvider = blueMap.getConfig();
|
|
if (configProvider instanceof BlueMapConfigManager) {
|
|
Logger.global.logWarning("Please check: " + ((BlueMapConfigManager) configProvider).getConfigManager().findConfigPath(Path.of("core")).toAbsolutePath().normalize());
|
|
}
|
|
}
|
|
System.exit(2);
|
|
} catch (ParseException e) {
|
|
Logger.global.logError("Failed to parse provided arguments!", e);
|
|
BlueMapCLI.printHelp();
|
|
System.exit(1);
|
|
} catch (ConfigurationException e) {
|
|
Logger.global.logWarning(e.getFormattedExplanation());
|
|
Throwable cause = e.getRootCause();
|
|
if (cause != null) {
|
|
Logger.global.logError("Detailed error:", e);
|
|
}
|
|
} catch (IOException e) {
|
|
Logger.global.logError("An IO-error occurred!", e);
|
|
System.exit(1);
|
|
} catch (InterruptedException ex) {
|
|
System.exit(1);
|
|
} catch (RuntimeException e) {
|
|
Logger.global.logError("An unexpected error occurred!", e);
|
|
System.exit(1);
|
|
}
|
|
}
|
|
|
|
private static Options createOptions() {
|
|
Options options = new Options();
|
|
|
|
options.addOption("h", "help", false, "Displays this message");
|
|
|
|
options.addOption(
|
|
Option.builder("c")
|
|
.longOpt("config")
|
|
.hasArg()
|
|
.argName("config-folder")
|
|
.desc("Sets path of the folder containing the configuration-files to use (configurations will be generated here if they don't exist)")
|
|
.build()
|
|
);
|
|
|
|
options.addOption(
|
|
Option.builder("v")
|
|
.longOpt("mc-version")
|
|
.hasArg()
|
|
.argName("mc-version")
|
|
.desc("Sets the minecraft-version, used e.g. to load resource-packs correctly. Defaults to the latest compatible version.")
|
|
.build()
|
|
);
|
|
|
|
options.addOption(
|
|
Option.builder("l")
|
|
.longOpt("log-file")
|
|
.hasArg()
|
|
.argName("file-name")
|
|
.desc("Sets a file to save the log to. If not specified, no log will be saved.")
|
|
.build()
|
|
);
|
|
options.addOption("a", "append", false, "Causes log save file to be appended rather than replaced.");
|
|
|
|
options.addOption("w", "webserver", false, "Starts the web-server, configured in the 'webserver.conf' file");
|
|
options.addOption("b", "verbose", false, "Causes the web-server to log requests to the console");
|
|
|
|
options.addOption("g", "generate-webapp", false, "Generates the files for the web-app to the folder configured in the 'webapp.conf' file");
|
|
options.addOption("s", "generate-websettings", false, "Updates the settings.json for the web-app");
|
|
|
|
options.addOption("r", "render", false, "Renders the maps configured in the 'render.conf' file");
|
|
options.addOption("f", "force-render", false, "Forces rendering everything, instead of only rendering chunks that have been modified since the last render");
|
|
options.addOption("m", "maps", true, "A comma-separated list of map-id's that should be rendered. Example: 'world,nether'");
|
|
|
|
options.addOption("u", "watch", false, "Watches for file-changes after rendering and updates the map");
|
|
|
|
options.addOption("V", "version", false, "Print the current BlueMap version");
|
|
|
|
return options;
|
|
}
|
|
|
|
private static void printHelp() {
|
|
HelpFormatter formatter = new HelpFormatter();
|
|
|
|
String command = getCliCommand();
|
|
|
|
@SuppressWarnings("StringBufferReplaceableByString")
|
|
StringBuilder footer = new StringBuilder();
|
|
footer.append("Examples:\n\n");
|
|
footer.append(command).append(" -c './config/'\n");
|
|
footer.append("Generates the default/example configurations in a folder named 'config' if they are not already present\n\n");
|
|
footer.append(command).append(" -r\n");
|
|
footer.append("Render the configured maps\n\n");
|
|
footer.append(command).append(" -w\n");
|
|
footer.append("Start only the webserver without doing anything else\n\n");
|
|
footer.append(command).append(" -ru\n");
|
|
footer.append("Render the configured maps and then keeps watching the world-files and updates the map once something changed.\n\n");
|
|
|
|
formatter.printHelp(command + " [options]", "\nOptions:", createOptions(), "\n" + footer);
|
|
}
|
|
|
|
private static String getCliCommand() {
|
|
String filename = "bluemap-cli.jar";
|
|
try {
|
|
File file = new File(BlueMapCLI.class.getProtectionDomain()
|
|
.getCodeSource()
|
|
.getLocation()
|
|
.getPath());
|
|
|
|
if (file.isFile()) {
|
|
try {
|
|
filename = "." + File.separator + new File("").getCanonicalFile().toPath().relativize(file.toPath());
|
|
} catch (IllegalArgumentException ex) {
|
|
filename = file.getAbsolutePath();
|
|
}
|
|
}
|
|
} catch (IOException ignore) {}
|
|
return "java -jar " + filename;
|
|
}
|
|
|
|
private static void printVersion() {
|
|
System.out.printf("%s\n%s\n", BlueMap.VERSION, BlueMap.GIT_HASH);
|
|
}
|
|
}
|