/* * 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; import de.bluecolored.bluemap.api.debug.DebugDump; import de.bluecolored.bluemap.common.BlueMapConfiguration; import de.bluecolored.bluemap.common.BlueMapService; import de.bluecolored.bluemap.common.InterruptableReentrantLock; import de.bluecolored.bluemap.common.MissingResourcesException; import de.bluecolored.bluemap.common.api.BlueMapAPIImpl; import de.bluecolored.bluemap.common.config.*; import de.bluecolored.bluemap.common.live.LivePlayersDataSupplier; import de.bluecolored.bluemap.common.plugin.skins.PlayerSkinUpdater; import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask; import de.bluecolored.bluemap.common.rendermanager.RenderManager; import de.bluecolored.bluemap.common.serverinterface.ServerEventListener; import de.bluecolored.bluemap.common.serverinterface.Server; import de.bluecolored.bluemap.common.serverinterface.ServerWorld; import de.bluecolored.bluemap.common.web.*; import de.bluecolored.bluemap.common.web.http.HttpServer; import de.bluecolored.bluemap.core.debug.StateDumper; import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.map.BmMap; import de.bluecolored.bluemap.core.metrics.Metrics; import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.storage.Storage; import de.bluecolored.bluemap.core.util.FileHelper; import de.bluecolored.bluemap.core.util.Tristate; import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.mca.MCAWorld; import org.jetbrains.annotations.Nullable; import org.spongepowered.configurate.gson.GsonConfigurationLoader; import org.spongepowered.configurate.serialize.SerializationException; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.BindException; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.file.Path; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.regex.Pattern; @DebugDump public class Plugin implements ServerEventListener { public static final String PLUGIN_ID = "bluemap"; public static final String PLUGIN_NAME = "BlueMap"; private static final String DEBUG_FILE_LOG_NAME = "file-debug-log"; 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 Timer daemonTimer; private Map regionFileWatchServices; private PlayerSkinUpdater skinUpdater; private boolean loaded = false; public Plugin(String implementationType, Server serverInterface) { this.implementationType = implementationType.toLowerCase(); this.serverInterface = serverInterface; StateDumper.global().register(this); } public void load() throws IOException { load(null); } private void load(@Nullable ResourcePack preloadedResourcePack) throws IOException { loadingLock.lock(); try { synchronized (this) { if (loaded) return; unload(); //ensure nothing is left running (from a failed load or something) //load configs BlueMapConfigManager configManager = BlueMapConfigManager.builder() .minecraftVersion(serverInterface.getMinecraftVersion()) .configRoot(serverInterface.getConfigFolder()) .resourcePacksFolder(serverInterface.getConfigFolder().resolve("resourcepacks")) .modsFolder(serverInterface.getModsFolder().orElse(null)) .useMetricsConfig(serverInterface.isMetricsEnabled() == Tristate.UNDEFINED) .autoConfigWorlds(serverInterface.getLoadedServerWorlds()) .build(); CoreConfig coreConfig = configManager.getCoreConfig(); WebserverConfig webserverConfig = configManager.getWebserverConfig(); WebappConfig webappConfig = configManager.getWebappConfig(); PluginConfig pluginConfig = configManager.getPluginConfig(); //apply new file-logger config if (coreConfig.getLog().getFile() != null) { ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()); Logger.global.put(DEBUG_FILE_LOG_NAME, () -> Logger.file( Path.of(String.format(coreConfig.getLog().getFile(), zdt)), coreConfig.getLog().isAppend() )); } else { Logger.global.remove(DEBUG_FILE_LOG_NAME); } //load plugin state try { GsonConfigurationLoader loader = GsonConfigurationLoader.builder() .path(coreConfig.getData().resolve("pluginState.json")) .build(); pluginState = loader.load().get(PluginState.class); } catch (SerializationException ex) { Logger.global.logWarning("Failed to load pluginState.json (invalid format), creating a new one..."); pluginState = new PluginState(); } //create bluemap-service blueMap = new BlueMapService(configManager, preloadedResourcePack); //try load resources try { blueMap.getOrLoadResourcePack(); } catch (MissingResourcesException ex) { Logger.global.logWarning("BlueMap is missing important resources!"); Logger.global.logWarning("You must accept the required file download in order for BlueMap to work!"); BlueMapConfiguration configProvider = blueMap.getConfig(); if (configProvider instanceof BlueMapConfigManager) { Logger.global.logWarning("Please check: " + ((BlueMapConfigManager) configProvider).getConfigManager().findConfigPath(Path.of("core")).toAbsolutePath().normalize()); } Logger.global.logInfo("If you have changed the config you can simply reload the plugin using: /bluemap reload"); unload(); return; } //load maps Map maps = blueMap.getOrLoadMaps(); //create and start webserver if (webserverConfig.isEnabled()) { Path webroot = webserverConfig.getWebroot(); FileHelper.createDirectories(webroot); RoutingRequestHandler routingRequestHandler = new RoutingRequestHandler(); // default route routingRequestHandler.register(".*", new FileRequestHandler(webroot)); // map route for (var mapConfigEntry : configManager.getMapConfigs().entrySet()) { String id = mapConfigEntry.getKey(); MapConfig mapConfig = mapConfigEntry.getValue(); MapRequestHandler mapRequestHandler; BmMap map = maps.get(id); if (map != null) { mapRequestHandler = new MapRequestHandler(map, serverInterface, pluginConfig, Predicate.not(pluginState::isPlayerHidden)); } else { Storage storage = blueMap.getOrLoadStorage(mapConfig.getStorage()); mapRequestHandler = new MapRequestHandler(id, storage); } routingRequestHandler.register( "maps/" + Pattern.quote(id) + "/(.*)", "$1", new BlueMapResponseModifier(mapRequestHandler) ); } // create web-logger List webLoggerList = new ArrayList<>(); if (webserverConfig.getLog().getFile() != null) { ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()); webLoggerList.add(Logger.file( Path.of(String.format(webserverConfig.getLog().getFile(), zdt)), webserverConfig.getLog().isAppend() )); } webLogger = Logger.combine(webLoggerList); try { webServer = new HttpServer(new LoggingRequestHandler( routingRequestHandler, webserverConfig.getLog().getFormat(), webLogger )); webServer.bind(new InetSocketAddress( webserverConfig.resolveIp(), webserverConfig.getPort() )); webServer.start(); } catch (UnknownHostException ex) { throw new ConfigurationException("BlueMap failed to resolve the ip in your webserver-config.\n" + "Check if that is correctly configured.", ex); } catch (BindException ex) { throw new ConfigurationException("BlueMap failed to bind to the configured address.\n" + "This usually happens when the configured port (" + webserverConfig.getPort() + ") is already in use by some other program.", ex); } catch (IOException ex) { throw new ConfigurationException("BlueMap failed to initialize the webserver.\n" + "Check your webserver-config if everything is configured correctly.\n" + "(Make sure you DON'T use the same port for bluemap that you also use for your minecraft server)", ex); } } //warn if no maps are configured if (maps.isEmpty()) { Logger.global.logWarning("There are no valid maps configured, please check your map-configs! Disabling BlueMap..."); unload(true); return; } //initialize render manager renderManager = new RenderManager(); //update all maps maps.values().stream() .sorted(Comparator.comparing(bmMap -> bmMap.getMapSettings().getSorting())) .forEach(map -> { if (pluginState.getMapState(map).isUpdateEnabled()) { renderManager.scheduleRenderTask(new MapUpdateTask(map)); } }); //update webapp and settings if (webappConfig.isEnabled()) blueMap.createOrUpdateWebApp(false); //start skin updater this.skinUpdater = new PlayerSkinUpdater(this); if (pluginConfig.isLivePlayerMarkers()) { serverInterface.registerListener(skinUpdater); } //init timer daemonTimer = new Timer("BlueMap-Plugin-DaemonTimer", true); //periodically save TimerTask saveTask = new TimerTask() { @Override public void run() { save(); } }; daemonTimer.schedule(saveTask, TimeUnit.MINUTES.toMillis(2), TimeUnit.MINUTES.toMillis(2)); //periodically save markers int writeMarkersInterval = pluginConfig.getWriteMarkersInterval(); if (writeMarkersInterval > 0) { TimerTask saveMarkersTask = new TimerTask() { @Override public void run() { saveMarkerStates(); } }; daemonTimer.schedule(saveMarkersTask, TimeUnit.SECONDS.toMillis(writeMarkersInterval), TimeUnit.SECONDS.toMillis(writeMarkersInterval)); } //periodically save players int writePlayersInterval = pluginConfig.getWritePlayersInterval(); if (writePlayersInterval > 0) { TimerTask savePlayersTask = new TimerTask() { @Override public void run() { savePlayerStates(); } }; daemonTimer.schedule(savePlayersTask, TimeUnit.SECONDS.toMillis(writePlayersInterval), TimeUnit.SECONDS.toMillis(writePlayersInterval)); } //periodically restart the file-watchers TimerTask fileWatcherRestartTask = new TimerTask() { @Override public void run() { regionFileWatchServices.values().forEach(RegionFileWatchService::close); regionFileWatchServices.clear(); initFileWatcherTasks(); } }; daemonTimer.schedule(fileWatcherRestartTask, TimeUnit.HOURS.toMillis(1), TimeUnit.HOURS.toMillis(1)); //periodically update all (non frozen) maps if (pluginConfig.getFullUpdateInterval() > 0) { long fullUpdateTime = TimeUnit.MINUTES.toMillis(pluginConfig.getFullUpdateInterval()); TimerTask updateAllMapsTask = new TimerTask() { @Override public void run() { for (BmMap map : maps.values()) { if (pluginState.getMapState(map).isUpdateEnabled()) { renderManager.scheduleRenderTask(new MapUpdateTask(map)); } } } }; daemonTimer.scheduleAtFixedRate(updateAllMapsTask, fullUpdateTime, fullUpdateTime); } //metrics TimerTask metricsTask = new TimerTask() { @Override public void run() { if (Plugin.this.serverInterface.isMetricsEnabled().getOr(coreConfig::isMetrics)) Metrics.sendReport(Plugin.this.implementationType); } }; daemonTimer.scheduleAtFixedRate(metricsTask, TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(30)); //watch map-changes this.regionFileWatchServices = new HashMap<>(); initFileWatcherTasks(); //register listener serverInterface.registerListener(this); //enable api 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 } else { Logger.global.logInfo("Render-Threads are STOPPED! Use the command 'bluemap start' to start them."); } //done loaded = true; } } catch (ConfigurationException ex) { Logger.global.logWarning(ex.getFormattedExplanation()); throw new IOException(ex); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Logger.global.logWarning("Loading has been interrupted!"); } finally { loadingLock.unlock(); } } public void unload() { this.unload(false); } public void unload(boolean keepWebserver) { loadingLock.interruptAndLock(); try { synchronized (this) { //save save(); //disable api if (api != null) api.unregister(); api = null; //unregister listeners serverInterface.unregisterAllListeners(); skinUpdater = null; //stop scheduled threads if (daemonTimer != null) daemonTimer.cancel(); daemonTimer = null; //stop file-watchers if (regionFileWatchServices != null) { regionFileWatchServices.values().forEach(RegionFileWatchService::close); regionFileWatchServices.clear(); } regionFileWatchServices = null; //stop services if (renderManager != null){ renderManager.stop(); try { renderManager.awaitShutdown(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } renderManager = null; if (webServer != null && !keepWebserver) { try { webServer.close(); } catch (IOException ex) { Logger.global.logError("Failed to close the webserver!", ex); } webServer = null; } if (webLogger != null) { try { webLogger.close(); } catch (Exception ex) { Logger.global.logError("Failed to close the webserver-logger!", ex); } webLogger = null; } //close bluemap if (blueMap != null) { try { blueMap.close(); } catch (IOException ex) { Logger.global.logError("Failed to close a bluemap-service!", ex); } } blueMap = null; // remove file-logger Logger.global.remove(DEBUG_FILE_LOG_NAME); //clear resources pluginState = null; //done loaded = false; } } finally { loadingLock.unlock(); } } public void reload() throws IOException { unload(); load(); } /** * {@link #reload()} but without reloading the resourcepack (if it is loaded). */ public void lightReload() throws IOException { loadingLock.lock(); try { synchronized (this) { if (!loaded) { reload(); // reload normally return; } // hold and reuse loaded resourcepack ResourcePack preloadedResourcePack = this.blueMap.getResourcePack(); unload(); load(preloadedResourcePack); } } finally { loadingLock.unlock(); } } public synchronized void save() { if (blueMap == null) return; if (pluginState != null) { try { GsonConfigurationLoader loader = GsonConfigurationLoader.builder() .path(blueMap.getConfig().getCoreConfig().getData().resolve("pluginState.json")) .build(); loader.save(loader.createNode().set(PluginState.class, pluginState)); } catch (IOException ex) { Logger.global.logError("Failed to save pluginState.json!", ex); } } var maps = blueMap.getMaps(); for (BmMap map : maps.values()) { map.save(); } } public void saveMarkerStates() { if (blueMap == null) return; var maps = blueMap.getMaps(); for (BmMap map : maps.values()) { map.saveMarkerState(); } } public void savePlayerStates() { if (blueMap == null) return; var maps = blueMap.getMaps(); for (BmMap map : maps.values()) { var serverWorld = serverInterface.getServerWorld(map.getWorld()).orElse(null); if (serverWorld == null) continue; var dataSupplier = new LivePlayersDataSupplier( serverInterface, getBlueMap().getConfig().getPluginConfig(), serverWorld, Predicate.not(pluginState::isPlayerHidden) ); try ( OutputStream out = map.getStorage().writeMeta(map.getId(), BmMap.META_FILE_PLAYERS); Writer writer = new OutputStreamWriter(out) ) { writer.write(dataSupplier.get()); } catch (Exception ex) { Logger.global.logError("Failed to save players for map '" + map.getId() + "'!", ex); } } } public synchronized void startWatchingMap(BmMap map) { stopWatchingMap(map); try { RegionFileWatchService watcher = new RegionFileWatchService(renderManager, map); 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(World world) throws IOException { var implWorld = serverInterface.getServerWorld(world).orElse(null); if (implWorld != null) return implWorld.persistWorldChanges(); return false; } @Override public void onPlayerJoin(UUID playerUuid) { checkPausedByPlayerCountSoon(); } @Override public void onPlayerLeave(UUID playerUuid) { checkPausedByPlayerCountSoon(); } private void checkPausedByPlayerCountSoon() { // check is done a second later to make sure the player has actually joined/left and is no longer on the list try { daemonTimer.schedule(new TimerTask() { @Override public void run() { checkPausedByPlayerCount(); } }, 1000); } catch (IllegalStateException ex) { // Timer is cancelled for some reason Logger.global.logWarning("Timer is already cancelled, skipping player-limit checks!"); } } public boolean checkPausedByPlayerCount() { CoreConfig coreConfig = getBlueMap().getConfig().getCoreConfig(); PluginConfig pluginConfig = getBlueMap().getConfig().getPluginConfig(); if ( pluginConfig.getPlayerRenderLimit() > 0 && getServerInterface().getOnlinePlayers().size() >= pluginConfig.getPlayerRenderLimit() ) { if (renderManager.isRunning()) renderManager.stop(); return true; } else { if (!renderManager.isRunning() && getPluginState().isRenderThreadsEnabled()) renderManager.start(coreConfig.resolveRenderThreadCount()); return false; } } public @Nullable World getWorld(ServerWorld serverWorld) { String id = MCAWorld.id(serverWorld.getWorldFolder(), serverWorld.getDimension()); 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) { for (BmMap map : maps.values()) { if (pluginState.getMapState(map).isUpdateEnabled()) { startWatchingMap(map); } } } } }