diff --git a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java index cec70113..a45e289c 100644 --- a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java +++ b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java @@ -26,12 +26,13 @@ import java.io.File; import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ForkJoinPool; @@ -47,223 +48,184 @@ import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector3i; +import com.google.common.base.Preconditions; +import de.bluecolored.bluemap.core.config.Configuration; +import de.bluecolored.bluemap.core.config.Configuration.MapConfig; +import de.bluecolored.bluemap.core.config.ConfigurationFile; 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.metrics.Metrics; 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.BlueMapWebServer; 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 ConfigurationFile configFile; + private Configuration config; + private ResourcePack resourcePack; + private boolean forceRender; - 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 BlueMapCLI(ConfigurationFile configFile, boolean forceRender) { + this.configFile = configFile; + this.config = configFile.getConfig(); + this.forceRender = forceRender; + this.resourcePack = null; } - 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()); + public void renderMaps() throws IOException, NoSuchResourceException { + Preconditions.checkNotNull(resourcePack); - if (mapName == null) { - mapName = world.getName(); - } + config.getWebDataPath().toFile().mkdirs(); - if (mapId == null) { - mapId = mapPath.getName().toLowerCase(); - } + Map maps = new HashMap<>(); - 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 chunks; - if (updateOnly) { - long lastRender = webSettings.getLong(mapId, "last-render"); - chunks = world.getChunkList(lastRender); - } else { - chunks = world.getChunkList(); - } - - Set 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."); + for (MapConfig mapConfig : config.getMapConfigs()) { + File mapPath = new File(mapConfig.getWorldPath()); + if (!mapPath.exists() || !mapPath.isDirectory()) { + throw new IOException("Save folder '" + mapPath + "' does not exist or is not a directory!"); } + + Logger.global.logInfo("Preparing renderer for map '" + mapConfig.getId() + "' ..."); + World world = MCAWorld.load(mapPath.toPath(), UUID.randomUUID()); + + HiresModelManager hiresModelManager = new HiresModelManager( + config.getWebDataPath().resolve("hires").resolve(mapConfig.getId()), + resourcePack, + new Vector2i(mapConfig.getHiresTileSize(), mapConfig.getHiresTileSize()), + ForkJoinPool.commonPool() + ); + + LowresModelManager lowresModelManager = new LowresModelManager( + config.getWebDataPath().resolve("lowres").resolve(mapConfig.getId()), + new Vector2i(mapConfig.getLowresPointsPerLowresTile(), mapConfig.getLowresPointsPerLowresTile()), + new Vector2i(mapConfig.getLowresPointsPerHiresTile(), mapConfig.getLowresPointsPerHiresTile()) + ); + + TileRenderer tileRenderer = new TileRenderer(hiresModelManager, lowresModelManager, mapConfig); + + MapType mapType = new MapType(mapConfig.getId(), mapConfig.getName(), world, tileRenderer); + maps.put(mapConfig.getId(), mapType); + } + Logger.global.logInfo("Writing settings.json ..."); + WebSettings webSettings = new WebSettings(config.getWebDataPath().resolve("settings.json").toFile()); + for (MapType map : maps.values()) { + webSettings.setName(map.getName(), map.getId()); + webSettings.setFrom(map.getTileRenderer(), map.getId()); + } + for (MapConfig map : config.getMapConfigs()) { + webSettings.setHiresViewDistance(map.getHiresViewDistance(), map.getId()); + webSettings.setLowresViewDistance(map.getLowresViewDistance(), map.getId()); + } + webSettings.save(); + + for (MapType map : maps.values()) { + Logger.global.logInfo("Rendering map '" + map.getId() + "' ..."); + Logger.global.logInfo("Collecting tiles to render..."); + + Collection chunks; + if (!forceRender) { + long lastRender = webSettings.getLong(map.getId(), "last-render"); + chunks = map.getWorld().getChunkList(lastRender); + } else { + chunks = map.getWorld().getChunkList(); + } + + HiresModelManager hiresModelManager = map.getTileRenderer().getHiresModelManager(); + Set 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 (!forceRender && chunks.size() == 0) { + Logger.global.logInfo("(This is normal if nothing has changed in the world since the last render. Use -f on the command-line to force a render of all chunks)"); + } + + if (tiles.isEmpty()) { + Logger.global.logInfo("Render finished!"); + return; + } + + Logger.global.logInfo("Starting Render..."); + long starttime = System.currentTimeMillis(); + + RenderTask task = new RenderTask(map, tiles, config.getRenderThreadCount()); + task.render(); + try { - webSettings.set(starttime, mapId, "last-render"); + webSettings.set(starttime, map.getId(), "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(); + Logger.global.logInfo("Waiting for all 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."); + } + + Logger.global.logInfo("Render finished!"); } - 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()) - ); + public void startWebserver() throws IOException { + Logger.global.logInfo("Starting webserver..."); + BlueMapWebServer webserver = new BlueMapWebServer(config); + webserver.updateWebfiles(); 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.downloadDefaultResource(defaultResourceFile); - } catch (IOException e) { - throw new IOException("Failed to create default resources!", e); + private boolean loadResources() throws IOException, NoSuchResourceException { + File defaultResourceFile = config.getDataPath().resolve("minecraft-client-" + ResourcePack.MINECRAFT_CLIENT_VERSION + ".jar").toFile(); + File textureExportFile = config.getWebDataPath().resolve("textures.json").toFile(); + + if (!defaultResourceFile.exists()) { + if (!handleMissingResources(defaultResourceFile)) return false; } - List resourcePacks = new ArrayList<>(); - resourcePacks.add(defaultResourceFile); - if (this.extraResourceFile != null) resourcePacks.add(extraResourceFile); + //find more resource packs + File resourcePackFolder = configFile.getFile().toPath().resolveSibling("resourcepacks").toFile(); + resourcePackFolder.mkdirs(); + File[] resourcePacks = resourcePackFolder.listFiles(); + Arrays.sort(resourcePacks); - ResourcePack resourcePack = new ResourcePack(resourcePacks, new File(dataPath, "textures.json")); + List resources = new ArrayList<>(resourcePacks.length + 1); + resources.add(defaultResourceFile); + for (File file : resourcePacks) resources.add(file); - defaultResourceFile.delete(); + resourcePack = new ResourcePack(resources, textureExportFile); - return resourcePack; + return true; + } + + private boolean handleMissingResources(File resourceFile) { + if (config.isDownloadAccepted()) { + try { + Logger.global.logInfo("Downloading " + ResourcePack.MINECRAFT_CLIENT_URL + " to " + resourceFile + " ..."); + ResourcePack.downloadDefaultResource(resourceFile); + return true; + } catch (IOException e) { + Logger.global.logError("Failed to download resources!", e); + return false; + } + } else { + Logger.global.logWarning("BlueMap is missing important resources!"); + Logger.global.logWarning("You need to accept the download of the required files in order of BlueMap to work!"); + Logger.global.logWarning("Please check: " + configFile.getFile() + " and try again!"); + return false; + } } public static void main(String[] args) throws IOException, NoSuchResourceException { @@ -271,253 +233,73 @@ public static void main(String[] args) throws IOException, NoSuchResourceExcepti try { CommandLine cmd = parser.parse(BlueMapCLI.createOptions(), args, false); - + + //help 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))); + //load config + File configFile = new File("bluemap.conf").getAbsoluteFile(); if (cmd.hasOption("c")) { - bluemapcli.updateWebFiles(); - executed = true; + configFile = new File(cmd.getOptionValue("c")); + configFile.getParentFile().mkdirs(); } - if (cmd.hasOption("s")) { - bluemapcli.startWebserver(); - executed = true; + boolean configCreated = !configFile.exists(); + + ConfigurationFile config = ConfigurationFile.loadOrCreate(configFile, BlueMapCLI.class.getResource("/bluemap-cli.conf")); + + if (configCreated) { + Logger.global.logInfo("No config file found! Created an example config here: " + configFile); + return; } - if (cmd.hasOption("w")) { - bluemapcli.renderMap(new File(cmd.getOptionValue("w")), !cmd.hasOption("f")); - executed = true; + BlueMapCLI bluemap = new BlueMapCLI(config, cmd.hasOption("f")); + + if (config.getConfig().isWebserverEnabled()) { + //start webserver + bluemap.startWebserver(); } - if (executed) return; + if (!config.getConfig().getMapConfigs().isEmpty()) { + //load resources + if (bluemap.loadResources()) { + + //metrics + if (config.getConfig().isMetricsEnabled()) Metrics.sendReportAsync("CLI"); + + //render maps + bluemap.renderMaps(); + + //since we don't need it any more, free some memory + bluemap.resourcePack = null; + } + } } 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(); + return; } - - 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 '/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() - ); + Option.builder("c") + .longOpt("config") + .hasArg() + .argName("config-file") + .desc("Sets path of the configuration file to use") + .build() + ); + + options.addOption("f", "force-render", false, "Forces rendering everything, instead of only rendering chunks that have been modified since the last render"); return options; } @@ -534,7 +316,7 @@ private static void printHelp() { if (file.isFile()) { try { - filename = "./" + new File(".").toPath().relativize(file.toPath()).toString(); + filename = "." + File.separator + new File("").getAbsoluteFile().toPath().relativize(file.toPath()).toString(); } catch (IllegalArgumentException ex) { filename = file.getAbsolutePath(); } @@ -543,13 +325,7 @@ private static void printHelp() { 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)" - ); + formatter.printHelp(command + " [options]", "\nOptions:", createOptions(), ""); } } diff --git a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/MapType.java b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/MapType.java new file mode 100644 index 00000000..fc9c7e95 --- /dev/null +++ b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/MapType.java @@ -0,0 +1,92 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * 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.IOException; + +import com.flowpowered.math.vector.Vector2i; +import com.google.common.base.Preconditions; + +import de.bluecolored.bluemap.core.render.TileRenderer; +import de.bluecolored.bluemap.core.render.WorldTile; +import de.bluecolored.bluemap.core.world.ChunkNotGeneratedException; +import de.bluecolored.bluemap.core.world.World; + +public class MapType { + + private final String id; + private String name; + private World world; + private TileRenderer tileRenderer; + + public MapType(String id, String name, World world, TileRenderer tileRenderer) { + Preconditions.checkNotNull(id); + Preconditions.checkNotNull(name); + Preconditions.checkNotNull(world); + Preconditions.checkNotNull(tileRenderer); + + this.id = id; + this.name = name; + this.world = world; + this.tileRenderer = tileRenderer; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public World getWorld() { + return world; + } + + public TileRenderer getTileRenderer() { + return tileRenderer; + } + + public void renderTile(Vector2i tile) throws IOException, ChunkNotGeneratedException { + getTileRenderer().render(new WorldTile(getWorld(), tile)); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj != null && obj instanceof MapType) { + MapType that = (MapType) obj; + + return this.id.equals(that.id); + } + + return false; + } + +} diff --git a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderManager.java b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderTask.java similarity index 93% rename from BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderManager.java rename to BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderTask.java index 78f07dc2..e1e9fac3 100644 --- a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderManager.java +++ b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderTask.java @@ -42,7 +42,7 @@ import de.bluecolored.bluemap.core.world.ChunkNotGeneratedException; import de.bluecolored.bluemap.core.world.World; -public class RenderManager extends Thread { +public class RenderTask { private World world; private TileRenderer tileRenderer; @@ -54,11 +54,9 @@ public class RenderManager extends Thread { private Thread[] threads; - private Runnable onFinished; - - public RenderManager(World world, TileRenderer tileRenderer, Collection tilesToRender, int threadCount) { - this.world = world; - this.tileRenderer = tileRenderer; + public RenderTask(MapType map, Collection tilesToRender, int threadCount) { + this.world = map.getWorld(); + this.tileRenderer = map.getTileRenderer(); //Sort the chunks to opimize the chunk-cache usage of MCAWorld and generate the world in a nicer order, so you can see the first results early in the web-map during render Vector2d sortGridSize = new Vector2d(20, 20).div(tileRenderer.getHiresModelManager().getTileSize().toDouble().div(16)).ceil().max(1, 1); //Find a good grid size to match the MCAWorlds chunk-cache size of 500 @@ -96,14 +94,7 @@ public RenderManager(World world, TileRenderer tileRenderer, Collection/minecraft-client-%minecraft-client-version%.jar) +# This file contains resources that belong to mojang and you must not redistribute it or do anything else that is not compilant with mojang's EULA. +# BlueMap uses resources in this file to generate the 3D-Models used for the map and texture them. (BlueMap will not work without those resources.) +# %datetime-iso% +accept-download: false + +# If this is true, BlueMap might send really basic metrics reports containg only the implementation-type and the version that is being used to https://metrics.bluecolored.de/bluemap/ +# This allows me to track the basic usage of BlueMap and helps me stay motivated to further develop this tool! Please leave it on :) +# An example report looks like this: {"implementation":"CLI","version":"%version%"} +metrics: true + +# The folder where bluemap saves data-files it needs during runtime or to save e.g. the render-progress to resume it later. +data: "data" + +web { + # With this setting you can enable the integrated web-server. + enabled: false + + # The webroot of the website that displays the map. + webroot: "web" + + # The IP-Adress that the webserver binds to. + # If this setting is commented out, bluemap tries to find the default ip-adress of your system. + # If you only want to access it locally use "localhost". + #ip: "localhost" + #ip: "127.0.0.1" + + # The port that the webserver listenes to. + # Default is 8100 + port: 8100 + + # Max number of simultaneous connections that the webserver allows + # Default is 100 + maxConnectionCount: 100 + + # Unncomment this to override the path where bluemap stores the data-files. + # Default is "/data" + #web-data: "path/to/data/folder" +} + +# This changes the amount of threads that BlueMap will use to render the maps. +# A higher value can improve render-speed but could impact performance on the host machine. +# This should be always below or equal to the number of available processor-cores. +# If this value is commented out BlueMap tries to find the optimal thread count to max out render-performance +#renderThreadCount: 2 + +# This is an array with multiple configured maps. +# You can define multiple maps, for different worlds with different render-settings here +maps: [ + + { + # The id of this map + # Should only contain word-charactes: [a-zA-Z0-9_] + id: "world" + + # The name of this map + # This defines the display name of this map, you can change this at any time + # Default is the id of this map + name: "World" + + # The path to the save-folder of the world to render + world: "world" + + # If this is false, BlueMap tries to omit all blocks that are not visible from above-ground. + # More specific: Block-Faces that have a sunlight/skylight value of 0 are removed. + # This improves the performance of the map on slower devices by a lot, but might cause some blocks to disappear that should normally be visible. + # Default is false + renderCaves: false + + # AmbientOcclusion adds soft shadows into corners, which gives the map a much better look. + # This has only a small impact on render-time and has no impact on the web-performance of the map. + # The value defines the strength of the shading, a value of 0 disables ambientOcclusion. + # Default is 0.25 + ambientOcclusion: 0.25 + + # Lighting uses the light-data in minecraft to shade each block-face. + # If this is enabled, caves and inside buildings without torches will be darker. + # The value defines the strength of the shading and a value of 0 disables lighting (every block will be fully lit). + # Default is 0.8 + lighting: 0.8 + + # Using this, BlueMap pretends that every Block above the defined value is AIR. + # Default is disabled + #sliceY: 90 + + # With the below values you can just not render blocks at certain heights. + # This can be used to ignore the nethers ceiling. + # Default is no min or max y value + #minY: 50 + #maxY: 126 + + # HIRES is the high-resolution render of the map. Where you see every block. + hires { + # 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 + tileSize: 32 + + # The View-Distance for hires tiles on the web-map (the value is the radius in tiles) + # Default is 3.5 + viewDistance: 3.5 + } + + # LOWRES is the low-resolution render of the map. THats the model that you see if you zoom far out to get an overview. + lowres { + # 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 can only use values that result in an integer if you use the above calculation! + # Default is 4 + pointsPerHiresTile: 4 + + # Defines the size of one lowres-map-tile in points. + # Default is 50 + pointsPerLowresTile: 50 + + # The View-Distance for lowres tiles on the web-map (the value is the radius in tiles) + # Default is 4 + viewDistance: 4 + } + } + + # Here another example for the End-Map + # Things we dont want to change from default we can just omit + { + id: "end" + name: "End" + world: "world/DIM1" + + # In the end is no light, so we need to enable this or we don't see anything. + renderCaves: true + + # Same here, we don't want a dark map. But not completely disabled, so we see the effect of e.g torches. + lighting: 0.4 + } + + # Here another example for the Nether-Map + { + id: "nether" + name: "Nether" + world: "world/DIM-1" + + renderCaves: true + lighting: 0.6 + + # We slice the whole world at y:90 so evrery block above 90 will be air. + # This way we dont render the nethers ceiling. + sliceY: 90 + + # Instead of slicing we also could do this, that would look like an x-ray view through the ceiling. + #maxY: 126 + } + +] diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/Configuration.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/Configuration.java index 8303c9e5..34b44fe7 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/Configuration.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/Configuration.java @@ -43,6 +43,7 @@ public class Configuration implements WebServerConfig { private String version; private boolean downloadAccepted = false; + private boolean metricsEnabled = false; private boolean webserverEnabled = true; private int webserverPort = 8100; @@ -61,6 +62,7 @@ public class Configuration implements WebServerConfig { public Configuration(ConfigurationNode node) throws IOException { version = node.getNode("version").getString("-"); downloadAccepted = node.getNode("accept-download").getBoolean(false); + metricsEnabled = node.getNode("metrics").getBoolean(false); dataPath = toFolder(node.getNode("data").getString("data")); @@ -157,6 +159,10 @@ public boolean isDownloadAccepted() { return downloadAccepted; } + public boolean isMetricsEnabled() { + return metricsEnabled; + } + public int getRenderThreadCount() { return renderThreadCount; } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/ConfigurationFile.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/ConfigurationFile.java index cf949883..0203a265 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/ConfigurationFile.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/ConfigurationFile.java @@ -52,16 +52,23 @@ public class ConfigurationFile { CONFIG_PLACEHOLDERS.add(new Placeholder("minecraft-client-version", ResourcePack.MINECRAFT_CLIENT_VERSION)); } + private File configFile; private Configuration config; private ConfigurationFile(File configFile) throws IOException { + this.configFile = configFile; + ConfigurationLoader configLoader = HoconConfigurationLoader.builder() .setFile(configFile) .build(); CommentedConfigurationNode rootNode = configLoader.load(); - config = new Configuration(rootNode); + this.config = new Configuration(rootNode); + } + + public File getFile() { + return configFile; } public Configuration getConfig() {