Add cancel command, rework purge command and rename render command

This commit is contained in:
Blue (Lukas Rieger) 2021-05-11 00:34:12 +02:00
parent e40e45ad12
commit 00d7d3e397
No known key found for this signature in database
GPG Key ID: 904C4995F9E1F800
6 changed files with 304 additions and 40 deletions

View File

@ -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<String, WeakReference<RenderTask>> taskRefMap;
public CommandHelper(Plugin plugin) {
this.plugin = plugin;
this.taskRefMap = new HashMap<>();
}
public List<Text> createStatusMessage(){
@ -77,15 +80,15 @@ public List<Text> 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<Vector2i> getRegions(World world) {
return getRegions(world, null, -1);
}
public List<Vector2i> getRegions(World world, Vector2i center, int radius) {
if (center == null || radius < 0) return new ArrayList<>(world.listRegions());
@ -152,4 +159,49 @@ public List<Vector2i> 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<Vector2i> regions) {
List<WorldRegionRenderTask> 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<RenderTask> getTaskForRef(String ref) {
return Optional.ofNullable(taskRefMap.get(ref)).map(WeakReference::get);
}
public synchronized Collection<String> getTaskRefs() {
return new ArrayList<>(taskRefMap.keySet());
}
private synchronized String getRefForTask(RenderTask task) {
Iterator<Map.Entry<String, WeakReference<RenderTask>>> iterator = taskRefMap.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, WeakReference<RenderTask>> 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();
}
}

View File

@ -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<S> renderCommand =
LiteralCommandNode<S> forceUpdateCommand =
addRenderArguments(
literal("render")
.requires(requirements("bluemap.render")),
this::renderCommand
literal("force-update")
.requires(requirements("bluemap.update.force")),
this::forceUpdateCommand
).build();
LiteralCommandNode<S> updateCommand =
@ -171,9 +171,17 @@ public void init() {
LiteralCommandNode<S> 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<S> 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<S> 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<S> context) {
}
}
public int renderCommand(CommandContext<S> context) {
public int forceUpdateCommand(CommandContext<S> context) {
return updateCommand(context, true);
}
@ -579,7 +588,7 @@ public int updateCommand(CommandContext<S> 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 <world|map>")));
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") + " <world|map>")));
return 0;
}
}
@ -596,7 +605,7 @@ public int updateCommand(CommandContext<S> 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 <x> <z> " + 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") + " <x> <z> " + radius)));
return 0;
}
@ -622,32 +631,21 @@ public int updateCommand(CommandContext<S> context, boolean force) {
world = mapToRender.getWorld();
}
String taskType = "Update";
List<Vector2i> 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<WorldRegionRenderTask> 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<S> context, boolean force) {
return 1;
}
public int cancelCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
Optional<String> 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<RenderTask> 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<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
@ -665,14 +689,34 @@ public int purgeCommand(CommandContext<S> 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<BmMap> 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);

View File

@ -0,0 +1,42 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.plugin.commands;
import java.util.Collection;
public class TaskRefSuggestionProvider<S> extends AbstractSuggestionProvider<S> {
private CommandHelper helper;
public TaskRefSuggestionProvider(CommandHelper helper) {
this.helper = helper;
}
@Override
public Collection<String> getPossibleValues() {
return helper.getTaskRefs();
}
}

View File

@ -0,0 +1,118 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.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<Path> 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();
}
}

View File

@ -112,6 +112,10 @@ public World getWorld() {
return world;
}
public Path getFileRoot() {
return fileRoot;
}
public MapRenderState getRenderState() {
return renderState;
}

View File

@ -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);