diff --git a/src/main/java/com/alttd/AltitudeParticles.java b/src/main/java/com/alttd/AltitudeParticles.java index ae0630f..62723bc 100644 --- a/src/main/java/com/alttd/AltitudeParticles.java +++ b/src/main/java/com/alttd/AltitudeParticles.java @@ -7,16 +7,23 @@ import com.alttd.config.ParticleConfig; import com.alttd.database.Database; import com.alttd.listeners.*; import com.alttd.objects.APartType; +import com.alttd.storage.AutoReload; import com.alttd.util.Logger; import lombok.Getter; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + public class AltitudeParticles extends JavaPlugin { @Getter public static AltitudeParticles instance; + private static AutoReload autoReload = null; + @Override public void onLoad() { instance = this; @@ -46,9 +53,44 @@ public class AltitudeParticles extends JavaPlugin { } public void reload() { + Logger.info("Reloading AltitudeParticles..."); Config.reload(); + Logger.info("1"); DatabaseConfig.reload(); + Logger.info("2"); ParticleConfig.reload(); + Logger.info("3"); + Path path = Path.of(Config.AUTO_RELOAD_PATH); + Logger.info("4"); + File file = path.toFile(); + Logger.info("5"); + if (file.exists() && file.isDirectory()) { + Logger.info("6"); + try { + Logger.info("7"); + if (autoReload != null) { + Logger.info("8-bad"); + autoReload.stop(); + Logger.info("9-bad"); + } + Logger.info("8"); + autoReload = new AutoReload(path); + Logger.info("9"); + autoReload.startWatching(); + Logger.info("10"); + } catch (IOException e) { + Logger.info("error 1"); + Logger.severe("Failed to start AutoReload at path %", Config.AUTO_RELOAD_PATH); + Logger.info("error 2"); + Logger.error("Failed to start AutoReload", e); + Logger.info("error 3"); + } + Logger.info("11"); + } else { + Logger.info("6-bad"); + Logger.severe("Failed to start AutoReload at path %", Config.AUTO_RELOAD_PATH); + } + Logger.info("12"); } } diff --git a/src/main/java/com/alttd/config/Config.java b/src/main/java/com/alttd/config/Config.java index b820138..fc8cf6c 100644 --- a/src/main/java/com/alttd/config/Config.java +++ b/src/main/java/com/alttd/config/Config.java @@ -98,4 +98,9 @@ public final class Config extends AbstractConfig { CLICK_BLOCK_COOL_DOWN = config.getInt("cool_down.click-block", CLICK_BLOCK_COOL_DOWN); TELEPORT_ARRIVE_COOL_DOWN = config.getInt("cool_down.teleport-arrive", TELEPORT_ARRIVE_COOL_DOWN); } + + public static String AUTO_RELOAD_PATH = "/mnt/configs/AltitudeParticles/particles"; + private static void loadAutoReload() { + AUTO_RELOAD_PATH = config.getString("auto-reload.path", AUTO_RELOAD_PATH); + } } diff --git a/src/main/java/com/alttd/config/ParticleConfig.java b/src/main/java/com/alttd/config/ParticleConfig.java index 31dc9bb..a123741 100644 --- a/src/main/java/com/alttd/config/ParticleConfig.java +++ b/src/main/java/com/alttd/config/ParticleConfig.java @@ -8,26 +8,32 @@ import com.alttd.objects.ParticleSet; import com.alttd.storage.ParticleStorage; import com.alttd.util.Logger; import com.destroystokyo.paper.ParticleBuilder; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.bukkit.Color; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.block.data.BlockData; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.HexFormat; -import java.util.List; -import java.util.Map; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; public class ParticleConfig { + private static final int MAX_DEPTH = 1; private static final File particlesDir = new File(File.separator + "mnt" + File.separator + "configs" + File.separator + "AltitudeParticles" + File.separator + "particles"); private static ParticleConfig instance = null; private static final ObjectMapper objectMapper = new ObjectMapper(); + static { + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + objectMapper.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE); + } private static ParticleConfig getInstance() { if (instance == null) @@ -37,27 +43,89 @@ public class ParticleConfig { /** * Finds all files in particles directory that are valid .json files + * Only searches one level deep into subdirectories * * @return all files found */ private List getJsonFiles() { List files = new ArrayList<>(); - if (!particlesDir.exists()) { - if (!particlesDir.mkdir()) - Logger.warning("Unable to create particles directory"); + + // Ensure particles directory exists + if (!ensureParticlesDirectoryExists()) { return files; } - if (!particlesDir.isDirectory()) { - Logger.warning("Particles directory doesn't exist (it's a file??)"); - return files; + + try { + Files.walkFileTree(particlesDir.toPath(), getJsonFileVistor(files)); + } catch (IOException e) { + Logger.warning("Error while traversing directory: " + e.getMessage()); } - File[] validFiles = particlesDir.listFiles(file -> file.isFile() && file.canRead() && file.getName().endsWith(".json")); - if (validFiles == null) - return files; - files.addAll(List.of(validFiles)); + return files; } + private FileVisitor getJsonFileVistor(List files) { + return new SimpleFileVisitor<>() { + private int depth = 0; + + @Override + public @NotNull FileVisitResult preVisitDirectory(@NotNull Path dir, @NotNull BasicFileAttributes attrs) { + if (depth > ParticleConfig.MAX_DEPTH) { + return FileVisitResult.SKIP_SUBTREE; + } + depth++; + return FileVisitResult.CONTINUE; + } + + @Override + public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) { + File physicalFile = file.toFile(); + if (isValidJsonFile(physicalFile)) { + files.add(physicalFile); + } + return FileVisitResult.CONTINUE; + } + + @Override + public @NotNull FileVisitResult postVisitDirectory(@NotNull Path dir, IOException exc) { + depth--; + return FileVisitResult.CONTINUE; + } + }; + } + + /** + * Ensures that the particles directory exists and is a directory + * + * @return true if directory exists or was created successfully, false otherwise + */ + private boolean ensureParticlesDirectoryExists() { + if (!particlesDir.exists()) { + if (!particlesDir.mkdirs()) { + Logger.warning("Unable to create particles directory"); + return false; + } + return true; + } + + if (!particlesDir.isDirectory()) { + Logger.warning("Particles path exists but is not a directory: " + particlesDir.getAbsolutePath()); + return false; + } + + return true; + } + + /** + * Checks if a file is a valid JSON file + * + * @param file the file to check + * @return true if the file is a valid JSON file + */ + private boolean isValidJsonFile(File file) { + return file.isFile() && file.canRead() && file.getName().endsWith(".json"); + } + /** * Converts a ParticleData object to a ParticleSet * @@ -149,20 +217,25 @@ public class ParticleConfig { public static void reload() { ParticleStorage.clear(); - ParticleConfig instance = getInstance(); + instance = getInstance(); for (File file : instance.getJsonFiles()) { - try { - ParticleData particleData = objectMapper.readValue(file, ParticleData.class); + loadParticleFromFile(file); + } + } - ParticleSet particleSet = instance.convertToParticleSet(particleData); + public static void loadParticleFromFile(File file) { + instance = getInstance(); + try { + ParticleData particleData = objectMapper.readValue(file, ParticleData.class); - ParticleStorage.addParticleSet(particleSet.getAPartType(), particleSet); - } catch (IOException e) { - Logger.error("Error reading particle file " + file.getName(), e); - } catch (Exception exception) { - Logger.error("Error processing particle file " + file.getName(), exception); - } + ParticleSet particleSet = instance.convertToParticleSet(particleData); + + ParticleStorage.addParticleSet(particleSet.getAPartType(), particleSet); + } catch (IOException e) { + Logger.error("Error reading particle file " + file.getName(), e); + } catch (Exception exception) { + Logger.error("Error processing particle file " + file.getName(), exception); } } } diff --git a/src/main/java/com/alttd/models/ParticleData.java b/src/main/java/com/alttd/models/ParticleData.java index 4a2c336..4d94ce0 100644 --- a/src/main/java/com/alttd/models/ParticleData.java +++ b/src/main/java/com/alttd/models/ParticleData.java @@ -36,6 +36,14 @@ import java.util.List; @Setter @Getter public class ParticleData { + // TODO add optional property for a list of users that can use the particle + // If that list is present the particle should be loaded as a dev particle + // Dev particles should disable all others while in use and all be grouped together + // (since the dev should know what each particle is and does) + // Seeing dev particles should require a permission + @JsonProperty("user_list") + private List userList; + @JsonProperty("particle_name") private String particleName; diff --git a/src/main/java/com/alttd/storage/AutoReload.java b/src/main/java/com/alttd/storage/AutoReload.java new file mode 100644 index 0000000..62d7a9e --- /dev/null +++ b/src/main/java/com/alttd/storage/AutoReload.java @@ -0,0 +1,171 @@ +package com.alttd.storage; + +import com.alttd.config.ParticleConfig; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +public class AutoReload { + private final WatchService watchService; + private final Map keys; + private final Path rootDirectory; + private volatile boolean running = true; + + public AutoReload(Path directory) throws IOException { + this.watchService = FileSystems.getDefault().newWatchService(); + this.keys = new HashMap<>(); + this.rootDirectory = directory; + register(directory); + registerAll(directory); + } + + private void registerAll(Path start) throws IOException { + Files.walkFileTree(start, new SimpleFileVisitor<>() { + @Override + public @NotNull FileVisitResult preVisitDirectory(@NotNull Path path, @NotNull BasicFileAttributes attrs) throws IOException { + if (path.toFile().isDirectory()) { + register(path); + } + return FileVisitResult.CONTINUE; + } + }); + } + + private void register(@NotNull Path dir) throws IOException { + WatchKey key = dir.register(watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.ENTRY_MODIFY); + keys.put(key, dir); + } + + public void startWatching() { + log.info("Starting watch thread."); + Thread watchThread = new Thread(() -> { + log.info("Watch thread started."); + while (running) { + log.info("Watch thread loop start"); + WatchKey key; + try { + key = watchService.take(); + log.info("Watch thread loop key {}", key.toString()); + } catch (InterruptedException e) { + log.error("Interrupted while waiting for key", e); + return; + } + + if (!running) { + log.info("Exiting watch thread."); + return; + } + + Path dir = keys.get(key); + if (dir == null) { + log.warn("Detected unknown key: {}. Ignoring.", key.toString()); + continue; + } + + detectChanges(key, dir); + + if (!key.reset()) { + keys.remove(key); + if (keys.isEmpty()) { + log.info("No longer watching any directories. Exiting."); + break; + } + } + } + }); + watchThread.start(); + } + + private void detectChanges(@NotNull WatchKey key, Path dir) { + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + + if (kind == StandardWatchEventKinds.OVERFLOW) { + log.warn("Detected overflow event. Ignoring."); + continue; + } + + Path child = resolveEventPath(event, dir); + boolean isDirectory = Files.isDirectory(child); + + if (shouldIgnoreDirectoryEvent(isDirectory, dir)) { + continue; + } + + if (kind == StandardWatchEventKinds.ENTRY_CREATE && isDirectory) { + handleNewDirectoryCreation(child); + continue; + } + + if (isDirectory) { + continue; + } + + handleFileEvent(kind, child); + } + } + + private @NotNull Path resolveEventPath(@NotNull WatchEvent event, Path dir) { + Object context = event.context(); + if (!(context instanceof Path path)) { + throw new IllegalArgumentException("Expected event context to be a Path, but got: " + context); + } + + return dir.resolve(path); + } + + + private boolean shouldIgnoreDirectoryEvent(boolean isDirectory, Path dir) { + if (isDirectory && !dir.equals(rootDirectory)) { + log.warn("Detected directory {} outside of root directory. Ignoring.", dir); + return true; + } + return false; + } + + private void handleNewDirectoryCreation(Path child) { + try { + log.info("Registering new directory: {}", child); + registerAll(child); + } catch (IOException e) { + log.error("Failed to register directory: {}", child); + } + } + + private void handleFileEvent(WatchEvent.Kind kind, Path child) { + if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { + log.debug("Detected file modification: {}", child); + reloadFile(child); + } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) { + log.debug("Detected file deletion: {}", child); + handleFileDeletion(); + } else if (kind == StandardWatchEventKinds.ENTRY_CREATE) { + log.debug("Detected file creation: {}", child); + reloadFile(child); + } else { + log.warn("Unknown event kind: {}", kind); + } + } + + private void reloadFile(Path child) { + ParticleConfig.loadParticleFromFile(child.toFile()); + } + + private void handleFileDeletion() { + log.info("Detected file deletion. Reloading all particles."); + ParticleConfig.reload(); + } + + public void stop() { + running = false; + } +} diff --git a/src/main/java/com/alttd/storage/ParticleStorage.java b/src/main/java/com/alttd/storage/ParticleStorage.java index 023e3c9..907bea6 100644 --- a/src/main/java/com/alttd/storage/ParticleStorage.java +++ b/src/main/java/com/alttd/storage/ParticleStorage.java @@ -2,18 +2,26 @@ package com.alttd.storage; import com.alttd.objects.APartType; import com.alttd.objects.ParticleSet; +import com.alttd.util.Logger; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Optional; public class ParticleStorage { private static final HashMap> particles = new HashMap<>(); public static void addParticleSet(APartType aPartType, ParticleSet particleSet) { List particleSets = particles.getOrDefault(aPartType, new ArrayList<>()); - if (particleSets.contains(particleSet)) + Optional existingParticleSet = particleSets.stream() + .filter(p -> p.getParticleId().equalsIgnoreCase(particleSet.getParticleId())) + .findAny(); + if (existingParticleSet.isPresent()) { + Logger.warning("Overwriting particle set %", particleSet.getParticleId()); + particleSets.remove(existingParticleSet.get()); return; + } particleSets.add(particleSet); particles.put(aPartType, particleSets); }