diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/InterruptableReentrantLock.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/InterruptableReentrantLock.java index a1264586..9fe3a0a7 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/InterruptableReentrantLock.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/InterruptableReentrantLock.java @@ -38,7 +38,7 @@ public InterruptableReentrantLock(boolean fair) { } /** - * Aquires the lock and interrupts the currently holding thread if there is any. + * Acquires the lock and interrupts the currently holding thread if there is any. */ public void interruptAndLock() { while (!tryLock()) { diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java index 3cb0c8eb..1b7b64eb 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java @@ -32,8 +32,7 @@ import de.bluecolored.bluemap.common.live.LiveAPIRequestHandler; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; import de.bluecolored.bluemap.common.plugin.skins.PlayerSkinUpdater; -import de.bluecolored.bluemap.common.rendermanager.RenderManager; -import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask; +import de.bluecolored.bluemap.common.rendermanager.*; import de.bluecolored.bluemap.core.BlueMap; import de.bluecolored.bluemap.core.MinecraftVersion; import de.bluecolored.bluemap.core.config.CoreConfig; @@ -79,7 +78,7 @@ public class Plugin { private RenderManager renderManager; private WebServer webServer; - private final Timer daemonTimer; + private Timer daemonTimer; private TimerTask saveTask; private TimerTask metricsTask; @@ -93,8 +92,6 @@ public Plugin(MinecraftVersion minecraftVersion, String implementationType, Serv this.minecraftVersion = minecraftVersion; this.implementationType = implementationType.toLowerCase(); this.serverInterface = serverInterface; - - this.daemonTimer = new Timer("BlueMap-Plugin-Daemon-Timer", true); } public void load() throws IOException, ParseResourceException { @@ -120,7 +117,7 @@ public void load() throws IOException, ParseResourceException { true, true )); - + //create and start webserver if (webServerConfig.isWebserverEnabled()) { FileUtils.mkDirs(webServerConfig.getWebRoot()); @@ -161,10 +158,28 @@ public void load() throws IOException, ParseResourceException { //warn if no maps are configured if (maps.isEmpty()) { Logger.global.logWarning("There are no valid maps configured, please check your render-config! Disabling BlueMap..."); + + unload(); + return; } //initialize render manager renderManager = new RenderManager(); + + //update all maps + for (BmMap map : maps.values()) { + Collection regions = map.getWorld().listRegions(); + List mapTasks = new ArrayList<>(regions.size()); + + for (Vector2i region : regions) + mapTasks.add(new WorldRegionRenderTask(map, region)); + mapTasks.sort(WorldRegionRenderTask::compare); + + CombinedRenderTask mapUpdateTask = new CombinedRenderTask<>("Update map '" + map.getId() + "'", mapTasks); + renderManager.scheduleRenderTask(mapUpdateTask); + } + + //start render-manager renderManager.start(coreConfig.getRenderThreadCount()); //update webapp and settings @@ -180,6 +195,9 @@ public void load() throws IOException, ParseResourceException { serverInterface.registerListener(skinUpdater); } + //init timer + daemonTimer = new Timer("BlueMap-Plugin-Daemon-Timer", true); + //periodically save saveTask = new TimerTask() { @Override @@ -193,7 +211,7 @@ public void run() { } }; daemonTimer.schedule(saveTask, TimeUnit.MINUTES.toMillis(2), TimeUnit.MINUTES.toMillis(2)); - + //metrics metricsTask = new TimerTask() { @Override @@ -203,12 +221,6 @@ public void run() { } }; daemonTimer.scheduleAtFixedRate(metricsTask, TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(30)); - - loaded = true; - - //enable api - this.api = new BlueMapAPIImpl(this); - this.api.register(); //watch map-changes this.regionFileWatchServices = new ArrayList<>(); @@ -222,13 +234,12 @@ public void run() { } } - //update all maps - for (BmMap map : maps.values()) { - for (Vector2i region : map.getWorld().listRegions()){ - renderManager.scheduleRenderTask(new WorldRegionRenderTask(map, region)); - } - } - + //enable api + this.api = new BlueMapAPIImpl(this); + this.api.register(); + + //done + loaded = true; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -249,22 +260,31 @@ public void unload() { //unregister listeners serverInterface.unregisterAllListeners(); + skinUpdater = null; //stop scheduled threads if (metricsTask != null) metricsTask.cancel(); + metricsTask = null; if (saveTask != null) saveTask.cancel(); + saveTask = null; + if (daemonTimer != null) daemonTimer.cancel(); + daemonTimer = null; - // stop file-watchers + //stop file-watchers if (regionFileWatchServices != null) { for (RegionFileWatchService watcher : regionFileWatchServices) { watcher.close(); } regionFileWatchServices.clear(); } + regionFileWatchServices = null; //stop services if (renderManager != null) renderManager.stop(); + renderManager = null; + if (webServer != null) webServer.close(); + webServer = null; //save renders if (maps != null) { @@ -277,13 +297,14 @@ public void unload() { blueMap = null; worlds = null; maps = null; - renderManager = null; - webServer = null; + coreConfig = null; + renderConfig = null; + webServerConfig = null; pluginConfig = null; - + + //done loaded = false; - } } finally { loadingLock.unlock(); @@ -334,7 +355,7 @@ public Collection getMapTypes(){ public RenderManager getRenderManager() { return renderManager; } - + public WebServer getWebServer() { return webServer; } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/AbstractSuggestionProvider.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/AbstractSuggestionProvider.java index f717ecca..6228087d 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/AbstractSuggestionProvider.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/AbstractSuggestionProvider.java @@ -24,9 +24,6 @@ */ package de.bluecolored.bluemap.common.plugin.commands; -import java.util.Collection; -import java.util.concurrent.CompletableFuture; - import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; @@ -34,6 +31,9 @@ import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; + public abstract class AbstractSuggestionProvider implements SuggestionProvider { @Override diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/CommandHelper.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/CommandHelper.java index a81ec93f..5d2f43a4 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/CommandHelper.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/CommandHelper.java @@ -24,13 +24,16 @@ */ package de.bluecolored.bluemap.common.plugin.commands; +import com.flowpowered.math.vector.Vector2i; import de.bluecolored.bluemap.common.plugin.Plugin; import de.bluecolored.bluemap.common.plugin.text.Text; import de.bluecolored.bluemap.common.plugin.text.TextColor; import de.bluecolored.bluemap.common.rendermanager.RenderManager; import de.bluecolored.bluemap.common.rendermanager.RenderTask; import de.bluecolored.bluemap.core.map.BmMap; +import de.bluecolored.bluemap.core.world.Grid; import de.bluecolored.bluemap.core.world.World; +import org.apache.commons.lang3.time.DurationFormatUtils; import java.util.ArrayList; import java.util.List; @@ -48,25 +51,61 @@ public List createStatusMessage(){ List lines = new ArrayList<>(); RenderManager renderer = plugin.getRenderManager(); + List tasks = renderer.getScheduledRenderTasks(); lines.add(Text.of(TextColor.BLUE, "BlueMap - Status:")); if (renderer.isRunning()) { - lines.add(Text.of(TextColor.WHITE, " Render-Threads are ", - Text.of(TextColor.GREEN, "running") - .setHoverText(Text.of("click to pause rendering")) - .setClickAction(Text.ClickAction.RUN_COMMAND, "/bluemap pause"), - TextColor.GRAY, "!")); + Text status; + if (tasks.isEmpty()) { + status = Text.of(TextColor.GRAY, "idle"); + } else { + status = Text.of(TextColor.GREEN, "running"); + } + + status.setHoverText(Text.of("click to stop rendering")); + status.setClickAction(Text.ClickAction.RUN_COMMAND, "/bluemap stop"); + + lines.add(Text.of(TextColor.WHITE, " Render-Threads are ", status, TextColor.WHITE, "!")); + + if (!tasks.isEmpty()) { + lines.add(Text.of(TextColor.WHITE, " Queued Tasks (" + tasks.size() + "):")); + for (int i = 0; i < tasks.size(); i++) { + if (i >= 10){ + lines.add(Text.of(TextColor.GRAY, "...")); + break; + } + + RenderTask task = tasks.get(i); + lines.add(Text.of(TextColor.GRAY, " - ", TextColor.GOLD, task.getDescription())); + + if (i == 0) { + lines.add(Text.of(TextColor.GRAY, " Progress: ", TextColor.WHITE, + (Math.round(task.estimateProgress() * 10000) / 100.0) + "%")); + lines.add(Text.of(TextColor.GRAY, " ETA: ", TextColor.WHITE, DurationFormatUtils.formatDuration(renderer.estimateCurrentRenderTaskTimeRemaining(), "HH:mm:ss"))); + } + } + } } else { lines.add(Text.of(TextColor.WHITE, " Render-Threads are ", - Text.of(TextColor.RED, "paused") - .setHoverText(Text.of("click to resume rendering")) - .setClickAction(Text.ClickAction.RUN_COMMAND, "/bluemap resume"), + Text.of(TextColor.RED, "stopped") + .setHoverText(Text.of("click to start rendering")) + .setClickAction(Text.ClickAction.RUN_COMMAND, "/bluemap start"), TextColor.GRAY, "!")); - } - List tasks = renderer.getScheduledRenderTasks(); - lines.add(Text.of(TextColor.WHITE, " Scheduled tasks: ", TextColor.GOLD, tasks.size())); + if (!tasks.isEmpty()) { + lines.add(Text.of(TextColor.WHITE, " Queued Tasks (" + tasks.size() + "):")); + for (int i = 0; i < tasks.size(); i++) { + if (i >= 10){ + lines.add(Text.of(TextColor.GRAY, "...")); + break; + } + + RenderTask task = tasks.get(i); + lines.add(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, task.getDescription())); + } + } + } return lines; } @@ -88,4 +127,25 @@ public Text mapHelperHover() { return Text.of("map").setHoverText(Text.of(TextColor.WHITE, "Available maps: \n", TextColor.GRAY, joiner.toString())); } + + public List getRegions(World world, Vector2i center, int radius) { + if (center == null || radius < 0) return new ArrayList<>(world.listRegions()); + + List regions = new ArrayList<>(); + + Grid regionGrid = world.getRegionGrid(); + Vector2i halfCell = regionGrid.getGridSize().div(2); + int increasedRadiusSquared = (int) Math.pow(radius + Math.ceil(halfCell.length()), 2); + + for (Vector2i region : world.listRegions()) { + Vector2i min = regionGrid.getCellMin(region); + Vector2i regionCenter = min.add(halfCell); + + if (regionCenter.distanceSquared(center) <= increasedRadiusSquared) + regions.add(region); + } + + return regions; + } + } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/Commands.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/Commands.java index 6bcacf15..72c32627 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/Commands.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/Commands.java @@ -28,11 +28,13 @@ import com.flowpowered.math.vector.Vector3d; import com.flowpowered.math.vector.Vector3i; import com.google.common.collect.Lists; +import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.DoubleArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; @@ -47,12 +49,15 @@ import de.bluecolored.bluemap.common.plugin.text.Text; import de.bluecolored.bluemap.common.plugin.text.TextColor; import de.bluecolored.bluemap.common.plugin.text.TextFormat; +import de.bluecolored.bluemap.common.rendermanager.CombinedRenderTask; +import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask; 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.mca.MCAChunk; +import de.bluecolored.bluemap.core.map.MapRenderState; import de.bluecolored.bluemap.core.mca.ChunkAnvil112; +import de.bluecolored.bluemap.core.mca.MCAChunk; import de.bluecolored.bluemap.core.mca.MCAWorld; import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; import de.bluecolored.bluemap.core.world.Block; @@ -61,6 +66,8 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.function.Function; @@ -137,45 +144,37 @@ public void init() { .build(); LiteralCommandNode pauseCommand = - literal("pause") - .requires(requirements("bluemap.pause")) - .executes(this::pauseCommand) + literal("stop") + .requires(requirements("bluemap.stop")) + .executes(this::stopCommand) .build(); LiteralCommandNode resumeCommand = - literal("resume") - .requires(requirements("bluemap.resume")) - .executes(this::resumeCommand) + literal("start") + .requires(requirements("bluemap.start")) + .executes(this::startCommand) .build(); LiteralCommandNode renderCommand = - literal("render") - .requires(requirements("bluemap.render")) - .executes(this::renderCommand) // /bluemap render + addRenderArguments( + literal("render") + .requires(requirements("bluemap.render")), + this::renderCommand + ).build(); - .then(argument("radius", IntegerArgumentType.integer()) - .executes(this::renderCommand)) // /bluemap render - - .then(argument("x", DoubleArgumentType.doubleArg()) - .then(argument("z", DoubleArgumentType.doubleArg()) - .then(argument("radius", IntegerArgumentType.integer()) - .executes(this::renderCommand)))) // /bluemap render - - .then(argument("world|map", StringArgumentType.string()).suggests(new WorldOrMapSuggestionProvider<>(plugin)) - .executes(this::renderCommand) // /bluemap render - - .then(argument("x", DoubleArgumentType.doubleArg()) - .then(argument("z", DoubleArgumentType.doubleArg()) - .then(argument("radius", IntegerArgumentType.integer()) - .executes(this::renderCommand))))) // /bluemap render - .build(); + LiteralCommandNode updateCommand = + addRenderArguments( + literal("update") + .requires(requirements("bluemap.update")), + this::updateCommand + ).build(); LiteralCommandNode purgeCommand = literal("purge") - .requires(requirements("bluemap.render")) - .then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin)) - .executes(this::purgeCommand)) - .build(); + .requires(requirements("bluemap.render")) + .then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin)) + .executes(this::purgeCommand)) + .build(); LiteralCommandNode worldsCommand = literal("worlds") @@ -225,7 +224,8 @@ public void init() { baseCommand.addChild(debugCommand); baseCommand.addChild(pauseCommand); baseCommand.addChild(resumeCommand); - //baseCommand.addChild(renderCommand); + baseCommand.addChild(renderCommand); + baseCommand.addChild(updateCommand); baseCommand.addChild(purgeCommand); baseCommand.addChild(worldsCommand); baseCommand.addChild(mapsCommand); @@ -233,6 +233,27 @@ public void init() { markerCommand.addChild(createMarkerCommand); markerCommand.addChild(removeMarkerCommand); } + + private > B addRenderArguments(B builder, Command command) { + return builder + .executes(command) // /bluemap render + + .then(argument("radius", IntegerArgumentType.integer()) + .executes(command)) // /bluemap render + + .then(argument("x", DoubleArgumentType.doubleArg()) + .then(argument("z", DoubleArgumentType.doubleArg()) + .then(argument("radius", IntegerArgumentType.integer()) + .executes(command)))) // /bluemap render + + .then(argument("world|map", StringArgumentType.string()).suggests(new WorldOrMapSuggestionProvider<>(plugin)) + .executes(command) // /bluemap render + + .then(argument("x", DoubleArgumentType.doubleArg()) + .then(argument("z", DoubleArgumentType.doubleArg()) + .then(argument("radius", IntegerArgumentType.integer()) + .executes(command))))); // /bluemap render + } private Predicate requirements(String permission){ return s -> { @@ -498,58 +519,66 @@ public int debugBlockCommand(CommandContext context) { return 1; } - public int pauseCommand(CommandContext context) { + public int stopCommand(CommandContext context) { CommandSource source = commandSourceInterface.apply(context.getSource()); if (plugin.getRenderManager().isRunning()) { plugin.getRenderManager().stop(); - source.sendMessage(Text.of(TextColor.GREEN, "BlueMap rendering paused!")); + source.sendMessage(Text.of(TextColor.GREEN, "Render-Threads stopped!")); return 1; } else { - source.sendMessage(Text.of(TextColor.RED, "BlueMap rendering are already paused!")); + source.sendMessage(Text.of(TextColor.RED, "Render-Threads are already stopped!")); return 0; } } - public int resumeCommand(CommandContext context) { + public int startCommand(CommandContext context) { CommandSource source = commandSourceInterface.apply(context.getSource()); if (!plugin.getRenderManager().isRunning()) { plugin.getRenderManager().start(plugin.getCoreConfig().getRenderThreadCount()); - source.sendMessage(Text.of(TextColor.GREEN, "BlueMap renders resumed!")); + source.sendMessage(Text.of(TextColor.GREEN, "Render-Threads started!")); return 1; } else { - source.sendMessage(Text.of(TextColor.RED, "BlueMap renders are already running!")); + source.sendMessage(Text.of(TextColor.RED, "Render-Threads are already running!")); return 0; } } public int renderCommand(CommandContext context) { + return updateCommand(context, true); + } + + public int updateCommand(CommandContext context) { + return updateCommand(context, false); + } + + public int updateCommand(CommandContext context, boolean force) { final CommandSource source = commandSourceInterface.apply(context.getSource()); // parse world/map argument Optional worldOrMap = getOptionalArgument(context, "world|map", String.class); - final World world; - final BmMap map; + final World worldToRender; + final BmMap mapToRender; if (worldOrMap.isPresent()) { - world = parseWorld(worldOrMap.get()).orElse(null); + worldToRender = parseWorld(worldOrMap.get()).orElse(null); - if (world == null) { - map = parseMap(worldOrMap.get()).orElse(null); + if (worldToRender == null) { + mapToRender = parseMap(worldOrMap.get()).orElse(null); - if (map == null) { + if (mapToRender == null) { source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " or ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, worldOrMap.get())); return 0; } } else { - map = null; + mapToRender = null; } } else { - world = source.getWorld().orElse(null); - map = null; + worldToRender = source.getWorld().orElse(null); + mapToRender = null; - if (world == null) { + if (worldToRender == null) { source.sendMessage(Text.of(TextColor.RED, "Can't detect a world from this command-source, you'll have to define a world or a map to render!").setHoverText(Text.of(TextColor.GRAY, "/bluemap render "))); return 0; } @@ -580,13 +609,45 @@ public int renderCommand(CommandContext context) { // execute render new Thread(() -> { try { - if (world != null) { - plugin.getServerInterface().persistWorldChanges(world.getUUID()); - //TODO: helper.createWorldRenderTask(source, world, center, radius); + List maps = new ArrayList<>(); + World world = worldToRender; + if (worldToRender != null) { + plugin.getServerInterface().persistWorldChanges(worldToRender.getUUID()); + for (BmMap map : plugin.getMapTypes()) { + if (map.getWorld().equals(worldToRender)) maps.add(map); + } } else { - plugin.getServerInterface().persistWorldChanges(map.getWorld().getUUID()); - //TODO: helper.createMapRenderTask(source, map, center, radius); + plugin.getServerInterface().persistWorldChanges(mapToRender.getWorld().getUUID()); + maps.add(mapToRender); + world = mapToRender.getWorld(); } + + String taskType = "Update"; + List regions = helper.getRegions(world, center, radius); + + if (force) { + taskType = "Render"; + for (BmMap map : maps) { + MapRenderState state = map.getRenderState(); + regions.forEach(region -> state.setRenderTime(region, -1)); + } + } + + if (center != null) { + taskType = "Radius-" + taskType; + } + + for (BmMap map : maps) { + List tasks = new ArrayList<>(regions.size()); + regions.forEach(region -> tasks.add(new WorldRegionRenderTask(map, region))); + tasks.sort(WorldRegionRenderTask::compare); + plugin.getRenderManager().scheduleRenderTask(new CombinedRenderTask<>( + taskType + " map '" + map.getId() + "'", + tasks + )); + source.sendMessage(Text.of(TextColor.GREEN, "Created new " + taskType + "-Task for map '" + map.getId() + "' ", TextColor.GRAY, "(" + regions.size() + " regions, ~" + regions.size() * 1024L + " chunks)")); + } + source.sendMessage(Text.of(TextColor.GREEN, "Use ", TextColor.GRAY, "/bluemap", TextColor.GREEN, " to see the progress.")); } catch (IOException ex) { source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to save the world. Please check the console for more details...")); Logger.global.logError("Unexpected exception trying to save the world!", ex); diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/CombinedRenderTask.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/CombinedRenderTask.java index 874606f5..5cb0cca8 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/CombinedRenderTask.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/CombinedRenderTask.java @@ -24,18 +24,20 @@ */ package de.bluecolored.bluemap.common.rendermanager; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; public class CombinedRenderTask implements RenderTask { + private final String description; private final List tasks; + private final Set taskSet; private int currentTaskIndex; - public CombinedRenderTask(Collection tasks) { - this.tasks = new ArrayList<>(); - this.tasks.addAll(tasks); + public CombinedRenderTask(String description, Collection tasks) { + this.description = description; + this.tasks = Collections.unmodifiableList(new ArrayList<>(tasks)); + this.taskSet = Collections.unmodifiableSet(new HashSet<>(tasks)); + this.currentTaskIndex = 0; } @@ -43,7 +45,7 @@ public CombinedRenderTask(Collection tasks) { public void doWork() throws Exception { T task; - synchronized (this.tasks) { + synchronized (this) { if (!hasMoreWork()) return; task = this.tasks.get(this.currentTaskIndex); @@ -57,20 +59,18 @@ public void doWork() throws Exception { } @Override - public boolean hasMoreWork() { + public synchronized boolean hasMoreWork() { return this.currentTaskIndex < this.tasks.size(); } @Override - public double estimateProgress() { - synchronized (this.tasks) { - if (!hasMoreWork()) return 1; + public synchronized double estimateProgress() { + if (!hasMoreWork()) return 1; - double total = currentTaskIndex; - total += this.tasks.get(this.currentTaskIndex).estimateProgress(); + double total = currentTaskIndex; + total += this.tasks.get(this.currentTaskIndex).estimateProgress(); - return total / tasks.size(); - } + return total / tasks.size(); } @Override @@ -78,4 +78,22 @@ public void cancel() { for (T task : tasks) task.cancel(); } + @Override + public boolean contains(RenderTask task) { + if (this.equals(task)) return true; + if (taskSet.contains(task)) return true; + + for (RenderTask subTask : this.tasks) { + if (subTask.contains(task)) return true; + } + + return false; + } + + @Override + public String getDescription() { + //return description + " (" + (this.currentTaskIndex + 1) + "/" + tasks.size() + ")"; + return description; + } + } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/RenderManager.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/RenderManager.java index f099f8d4..9b12bf90 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/RenderManager.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/RenderManager.java @@ -36,6 +36,9 @@ public class RenderManager { private final int id; private volatile boolean running; + private volatile long currentTaskStartTime; + private volatile double currentTaskStartProgress; + private final AtomicInteger nextWorkerThreadIndex; private final Collection workerThreads; private final AtomicInteger busyCount; @@ -53,6 +56,9 @@ public RenderManager() { this.renderTasks = new LinkedList<>(); this.renderTaskSet = new HashSet<>(); + + this.currentTaskStartTime = System.currentTimeMillis(); + this.currentTaskStartProgress = -1; } public void start(int threadCount) throws IllegalStateException { @@ -65,6 +71,9 @@ public void start(int threadCount) throws IllegalStateException { this.running = true; + this.currentTaskStartTime = System.currentTimeMillis(); + this.currentTaskStartProgress = -1; + for (int i = 0; i < threadCount; i++) { WorkerThread worker = new WorkerThread(); this.workerThreads.add(worker); @@ -106,27 +115,38 @@ public void awaitShutdown() throws InterruptedException { public boolean scheduleRenderTask(RenderTask task) { synchronized (this.renderTasks) { - if (renderTaskSet.add(task)) { - renderTasks.addLast(task); - renderTasks.notifyAll(); - return true; - } else { - return false; + if (containsRenderTask(task)) return false; + + renderTaskSet.add(task); + renderTasks.addLast(task); + renderTasks.notifyAll(); + return true; + } + } + + public int scheduleRenderTasks(RenderTask... tasks) { + return scheduleRenderTasks(Arrays.asList(tasks)); + } + + public int scheduleRenderTasks(Collection tasks) { + synchronized (this.renderTasks) { + int count = 0; + for (RenderTask task : tasks) { + if (scheduleRenderTask(task)) count++; } + return count; } } public boolean scheduleRenderTaskNext(RenderTask task) { synchronized (this.renderTasks) { if (renderTasks.size() <= 1) return scheduleRenderTask(task); + if (containsRenderTask(task)) return false; - if (renderTaskSet.add(task)) { - renderTasks.add(1, task); - renderTasks.notifyAll(); - return true; - } else { - return false; - } + renderTaskSet.add(task); + renderTasks.add(1, task); + renderTasks.notifyAll(); + return true; } } @@ -140,7 +160,7 @@ public void reorderRenderTasks(Comparator taskComparator) { } } - public boolean removeTask(RenderTask task) { + public boolean removeRenderTask(RenderTask task) { synchronized (this.renderTasks) { if (this.renderTasks.isEmpty()) return false; @@ -156,7 +176,7 @@ public boolean removeTask(RenderTask task) { } } - public void removeAllTasks() { + public void removeAllRenderTasks() { synchronized (this.renderTasks) { if (this.renderTasks.isEmpty()) return; @@ -167,8 +187,51 @@ public void removeAllTasks() { } } + public long estimateCurrentRenderTaskTimeRemaining() { + synchronized (this.renderTasks) { + long now = System.currentTimeMillis(); + double progress = getCurrentRenderTask().estimateProgress(); + + long deltaTime = now - currentTaskStartTime; + double deltaProgress = progress - currentTaskStartProgress; + + double estimatedTotalDuration = deltaTime / deltaProgress; + double estimatedRemainingDuration = (1 - progress) * estimatedTotalDuration; + + return (long) estimatedRemainingDuration; + } + } + + public RenderTask getCurrentRenderTask() { + synchronized (this.renderTasks) { + return this.renderTasks.getFirst(); + } + } + public List getScheduledRenderTasks() { - return Collections.unmodifiableList(renderTasks); + synchronized (this.renderTasks) { + return new ArrayList<>(this.renderTasks); + } + } + + public boolean containsRenderTask(RenderTask task) { + synchronized (this.renderTasks) { + // checking all scheduled renderTasks except the first one, since that is already being processed + + // quick check + if (renderTaskSet.contains(task) && !getCurrentRenderTask().equals(task)) return true; + + // iterate over all (skipping the first) using the "contains" method + Iterator iterator = renderTasks.iterator(); + if (!iterator.hasNext()) return false; + iterator.next(); // skip first + + while(iterator.hasNext()) { + if (iterator.next().contains(task)) return true; + } + + return false; + } } public int getWorkerThreadCount() { @@ -184,12 +247,21 @@ private void doWork() throws Exception { task = this.renderTasks.getFirst(); + if (this.currentTaskStartProgress < 0) { + this.currentTaskStartTime = System.currentTimeMillis(); + this.currentTaskStartProgress = task.estimateProgress(); + } + // the following is making sure every render-thread is done working on this task (no thread is "busy") // before continuing working on the next RenderTask if (!task.hasMoreWork()) { if (busyCount.get() <= 0) { this.renderTaskSet.remove(this.renderTasks.removeFirst()); this.renderTasks.notifyAll(); + + this.currentTaskStartTime = System.currentTimeMillis(); + this.currentTaskStartProgress = -1; + busyCount.set(0); } else { this.renderTasks.wait(10000); diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/RenderTask.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/RenderTask.java index b2ac148b..3d5f7ac9 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/RenderTask.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/RenderTask.java @@ -46,4 +46,13 @@ default double estimateProgress() { */ void cancel(); + /** + * Checks if the given task is somehow included with this task + */ + default boolean contains(RenderTask task) { + return equals(task); + } + + String getDescription(); + } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/WorldRegionRenderTask.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/WorldRegionRenderTask.java index 12d4a998..f5f16844 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/WorldRegionRenderTask.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/WorldRegionRenderTask.java @@ -25,9 +25,11 @@ package de.bluecolored.bluemap.common.rendermanager; import com.flowpowered.math.vector.Vector2i; +import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.map.BmMap; import de.bluecolored.bluemap.core.world.Grid; import de.bluecolored.bluemap.core.world.Region; +import de.bluecolored.bluemap.core.world.World; import java.util.Collection; import java.util.TreeSet; @@ -66,6 +68,8 @@ private synchronized void init() { tiles = new TreeSet<>(WorldRegionRenderTask::tileComparator); startTime = System.currentTimeMillis(); + //Logger.global.logInfo("Starting: " + worldRegion); + long changesSince = 0; if (!force) changesSince = map.getRenderState().getRenderTime(worldRegion); @@ -106,6 +110,7 @@ public void doWork() { this.atWork++; } + //Logger.global.logInfo("Working on " + worldRegion + " - Tile " + tile); map.renderTile(tile); // <- actual work synchronized (this) { @@ -119,10 +124,12 @@ public void doWork() { private void complete() { map.getRenderState().setRenderTime(worldRegion, startTime); + + //Logger.global.logInfo("Done with: " + worldRegion); } @Override - public boolean hasMoreWork() { + public synchronized boolean hasMoreWork() { return !cancelled && (tiles == null || !tiles.isEmpty()); } @@ -144,6 +151,27 @@ public void cancel() { } } + public BmMap getMap() { + return map; + } + + public Vector2i getWorldRegion() { + return worldRegion; + } + + public boolean isForce() { + return force; + } + + @Override + public String getDescription() { + if (force) { + return "Render region " + getWorldRegion() + " for map '" + map.getId() + "'"; + } else { + return "Update region " + getWorldRegion() + " for map '" + map.getId() + "'"; + } + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -164,4 +192,27 @@ private static int tileComparator(Vector2i v1, Vector2i v2) { return v2.getY() - v1.getY(); } + public static int compare(WorldRegionRenderTask task1, WorldRegionRenderTask task2) { + if (task1.equals(task2)) return 0; + + int comp = task1.getMap().getId().compareTo(task2.getMap().getId()); + if (comp != 0) return comp; + + //sort based on the worlds spawn-point + World world = task1.getMap().getWorld(); + Vector2i spawnPoint = world.getSpawnPoint().toVector2(true); + Grid regionGrid = world.getRegionGrid(); + Vector2i spawnRegion = regionGrid.getCell(spawnPoint); + + Vector2i task1Rel = task1.getWorldRegion().sub(spawnRegion); + Vector2i task2Rel = task2.getWorldRegion().sub(spawnRegion); + + comp = tileComparator(task1Rel, task2Rel); + if (comp != 0) return comp; + + if (task1.isForce() == task2.isForce()) return 0; + if (task1.isForce()) return -1; + return 1; + } + } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpConnection.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpConnection.java index fc1b3b5f..1b147ac7 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpConnection.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/webserver/HttpConnection.java @@ -136,15 +136,7 @@ public boolean isClosed(){ } public void close() throws IOException { - try { - in.close(); - } finally { - try { - out.close(); - } finally { - connection.close(); - } - } + connection.close(); } public static class ConnectionClosedException extends IOException { diff --git a/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java b/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java index f7b5dc9d..9c6e90f4 100644 --- a/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java +++ b/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java @@ -86,11 +86,13 @@ public void renderMaps(BlueMapService blueMap, boolean watch, boolean forceRende } //update all maps - for (BmMap map : maps.values()) { - for (Vector2i region : map.getWorld().listRegions()){ - renderManager.scheduleRenderTask(new WorldRegionRenderTask(map, region, forceRender)); - } - } + List tasks = new ArrayList<>(); + for (BmMap map : maps.values()) + for (Vector2i region : map.getWorld().listRegions()) + tasks.add(new WorldRegionRenderTask(map, region, forceRender)); + tasks.sort(WorldRegionRenderTask::compare); + tasks.forEach(renderManager::scheduleRenderTask); + int totalRegions = renderManager.getScheduledRenderTasks().size(); Logger.global.logInfo("Start " + (forceRender ? "rendering " : "updating ") + maps.size() + " maps (" + totalRegions + " regions, ~" + totalRegions * 1024L + " chunks)..."); @@ -115,7 +117,7 @@ public void run() { long etr = (long) ((elapsedTime / progress) * (1 - progress)); String etrDurationString = DurationFormatUtils.formatDuration(etr, "HH:mm:ss"); - Logger.global.logInfo("Rendering: " + (Math.round(progress * 100000) / 1000.0) + "% (ETR: " + etrDurationString + ")"); + Logger.global.logInfo("Rendering: " + (Math.round(progress * 100000) / 1000.0) + "% (ETA: " + etrDurationString + ")"); } }; timer.scheduleAtFixedRate(updateInfoTask, TimeUnit.SECONDS.toMillis(10), TimeUnit.SECONDS.toMillis(10));