diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/AddonException.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/AddonException.java new file mode 100644 index 00000000..136c4455 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/AddonException.java @@ -0,0 +1,6 @@ +package de.bluecolored.bluemap.common.addons; + +import lombok.experimental.StandardException; + +@StandardException +public class AddonException extends Exception {} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/AddonInfo.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/AddonInfo.java new file mode 100644 index 00000000..b96f7510 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/AddonInfo.java @@ -0,0 +1,12 @@ +package de.bluecolored.bluemap.common.addons; + +import lombok.Getter; + +@Getter +public class AddonInfo { + public static final String ADDON_INFO_FILE = "bluemap.addon.json"; + + private String id; + private String entrypoint; + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/AddonManager.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/AddonManager.java new file mode 100644 index 00000000..0ff869f3 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/AddonManager.java @@ -0,0 +1,139 @@ +package de.bluecolored.bluemap.common.addons; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import de.bluecolored.bluemap.core.BlueMap; +import de.bluecolored.bluemap.core.logger.Logger; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.Reader; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import static de.bluecolored.bluemap.common.addons.AddonInfo.ADDON_INFO_FILE; + +public final class AddonManager { + + private static final Gson GSON = new GsonBuilder().create(); + private static final Map LOADED_ADDONS = new ConcurrentHashMap<>(); + + private AddonManager() { + throw new UnsupportedOperationException("Utility class"); + } + + public static void tryLoadAddons(Path root) { + tryLoadAddons(root, false); + } + + public static void tryLoadAddons(Path root, boolean expectOnlyAddons) { + try (Stream files = Files.list(root)) { + files + .filter(Files::isRegularFile) + .filter(f -> f.getFileName().toString().endsWith(".jar")) + .forEach(expectOnlyAddons ? AddonManager::tryLoadAddon : AddonManager::tryLoadJar); + } catch (IOException e) { + Logger.global.logError("Failed to load addons from '%s'".formatted(root), e); + } + } + + public static void tryLoadAddon(Path addonJarFile) { + try { + AddonInfo addonInfo = loadAddonInfo(addonJarFile); + if (addonInfo == null) throw new AddonException("No %s found in '%s'".formatted(ADDON_INFO_FILE, addonJarFile)); + + if (LOADED_ADDONS.containsKey(addonInfo.getId())) return; + + loadAddon(addonJarFile, addonInfo); + } catch (IOException | AddonException e) { + Logger.global.logError("Failed to load addon '%s'".formatted(addonJarFile), e); + } + } + + public static void tryLoadJar(Path addonJarFile) { + try { + AddonInfo addonInfo = loadAddonInfo(addonJarFile); + if (addonInfo == null) { + Logger.global.logDebug("No %s found in '%s', skipping...".formatted(ADDON_INFO_FILE, addonJarFile)); + return; + } + + if (LOADED_ADDONS.containsKey(addonInfo.getId())) return; + + loadAddon(addonJarFile, addonInfo); + } catch (IOException | AddonException e) { + Logger.global.logError("Failed to load addon '%s'".formatted(addonJarFile), e); + } + } + + public synchronized static void loadAddon(Path jarFile, AddonInfo addonInfo) throws AddonException { + Logger.global.logInfo("Loading BlueMap Addon: %s (%s)".formatted(addonInfo.getId(), jarFile)); + + if (LOADED_ADDONS.containsKey(addonInfo.getId())) + throw new AddonException("Addon with id '%s' is already loaded".formatted(addonInfo.getId())); + + try { + ClassLoader addonClassLoader = BlueMap.class.getClassLoader(); + Class entrypointClass; + + // try to find entrypoint class and load jar with new classloader if needed + try { + entrypointClass = BlueMap.class.getClassLoader().loadClass(addonInfo.getEntrypoint()); + } catch (ClassNotFoundException e) { + addonClassLoader = new URLClassLoader( + new URL[]{ jarFile.toUri().toURL() }, + BlueMap.class.getClassLoader() + ); + entrypointClass = addonClassLoader.loadClass(addonInfo.getEntrypoint()); + } + + // create addon instance + Object instance = entrypointClass.getConstructor().newInstance(); + LoadedAddon addon = new LoadedAddon( + addonInfo, + addonClassLoader, + instance + ); + LOADED_ADDONS.put(addonInfo.getId(), addon); + + // run addon + if (instance instanceof Runnable runnable) + runnable.run(); + + } catch (Exception e) { + throw new AddonException("Failed to load addon '%s'".formatted(jarFile), e); + } + } + + public static @Nullable AddonInfo loadAddonInfo(Path addonJarFile) throws IOException, AddonException { + try (FileSystem fileSystem = FileSystems.newFileSystem(addonJarFile, (ClassLoader) null)) { + for (Path root : fileSystem.getRootDirectories()) { + Path addonInfoFile = root.resolve(ADDON_INFO_FILE); + if (!Files.exists(addonInfoFile)) continue; + + try (Reader reader = Files.newBufferedReader(addonInfoFile, StandardCharsets.UTF_8)) { + AddonInfo addonInfo = GSON.fromJson(reader, AddonInfo.class); + + if (addonInfo.getId() == null) + throw new AddonException("'id' is missing"); + + if (addonInfo.getEntrypoint() == null) + throw new AddonException("'entrypoint' is missing"); + + return addonInfo; + } + } + } + + return null; + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/LoadedAddon.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/LoadedAddon.java new file mode 100644 index 00000000..a3b6598a --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/addons/LoadedAddon.java @@ -0,0 +1,7 @@ +package de.bluecolored.bluemap.common.addons; + +public record LoadedAddon ( + AddonInfo addonInfo, + ClassLoader classLoader, + Object instance +) {} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapAPIImpl.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapAPIImpl.java index b544d4c8..2652324a 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapAPIImpl.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapAPIImpl.java @@ -29,12 +29,14 @@ import de.bluecolored.bluemap.api.BlueMapAPI; import de.bluecolored.bluemap.api.BlueMapMap; import de.bluecolored.bluemap.api.BlueMapWorld; +import de.bluecolored.bluemap.common.BlueMapService; import de.bluecolored.bluemap.common.plugin.Plugin; import de.bluecolored.bluemap.common.serverinterface.ServerWorld; import de.bluecolored.bluemap.core.BlueMap; import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.map.BmMap; import de.bluecolored.bluemap.core.world.World; +import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.Map; @@ -43,12 +45,28 @@ public class BlueMapAPIImpl extends BlueMapAPI { - private final Plugin plugin; + private final BlueMapService blueMapService; + private final @Nullable Plugin plugin; + + private final WebAppImpl webAppImpl; + private final @Nullable RenderManagerImpl renderManagerImpl; + private final @Nullable PluginImpl pluginImpl; + private final LoadingCache> worldCache; private final LoadingCache> mapCache; public BlueMapAPIImpl(Plugin plugin) { + this(plugin.getBlueMap(), plugin); + } + + public BlueMapAPIImpl(BlueMapService blueMapService, @Nullable Plugin plugin) { + this.blueMapService = blueMapService; this.plugin = plugin; + + this.renderManagerImpl = plugin != null ? new RenderManagerImpl(this, plugin) : null; + this.webAppImpl = new WebAppImpl(blueMapService, plugin); + this.pluginImpl = plugin != null ? new PluginImpl(plugin) : null; + this.worldCache = Caffeine.newBuilder() .executor(BlueMap.THREAD_POOL) .weakKeys() @@ -59,24 +77,9 @@ public BlueMapAPIImpl(Plugin plugin) { .build(this::getMapUncached); } - @Override - public RenderManagerImpl getRenderManager() { - return new RenderManagerImpl(this, plugin); - } - - @Override - public WebAppImpl getWebApp() { - return new WebAppImpl(plugin); - } - - @Override - public de.bluecolored.bluemap.api.plugin.Plugin getPlugin() { - return new PluginImpl(plugin); - } - @Override public Collection getMaps() { - Map maps = plugin.getBlueMap().getMaps(); + Map maps = blueMapService.getMaps(); return maps.keySet().stream() .map(this::getMap) .filter(Optional::isPresent) @@ -86,7 +89,7 @@ public Collection getMaps() { @Override public Collection getWorlds() { - Map worlds = plugin.getBlueMap().getWorlds(); + Map worlds = blueMapService.getWorlds(); return worlds.keySet().stream() .map(this::getWorld) .filter(Optional::isPresent) @@ -102,21 +105,24 @@ public Optional getWorld(Object world) { public Optional getWorldUncached(Object world) { if (world instanceof String) { - var coreWorld = plugin.getBlueMap().getWorlds().get(world); + var coreWorld = blueMapService.getWorlds().get(world); if (coreWorld != null) world = coreWorld; } if (world instanceof World coreWorld) { - return Optional.of(new BlueMapWorldImpl(plugin, coreWorld)); + return Optional.of(new BlueMapWorldImpl(coreWorld, blueMapService, plugin)); } + if (plugin == null) return Optional.empty(); + ServerWorld serverWorld = plugin.getServerInterface().getServerWorld(world).orElse(null); if (serverWorld == null) return Optional.empty(); World coreWorld = plugin.getWorld(serverWorld); if (coreWorld == null) return Optional.empty(); - return Optional.of(new BlueMapWorldImpl(plugin, coreWorld)); + return Optional.of(new BlueMapWorldImpl(coreWorld, blueMapService, plugin)); + } @Override @@ -125,7 +131,7 @@ public Optional getMap(String id) { } public Optional getMapUncached(String id) { - var maps = plugin.getBlueMap().getMaps(); + var maps = blueMapService.getMaps(); var map = maps.get(id); if (map == null) return Optional.empty(); @@ -133,7 +139,7 @@ public Optional getMapUncached(String id) { var world = getWorld(map.getWorld()).orElse(null); if (world == null) return Optional.empty(); - return Optional.of(new BlueMapMapImpl(plugin, map, (BlueMapWorldImpl) world)); + return Optional.of(new BlueMapMapImpl(map, (BlueMapWorldImpl) world, plugin)); } @Override @@ -141,6 +147,23 @@ public String getBlueMapVersion() { return BlueMap.VERSION; } + @Override + public WebAppImpl getWebApp() { + return webAppImpl; + } + + @Override + public RenderManagerImpl getRenderManager() { + if (renderManagerImpl == null) throw new UnsupportedOperationException("RenderManager API is not supported on this platform"); + return renderManagerImpl; + } + + @Override + public de.bluecolored.bluemap.api.plugin.Plugin getPlugin() { + if (pluginImpl == null) throw new UnsupportedOperationException("Plugin API is not supported on this platform"); + return pluginImpl; + } + public void register() { try { BlueMapAPI.registerInstance(this); @@ -157,4 +180,26 @@ public void unregister() { } } + /** + * Easy-access method for addons depending on BlueMapCommon:
+ *
+     *     BlueMapService bluemap = ((BlueMapAPIImpl) blueMapAPI).blueMapService();
+     * 
+ */ + @SuppressWarnings("unused") + public BlueMapService blueMapService() { + return blueMapService; + } + + /** + * Easy-access method for addons depending on BlueMapCommon:
+ *
+     *     Plugin plugin = ((BlueMapAPIImpl) blueMapAPI).plugin();
+     * 
+ */ + @SuppressWarnings("unused") + public @Nullable Plugin plugin() { + return plugin; + } + } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapMapImpl.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapMapImpl.java index 027cfc8d..199a57f0 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapMapImpl.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapMapImpl.java @@ -25,16 +25,16 @@ package de.bluecolored.bluemap.common.api; import com.flowpowered.math.vector.Vector2i; +import de.bluecolored.bluemap.api.AssetStorage; import de.bluecolored.bluemap.api.BlueMapMap; import de.bluecolored.bluemap.api.BlueMapWorld; -import de.bluecolored.bluemap.api.AssetStorage; import de.bluecolored.bluemap.api.markers.MarkerSet; import de.bluecolored.bluemap.common.plugin.Plugin; import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask; import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask; import de.bluecolored.bluemap.core.map.BmMap; +import org.jetbrains.annotations.Nullable; -import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Map; import java.util.Objects; @@ -42,27 +42,16 @@ public class BlueMapMapImpl implements BlueMapMap { - private final WeakReference plugin; + private final String mapId; private final WeakReference map; private final BlueMapWorldImpl world; - private final String mapId; + private final WeakReference plugin; - public BlueMapMapImpl(Plugin plugin, BmMap map) throws IOException { - this.plugin = new WeakReference<>(plugin); - this.map = new WeakReference<>(map); - this.world = new BlueMapWorldImpl(plugin, map.getWorld()); + public BlueMapMapImpl(BmMap map, BlueMapWorldImpl world, @Nullable Plugin plugin) { this.mapId = map.getId(); - } - - public BlueMapMapImpl(Plugin plugin, BmMap map, BlueMapWorldImpl world) { - this.plugin = new WeakReference<>(plugin); this.map = new WeakReference<>(map); this.world = world; - this.mapId = map.getId(); - } - - public BmMap getBmMap() { - return unpack(map); + this.plugin = new WeakReference<>(plugin); } @Override @@ -122,7 +111,9 @@ public synchronized void setFrozen(boolean frozen) { } private synchronized void unfreeze() { - Plugin plugin = unpack(this.plugin); + Plugin plugin = this.plugin.get(); + if (plugin == null) return; // fail silently: not supported on non-plugin platforms + BmMap map = unpack(this.map); plugin.startWatchingMap(map); plugin.getPluginState().getMapState(map).setUpdateEnabled(true); @@ -130,7 +121,9 @@ private synchronized void unfreeze() { } private synchronized void freeze() { - Plugin plugin = unpack(this.plugin); + Plugin plugin = this.plugin.get(); + if (plugin == null) return; // fail silently: not supported on non-plugin platforms + BmMap map = unpack(this.map); plugin.stopWatchingMap(map); plugin.getPluginState().getMapState(map).setUpdateEnabled(false); @@ -147,7 +140,10 @@ private synchronized void freeze() { @Override public boolean isFrozen() { - return !unpack(plugin).getPluginState().getMapState(unpack(map)).isUpdateEnabled(); + Plugin plugin = this.plugin.get(); + if (plugin == null) return false; // fail silently: not supported on non-plugin platforms + + return !plugin.getPluginState().getMapState(unpack(map)).isUpdateEnabled(); } @Override @@ -169,4 +165,14 @@ private T unpack(WeakReference ref) { return Objects.requireNonNull(ref.get(), "Reference lost to delegate object. Most likely BlueMap got reloaded and this instance is no longer valid."); } + /** + * Easy-access method for addons depending on BlueMapCore:
+ *
+     *     BmMap map = ((BlueMapMapImpl) blueMapMap).map();
+     * 
+ */ + public BmMap map() { + return unpack(map); + } + } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapWorldImpl.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapWorldImpl.java index 492936bc..7186f8ac 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapWorldImpl.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/BlueMapWorldImpl.java @@ -26,9 +26,11 @@ import de.bluecolored.bluemap.api.BlueMapMap; import de.bluecolored.bluemap.api.BlueMapWorld; +import de.bluecolored.bluemap.common.BlueMapService; import de.bluecolored.bluemap.common.plugin.Plugin; import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.mca.MCAWorld; +import org.jetbrains.annotations.Nullable; import java.lang.ref.WeakReference; import java.nio.file.Path; @@ -39,17 +41,15 @@ public class BlueMapWorldImpl implements BlueMapWorld { private final String id; - private final WeakReference plugin; private final WeakReference world; + private final WeakReference blueMapService; + private final WeakReference plugin; - public BlueMapWorldImpl(Plugin plugin, World world) { + public BlueMapWorldImpl(World world, BlueMapService blueMapService, @Nullable Plugin plugin) { this.id = world.getId(); - this.plugin = new WeakReference<>(plugin); this.world = new WeakReference<>(world); - } - - public World getWorld() { - return unpack(world); + this.blueMapService = new WeakReference<>(blueMapService); + this.plugin = new WeakReference<>(plugin); } @Override @@ -70,11 +70,10 @@ public Path getSaveFolder() { @Override public Collection getMaps() { - Plugin plugin = unpack(this.plugin); World world = unpack(this.world); - return plugin.getBlueMap().getMaps().values().stream() + return unpack(blueMapService).getMaps().values().stream() .filter(map -> map.getWorld().equals(world)) - .map(map -> new BlueMapMapImpl(plugin, map, this)) + .map(map -> new BlueMapMapImpl(map, this, plugin.get())) .collect(Collectors.toUnmodifiableSet()); } @@ -97,4 +96,14 @@ private T unpack(WeakReference ref) { return Objects.requireNonNull(ref.get(), "Reference lost to delegate object. Most likely BlueMap got reloaded and this instance is no longer valid."); } + /** + * Easy-access method for addons depending on BlueMapCore:
+ *
+     *     World world = ((BlueMapWorldImpl) blueMapWorld).world();
+     * 
+ */ + public World world() { + return unpack(world); + } + } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/RenderManagerImpl.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/RenderManagerImpl.java index de203f28..bc104e62 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/RenderManagerImpl.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/RenderManagerImpl.java @@ -48,19 +48,19 @@ public RenderManagerImpl(BlueMapAPIImpl api, Plugin plugin) { @Override public boolean scheduleMapUpdateTask(BlueMapMap map, boolean force) { BlueMapMapImpl cmap = castMap(map); - return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.getBmMap(), s -> force)); + return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.map(), s -> force)); } @Override public boolean scheduleMapUpdateTask(BlueMapMap map, Collection regions, boolean force) { BlueMapMapImpl cmap = castMap(map); - return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.getBmMap(), regions, s -> force)); + return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.map(), regions, s -> force)); } @Override public boolean scheduleMapPurgeTask(BlueMapMap map) { BlueMapMapImpl cmap = castMap(map); - return renderManager.scheduleRenderTask(new MapPurgeTask(cmap.getBmMap())); + return renderManager.scheduleRenderTask(new MapPurgeTask(cmap.map())); } @Override diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/WebAppImpl.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/WebAppImpl.java index 64af5bda..9e6bde03 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/WebAppImpl.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/api/WebAppImpl.java @@ -25,35 +25,47 @@ package de.bluecolored.bluemap.common.api; import de.bluecolored.bluemap.api.WebApp; +import de.bluecolored.bluemap.common.BlueMapService; import de.bluecolored.bluemap.common.plugin.Plugin; import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.util.FileHelper; +import org.jetbrains.annotations.Nullable; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.stream.Stream; public class WebAppImpl implements WebApp { - private final Plugin plugin; + private final BlueMapService blueMapService; + private final @Nullable Plugin plugin; + + private final Timer timer = new Timer(); + private @Nullable TimerTask scheduledWebAppSettingsUpdate; + + public WebAppImpl(BlueMapService blueMapService, @Nullable Plugin plugin) { + this.blueMapService = blueMapService; + this.plugin = plugin; + } public WebAppImpl(Plugin plugin) { + this.blueMapService = plugin.getBlueMap(); this.plugin = plugin; } @Override public Path getWebRoot() { - return plugin.getBlueMap().getConfig().getWebappConfig().getWebroot(); + return blueMapService.getConfig().getWebappConfig().getWebroot(); } @Override public void setPlayerVisibility(UUID player, boolean visible) { + if (plugin == null) return; // fail silently: not supported on non-plugin platforms + if (visible) { plugin.getPluginState().removeHiddenPlayer(player); } else { @@ -63,19 +75,48 @@ public void setPlayerVisibility(UUID player, boolean visible) { @Override public boolean getPlayerVisibility(UUID player) { + if (plugin == null) return false; // fail silently: not supported on non-plugin platforms + return !plugin.getPluginState().isPlayerHidden(player); } @Override - public void registerScript(String url) { + public synchronized void registerScript(String url) { Logger.global.logDebug("Registering script from API: " + url); - plugin.getBlueMap().getWebFilesManager().getScripts().add(url); + blueMapService.getWebFilesManager().getScripts().add(url); + scheduleUpdateWebAppSettings(); } @Override - public void registerStyle(String url) { + public synchronized void registerStyle(String url) { Logger.global.logDebug("Registering style from API: " + url); - plugin.getBlueMap().getWebFilesManager().getStyles().add(url); + blueMapService.getWebFilesManager().getStyles().add(url); + scheduleUpdateWebAppSettings(); + } + + /** + * Save webapp-settings after a short delay, if no other save is already scheduled. + * (to bulk-save changes in case there is a lot of scripts being registered at once) + */ + private synchronized void scheduleUpdateWebAppSettings() { + if (!blueMapService.getConfig().getWebappConfig().isEnabled()) return; + if (scheduledWebAppSettingsUpdate != null) return; + + timer.schedule(new TimerTask() { + @Override + public void run() { + synchronized (WebAppImpl.this) { + try { + if (blueMapService.getConfig().getWebappConfig().isEnabled()) + blueMapService.getWebFilesManager().saveSettings(); + } catch (IOException ex) { + Logger.global.logError("Failed to update webapp settings", ex); + } finally { + scheduledWebAppSettingsUpdate = null; + } + } + } + }, 1000); } @Override 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 ba5aeb5e..36178dd4 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 @@ -28,6 +28,7 @@ import de.bluecolored.bluemap.common.BlueMapService; import de.bluecolored.bluemap.common.InterruptableReentrantLock; import de.bluecolored.bluemap.common.MissingResourcesException; +import de.bluecolored.bluemap.common.addons.AddonManager; import de.bluecolored.bluemap.common.api.BlueMapAPIImpl; import de.bluecolored.bluemap.common.config.*; import de.bluecolored.bluemap.common.live.LivePlayersDataSupplier; @@ -50,6 +51,8 @@ import de.bluecolored.bluemap.core.util.Tristate; import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.mca.MCAWorld; +import lombok.AccessLevel; +import lombok.Getter; import org.jetbrains.annotations.Nullable; import org.spongepowered.configurate.gson.GsonConfigurationLoader; import org.spongepowered.configurate.serialize.SerializationException; @@ -61,6 +64,7 @@ import java.net.BindException; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.time.ZoneId; @@ -70,6 +74,7 @@ import java.util.function.Predicate; import java.util.regex.Pattern; +@Getter public class Plugin implements ServerEventListener { public static final String PLUGIN_ID = "bluemap"; @@ -77,25 +82,23 @@ public class Plugin implements ServerEventListener { private static final String DEBUG_FILE_LOG_NAME = "file-debug-log"; + @Getter(AccessLevel.NONE) private final InterruptableReentrantLock loadingLock = new InterruptableReentrantLock(); private final String implementationType; private final Server serverInterface; private BlueMapService blueMap; - private PluginState pluginState; - private RenderManager renderManager; - private HttpServer webServer; - private Logger webLogger; - private BlueMapAPIImpl api; + private HttpServer webServer; + private RoutingRequestHandler webRequestHandler; + private Logger webLogger; + private Timer daemonTimer; - private Map mapUpdateServices; - private PlayerSkinUpdater skinUpdater; private boolean loaded = false; @@ -119,6 +122,12 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti if (loaded) return; unload(); //ensure nothing is left running (from a failed load or something) + //load addons + Path addonsFolder = serverInterface.getConfigFolder().resolve("addons"); + Files.createDirectories(addonsFolder); + AddonManager.tryLoadAddons(addonsFolder, true); + serverInterface.getModsFolder().ifPresent(AddonManager::tryLoadAddons); + //load configs BlueMapConfigManager configManager = BlueMapConfigManager.builder() .minecraftVersion(serverInterface.getMinecraftVersion()) @@ -184,10 +193,10 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti Path webroot = webserverConfig.getWebroot(); FileHelper.createDirectories(webroot); - RoutingRequestHandler routingRequestHandler = new RoutingRequestHandler(); + this.webRequestHandler = new RoutingRequestHandler(); // default route - routingRequestHandler.register(".*", new FileRequestHandler(webroot)); + webRequestHandler.register(".*", new FileRequestHandler(webroot)); // map route for (var mapConfigEntry : configManager.getMapConfigs().entrySet()) { @@ -203,7 +212,7 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti mapRequestHandler = new MapRequestHandler(storage.map(id)); } - routingRequestHandler.register( + webRequestHandler.register( "maps/" + Pattern.quote(id) + "/(.*)", "$1", new BlueMapResponseModifier(mapRequestHandler) @@ -223,7 +232,7 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti try { webServer = new HttpServer(new LoggingRequestHandler( - routingRequestHandler, + webRequestHandler, webserverConfig.getLog().getFormat(), webLogger )); @@ -361,10 +370,6 @@ public void run() { this.api = new BlueMapAPIImpl(this); this.api.register(); - //save webapp settings again (for api-registered scripts and styles) - if (webappConfig.isEnabled()) - this.getBlueMap().getWebFilesManager().saveSettings(); - //start render-manager if (pluginState.isRenderThreadsEnabled()) { checkPausedByPlayerCount(); // <- this also starts the render-manager if it should start @@ -638,38 +643,6 @@ public boolean checkPausedByPlayerCount() { return getBlueMap().getWorlds().get(id); } - public Server getServerInterface() { - return serverInterface; - } - - public BlueMapService getBlueMap() { - return blueMap; - } - - public PluginState getPluginState() { - return pluginState; - } - - public RenderManager getRenderManager() { - return renderManager; - } - - public HttpServer getWebServer() { - return webServer; - } - - public boolean isLoaded() { - return loaded; - } - - public String getImplementationType() { - return implementationType; - } - - public PlayerSkinUpdater getSkinUpdater() { - return skinUpdater; - } - private void initFileWatcherTasks() { var maps = blueMap.getMaps(); if (maps != null) { 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 7872b067..2fa38758 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 @@ -27,6 +27,8 @@ import de.bluecolored.bluemap.common.BlueMapConfiguration; import de.bluecolored.bluemap.common.BlueMapService; import de.bluecolored.bluemap.common.MissingResourcesException; +import de.bluecolored.bluemap.common.addons.AddonManager; +import de.bluecolored.bluemap.common.api.BlueMapAPIImpl; import de.bluecolored.bluemap.common.config.BlueMapConfigManager; import de.bluecolored.bluemap.common.config.ConfigurationException; import de.bluecolored.bluemap.common.config.CoreConfig; @@ -113,6 +115,10 @@ public void renderMaps(BlueMapService blueMap, boolean watch, boolean forceRende totalRegions += updateTask.getRegions().size(); } + // enable api + BlueMapAPIImpl api = new BlueMapAPIImpl(blueMap, null); + api.register(); + Logger.global.logInfo("Start updating " + maps.size() + " maps (" + totalRegions + " regions, ~" + totalRegions * 1024L + " chunks)..."); // start rendering @@ -150,6 +156,8 @@ public void run() { Runnable shutdown = () -> { Logger.global.logInfo("Stopping..."); + api.unregister(); + updateInfoTask.cancel(); saveTask.cancel(); @@ -301,6 +309,12 @@ public static void main(String[] args) { cli.minecraftVersion = cmd.getOptionValue("v"); } + // load addons + Path addonsFolder = cli.configFolder.resolve("addons"); + Files.createDirectories(addonsFolder); + AddonManager.tryLoadAddons(cli.configFolder.resolve("addons"), true); + + // load configs BlueMapConfigManager configs = BlueMapConfigManager.builder() .minecraftVersion(cli.minecraftVersion) .configRoot(cli.configFolder)