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 b8f16018..8c590e10 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 @@ -47,6 +47,8 @@ import de.bluecolored.bluemap.core.webserver.HttpRequestHandler; import de.bluecolored.bluemap.core.webserver.WebServer; import de.bluecolored.bluemap.core.world.World; +import org.spongepowered.configurate.gson.GsonConfigurationLoader; +import org.spongepowered.configurate.serialize.SerializationException; import java.io.File; import java.io.IOException; @@ -72,6 +74,8 @@ public class Plugin { private WebServerConfig webServerConfig; private PluginConfig pluginConfig; + private PluginStatus pluginStatus; + private Map worlds; private Map maps; @@ -82,7 +86,7 @@ public class Plugin { private TimerTask saveTask; private TimerTask metricsTask; - private Collection regionFileWatchServices; + private Map regionFileWatchServices; private PlayerSkinUpdater skinUpdater; @@ -118,6 +122,17 @@ public void load() throws IOException, ParseResourceException { true )); + //load plugin status + try { + GsonConfigurationLoader loader = GsonConfigurationLoader.builder() + .file(new File(getCoreConfig().getDataFolder(), "pluginStatus.json")) + .build(); + pluginStatus = loader.load().get(PluginStatus.class); + } catch (SerializationException ex) { + Logger.global.logWarning("Failed to load pluginStatus.json (invalid format), creating a new one..."); + pluginStatus = new PluginStatus(); + } + //create and start webserver if (webServerConfig.isWebserverEnabled()) { FileUtils.mkDirs(webServerConfig.getWebRoot()); @@ -168,11 +183,17 @@ public void load() throws IOException, ParseResourceException { //update all maps for (BmMap map : maps.values()) { - renderManager.scheduleRenderTask(new MapUpdateTask(map)); + if (pluginStatus.getMapStatus(map).isUpdateEnabled()) { + renderManager.scheduleRenderTask(new MapUpdateTask(map)); + } } //start render-manager - renderManager.start(coreConfig.getRenderThreadCount()); + if (pluginStatus.isRenderThreadsEnabled()) { + renderManager.start(coreConfig.getRenderThreadCount()); + } else { + Logger.global.logInfo("Render-Threads are STOPPED! Use the command 'bluemap start' to start them."); + } //update webapp and settings blueMap.createOrUpdateWebApp(false); @@ -194,12 +215,7 @@ public void load() throws IOException, ParseResourceException { saveTask = new TimerTask() { @Override public void run() { - synchronized (Plugin.this) { - if (maps == null) return; - for (BmMap map : maps.values()) { - map.save(); - } - } + save(); } }; daemonTimer.schedule(saveTask, TimeUnit.MINUTES.toMillis(2), TimeUnit.MINUTES.toMillis(2)); @@ -215,14 +231,10 @@ public void run() { daemonTimer.scheduleAtFixedRate(metricsTask, TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(30)); //watch map-changes - this.regionFileWatchServices = new ArrayList<>(); + this.regionFileWatchServices = new HashMap<>(); for (BmMap map : maps.values()) { - try { - RegionFileWatchService watcher = new RegionFileWatchService(renderManager, map, false); - watcher.start(); - regionFileWatchServices.add(watcher); - } catch (IOException ex) { - Logger.global.logError("Failed to create file-watcher for map: " + map.getId() + " (This map might not automatically update)", ex); + if (pluginStatus.getMapStatus(map).isUpdateEnabled()) { + startWatchingMap(map); } } @@ -245,6 +257,8 @@ public void unload() { try { loadingLock.interruptAndLock(); synchronized (this) { + //save + save(); //disable api if (api != null) api.unregister(); @@ -264,7 +278,7 @@ public void unload() { //stop file-watchers if (regionFileWatchServices != null) { - for (RegionFileWatchService watcher : regionFileWatchServices) { + for (RegionFileWatchService watcher : regionFileWatchServices.values()) { watcher.close(); } regionFileWatchServices.clear(); @@ -278,13 +292,6 @@ public void unload() { if (webServer != null) webServer.close(); webServer = null; - //save renders - if (maps != null) { - for (BmMap map : maps.values()) { - map.save(); - } - } - //clear resources and configs blueMap = null; worlds = null; @@ -295,6 +302,8 @@ public void unload() { webServerConfig = null; pluginConfig = null; + pluginStatus = null; + //done loaded = false; } @@ -308,6 +317,44 @@ public void reload() throws IOException, ParseResourceException { load(); } + public synchronized void save() { + if (pluginStatus != null) { + try { + GsonConfigurationLoader loader = GsonConfigurationLoader.builder() + .file(new File(getCoreConfig().getDataFolder(), "pluginStatus.json")) + .build(); + loader.save(loader.createNode().set(PluginStatus.class, pluginStatus)); + } catch (IOException ex) { + Logger.global.logError("Failed to save pluginStatus.json!", ex); + } + } + + if (maps != null) { + for (BmMap map : maps.values()) { + map.save(); + } + } + } + + public synchronized void startWatchingMap(BmMap map) { + stopWatchingMap(map); + + try { + RegionFileWatchService watcher = new RegionFileWatchService(renderManager, map, false); + watcher.start(); + regionFileWatchServices.put(map.getId(), watcher); + } catch (IOException ex) { + Logger.global.logError("Failed to create file-watcher for map: " + map.getId() + " (This means the map might not automatically update)", ex); + } + } + + public synchronized void stopWatchingMap(BmMap map) { + RegionFileWatchService watcher = regionFileWatchServices.remove(map.getId()); + if (watcher != null) { + watcher.close(); + } + } + public boolean flushWorldUpdates(UUID worldUUID) throws IOException { return serverInterface.persistWorldChanges(worldUUID); } @@ -331,6 +378,10 @@ public WebServerConfig getWebServerConfig() { public PluginConfig getPluginConfig() { return pluginConfig; } + + public PluginStatus getPluginStatus() { + return pluginStatus; + } public World getWorld(UUID uuid){ return worlds.get(uuid); diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/PluginStatus.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/PluginStatus.java index 22ec3581..98c9b2ac 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/PluginStatus.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/PluginStatus.java @@ -24,11 +24,44 @@ */ package de.bluecolored.bluemap.common.plugin; +import de.bluecolored.bluemap.core.map.BmMap; import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("FieldMayBeFinal") @ConfigSerializable public class PluginStatus { + private boolean renderThreadsEnabled = true; + private Map maps = new HashMap<>(); + public boolean isRenderThreadsEnabled() { + return renderThreadsEnabled; + } + + public void setRenderThreadsEnabled(boolean renderThreadsEnabled) { + this.renderThreadsEnabled = renderThreadsEnabled; + } + + public MapStatus getMapStatus(BmMap map) { + return maps.computeIfAbsent(map.getId(), k -> new MapStatus()); + } + + @ConfigSerializable + public static class MapStatus { + + private boolean updateEnabled = true; + + public boolean isUpdateEnabled() { + return updateEnabled; + } + + public void setUpdateEnabled(boolean update) { + this.updateEnabled = update; + } + + } } 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 592d3fea..c5d7378d 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 @@ -44,13 +44,12 @@ import de.bluecolored.bluemap.api.marker.MarkerSet; import de.bluecolored.bluemap.api.marker.POIMarker; import de.bluecolored.bluemap.common.plugin.Plugin; +import de.bluecolored.bluemap.common.plugin.PluginStatus; import de.bluecolored.bluemap.common.plugin.serverinterface.CommandSource; 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.MapPurgeTask; -import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask; -import de.bluecolored.bluemap.common.rendermanager.RenderTask; +import de.bluecolored.bluemap.common.rendermanager.*; import de.bluecolored.bluemap.core.BlueMap; import de.bluecolored.bluemap.core.MinecraftVersion; import de.bluecolored.bluemap.core.logger.Logger; @@ -140,18 +139,32 @@ public void init() { .build(); - LiteralCommandNode pauseCommand = + LiteralCommandNode stopCommand = literal("stop") .requires(requirements("bluemap.stop")) .executes(this::stopCommand) .build(); - LiteralCommandNode resumeCommand = + LiteralCommandNode startCommand = literal("start") .requires(requirements("bluemap.start")) .executes(this::startCommand) .build(); + LiteralCommandNode freezeCommand = + literal("freeze") + .requires(requirements("bluemap.freeze")) + .then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin)) + .executes(this::freezeCommand)) + .build(); + + LiteralCommandNode unfreezeCommand = + literal("unfreeze") + .requires(requirements("bluemap.freeze")) + .then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin)) + .executes(this::unfreezeCommand)) + .build(); + LiteralCommandNode forceUpdateCommand = addRenderArguments( literal("force-update") @@ -227,8 +240,10 @@ public void init() { baseCommand.addChild(helpCommand); baseCommand.addChild(reloadCommand); baseCommand.addChild(debugCommand); - baseCommand.addChild(pauseCommand); - baseCommand.addChild(resumeCommand); + baseCommand.addChild(stopCommand); + baseCommand.addChild(startCommand); + baseCommand.addChild(freezeCommand); + baseCommand.addChild(unfreezeCommand); baseCommand.addChild(forceUpdateCommand); baseCommand.addChild(updateCommand); baseCommand.addChild(cancelCommand); @@ -521,26 +536,113 @@ public int stopCommand(CommandContext context) { CommandSource source = commandSourceInterface.apply(context.getSource()); if (plugin.getRenderManager().isRunning()) { - plugin.getRenderManager().stop(); - source.sendMessage(Text.of(TextColor.GREEN, "Render-Threads stopped!")); - return 1; + new Thread(() -> { + plugin.getPluginStatus().setRenderThreadsEnabled(false); + + plugin.getRenderManager().stop(); + source.sendMessage(Text.of(TextColor.GREEN, "Render-Threads stopped!")); + + plugin.save(); + }).start(); } else { source.sendMessage(Text.of(TextColor.RED, "Render-Threads are already stopped!")); return 0; } + + return 1; } 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, "Render-Threads started!")); - return 1; + new Thread(() -> { + plugin.getPluginStatus().setRenderThreadsEnabled(true); + + plugin.getRenderManager().start(plugin.getCoreConfig().getRenderThreadCount()); + source.sendMessage(Text.of(TextColor.GREEN, "Render-Threads started!")); + + plugin.save(); + }).start(); } else { source.sendMessage(Text.of(TextColor.RED, "Render-Threads are already running!")); return 0; } + + return 1; + } + + public int freezeCommand(CommandContext context) { + CommandSource source = commandSourceInterface.apply(context.getSource()); + + // parse map argument + String mapString = context.getArgument("map", String.class); + BmMap map = parseMap(mapString).orElse(null); + + if (map == null) { + source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString)); + return 0; + } + + PluginStatus.MapStatus mapStatus = plugin.getPluginStatus().getMapStatus(map); + if (mapStatus.isUpdateEnabled()) { + new Thread(() -> { + mapStatus.setUpdateEnabled(false); + + plugin.stopWatchingMap(map); + plugin.getRenderManager().removeRenderTasksIf(task -> { + if (task instanceof MapUpdateTask) + return ((MapUpdateTask) task).getMap().equals(map); + + if (task instanceof WorldRegionRenderTask) + return ((WorldRegionRenderTask) task).getMap().equals(map); + + return false; + }); + + source.sendMessage(Text.of(TextColor.GREEN, "Map ", TextColor.WHITE, mapString, TextColor.GREEN, " is now frozen and will no longer be automatically updated!")); + source.sendMessage(Text.of(TextColor.GRAY, "Any currently scheduled updates for this map have been cancelled.")); + + plugin.save(); + }).start(); + } else { + source.sendMessage(Text.of(TextColor.RED, "This map is already frozen!")); + return 0; + } + + return 1; + } + + public int unfreezeCommand(CommandContext context) { + CommandSource source = commandSourceInterface.apply(context.getSource()); + + // parse map argument + String mapString = context.getArgument("map", String.class); + BmMap map = parseMap(mapString).orElse(null); + + if (map == null) { + source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString)); + return 0; + } + + PluginStatus.MapStatus mapStatus = plugin.getPluginStatus().getMapStatus(map); + if (!mapStatus.isUpdateEnabled()) { + new Thread(() -> { + mapStatus.setUpdateEnabled(true); + + plugin.startWatchingMap(map); + plugin.getRenderManager().scheduleRenderTask(new MapUpdateTask(map)); + + source.sendMessage(Text.of(TextColor.GREEN, "Map ", TextColor.WHITE, mapString, TextColor.GREEN, " is no longer frozen and will be automatically updated!")); + + plugin.save(); + }).start(); + } else { + source.sendMessage(Text.of(TextColor.RED, "This map is not frozen!")); + return 0; + } + + return 1; } public int forceUpdateCommand(CommandContext context) { @@ -727,7 +829,21 @@ public int mapsCommand(CommandContext context) { source.sendMessage(Text.of(TextColor.BLUE, "Maps loaded by BlueMap:")); for (BmMap map : plugin.getMapTypes()) { - source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, map.getId(), TextColor.GRAY, " (" + map.getName() + ")").setHoverText(Text.of(TextColor.WHITE, "World: ", TextColor.GRAY, map.getWorld().getName()))); + boolean unfrozen = plugin.getPluginStatus().getMapStatus(map).isUpdateEnabled(); + if (unfrozen) { + source.sendMessage(Text.of( + TextColor.GRAY, " - ", + TextColor.WHITE, map.getId(), + TextColor.GRAY, " (" + map.getName() + ")" + ).setHoverText(Text.of(TextColor.WHITE, "World: ", TextColor.GRAY, map.getWorld().getName()))); + } else { + source.sendMessage(Text.of( + TextColor.GRAY, " - ", + TextColor.WHITE, map.getId(), + TextColor.GRAY, " (" + map.getName() + ") - ", + TextColor.AQUA, TextFormat.ITALIC, "frozen!" + ).setHoverText(Text.of(TextColor.WHITE, "World: ", TextColor.GRAY, map.getWorld().getName()))); + } } return 1; @@ -741,7 +857,7 @@ public int createMarkerCommand(CommandContext context) { .replace("<", "<") .replace(">", ">"); //no html via commands - // parse world/map argument + // parse map argument String mapString = context.getArgument("map", String.class); BmMap map = parseMap(mapString).orElse(null); 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 09ecfb38..9291eda4 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 @@ -29,6 +29,7 @@ import java.util.*; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; public class RenderManager { private static final AtomicInteger nextRenderManagerIndex = new AtomicInteger(0); @@ -176,6 +177,17 @@ public boolean removeRenderTask(RenderTask task) { } } + public void removeRenderTasksIf(Predicate removeCondition) { + synchronized (this.renderTasks) { + if (this.renderTasks.isEmpty()) return; + + RenderTask first = renderTasks.removeFirst(); + if (removeCondition.test(first)) first.cancel(); + renderTasks.removeIf(removeCondition); + renderTasks.addFirst(first); + } + } + public void removeAllRenderTasks() { synchronized (this.renderTasks) { if (this.renderTasks.isEmpty()) return;