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 c55df93b..cc94aa65 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 @@ -28,23 +28,26 @@ 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.CombinedRenderTask; import de.bluecolored.bluemap.common.rendermanager.RenderManager; import de.bluecolored.bluemap.common.rendermanager.RenderTask; +import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask; 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; -import java.util.StringJoiner; +import java.lang.ref.WeakReference; +import java.util.*; public class CommandHelper { private final Plugin plugin; + private final Map> taskRefMap; public CommandHelper(Plugin plugin) { this.plugin = plugin; + this.taskRefMap = new HashMap<>(); } public List createStatusMessage(){ @@ -77,15 +80,15 @@ public List createStatusMessage(){ } RenderTask task = tasks.get(i); - lines.add(Text.of(TextColor.GRAY, " - ", TextColor.GOLD, task.getDescription())); + lines.add(Text.of(TextColor.GRAY, " [" + getRefForTask(task) + "] ", TextColor.GOLD, task.getDescription())); if (i == 0) { - lines.add(Text.of(TextColor.GRAY, " Progress: ", TextColor.WHITE, + lines.add(Text.of(TextColor.GRAY, " Progress: ", TextColor.WHITE, (Math.round(task.estimateProgress() * 10000) / 100.0) + "%")); long etaMs = renderer.estimateCurrentRenderTaskTimeRemaining(); if (etaMs > 0) { - lines.add(Text.of(TextColor.GRAY, " ETA: ", TextColor.WHITE, DurationFormatUtils.formatDuration(etaMs, "HH:mm:ss"))); + lines.add(Text.of(TextColor.GRAY, " ETA: ", TextColor.WHITE, DurationFormatUtils.formatDuration(etaMs, "HH:mm:ss"))); } } } @@ -132,6 +135,10 @@ public Text mapHelperHover() { return Text.of("map").setHoverText(Text.of(TextColor.WHITE, "Available maps: \n", TextColor.GRAY, joiner.toString())); } + public List getRegions(World world) { + return getRegions(world, null, -1); + } + public List getRegions(World world, Vector2i center, int radius) { if (center == null || radius < 0) return new ArrayList<>(world.listRegions()); @@ -152,4 +159,49 @@ public List getRegions(World world, Vector2i center, int radius) { return regions; } + public RenderTask createMapUpdateTask(BmMap map) { + return createMapUpdateTask(map, getRegions(map.getWorld())); + } + + public RenderTask createMapUpdateTask(BmMap map, Collection regions) { + List tasks = new ArrayList<>(regions.size()); + regions.forEach(region -> tasks.add(new WorldRegionRenderTask(map, region))); + tasks.sort(WorldRegionRenderTask::compare); + return new CombinedRenderTask<>("Update map '" + map.getId() + "'", tasks); + } + + public synchronized Optional getTaskForRef(String ref) { + return Optional.ofNullable(taskRefMap.get(ref)).map(WeakReference::get); + } + + public synchronized Collection getTaskRefs() { + return new ArrayList<>(taskRefMap.keySet()); + } + + private synchronized String getRefForTask(RenderTask task) { + Iterator>> iterator = taskRefMap.entrySet().iterator(); + while (iterator.hasNext()){ + Map.Entry> entry = iterator.next(); + if (entry.getValue().get() == null) iterator.remove(); + if (entry.getValue().get() == task) return entry.getKey(); + } + + String newRef = safeRandomRef(); + + taskRefMap.put(newRef, new WeakReference<>(task)); + return newRef; + } + + private synchronized String safeRandomRef() { + String ref = randomRef(); + while (taskRefMap.containsKey(ref)) ref = randomRef(); + return ref; + } + + private String randomRef() { + StringBuilder ref = new StringBuilder(Integer.toString(Math.abs(new Random().nextInt()), 16)); + while (ref.length() < 4) ref.insert(0, "0"); + return ref.subSequence(0, 4).toString(); + } + } 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 72c32627..038f544d 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 @@ -49,8 +49,8 @@ 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.common.rendermanager.MapPurgeTask; +import de.bluecolored.bluemap.common.rendermanager.RenderTask; import de.bluecolored.bluemap.core.BlueMap; import de.bluecolored.bluemap.core.MinecraftVersion; import de.bluecolored.bluemap.core.logger.Logger; @@ -62,10 +62,10 @@ import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; import de.bluecolored.bluemap.core.world.Block; import de.bluecolored.bluemap.core.world.World; -import org.apache.commons.io.FileUtils; -import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -155,11 +155,11 @@ public void init() { .executes(this::startCommand) .build(); - LiteralCommandNode renderCommand = + LiteralCommandNode forceUpdateCommand = addRenderArguments( - literal("render") - .requires(requirements("bluemap.render")), - this::renderCommand + literal("force-update") + .requires(requirements("bluemap.update.force")), + this::forceUpdateCommand ).build(); LiteralCommandNode updateCommand = @@ -171,9 +171,17 @@ public void init() { LiteralCommandNode purgeCommand = literal("purge") - .requires(requirements("bluemap.render")) - .then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin)) - .executes(this::purgeCommand)) + .requires(requirements("bluemap.purge")) + .then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin)) + .executes(this::purgeCommand)) + .build(); + + LiteralCommandNode cancelCommand = + literal("cancel") + .requires(requirements("bluemap.cancel")) + .executes(this::cancelCommand) + .then(argument("task-ref", StringArgumentType.string()).suggests(new TaskRefSuggestionProvider<>(helper)) + .executes(this::cancelCommand)) .build(); LiteralCommandNode worldsCommand = @@ -224,8 +232,9 @@ public void init() { baseCommand.addChild(debugCommand); baseCommand.addChild(pauseCommand); baseCommand.addChild(resumeCommand); - baseCommand.addChild(renderCommand); + baseCommand.addChild(forceUpdateCommand); baseCommand.addChild(updateCommand); + baseCommand.addChild(cancelCommand); baseCommand.addChild(purgeCommand); baseCommand.addChild(worldsCommand); baseCommand.addChild(mapsCommand); @@ -545,7 +554,7 @@ public int startCommand(CommandContext context) { } } - public int renderCommand(CommandContext context) { + public int forceUpdateCommand(CommandContext context) { return updateCommand(context, true); } @@ -579,7 +588,7 @@ public int updateCommand(CommandContext context, boolean force) { mapToRender = 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 "))); + 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 update!").setHoverText(Text.of(TextColor.GRAY, "/bluemap " + (force ? "force-update" : "update") + " "))); return 0; } } @@ -596,7 +605,7 @@ public int updateCommand(CommandContext context, boolean force) { } else { Vector3d position = source.getPosition().orElse(null); if (position == null) { - source.sendMessage(Text.of(TextColor.RED, "Can't detect a position from this command-source, you'll have to define x,z coordinates to render with a radius!").setHoverText(Text.of(TextColor.GRAY, "/bluemap render " + radius))); + source.sendMessage(Text.of(TextColor.RED, "Can't detect a position from this command-source, you'll have to define x,z coordinates to update with a radius!").setHoverText(Text.of(TextColor.GRAY, "/bluemap " + (force ? "force-update" : "update") + " " + radius))); return 0; } @@ -622,32 +631,21 @@ public int updateCommand(CommandContext context, boolean force) { 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)")); + plugin.getRenderManager().scheduleRenderTask(helper.createMapUpdateTask(map, regions)); + source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-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); @@ -656,6 +654,32 @@ public int updateCommand(CommandContext context, boolean force) { return 1; } + + public int cancelCommand(CommandContext context) { + CommandSource source = commandSourceInterface.apply(context.getSource()); + + Optional ref = getOptionalArgument(context,"task-ref", String.class); + if (!ref.isPresent()) { + plugin.getRenderManager().removeAllRenderTasks(); + source.sendMessage(Text.of(TextColor.GREEN, "All tasks cancelled!")); + return 1; + } + + Optional task = helper.getTaskForRef(ref.get()); + + if (!task.isPresent()) { + source.sendMessage(Text.of(TextColor.RED, "There is no task with this reference '" + ref.get() + "'!")); + return 0; + } + + if (plugin.getRenderManager().removeRenderTask(task.get())) { + source.sendMessage(Text.of(TextColor.GREEN, "Task cancelled!")); + return 1; + } else { + source.sendMessage(Text.of(TextColor.RED, "This task is either completed or got cancelled already!")); + return 0; + } + } public int purgeCommand(CommandContext context) { CommandSource source = commandSourceInterface.apply(context.getSource()); @@ -665,14 +689,34 @@ public int purgeCommand(CommandContext context) { new Thread(() -> { try { - File mapFolder = new File(plugin.getRenderConfig().getWebRoot(), "data" + File.separator + mapId); - if (!mapFolder.exists() || !mapFolder.isDirectory()) { + Path mapFolder = plugin.getRenderConfig().getWebRoot().toPath().resolve("data").resolve(mapId); + if (!Files.isDirectory(mapFolder)) { source.sendMessage(Text.of(TextColor.RED, "There is no map-data to purge for the map-id '" + mapId + "'!")); return; } - - FileUtils.deleteDirectory(mapFolder); - source.sendMessage(Text.of(TextColor.GREEN, "Map '" + mapId + "' has been successfully purged!")); + + Optional optMap = parseMap(mapId); + + // delete map + MapPurgeTask purgeTask; + if (optMap.isPresent()){ + purgeTask = new MapPurgeTask(optMap.get()); + } else { + purgeTask = new MapPurgeTask(mapFolder); + } + + plugin.getRenderManager().scheduleRenderTaskNext(purgeTask); + source.sendMessage(Text.of(TextColor.GREEN, "Created new Task to purge map '" + mapId + "'")); + + // if map is loaded, reset it and start updating it after the purge + if (optMap.isPresent()) { + RenderTask updateTask = helper.createMapUpdateTask(optMap.get()); + plugin.getRenderManager().scheduleRenderTask(updateTask); + source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + mapId + "'")); + source.sendMessage(Text.of(TextColor.GRAY, "If you don't want to render this map again, you need to remove it from your configuration first!")); + } + + source.sendMessage(Text.of(TextColor.GREEN, "Use ", TextColor.GRAY, "/bluemap", TextColor.GREEN, " to see the progress.")); } catch (IOException | IllegalArgumentException e) { source.sendMessage(Text.of(TextColor.RED, "There was an error trying to purge '" + mapId + "', see console for details.")); Logger.global.logError("Failed to purge map '" + mapId + "'!", e); diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/TaskRefSuggestionProvider.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/TaskRefSuggestionProvider.java new file mode 100644 index 00000000..2e49ad01 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/commands/TaskRefSuggestionProvider.java @@ -0,0 +1,42 @@ +/* + * 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.common.plugin.commands; + +import java.util.Collection; + +public class TaskRefSuggestionProvider extends AbstractSuggestionProvider { + + private CommandHelper helper; + + public TaskRefSuggestionProvider(CommandHelper helper) { + this.helper = helper; + } + + @Override + public Collection getPossibleValues() { + return helper.getTaskRefs(); + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/MapPurgeTask.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/MapPurgeTask.java new file mode 100644 index 00000000..5303049a --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/MapPurgeTask.java @@ -0,0 +1,118 @@ +/* + * 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.common.rendermanager; + +import de.bluecolored.bluemap.core.map.BmMap; +import de.bluecolored.bluemap.core.util.FileUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.stream.Collectors; + +public class MapPurgeTask implements RenderTask { + + private final BmMap map; + private final Path directory; + private final int subFilesCount; + private final LinkedList subFiles; + + private volatile boolean hasMoreWork; + private volatile boolean cancelled; + + public MapPurgeTask(Path mapDirectory) throws IOException { + this(null, mapDirectory); + } + + public MapPurgeTask(BmMap map) throws IOException { + this(map, map.getFileRoot()); + } + + private MapPurgeTask(BmMap map, Path directory) throws IOException { + this.map = map; + this.directory = directory; + this.subFiles = Files.walk(directory, 3) + .collect(Collectors.toCollection(LinkedList::new)); + this.subFilesCount = subFiles.size(); + this.hasMoreWork = true; + this.cancelled = false; + } + + @Override + public void doWork() throws Exception { + synchronized (this) { + if (!this.hasMoreWork) return; + this.hasMoreWork = false; + } + + // delete subFiles first to be able to track the progress and cancel + while (!subFiles.isEmpty()) { + Path subFile = subFiles.getLast(); + FileUtils.delete(subFile.toFile()); + subFiles.removeLast(); + if (this.cancelled) return; + } + + // make sure everything is deleted + FileUtils.delete(directory.toFile()); + + // reset map render state + if (this.map != null) { + this.map.getRenderState().reset(); + } + } + + @Override + public boolean hasMoreWork() { + return this.hasMoreWork; + } + + @Override + public double estimateProgress() { + return 1d - (subFiles.size() / (double) subFilesCount); + } + + @Override + public void cancel() { + this.cancelled = true; + } + + @Override + public boolean contains(RenderTask task) { + if (task == this) return true; + if (task instanceof MapPurgeTask) { + return ((MapPurgeTask) task).directory.toAbsolutePath().normalize().startsWith(this.directory.toAbsolutePath().normalize()); + } + + return false; + } + + @Override + public String getDescription() { + return "Purge Map " + directory.getFileName(); + } + +} diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/map/BmMap.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/map/BmMap.java index 1614e023..2bd73178 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/map/BmMap.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/map/BmMap.java @@ -112,6 +112,10 @@ public World getWorld() { return world; } + public Path getFileRoot() { + return fileRoot; + } + public MapRenderState getRenderState() { return renderState; } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/map/MapRenderState.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/map/MapRenderState.java index 96ce4682..87bb2eb1 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/map/MapRenderState.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/map/MapRenderState.java @@ -51,6 +51,10 @@ public synchronized long getRenderTime(Vector2i regionPos) { else return renderTime; } + public synchronized void reset() { + regionRenderTimes.clear(); + } + public synchronized void save(File file) throws IOException { OutputStream fOut = AtomicFileHelper.createFilepartOutputStream(file); GZIPOutputStream gOut = new GZIPOutputStream(fOut);