448 lines
22 KiB
Java
448 lines
22 KiB
Java
/*
|
|
* 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.config;
|
|
|
|
import de.bluecolored.bluemap.api.debug.DebugDump;
|
|
import de.bluecolored.bluemap.common.BlueMapConfiguration;
|
|
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
|
|
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
|
|
import de.bluecolored.bluemap.core.BlueMap;
|
|
import de.bluecolored.bluemap.core.MinecraftVersion;
|
|
import de.bluecolored.bluemap.core.logger.Logger;
|
|
import de.bluecolored.bluemap.core.resources.datapack.DataPack;
|
|
import de.bluecolored.bluemap.core.util.FileHelper;
|
|
import de.bluecolored.bluemap.core.util.Key;
|
|
import lombok.Builder;
|
|
import lombok.Getter;
|
|
import lombok.NonNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.file.*;
|
|
import java.time.LocalDateTime;
|
|
import java.util.*;
|
|
import java.util.stream.Stream;
|
|
|
|
@DebugDump
|
|
@Getter
|
|
public class BlueMapConfigManager implements BlueMapConfiguration {
|
|
|
|
private final ConfigManager configManager;
|
|
|
|
private final MinecraftVersion minecraftVersion;
|
|
private final CoreConfig coreConfig;
|
|
private final WebserverConfig webserverConfig;
|
|
private final WebappConfig webappConfig;
|
|
private final PluginConfig pluginConfig;
|
|
private final Map<String, MapConfig> mapConfigs;
|
|
private final Map<String, StorageConfig> storageConfigs;
|
|
private final Path resourcePacksFolder;
|
|
private final @Nullable Path modsFolder;
|
|
|
|
@Builder
|
|
private BlueMapConfigManager(
|
|
@NonNull MinecraftVersion minecraftVersion,
|
|
@NonNull Path configRoot,
|
|
@Nullable Path defaultDataFolder,
|
|
@Nullable Path defaultWebroot,
|
|
@Nullable Collection<ServerWorld> autoConfigWorlds,
|
|
@Nullable Boolean usePluginConfig,
|
|
@Nullable Boolean useMetricsConfig,
|
|
@Nullable Path resourcePacksFolder,
|
|
@Nullable Path modsFolder
|
|
) throws ConfigurationException {
|
|
// set defaults
|
|
if (defaultDataFolder == null) defaultDataFolder = Path.of("bluemap");
|
|
if (defaultWebroot == null) defaultWebroot = Path.of("bluemap", "web");
|
|
if (autoConfigWorlds == null) autoConfigWorlds = Collections.emptyList();
|
|
if (usePluginConfig == null) usePluginConfig = true;
|
|
if (useMetricsConfig == null) useMetricsConfig = true;
|
|
if (resourcePacksFolder == null) resourcePacksFolder = configRoot.resolve("resourcepacks");
|
|
|
|
// load
|
|
this.minecraftVersion = minecraftVersion;
|
|
this.configManager = new ConfigManager(configRoot);
|
|
this.coreConfig = loadCoreConfig(defaultDataFolder, useMetricsConfig);
|
|
this.webappConfig = loadWebappConfig(defaultWebroot);
|
|
this.webserverConfig = loadWebserverConfig(webappConfig.getWebroot(), coreConfig.getData());
|
|
this.pluginConfig = usePluginConfig ? loadPluginConfig() : new PluginConfig();
|
|
this.storageConfigs = Collections.unmodifiableMap(loadStorageConfigs(webappConfig.getWebroot()));
|
|
this.mapConfigs = Collections.unmodifiableMap(loadMapConfigs(autoConfigWorlds));
|
|
this.resourcePacksFolder = resourcePacksFolder;
|
|
this.modsFolder = modsFolder;
|
|
}
|
|
|
|
private CoreConfig loadCoreConfig(Path defaultDataFolder, boolean useMetricsConfig) throws ConfigurationException {
|
|
Path configFileRaw = Path.of("core");
|
|
Path configFile = configManager.findConfigPath(configFileRaw);
|
|
Path configFolder = configFile.getParent();
|
|
|
|
if (!Files.exists(configFile)) {
|
|
try {
|
|
FileHelper.createDirectories(configFolder);
|
|
Files.writeString(
|
|
configFolder.resolve("core.conf"),
|
|
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/core.conf")
|
|
.setConditional("metrics", useMetricsConfig)
|
|
.setVariable("timestamp", LocalDateTime.now().withNano(0).toString())
|
|
.setVariable("version", BlueMap.VERSION)
|
|
.setVariable("data", formatPath(defaultDataFolder))
|
|
.setVariable("implementation", "bukkit")
|
|
.setVariable("render-thread-count", Integer.toString(suggestRenderThreadCount()))
|
|
.setVariable("logfile", formatPath(defaultDataFolder.resolve("logs").resolve("debug.log")))
|
|
.setVariable("logfile-with-time", formatPath(defaultDataFolder.resolve("logs").resolve("debug_%1$tF_%1$tT.log")))
|
|
.build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
} catch (IOException | NullPointerException ex) {
|
|
Logger.global.logWarning("Failed to create default core-configuration-file: " + ex);
|
|
}
|
|
}
|
|
|
|
return configManager.loadConfig(configFileRaw, CoreConfig.class);
|
|
}
|
|
|
|
/**
|
|
* determine render-thread preset (very pessimistic, rather let people increase it themselves)
|
|
*/
|
|
private int suggestRenderThreadCount() {
|
|
Runtime runtime = Runtime.getRuntime();
|
|
int availableCores = runtime.availableProcessors();
|
|
long availableMemoryMiB = runtime.maxMemory() / 1024L / 1024L;
|
|
int presetRenderThreadCount = 1;
|
|
if (availableCores >= 6 && availableMemoryMiB >= 4096)
|
|
presetRenderThreadCount = 2;
|
|
if (availableCores >= 10 && availableMemoryMiB >= 8192)
|
|
presetRenderThreadCount = 3;
|
|
return presetRenderThreadCount;
|
|
}
|
|
|
|
private WebserverConfig loadWebserverConfig(Path defaultWebroot, Path dataRoot) throws ConfigurationException {
|
|
Path configFileRaw = Path.of("webserver");
|
|
Path configFile = configManager.findConfigPath(configFileRaw);
|
|
Path configFolder = configFile.getParent();
|
|
|
|
if (!Files.exists(configFile)) {
|
|
try {
|
|
FileHelper.createDirectories(configFolder);
|
|
Files.writeString(
|
|
configFolder.resolve("webserver.conf"),
|
|
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/webserver.conf")
|
|
.setVariable("webroot", formatPath(defaultWebroot))
|
|
.setVariable("logfile", formatPath(dataRoot.resolve("logs").resolve("webserver.log")))
|
|
.setVariable("logfile-with-time", formatPath(dataRoot.resolve("logs").resolve("webserver_%1$tF_%1$tT.log")))
|
|
.build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
} catch (IOException | NullPointerException ex) {
|
|
Logger.global.logWarning("Failed to create default webserver-configuration-file: " + ex);
|
|
}
|
|
}
|
|
|
|
return configManager.loadConfig(configFileRaw, WebserverConfig.class);
|
|
}
|
|
|
|
private WebappConfig loadWebappConfig(Path defaultWebroot) throws ConfigurationException {
|
|
Path configFileRaw = Path.of("webapp");
|
|
Path configFile = configManager.findConfigPath(configFileRaw);
|
|
Path configFolder = configFile.getParent();
|
|
|
|
if (!Files.exists(configFile)) {
|
|
try {
|
|
FileHelper.createDirectories(configFolder);
|
|
Files.writeString(
|
|
configFolder.resolve("webapp.conf"),
|
|
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/webapp.conf")
|
|
.setVariable("webroot", formatPath(defaultWebroot))
|
|
.build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
} catch (IOException | NullPointerException ex) {
|
|
Logger.global.logWarning("Failed to create default webapp-configuration-file: " + ex);
|
|
}
|
|
}
|
|
|
|
return configManager.loadConfig(configFileRaw, WebappConfig.class);
|
|
}
|
|
|
|
private PluginConfig loadPluginConfig() throws ConfigurationException {
|
|
Path configFileRaw = Path.of("plugin");
|
|
Path configFile = configManager.findConfigPath(configFileRaw);
|
|
Path configFolder = configFile.getParent();
|
|
|
|
if (!Files.exists(configFile)) {
|
|
try {
|
|
FileHelper.createDirectories(configFolder);
|
|
Files.writeString(
|
|
configFolder.resolve("plugin.conf"),
|
|
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/plugin.conf")
|
|
.build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
} catch (IOException | NullPointerException ex) {
|
|
Logger.global.logWarning("Failed to create default webapp-configuration-file: " + ex);
|
|
}
|
|
}
|
|
|
|
return configManager.loadConfig(configFileRaw, PluginConfig.class);
|
|
}
|
|
|
|
private Map<String, MapConfig> loadMapConfigs(Collection<ServerWorld> autoConfigWorlds) throws ConfigurationException {
|
|
Map<String, MapConfig> mapConfigs = new HashMap<>();
|
|
|
|
Path mapFolder = Paths.get("maps");
|
|
Path mapConfigFolder = configManager.getConfigRoot().resolve(mapFolder);
|
|
|
|
if (!Files.exists(mapConfigFolder)){
|
|
try {
|
|
FileHelper.createDirectories(mapConfigFolder);
|
|
if (autoConfigWorlds.isEmpty()) {
|
|
Path worldFolder = Path.of("world");
|
|
Files.writeString(
|
|
mapConfigFolder.resolve("overworld.conf"),
|
|
createOverworldMapTemplate("Overworld", worldFolder,
|
|
DataPack.DIMENSION_OVERWORLD, 0).build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
Files.writeString(
|
|
mapConfigFolder.resolve("nether.conf"),
|
|
createNetherMapTemplate("Nether", worldFolder,
|
|
DataPack.DIMENSION_THE_NETHER, 0).build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
Files.writeString(
|
|
mapConfigFolder.resolve("end.conf"),
|
|
createEndMapTemplate("End", worldFolder,
|
|
DataPack.DIMENSION_THE_END, 0).build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
} else {
|
|
// make sure overworld-dimensions come first, so they are the ones where the
|
|
// dimension-key is omitted in the generated map-id
|
|
List<ServerWorld> overworldFirstAutoConfigWorlds = new ArrayList<>(autoConfigWorlds.size());
|
|
overworldFirstAutoConfigWorlds.addAll(autoConfigWorlds);
|
|
overworldFirstAutoConfigWorlds.sort(Comparator.comparingInt(w ->
|
|
DataPack.DIMENSION_OVERWORLD.equals(w.getDimension()) ? 0 : 1
|
|
));
|
|
|
|
Set<String> mapIds = new HashSet<>();
|
|
for (var world : overworldFirstAutoConfigWorlds) {
|
|
Path worldFolder = world.getWorldFolder().normalize();
|
|
Key dimension = world.getDimension();
|
|
|
|
String dimensionName = dimension.getNamespace().equals("minecraft") ?
|
|
dimension.getValue() : dimension.getFormatted();
|
|
|
|
// find unique map id
|
|
String id = sanitiseMapId(worldFolder.getFileName().toString()).toLowerCase(Locale.ROOT);
|
|
if (mapIds.contains(id))
|
|
id = sanitiseMapId(worldFolder.getFileName() + "_" + dimensionName).toLowerCase(Locale.ROOT);
|
|
int i = 1;
|
|
String uniqueId = id;
|
|
while (mapIds.contains(uniqueId))
|
|
uniqueId = id + "_" + (++i);
|
|
mapIds.add(uniqueId);
|
|
|
|
Path configFile = mapConfigFolder.resolve(uniqueId + ".conf");
|
|
String name = worldFolder.getFileName() + " (" + dimensionName + ")";
|
|
if (i > 1) name = name + " (" + i + ")";
|
|
|
|
ConfigTemplate template;
|
|
switch (world.getDimension().getFormatted()) {
|
|
case "minecraft:the_nether":
|
|
template = createNetherMapTemplate(name, worldFolder, dimension, i - 1);
|
|
break;
|
|
case "minecraft:the_end":
|
|
template = createEndMapTemplate(name, worldFolder, dimension, i - 1);
|
|
break;
|
|
default:
|
|
template = createOverworldMapTemplate(name, worldFolder, dimension, i - 1);
|
|
break;
|
|
}
|
|
|
|
Files.writeString(
|
|
configFile,
|
|
template.build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
}
|
|
}
|
|
} catch (IOException | NullPointerException ex) {
|
|
throw new ConfigurationException("BlueMap failed to create default map-configuration-files in\n" +
|
|
mapConfigFolder.toAbsolutePath().normalize() + "\n" +
|
|
"Check if BlueMap has the permission to create and read from this folder.",
|
|
ex);
|
|
}
|
|
}
|
|
|
|
try (Stream<Path> configFiles = Files.list(mapConfigFolder)) {
|
|
for (var configFile : configFiles.toArray(Path[]::new)) {
|
|
if (!configManager.isConfigFile(configFile)) continue;
|
|
Path rawConfig = configManager.getRaw(configFile);
|
|
String id = sanitiseMapId(rawConfig.getFileName().toString());
|
|
|
|
if (mapConfigs.containsKey(id)) {
|
|
throw new ConfigurationException("At least two of your map-config file-names result in ambiguous map-id's!\n" +
|
|
configFile.toAbsolutePath().normalize() + "\n" +
|
|
"To resolve this issue, rename this file to something else.");
|
|
}
|
|
|
|
MapConfig mapConfig = configManager.loadConfig(rawConfig, MapConfig.class);
|
|
mapConfigs.put(id, mapConfig);
|
|
}
|
|
} catch (IOException ex) {
|
|
throw new ConfigurationException("BlueMap failed to read your map configuration from\n" +
|
|
mapConfigFolder.toAbsolutePath().normalize() + "\n" +
|
|
"Check if BlueMap has the permission to create and read from this folder.",
|
|
ex);
|
|
}
|
|
|
|
return mapConfigs;
|
|
}
|
|
|
|
private Map<String, StorageConfig> loadStorageConfigs(Path defaultWebroot) throws ConfigurationException {
|
|
Map<String, StorageConfig> storageConfigs = new HashMap<>();
|
|
|
|
Path storageFolder = Paths.get("storages");
|
|
Path storageConfigFolder = configManager.getConfigRoot().resolve(storageFolder);
|
|
|
|
if (!Files.exists(storageConfigFolder)){
|
|
try {
|
|
FileHelper.createDirectories(storageConfigFolder);
|
|
Files.writeString(
|
|
storageConfigFolder.resolve("file.conf"),
|
|
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/storages/file.conf")
|
|
.setVariable("root", formatPath(defaultWebroot.resolve("maps")))
|
|
.build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
Files.writeString(
|
|
storageConfigFolder.resolve("sql.conf"),
|
|
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/storages/sql.conf").build(),
|
|
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
|
|
);
|
|
} catch (IOException | NullPointerException ex) {
|
|
throw new ConfigurationException("BlueMap failed to create default storage-configuration-files in\n" +
|
|
storageConfigFolder.toAbsolutePath().normalize() + "\n" +
|
|
"Check if BlueMap has the permission to create and read from this folder.",
|
|
ex);
|
|
}
|
|
}
|
|
|
|
|
|
try (Stream<Path> configFiles = Files.list(storageConfigFolder)) {
|
|
for (var configFile : configFiles.toArray(Path[]::new)) {
|
|
if (!configManager.isConfigFile(configFile)) continue;
|
|
Path rawConfig = configManager.getRaw(configFile);
|
|
String id = rawConfig.getFileName().toString();
|
|
|
|
StorageConfig storageConfig = configManager.loadConfig(rawConfig, StorageConfig.class); // load superclass
|
|
storageConfig = configManager.loadConfig(rawConfig, storageConfig.getStorageType().getConfigType()); // load actual config type
|
|
|
|
storageConfigs.put(id, storageConfig);
|
|
}
|
|
} catch (IOException ex) {
|
|
throw new ConfigurationException("BlueMap failed to read your map configuration from\n" +
|
|
storageConfigFolder.toAbsolutePath().normalize() + "\n" +
|
|
"Check if BlueMap has the permission to create and read from this folder.",
|
|
ex);
|
|
}
|
|
|
|
return storageConfigs;
|
|
}
|
|
|
|
private String sanitiseMapId(String id) {
|
|
return id.replaceAll("\\W", "_");
|
|
}
|
|
|
|
private ConfigTemplate createOverworldMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
|
|
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
|
|
.setVariable("name", name)
|
|
.setVariable("sorting", "" + index)
|
|
.setVariable("world", formatPath(worldFolder))
|
|
.setVariable("dimension", dimension.getFormatted())
|
|
.setVariable("sky-color", "#7dabff")
|
|
.setVariable("void-color", "#000000")
|
|
.setVariable("ambient-light", "0.1")
|
|
.setVariable("remove-caves-below-y", "55")
|
|
.setConditional("max-y-comment", true)
|
|
.setVariable("max-y", "100");
|
|
}
|
|
|
|
private ConfigTemplate createNetherMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
|
|
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
|
|
.setVariable("name", name)
|
|
.setVariable("sorting", "" + (100 + index))
|
|
.setVariable("world", formatPath(worldFolder))
|
|
.setVariable("dimension", dimension.getFormatted())
|
|
.setVariable("sky-color", "#290000")
|
|
.setVariable("void-color", "#150000")
|
|
.setVariable("ambient-light", "0.6")
|
|
.setVariable("remove-caves-below-y", "-10000")
|
|
.setConditional("max-y-comment", false)
|
|
.setVariable("max-y", "90");
|
|
}
|
|
|
|
private ConfigTemplate createEndMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
|
|
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
|
|
.setVariable("name", name)
|
|
.setVariable("sorting", "" + (200 + index))
|
|
.setVariable("world", formatPath(worldFolder))
|
|
.setVariable("dimension", dimension.getFormatted())
|
|
.setVariable("sky-color", "#080010")
|
|
.setVariable("void-color", "#080010")
|
|
.setVariable("ambient-light", "0.6")
|
|
.setVariable("remove-caves-below-y", "-10000")
|
|
.setConditional("max-y-comment", true)
|
|
.setVariable("max-y", "100");
|
|
}
|
|
|
|
private String formatPath(Path path) {
|
|
// normalize path
|
|
path = Path.of("")
|
|
.toAbsolutePath()
|
|
.relativize(path.toAbsolutePath())
|
|
.normalize();
|
|
String pathString = path.toString();
|
|
|
|
String formatted = pathString;
|
|
String separator = FileSystems.getDefault().getSeparator();
|
|
|
|
// try to replace separator with standardized forward slash
|
|
if (!separator.equals("/"))
|
|
formatted = pathString.replace(separator, "/");
|
|
|
|
// sanity check forward slash compatibility
|
|
if (!Path.of(formatted).equals(path))
|
|
formatted = pathString;
|
|
|
|
// escape all backslashes
|
|
formatted = formatted.replace("\\", "\\\\");
|
|
|
|
return formatted;
|
|
}
|
|
|
|
}
|