Automatic mod-resource-loading, loading fabric netsted jars and more

This commit is contained in:
Lukas Rieger (Blue) 2022-06-01 00:03:35 +02:00
parent f98376fe27
commit 79a12d4d80
No known key found for this signature in database
GPG Key ID: 2D09EC5ED2687FF2
36 changed files with 264 additions and 42 deletions

View File

@ -176,7 +176,7 @@ private synchronized void loadWorldsAndMaps() throws ConfigurationException, Int
String name = mapConfig.getName();
Path worldFolder = mapConfig.getWorld();
if (!Files.exists(worldFolder) || !Files.isDirectory(worldFolder)) {
if (!Files.isDirectory(worldFolder)) {
throw new ConfigurationException("Failed to load map '" + id + "': \n" +
"'" + worldFolder.toAbsolutePath().normalize() + "' does not exist or is no directory!\n" +
"Check if the 'world' setting in the config-file for that map is correct, or remove the entire config-file if you don't want that map.");
@ -323,10 +323,48 @@ public synchronized ResourcePack getResourcePack() throws ConfigurationException
});
}
if (configs.getCoreConfig().isScanForModResources()) {
// load from mods folder
Path modsFolder = serverInterface.getModsFolder().orElse(null);
if (modsFolder != null && Files.isDirectory(modsFolder)) {
try (Stream<Path> resourcepackFiles = Files.list(modsFolder)) {
resourcepackFiles
.filter(Files::isRegularFile)
.filter(file -> file.getFileName().toString().endsWith(".jar"))
.forEach(resourcepackFile -> {
try {
resourcePack.loadResources(resourcepackFile);
} catch (IOException e) {
throw new CompletionException(e);
}
});
}
}
// load from datapacks
for (Path worldFolder : getWorldFolders()) {
Path datapacksFolder = worldFolder.resolve("datapacks");
if (!Files.isDirectory(datapacksFolder)) continue;
try (Stream<Path> resourcepackFiles = Files.list(worldFolder.resolve("datapacks"))) {
resourcepackFiles
.forEach(resourcepackFile -> {
try {
resourcePack.loadResources(resourcepackFile);
} catch (IOException e) {
throw new CompletionException(e);
}
});
}
}
}
resourcePack.loadResources(resourceExtensionsFile);
resourcePack.loadResources(defaultResourceFile);
resourcePack.bake();
Logger.global.logInfo("Resources loaded.");
} catch (IOException | RuntimeException e) {
throw new ConfigurationException("Failed to parse resources!\n" +
"Is one of your resource-packs corrupted?", e);
@ -337,6 +375,17 @@ public synchronized ResourcePack getResourcePack() throws ConfigurationException
return resourcePack;
}
private Collection<Path> getWorldFolders() {
Set<Path> folders = new HashSet<>();
for (MapConfig mapConfig : configs.getMapConfigs().values()) {
Path folder = mapConfig.getWorld().toAbsolutePath().normalize();
if (Files.isDirectory(folder)) {
folders.add(folder);
}
}
return folders;
}
public BlueMapConfigs getConfigs() {
return configs;
}

View File

@ -2,14 +2,17 @@
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.util.Tristate;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
@ -36,7 +39,7 @@ public BlueMapConfigs(ServerInterface serverInterface) throws ConfigurationExcep
this.coreConfig = loadCoreConfig();
this.webserverConfig = loadWebserverConfig();
this.webappConfig = loadWebappConfig();
this.pluginConfig = loadPluginConfig();
this.pluginConfig = serverInterface.isPluginConfigEnabled() ? loadPluginConfig() : new PluginConfig();
this.storageConfigs = Collections.unmodifiableMap(loadStorageConfigs());
this.mapConfigs = Collections.unmodifiableMap(loadMapConfigs());
}
@ -75,11 +78,27 @@ private synchronized CoreConfig loadCoreConfig() throws ConfigurationException {
Path configFolder = configFile.getParent();
if (!Files.exists(configFile)) {
// determine render-thread preset (very pessimistic, rather let people increase it themselves)
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;
try {
Files.createDirectories(configFolder);
Files.writeString(
configFolder.resolve("core.conf"),
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/core.conf")
.setConditional("metrics", serverInterface.isMetricsEnabled() == Tristate.UNDEFINED)
.setVariable("timestamp", LocalDateTime.now().withNano(0).toString())
.setVariable("version", BlueMap.VERSION)
.setVariable("implementation", "bukkit")
.setVariable("render-thread-count", Integer.toString(presetRenderThreadCount))
.build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);

View File

@ -18,6 +18,8 @@ public class CoreConfig {
private Path data = Path.of("bluemap");
private boolean scanForModResources = true;
public boolean isAcceptDownload() {
return acceptDownload;
}
@ -39,4 +41,8 @@ public Path getData() {
return data;
}
public boolean isScanForModResources() {
return scanForModResources;
}
}

View File

@ -3,10 +3,6 @@
import de.bluecolored.bluemap.core.debug.DebugDump;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

View File

@ -60,6 +60,11 @@ default Optional<ServerWorld> getWorld(Path worldFolder) {
*/
Path getConfigFolder();
/**
* Returns the folder that contains the mod-jars
*/
Optional<Path> getModsFolder();
/**
* Gives the possibility to override the metrics-setting in the config
*/
@ -78,5 +83,8 @@ default Tristate isMetricsEnabled() {
*/
Optional<Player> getPlayer(UUID uuid);
default boolean isPluginConfigEnabled() {
return true;
}
}

View File

@ -11,20 +11,25 @@
# ${timestamp}
accept-download: false
# The folder where bluemap saves data-files it needs during runtime or to save e.g. the render-progress to resume it later.
# Default is "bluemap"
data: "bluemap"
# This changes the amount of threads that BlueMap will use to render the maps.
# A higher value can improve render-speed but could impact performance on the host machine.
# This should be always below or equal to the number of available processor-cores.
# Zero or a negative value means the amount of of available processor-cores subtracted by the value.
# (So a value of -2 with 6 cores results in 4 render-processes)
# Default is 1
render-thread-count: 1
render-thread-count: ${render-thread-count}
# Controls whether BlueMap should try to find and load mod-resources and datapacks from the server/world-directories.
# Default is true
scan-for-mod-resources: true
${metrics<<
# If this is true, BlueMap might send really basic metrics reports containing only the implementation-type and the version that is being used to https://metrics.bluecolored.de/bluemap/
# This allows me to track the basic usage of BlueMap and helps me stay motivated to further develop this tool! Please leave it on :)
# An example report looks like this: {"implementation":"${implementation}","version":"${version}"}
# Default is true
metrics: true
>>}
# The folder where bluemap saves data-files it needs during runtime or to save e.g. the render-progress to resume it later.
# Default is "bluemap"
data: "bluemap"
>>}

View File

@ -8,10 +8,6 @@
# Default is true
live-player-markers: true
# Download the skin from mojang-serves when a player joins your server, so it can be used for the player-markers.
# Default is true
skin-download: true
# A list of gamemodes that will prevent a player from appearing on the map.
# Possible values are: survival, creative, spectator, adventure
hidden-gamemodes: [
@ -30,6 +26,10 @@ hide-invisible: true
# Default is false
hide-sneaking: false
# Download the skin from mojang-serves when a player joins your server, so it can be used for the player-markers.
# Default is true
skin-download: true
# The amount of players that is needed to pause BlueMap's render-threads.
# -> If this amount of players or more is online, bluemap will stop rendering map-updates until enough players
# have logged off again

View File

@ -30,6 +30,7 @@
import java.io.IOException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
public class BlueMap {
@ -60,7 +61,12 @@ public class BlueMap {
public static final ForkJoinPool THREAD_POOL = new ForkJoinPool(
Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
pool -> {
ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
thread.setContextClassLoader(BlueMap.class.getClassLoader()); // use plugin-intended classloader
thread.setName("BlueMap-FJ-" + thread.getPoolIndex());
return thread;
},
(thread, ex) -> {
if (ex instanceof ClassNotFoundException && ex.getMessage().contains("RemovalCause")) {
Logger.global.noFloodWarning("RemovalCauseError", ex.getMessage());

View File

@ -60,7 +60,10 @@ public static TextureGallery readTexturesFile(InputStream in) throws IOException
Texture[] textures = ResourcesGson.INSTANCE.fromJson(reader, Texture[].class);
gallery.nextId = textures.length;
for (int ordinal = 0; ordinal < textures.length; ordinal++) {
gallery.ordinalMap.put(textures[ordinal].getResourcePath(), ordinal);
Texture texture = textures[ordinal];
if (texture != null) {
gallery.ordinalMap.put(textures[ordinal].getResourcePath(), ordinal);
}
}
} catch (JsonIOException ex) {
throw new IOException(ex);

View File

@ -19,6 +19,7 @@ public void write(JsonWriter out, Direction value) throws IOException {
public Direction read(JsonReader in) throws IOException {
String name = in.nextString();
if (name.equalsIgnoreCase("bottom")) return Direction.DOWN;
if (name.equalsIgnoreCase("top")) return Direction.UP;
return Direction.fromString(name);
}

View File

@ -7,11 +7,14 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.Face;
import de.bluecolored.bluemap.core.util.Direction;
import de.bluecolored.bluemap.core.util.math.Axis;
import de.bluecolored.bluemap.core.util.math.Color;
import java.io.IOException;
import java.util.EnumMap;
public class ResourcesGson {
@ -37,4 +40,9 @@ private static Gson createGson() {
}
public static String nextStringOrBoolean(JsonReader in) throws IOException {
if (in.peek() == JsonToken.BOOLEAN) return Boolean.toString(in.nextBoolean());
return in.nextString();
}
}

View File

@ -2,6 +2,8 @@
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
@ -31,13 +33,14 @@
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@DebugDump
public class ResourcePack {
public static final ResourcePath<BlockState> MISSING_BLOCK_STATE = new ResourcePath<>("bluemap", "missing");
public static final ResourcePath<BlockModel> MISSING_BLOCK_MODEL = new ResourcePath<>("bluemap", "missing");
public static final ResourcePath<Texture> MISSING_TEXTURE = new ResourcePath<>("bluemap", "missing");
public static final ResourcePath<BlockModel> MISSING_BLOCK_MODEL = new ResourcePath<>("bluemap", "block/missing");
public static final ResourcePath<Texture> MISSING_TEXTURE = new ResourcePath<>("bluemap", "block/missing");
private final Map<String, ResourcePath<BlockState>> blockStatePaths;
private final Map<ResourcePath<BlockState>, BlockState> blockStates;
@ -155,18 +158,38 @@ private BlockProperties loadBlockProperties(de.bluecolored.bluemap.core.world.Bl
}
public synchronized void loadResources(Path root) throws IOException {
Logger.global.logInfo("Loading resources from: " + root);
Logger.global.logDebug("Loading resources from: " + root + " ...");
loadResourcesInternal(root);
}
private synchronized void loadResourcesInternal(Path root) throws IOException {
if (!Files.isDirectory(root)) {
try (FileSystem fileSystem = FileSystems.newFileSystem(root, (ClassLoader) null)) {
for (Path fsRoot : fileSystem.getRootDirectories()) {
if (!Files.isDirectory(fsRoot)) continue;
this.loadResources(fsRoot);
this.loadResourcesInternal(fsRoot);
}
} catch (Exception ex) {
Logger.global.logDebug("Failed to read '" + root + "': " + ex);
}
return;
}
// load nested jars from fabric.mod.json if present
Path fabricModJson = root.resolve("fabric.mod.json");
if (Files.isRegularFile(fabricModJson)) {
try (BufferedReader reader = Files.newBufferedReader(fabricModJson)) {
JsonObject rootElement = ResourcesGson.INSTANCE.fromJson(reader, JsonObject.class);
for (JsonElement element : rootElement.getAsJsonArray("jars")) {
Path file = root.resolve(element.getAsJsonObject().get("file").getAsString());
if (Files.exists(file)) loadResourcesInternal(file);
}
} catch (Exception ex) {
Logger.global.logDebug("Failed to read fabric.mod.json: " + ex);
}
}
try {
// do those in parallel
CompletableFuture.allOf(
@ -184,12 +207,14 @@ public synchronized void loadResources(Path root) throws IOException {
return ResourcesGson.INSTANCE.fromJson(reader, BlockState.class);
}
}, blockStates));
}),
}, BlueMap.THREAD_POOL),
// load blockmodels
CompletableFuture.runAsync(() -> {
list(root.resolve("assets"))
.map(path -> path.resolve("models").resolve("block"))
.map(path -> path.resolve("models"))
.flatMap(ResourcePack::list)
.filter(path -> Pattern.matches("blocks?", path.getFileName().toString()))
.filter(Files::isDirectory)
.flatMap(ResourcePack::walk)
.filter(path -> path.getFileName().toString().endsWith(".json"))
@ -199,12 +224,14 @@ public synchronized void loadResources(Path root) throws IOException {
return ResourcesGson.INSTANCE.fromJson(reader, BlockModel.class);
}
}, blockModels));
}),
}, BlueMap.THREAD_POOL),
// load textures
CompletableFuture.runAsync(() -> {
list(root.resolve("assets"))
.map(path -> path.resolve("textures").resolve("block"))
.map(path -> path.resolve("textures"))
.flatMap(ResourcePack::list)
.filter(path -> Pattern.matches("blocks?", path.getFileName().toString()))
.filter(Files::isDirectory)
.flatMap(ResourcePack::walk)
.filter(path -> path.getFileName().toString().endsWith(".png"))
@ -215,7 +242,7 @@ public synchronized void loadResources(Path root) throws IOException {
return Texture.from(resourcePath, ImageIO.read(in));
}
}, textures));
}),
}, BlueMap.THREAD_POOL),
// load colormaps
CompletableFuture.runAsync(() -> {
@ -227,7 +254,7 @@ public synchronized void loadResources(Path root) throws IOException {
return ImageIO.read(in);
}
}, colormaps));
}),
}, BlueMap.THREAD_POOL),
// load block-color configs
CompletableFuture.runAsync(() -> {
@ -241,7 +268,7 @@ public synchronized void loadResources(Path root) throws IOException {
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
}
});
}),
}, BlueMap.THREAD_POOL),
// load biome configs
CompletableFuture.runAsync(() -> {
@ -269,7 +296,7 @@ public synchronized void loadResources(Path root) throws IOException {
}
})
);
}),
}, BlueMap.THREAD_POOL),
// load block-properties configs
CompletableFuture.runAsync(() -> {
@ -283,7 +310,7 @@ public synchronized void loadResources(Path root) throws IOException {
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
}
});
})
}, BlueMap.THREAD_POOL)
).join();
@ -296,7 +323,7 @@ public synchronized void loadResources(Path root) throws IOException {
}
public synchronized void bake() throws IOException {
Logger.global.logInfo("Baking resources...");
Logger.global.logDebug("Baking resources...");
// fill path maps
blockStates.keySet().forEach(path -> blockStatePaths.put(path.getFormatted(), path));

View File

@ -5,6 +5,7 @@
import com.google.gson.stream.JsonReader;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.AbstractTypeAdapterFactory;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.world.BlockState;
import org.apache.commons.lang3.StringUtils;
@ -52,8 +53,11 @@ public Multipart read(JsonReader in, Gson gson) throws IOException {
in.beginObject();
while (in.hasNext()) {
String key = in.nextName();
if (key.equals("when")) condition = readCondition(in);
if (key.equals("apply")) variantSet = gson.fromJson(in, VariantSet.class);
switch (key) {
case "when": condition = readCondition(in); break;
case "apply": variantSet = gson.fromJson(in, VariantSet.class); break;
default: in.skipValue(); break;
}
}
in.endObject();
@ -71,7 +75,10 @@ public BlockStateCondition readCondition(JsonReader in) throws IOException {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
if (name.equals(JSON_COMMENT)) continue;
if (name.equals(JSON_COMMENT)) {
in.skipValue();
continue;
}
if (name.equals("OR")) {
List<BlockStateCondition> orConditions = new ArrayList<>();
@ -82,8 +89,17 @@ public BlockStateCondition readCondition(JsonReader in) throws IOException {
in.endArray();
andConditions.add(
BlockStateCondition.or(orConditions.toArray(new BlockStateCondition[0])));
} else if (name.equals("AND")) {
List<BlockStateCondition> andArray = new ArrayList<>();
in.beginArray();
while (in.hasNext()) {
andArray.add(readCondition(in));
}
in.endArray();
andConditions.add(
BlockStateCondition.and(andArray.toArray(new BlockStateCondition[0])));
} else {
String[] values = StringUtils.split(in.nextString(), '|');
String[] values = StringUtils.split(ResourcesGson.nextStringOrBoolean(in), '|');
andConditions.add(BlockStateCondition.property(name, values));
}
}

View File

@ -4,6 +4,7 @@
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resources.AbstractTypeAdapterFactory;
import de.bluecolored.bluemap.core.world.BlockState;
import org.apache.commons.lang3.StringUtils;
@ -60,7 +61,10 @@ public Variants read(JsonReader in, Gson gson) throws IOException {
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
if (name.equals(JSON_COMMENT)) continue;
if (name.equals(JSON_COMMENT)) {
in.skipValue();
continue;
}
BlockStateCondition condition = parseConditionString(name);
VariantSet variantSet = gson.fromJson(in, VariantSet.class);
@ -68,7 +72,7 @@ public Variants read(JsonReader in, Gson gson) throws IOException {
if (variantSet.getCondition() == BlockStateCondition.all()) {
result.defaultVariant = variantSet;
} else {
} else if (variantSet.getCondition() != BlockStateCondition.none()) {
result.variants.add(variantSet);
}
}
@ -79,19 +83,23 @@ public Variants read(JsonReader in, Gson gson) throws IOException {
private BlockStateCondition parseConditionString(String conditionString) {
List<BlockStateCondition> conditions = new ArrayList<>();
boolean invalid = false;
if (!conditionString.isEmpty() && !conditionString.equals("default") && !conditionString.equals("normal")) {
String[] conditionSplit = StringUtils.split(conditionString, ',');
for (String element : conditionSplit) {
String[] keyval = StringUtils.split(element, "=", 2);
if (keyval.length < 2)
throw new IllegalArgumentException("Condition-String '" + conditionString + "' is invalid!");
if (keyval.length < 2) {
Logger.global.logDebug("Failed to parse condition: Condition-String '" + conditionString + "' is invalid!");
invalid = true;
continue;
}
conditions.add(BlockStateCondition.property(keyval[0], keyval[1]));
}
}
BlockStateCondition condition;
if (conditions.isEmpty()) {
condition = BlockStateCondition.all();
condition = invalid ? BlockStateCondition.none() : BlockStateCondition.all();
} else if (conditions.size() == 1) {
condition = conditions.get(0);
} else {

View File

@ -623,7 +623,7 @@ private int loadMapTileCompressionFK(Compression compression) throws SQLExceptio
return lookupFK("bluemap_map_tile_compression", "id", "compression", compression.getTypeId());
}
@SuppressWarnings("SameParameterValue")
@SuppressWarnings({"SameParameterValue", "SqlResolve"})
private int lookupFK(String table, String idField, String valueField, String value) throws SQLException, IOException {
return recoveringConnection(connection -> {
int key;

View File

@ -239,6 +239,11 @@ public Path getConfigFolder() {
return configFolder;
}
@Override
public Optional<Path> getModsFolder() {
return Optional.empty();
}
@Override
public Collection<Player> getOnlinePlayers() {
return Collections.emptyList();
@ -249,6 +254,11 @@ public Optional<Player> getPlayer(UUID uuid) {
return Optional.empty();
}
@Override
public boolean isPluginConfigEnabled() {
return false;
}
public static void main(String[] args) {
CommandLineParser parser = new DefaultParser();

View File

@ -151,6 +151,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
public void onPlayerJoin(MinecraftServer server, ServerPlayerEntity playerInstance) {
if (this.serverInstance != server) return;

View File

@ -151,6 +151,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
public void onPlayerJoin(MinecraftServer server, ServerPlayerEntity playerInstance) {
if (this.serverInstance != server) return;

View File

@ -151,6 +151,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
public void onPlayerJoin(MinecraftServer server, ServerPlayerEntity playerInstance) {
if (this.serverInstance != server) return;

View File

@ -151,6 +151,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
public void onPlayerJoin(MinecraftServer server, ServerPlayerEntity playerInstance) {
if (this.serverInstance != server) return;

View File

@ -151,6 +151,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
public void onPlayerJoin(MinecraftServer server, ServerPlayerEntity playerInstance) {
if (this.serverInstance != server) return;

View File

@ -164,6 +164,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
@SubscribeEvent
public void onPlayerJoin(PlayerLoggedInEvent evt) {
PlayerEntity playerInstance = evt.getPlayer();

View File

@ -164,6 +164,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
@SubscribeEvent
public void onPlayerJoin(PlayerLoggedInEvent evt) {
PlayerEntity playerInstance = evt.getPlayer();

View File

@ -164,6 +164,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
@SubscribeEvent
public void onPlayerJoin(PlayerLoggedInEvent evt) {
PlayerEntity playerInstance = evt.getPlayer();

View File

@ -163,6 +163,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
@SubscribeEvent
public void onPlayerJoin(PlayerLoggedInEvent evt) {
var playerInstance = evt.getPlayer();

View File

@ -163,6 +163,11 @@ public Path getConfigFolder() {
return Path.of("config", "bluemap");
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
@SubscribeEvent
public void onPlayerJoin(PlayerLoggedInEvent evt) {
var playerInstance = evt.getPlayer();

View File

@ -199,6 +199,11 @@ public Path getConfigFolder() {
return getDataFolder().toPath();
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods")); // in case this is a Bukkit/Forge hybrid
}
public Plugin getPlugin() {
return pluginInstance;
}

View File

@ -235,6 +235,11 @@ public Path getConfigFolder() {
return configurationDir;
}
@Override
public Optional<Path> getModsFolder() {
return Optional.of(Path.of("mods"));
}
@Override
public Collection<Player> getOnlinePlayers() {
return onlinePlayerMap.values();