From f7135fc9a6e8d21c9b71d6daf9ff8ff041409cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sun, 30 Jul 2023 17:58:03 +0200 Subject: [PATCH 01/30] Initial support for directories in image storage - Updated ImageStorage class - Updated ImageFile and ItemService classes - Updated ImageCommand class - Updated YamipaPlugin class --- .../josemmo/bukkit/plugin/YamipaPlugin.java | 4 +- .../bukkit/plugin/commands/ImageCommand.java | 5 +- .../bukkit/plugin/renderer/ItemService.java | 4 +- .../bukkit/plugin/storage/ImageFile.java | 50 +-- .../bukkit/plugin/storage/ImageStorage.java | 287 +++++++++++++----- 5 files changed, 243 insertions(+), 107 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index a13d7ef..b335d78 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -82,8 +82,8 @@ public void onEnable() { // Create image storage storage = new ImageStorage( - basePath.resolve(imagesPath).toString(), - basePath.resolve(cachePath).toString() + basePath.resolve(imagesPath).toAbsolutePath(), + basePath.resolve(cachePath).toAbsolutePath() ); try { storage.start(); diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java index bdbd13d..663fb5a 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java @@ -24,7 +24,6 @@ import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.*; @@ -93,7 +92,7 @@ public static void downloadImage(@NotNull CommandSender sender, @NotNull String YamipaPlugin plugin = YamipaPlugin.getInstance(); // Validate destination file - Path basePath = Paths.get(plugin.getStorage().getBasePath()); + Path basePath = plugin.getStorage().getBasePath(); Path destPath = basePath.resolve(filename); if (!destPath.getParent().equals(basePath)) { sender.sendMessage(ChatColor.RED + "Not a valid destination filename"); @@ -207,7 +206,7 @@ public static boolean placeImage( // Create new fake image instance Rotation rotation = FakeImage.getRotationFromPlayerEyesight(face, player.getEyeLocation()); - FakeImage fakeImage = new FakeImage(image.getName(), location, face, rotation, + FakeImage fakeImage = new FakeImage(image.getFilename(), location, face, rotation, width, height, new Date(), player, flags); // Make sure image can be placed diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java index 5e34c33..0bbb94b 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java @@ -48,9 +48,9 @@ public class ItemService extends InteractWithEntityListener implements Listener // Set metadata PersistentDataContainer itemData = itemMeta.getPersistentDataContainer(); - itemMeta.setDisplayName(image.getName() + ChatColor.AQUA + " (" + width + "x" + height + ")"); + itemMeta.setDisplayName(image.getFilename() + ChatColor.AQUA + " (" + width + "x" + height + ")"); itemMeta.setLore(Collections.singletonList("Yamipa image")); - itemData.set(NSK_FILENAME, PersistentDataType.STRING, image.getName()); + itemData.set(NSK_FILENAME, PersistentDataType.STRING, image.getFilename()); itemData.set(NSK_WIDTH, PersistentDataType.INTEGER, width); itemData.set(NSK_HEIGHT, PersistentDataType.INTEGER, height); itemData.set(NSK_FLAGS, PersistentDataType.INTEGER, flags); diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java index 3610eb9..c781366 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java @@ -20,7 +20,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.*; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -38,25 +38,25 @@ public class ImageFile { private final ConcurrentHashMap locks = new ConcurrentHashMap<>(); private final Map cache = new HashMap<>(); private final Map> subscribers = new HashMap<>(); - private final String name; - private final String path; + private final String filename; + private final Path path; /** * Class constructor - * @param name Image file name - * @param path Path to image file + * @param filename Image filename + * @param path Path to image file */ - protected ImageFile(@NotNull String name, @NotNull String path) { - this.name = name; + protected ImageFile(@NotNull String filename, @NotNull Path path) { + this.filename = filename; this.path = path; } /** - * Get image file name - * @return Image file name + * Get image filename + * @return Image filename */ - public @NotNull String getName() { - return name; + public @NotNull String getFilename() { + return filename; } /** @@ -65,7 +65,7 @@ protected ImageFile(@NotNull String name, @NotNull String path) { * @throws IOException if failed to get suitable image reader */ private @NotNull ImageReader getImageReader() throws IOException { - ImageInputStream inputStream = ImageIO.createImageInputStream(new File(path)); + ImageInputStream inputStream = ImageIO.createImageInputStream(path.toFile()); ImageReader reader = ImageIO.getImageReaders(inputStream).next(); reader.setInput(inputStream); return reader; @@ -187,7 +187,7 @@ protected ImageFile(@NotNull String name, @NotNull String path) { */ public long getLastModified() { try { - return Files.getLastModifiedTime(Paths.get(path)).toMillis(); + return Files.getLastModifiedTime(path).toMillis(); } catch (Exception __) { return 0L; } @@ -241,8 +241,8 @@ public long getLastModified() { } // Try to get maps from disk cache - String cacheFilename = name + "." + cacheKey + "." + CACHE_EXT; - File cacheFile = Paths.get(plugin.getStorage().getCachePath(), cacheFilename).toFile(); + String cacheFilename = filename + "." + cacheKey + "." + CACHE_EXT; + File cacheFile = plugin.getStorage().getCachePath().resolve(cacheFilename).toFile(); if (cacheFile.isFile() && cacheFile.lastModified() >= getLastModified()) { try { FakeMapsContainer container = readMapsFromCacheFile(cacheFile, width, height); @@ -282,6 +282,7 @@ public long getLastModified() { // Persist in disk cache container = new FakeMapsContainer(matrix, delay); try { + cacheFile.getParentFile().mkdirs(); writeMapsToCacheFile(container, cacheFile); } catch (IOException e) { plugin.log(Level.SEVERE, "Failed to write to cache file \"" + cacheFile.getAbsolutePath() + "\"", e); @@ -401,7 +402,7 @@ public synchronized void unsubscribe(@NotNull FakeImage subscriber) { if (currentSubscribers.isEmpty()) { subscribers.remove(cacheKey); cache.remove(cacheKey); - plugin.fine("Invalidated cached maps \"" + cacheKey + "\" in ImageFile#(" + name + ")"); + plugin.fine("Invalidated cached maps \"" + cacheKey + "\" in ImageFile#(" + filename + ")"); } } @@ -414,14 +415,21 @@ public synchronized void unsubscribe(@NotNull FakeImage subscriber) { public synchronized void invalidate() { cache.clear(); - // Delete disk cache files - File[] files = Paths.get(plugin.getStorage().getCachePath()).toFile().listFiles((__, filename) -> { - return filename.matches(Pattern.quote(name) + "\\.[0-9]+-[0-9]+\\." + CACHE_EXT); - }); + // Find cache files to delete + Path cachePath = plugin.getStorage().getCachePath(); + String cachePattern = Pattern.quote(path.getFileName().toString()) + "\\.[0-9]+-[0-9]+\\." + CACHE_EXT; + File cacheDirectory = cachePath.resolve(filename).getParent().toFile(); + if (!cacheDirectory.exists()) { + // Cache (sub)directory does not exist, no need to delete files + return; + } + File[] files = cacheDirectory.listFiles((__, item) -> item.matches(cachePattern)); if (files == null) { - plugin.warning("An error occurred when listing cache files for image \"" + name + "\""); + plugin.warning("An error occurred when listing cache files for image \"" + filename + "\""); return; } + + // Delete disk cache files for (File file : files) { if (!file.delete()) { plugin.warning("Failed to delete cache file \"" + file.getAbsolutePath() + "\""); diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index 51bda79..9b12c78 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -1,33 +1,39 @@ package io.josemmo.bukkit.plugin.storage; +import com.sun.nio.file.ExtendedWatchEventModifier; import io.josemmo.bukkit.plugin.YamipaPlugin; -import org.bukkit.Bukkit; -import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.nio.file.*; -import java.util.Objects; -import java.util.SortedMap; -import java.util.TreeMap; +import java.util.*; import java.util.logging.Level; +/** + * A service whose purpose is to keep track of all available image files in a given directory. + * It supports recursive storage (e.g., nested directories) and watches for file system changes in realtime. + *

+ * All files are indexed based on their filename. + * Due to recursion, filenames can contain forward slashes (i.e., "/") and act as relative paths to the base + * directory. + */ public class ImageStorage { - static public final long POLLING_INTERVAL = 20L * 5; // In server ticks - static private final YamipaPlugin plugin = YamipaPlugin.getInstance(); - private final String basePath; - private final String cachePath; - private final SortedMap cachedImages = new TreeMap<>(); - private BukkitTask task; + private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); + private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); + /** Map of registered files indexed by filename */ + private final SortedMap files = new TreeMap<>(); + private final Path basePath; + private final Path cachePath; private WatchService watchService; + private Thread watchServiceThread; /** * Class constructor * @param basePath Path to directory containing the images * @param cachePath Path to directory containing the cached image maps */ - public ImageStorage(@NotNull String basePath, @NotNull String cachePath) { + public ImageStorage(@NotNull Path basePath, @NotNull Path cachePath) { this.basePath = basePath; this.cachePath = cachePath; } @@ -36,7 +42,7 @@ public ImageStorage(@NotNull String basePath, @NotNull String cachePath) { * Get base path * @return Base path */ - public @NotNull String getBasePath() { + public @NotNull Path getBasePath() { return basePath; } @@ -44,84 +50,38 @@ public ImageStorage(@NotNull String basePath, @NotNull String cachePath) { * Get cache path * @return Cache path */ - public @NotNull String getCachePath() { + public @NotNull Path getCachePath() { return cachePath; } /** - * Start instance - * @throws SecurityException if failed to access filesystem - * @throws Exception if failed to start watch service + * Start service + * @throws IOException if failed to start watch service */ - public void start() throws Exception { - // Create directories if necessary - File directory = new File(basePath); - if (directory.mkdirs()) { + public void start() throws IOException { + // Create base directories if necessary + if (basePath.toFile().mkdirs()) { plugin.info("Created images directory as it did not exist"); } - if (new File(cachePath).mkdirs()) { + if (cachePath.toFile().mkdirs()) { plugin.info("Created cache directory as it did not exist"); } - // Do initial directory listing - for (File file : Objects.requireNonNull(directory.listFiles())) { - if (file.isDirectory()) continue; - String filename = file.getName(); - cachedImages.put(filename, new ImageFile(filename, file.getAbsolutePath())); - } - plugin.fine("Found " + cachedImages.size() + " file(s) in images directory"); - - // Prepare watch service + // Start watching files watchService = FileSystems.getDefault().newWatchService(); - Path directoryPath = directory.toPath(); - directoryPath.register( - watchService, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_DELETE, - StandardWatchEventKinds.ENTRY_MODIFY - ); - - // Start watching for changes - task = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> { - WatchKey watchKey = watchService.poll(); - if (watchKey == null) return; - - watchKey.pollEvents().forEach(event -> { - WatchEvent.Kind kind = event.kind(); - File file = directoryPath.resolve((Path) event.context()).toFile(); - if (file.isDirectory()) return; - - String filename = file.getName(); - synchronized (this) { - if (kind == StandardWatchEventKinds.ENTRY_DELETE) { - ImageFile imageFile = cachedImages.get(filename); - if (imageFile != null) { - imageFile.invalidate(); - cachedImages.remove(filename); - } - plugin.fine("Detected file deletion at " + filename); - } else if (cachedImages.containsKey(filename)) { - cachedImages.get(filename).invalidate(); - plugin.fine("Detected file update at " + filename); - } else { - cachedImages.put(filename, new ImageFile(filename, file.getAbsolutePath())); - plugin.fine("Detected file creation at " + filename); - } - } - }); - - watchKey.reset(); - }, POLLING_INTERVAL, POLLING_INTERVAL); - plugin.fine("Started watching for file changes in images directory"); + watchServiceThread = new WatcherThread(); + watchServiceThread.start(); + registerDirectory(basePath, true); } /** - * Stop instance + * Stop service */ public void stop() { - // Cancel async task - if (task != null) { - task.cancel(); + // Interrupt watch service thread + if (watchServiceThread != null) { + watchServiceThread.interrupt(); + watchServiceThread = null; } // Close watch service @@ -131,6 +91,7 @@ public void stop() { } catch (IOException e) { plugin.log(Level.WARNING, "Failed to close watch service", e); } + watchService = null; } } @@ -139,15 +100,15 @@ public void stop() { * @return Number of images */ public synchronized int size() { - return cachedImages.size(); + return files.size(); } /** * Get all image filenames - * @return Sorted array of image filenames + * @return Sorted array of filenames */ public synchronized @NotNull String[] getAllFilenames() { - return cachedImages.keySet().toArray(new String[0]); + return files.keySet().toArray(new String[0]); } /** @@ -156,6 +117,174 @@ public synchronized int size() { * @return Image instance or NULL if not found */ public synchronized @Nullable ImageFile get(@NotNull String filename) { - return cachedImages.get(filename); + return files.get(filename); + } + + /** + * Register directory + * @param path Path to directory + * @param isBase Whether is base directory or not + */ + private synchronized void registerDirectory(@NotNull Path path, boolean isBase) { + // Validate path + if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + plugin.warning("Cannot list files in \"" + path.toAbsolutePath() + "\" as it is not a valid directory"); + return; + } + + // Do initial directory listing + for (File child : Objects.requireNonNull(path.toFile().listFiles())) { + if (child.isDirectory()) { + registerDirectory(child.toPath(), false); + } else { + registerFile(child.toPath()); + } + } + + // Start watching for files changes + if (!IS_WINDOWS || isBase) { + try { + WatchEvent.Kind[] events = new WatchEvent.Kind[]{ + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.ENTRY_MODIFY + }; + WatchEvent.Modifier[] modifiers = IS_WINDOWS ? + new WatchEvent.Modifier[]{ExtendedWatchEventModifier.FILE_TREE} : + new WatchEvent.Modifier[0]; + path.register(watchService, events, modifiers); + plugin.fine("Started watching directory at \"" + path.toAbsolutePath() + "\""); + } catch (IOException e) { + plugin.log(Level.SEVERE, "Failed to register directory", e); + } + } + } + + /** + * Register file + * @param path Path to file + */ + private synchronized void registerFile(@NotNull Path path) { + // Validate path + if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + plugin.warning("Cannot register \"" + path.toAbsolutePath() + "\" as it is not a valid file"); + return; + } + + // Add file to map + String filename = getFilename(path); + ImageFile imageFile = new ImageFile(filename, path); + if (files.putIfAbsent(filename, imageFile) == null) { + plugin.fine("Registered file \"" + filename + "\""); + } + } + + /** + * Unregister directory + * @param filename Filename to directory + */ + private synchronized void unregisterDirectory(@NotNull String filename) { + boolean foundFirst = false; + Iterator> iter = files.entrySet().iterator(); + while (iter.hasNext()) { + String entryKey = iter.next().getKey(); + if (entryKey.startsWith(filename+"/")) { + foundFirst = true; + iter.remove(); + plugin.fine("Unregistered file \"" + entryKey + "\""); + } else if (foundFirst) { + // We can break early because set is alphabetically sorted by key + break; + } + } + } + + /** + * Unregister file + * @param filename Filename to file + */ + private synchronized void unregisterFile(@NotNull String filename) { + ImageFile imageFile = files.remove(filename); + if (imageFile != null) { + imageFile.invalidate(); + plugin.fine("Unregistered file \"" + filename + "\""); + } + } + + /** + * Invalidate file + * @param filename Filename to file + */ + private synchronized void invalidateFile(@NotNull String filename) { + ImageFile imageFile = files.get(filename); + if (imageFile != null) { + imageFile.invalidate(); + } + } + + /** + * Handle watch event + * @param path Path to file or directory + * @param kind Event kind + */ + private synchronized void handleWatchEvent(@NotNull Path path, WatchEvent.Kind kind) { + // Check whether file currently exists in file system (for CREATE and UPDATE events) + // or is registered in the file list (for DELETE event) + String filename = getFilename(path); + boolean isFile = path.toFile().isFile() || files.containsKey(filename); + + // Handle creation event + if (kind == StandardWatchEventKinds.ENTRY_CREATE) { + if (isFile) { + registerFile(path); + } else { + registerDirectory(path, false); + } + return; + } + + // Handle deletion event + if (kind == StandardWatchEventKinds.ENTRY_DELETE) { + if (isFile) { + unregisterFile(filename); + } else { + unregisterDirectory(filename); + } + return; + } + + // Handle modification event + if (kind == StandardWatchEventKinds.ENTRY_MODIFY && isFile) { + invalidateFile(filename); + } + } + + /** + * Get filename from path + * @param path Path to file + * @return Relative path used for indexing + */ + private @NotNull String getFilename(@NotNull Path path) { + return basePath.relativize(path).toString().replaceAll("\\\\", "/"); + } + + private class WatcherThread extends Thread { + @Override + public void run() { + try { + WatchKey key; + while ((key = watchService.take()) != null) { + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + Path keyPath = (Path) key.watchable(); + Path path = keyPath.resolve((Path) event.context()); + handleWatchEvent(path, kind); + } + key.reset(); + } + } catch (InterruptedException __) { + // Silently ignore exception, this is expected when service shuts down + } + } } } From a9308a5aac4f13fde8b80a2b3beefdb2400713af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sun, 30 Jul 2023 18:40:35 +0200 Subject: [PATCH 02/30] Added support for directories in configuration - Updated CsvConfiguration class --- .../bukkit/plugin/utils/CsvConfiguration.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java b/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java index 2c2e163..d0c1e29 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java @@ -1,7 +1,6 @@ package io.josemmo.bukkit.plugin.utils; import org.jetbrains.annotations.NotNull; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.Charset; @@ -12,9 +11,14 @@ import java.util.List; import java.util.stream.Stream; +/** + * A class for parsing simple CSV files. + * It's used for loading and writing Yamipa's configuration file ("image.dat" by default) and does not + * support escaping of special characters, as this is not currently needed. + */ public class CsvConfiguration { public static final Charset CHARSET = StandardCharsets.UTF_8; - public static final String COLUMN_DELIMITER = "/"; + public static final String COLUMN_DELIMITER = ";"; private final List data = new ArrayList<>(); /** @@ -39,13 +43,24 @@ public void addRow(@NotNull String[] row) { * @throws IOException if failed to read file */ public void load(@NotNull String path) throws IOException { - Stream stream = Files.lines(Paths.get(path), CHARSET); - stream.forEach(line -> { - line = line.trim(); - if (!line.isEmpty()) { + try (Stream stream = Files.lines(Paths.get(path), CHARSET)) { + stream.forEach(line -> { + line = line.trim(); + + // Ignore empty lines + if (line.isEmpty()) { + return; + } + + // Migrate legacy format + if (!line.contains(COLUMN_DELIMITER)) { + line = line.replaceAll("/", COLUMN_DELIMITER); + } + + // Parse line addRow(line.split(COLUMN_DELIMITER)); - } - }); + }); + } } /** @@ -54,7 +69,7 @@ public void load(@NotNull String path) throws IOException { * @throws IOException if failed to write file */ public void save(@NotNull String path) throws IOException { - try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(path), CHARSET)) { + try (OutputStreamWriter writer = new OutputStreamWriter(Files.newOutputStream(Paths.get(path)), CHARSET)) { for (String[] row : getRows()) { writer.write(String.join(COLUMN_DELIMITER, row) + "\n"); } From bb1d951c737cf075975c7acca522652b95580ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 12:49:56 +0200 Subject: [PATCH 03/30] Surrounded image file arguments in quotes - Updated ImageFileArgument class - Updated README.md --- README.md | 12 ++++++------ .../plugin/commands/arguments/ImageFileArgument.java | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c666a0d..1165c4f 100644 --- a/README.md +++ b/README.md @@ -75,17 +75,17 @@ This plugin adds the following commands: - Show help\ `/image` - Download an image from a URL and save it with another name\ - `/image download "https://www.example.com/a/b/c/1234.jpg" imagename.jpg` + `/image download "https://www.example.com/a/b/c/1234.jpg" "imagename.jpg"` - Give 10 image items to "TestPlayer" for the "test.jpg" image (3x5 blocks)\ - `/image give TestPlayer test.jpg 10 3 5` + `/image give TestPlayer "test.jpg" 10 3 5` - Give 10 image items to "TestPlayer" that will not drop an image item when removed\ - `/image give TestPlayer test.jpg 10 3 5 -DROP` + `/image give TestPlayer "test.jpg" 10 3 5 -DROP` - Start the dialog to place an image with a width of 3 blocks and auto height\ - `/image place imagename.jpg 3` + `/image place "imagename.jpg" 3` - Start the dialog to place a 3-blocks wide and 2-blocks high image\ - `/image place imagename.jpg 3 2` + `/image place "imagename.jpg" 3 2` - Start the dialog to place an image that glows in the dark\ - `/image place imagename.jpg 3 2 +GLOW` + `/image place "imagename.jpg" 3 2 +GLOW` - Start the dialog to remove a placed image while keeping the original file\ `/image remove` - Remove all placed images in a radius of 5 blocks around the spawn\ diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFileArgument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFileArgument.java index 877fc43..a2dfd9f 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFileArgument.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFileArgument.java @@ -39,7 +39,8 @@ public ImageFileArgument(@NotNull String name) { @NotNull SuggestionsBuilder builder ) { for (String filename : YamipaPlugin.getInstance().getStorage().getAllFilenames()) { - builder.suggest(filename); + String suggestion = "\"" + filename.replaceAll("\"","\\\\\"") + "\""; + builder.suggest(suggestion); } return builder.buildFuture(); } From f615f689f66ea2a16fad1d131b7147dc1cfc97b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 12:57:09 +0200 Subject: [PATCH 04/30] Improved logging - Updated logging methods in YamipaPlugin - Updated affected classes --- .../josemmo/bukkit/plugin/YamipaPlugin.java | 27 ++++++++++++------- .../bukkit/plugin/renderer/FakeEntity.java | 5 ++-- .../bukkit/plugin/renderer/ImageRenderer.java | 7 +++-- .../bukkit/plugin/storage/ImageFile.java | 7 +++-- .../bukkit/plugin/storage/ImageStorage.java | 5 ++-- .../bukkit/plugin/utils/ActionBar.java | 3 +-- .../utils/InteractWithEntityListener.java | 3 +-- .../bukkit/plugin/utils/Permissions.java | 3 +-- 8 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index b335d78..54d7c52 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -152,9 +152,9 @@ public void onDisable() { * Log message * @param level Record level * @param message Message - * @param e Throwable instance, NULL to ignore + * @param e Optional throwable to log */ - public void log(@NotNull Level level, @NotNull String message, @Nullable Throwable e) { + private void log(@NotNull Level level, @NotNull String message, @Nullable Throwable e) { // Fix log level if (level.intValue() < Level.INFO.intValue()) { if (!verbose) return; @@ -170,12 +170,21 @@ public void log(@NotNull Level level, @NotNull String message, @Nullable Throwab } /** - * Log message - * @param level Record level + * Log severe message + * @param message Message + * @param e Throwable to log + */ + public void severe(@NotNull String message, @NotNull Throwable e) { + log(Level.SEVERE, message, e); + } + + /** + * Log warning message * @param message Message + * @param e Throwable to log */ - public void log(@NotNull Level level, @NotNull String message) { - log(level, message, null); + public void warning(@NotNull String message, @NotNull Throwable e) { + log(Level.WARNING, message, e); } /** @@ -183,7 +192,7 @@ public void log(@NotNull Level level, @NotNull String message) { * @param message Message */ public void warning(@NotNull String message) { - log(Level.WARNING, message); + log(Level.WARNING, message, null); } /** @@ -191,7 +200,7 @@ public void warning(@NotNull String message) { * @param message Message */ public void info(@NotNull String message) { - log(Level.INFO, message); + log(Level.INFO, message, null); } /** @@ -199,6 +208,6 @@ public void info(@NotNull String message) { * @param message Message */ public void fine(@NotNull String message) { - log(Level.FINE, message); + log(Level.FINE, message, null); } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java index 419e524..506f99d 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java @@ -11,7 +11,6 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; -import java.util.logging.Level; public abstract class FakeEntity { protected static final YamipaPlugin plugin = YamipaPlugin.getInstance(); @@ -32,7 +31,7 @@ public abstract class FakeEntity { throw new RuntimeException("No valid candidate field found in ProtocolManager"); } } catch (Exception e) { - plugin.log(Level.SEVERE, "Failed to get PlayerInjectionHandler from ProtocolLib", e); + plugin.severe("Failed to get PlayerInjectionHandler from ProtocolLib", e); } } @@ -92,7 +91,7 @@ protected static void tryToSendPacket(@NotNull Player player, @NotNull PacketCon } catch (IllegalStateException e) { // Server is shutting down and cannot send the packet, ignore } catch (Exception e) { - plugin.log(Level.SEVERE, "Failed to send FakeEntity packet", e); + plugin.severe("Failed to send FakeEntity packet", e); } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java index deb0d59..05c8f8a 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java @@ -22,7 +22,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; import java.util.stream.Collectors; public class ImageRenderer implements Listener { @@ -91,7 +90,7 @@ private void loadConfig() { try { config.load(configPath); } catch (IOException e) { - plugin.log(Level.SEVERE, "Failed to load placed fake images from disk", e); + plugin.severe("Failed to load placed fake images from disk", e); return; } @@ -122,7 +121,7 @@ private void loadConfig() { placedAt, placedBy, flags); addImage(fakeImage, true); } catch (Exception e) { - plugin.log(Level.SEVERE, "Invalid fake image properties: " + String.join(";", row), e); + plugin.severe("Invalid fake image properties: " + String.join(";", row), e); } } } @@ -169,7 +168,7 @@ private void saveConfig() { config.save(configPath); plugin.info("Saved placed fake images to disk"); } catch (IOException e) { - plugin.log(Level.SEVERE, "Failed to save placed fake images to disk", e); + plugin.severe("Failed to save placed fake images to disk", e); } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java index c781366..a065edb 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java @@ -26,7 +26,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import java.util.logging.Level; import java.util.regex.Pattern; import java.util.stream.IntStream; @@ -251,7 +250,7 @@ public long getLastModified() { } catch (IllegalArgumentException e) { plugin.info("Cache file \"" + cacheFile.getAbsolutePath() + "\" is outdated and will be overwritten"); } catch (Exception e) { - plugin.log(Level.WARNING, "Cache file \"" + cacheFile.getAbsolutePath() + "\" is corrupted", e); + plugin.warning("Cache file \"" + cacheFile.getAbsolutePath() + "\" is corrupted", e); } } @@ -285,11 +284,11 @@ public long getLastModified() { cacheFile.getParentFile().mkdirs(); writeMapsToCacheFile(container, cacheFile); } catch (IOException e) { - plugin.log(Level.SEVERE, "Failed to write to cache file \"" + cacheFile.getAbsolutePath() + "\"", e); + plugin.severe("Failed to write to cache file \"" + cacheFile.getAbsolutePath() + "\"", e); } } catch (Exception e) { container = FakeMap.getErrorMatrix(width, height); - plugin.log(Level.SEVERE, "Failed to render image(s) from file \"" + path + "\"", e); + plugin.severe("Failed to render image(s) from file \"" + path + "\"", e); } // Persist in memory cache and return diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index 9b12c78..4c38a37 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.nio.file.*; import java.util.*; -import java.util.logging.Level; /** * A service whose purpose is to keep track of all available image files in a given directory. @@ -89,7 +88,7 @@ public void stop() { try { watchService.close(); } catch (IOException e) { - plugin.log(Level.WARNING, "Failed to close watch service", e); + plugin.warning("Failed to close watch service", e); } watchService = null; } @@ -155,7 +154,7 @@ private synchronized void registerDirectory(@NotNull Path path, boolean isBase) path.register(watchService, events, modifiers); plugin.fine("Started watching directory at \"" + path.toAbsolutePath() + "\""); } catch (IOException e) { - plugin.log(Level.SEVERE, "Failed to register directory", e); + plugin.severe("Failed to register directory", e); } } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/ActionBar.java b/src/main/java/io/josemmo/bukkit/plugin/utils/ActionBar.java index d5fda77..b4e0e9f 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/ActionBar.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/ActionBar.java @@ -7,7 +7,6 @@ import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; -import java.util.logging.Level; public class ActionBar { private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); @@ -72,7 +71,7 @@ public ActionBar sendOnce() { try { ProtocolLibrary.getProtocolManager().sendServerPacket(player, actionBarPacket); } catch (Exception e) { - plugin.log(Level.SEVERE, "Failed to send ActionBar to " + player.getName(), e); + plugin.severe("Failed to send ActionBar to " + player.getName(), e); } return this; } diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java b/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java index 753f545..2f5d834 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java @@ -14,7 +14,6 @@ import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import java.util.List; -import java.util.logging.Level; public abstract class InteractWithEntityListener implements PacketListener { public static final int MAX_BLOCK_DISTANCE = 5; // Server should only accept entities within a 4-block radius @@ -91,7 +90,7 @@ public final void onPacketReceiving(@NotNull PacketEvent event) { allowEvent = onInteract(player, targetBlock, targetBlockFace); } } catch (Exception e) { - YamipaPlugin.getInstance().log(Level.SEVERE, "Failed to notify entity listener handler", e); + YamipaPlugin.getInstance().severe("Failed to notify entity listener handler", e); } // Cancel event (if needed) diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java b/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java index e1c1586..4d6ce0b 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java @@ -19,7 +19,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.concurrent.Callable; -import java.util.logging.Level; public class Permissions { @Nullable private static WorldGuard worldGuard = null; @@ -109,7 +108,7 @@ private static boolean queryGriefPrevention(@NotNull Player player, @NotNull Loc canEditCallable.call() : Bukkit.getScheduler().callSyncMethod(plugin, canEditCallable).get(); } catch (Exception e) { - plugin.log(Level.SEVERE, "Failed to get player permissions from GriefPrevention", e); + plugin.severe("Failed to get player permissions from GriefPrevention", e); return false; } } From c2c697dc4a9869ef229c9e3c9db8457647b47ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 12:58:16 +0200 Subject: [PATCH 05/30] Fixed missing log for CI pipeline - Updated ImageStorage class --- src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index 4c38a37..2e7bfd6 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -71,6 +71,7 @@ public void start() throws IOException { watchServiceThread = new WatcherThread(); watchServiceThread.start(); registerDirectory(basePath, true); + plugin.fine("Found " + files.size() + " file(s) in images directory"); } /** From 2ec966b56728dbe39ed78a243d3e283aaa4e2d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 14:30:39 +0200 Subject: [PATCH 06/30] Improved logging (II) - Created Logger utils class - Removed logging methods from YamipaPlugin class - Updated dependent classes --- .../josemmo/bukkit/plugin/YamipaPlugin.java | 76 ++---------- .../bukkit/plugin/commands/ImageCommand.java | 6 +- .../plugin/commands/ImageCommandBridge.java | 6 +- .../bukkit/plugin/renderer/FakeEntity.java | 6 +- .../bukkit/plugin/renderer/FakeImage.java | 22 ++-- .../bukkit/plugin/renderer/FakeItemFrame.java | 4 +- .../bukkit/plugin/renderer/FakeMap.java | 8 +- .../bukkit/plugin/renderer/ImageRenderer.java | 20 ++-- .../bukkit/plugin/renderer/ItemService.java | 6 +- .../bukkit/plugin/storage/ImageFile.java | 16 +-- .../bukkit/plugin/storage/ImageStorage.java | 26 ++--- .../bukkit/plugin/utils/ActionBar.java | 3 +- .../utils/InteractWithEntityListener.java | 3 +- .../josemmo/bukkit/plugin/utils/Logger.java | 110 ++++++++++++++++++ .../bukkit/plugin/utils/Permissions.java | 9 +- .../bukkit/plugin/utils/SelectBlockTask.java | 5 +- 16 files changed, 200 insertions(+), 126 deletions(-) create mode 100644 src/main/java/io/josemmo/bukkit/plugin/utils/Logger.java diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index 54d7c52..1627485 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -3,6 +3,7 @@ import io.josemmo.bukkit.plugin.commands.ImageCommandBridge; import io.josemmo.bukkit.plugin.renderer.*; import io.josemmo.bukkit.plugin.storage.ImageStorage; +import io.josemmo.bukkit.plugin.utils.Logger; import org.bstats.bukkit.Metrics; import org.bstats.charts.SimplePie; import org.bukkit.Bukkit; @@ -20,6 +21,7 @@ public class YamipaPlugin extends JavaPlugin { public static final int BSTATS_PLUGIN_ID = 10243; private static YamipaPlugin instance; + private static final Logger LOGGER = Logger.getLogger(); private boolean verbose; private ImageStorage storage; private ImageRenderer renderer; @@ -68,7 +70,7 @@ public void onEnable() { // Initialize logger verbose = getConfig().getBoolean("verbose", false); if (verbose) { - info("Running on VERBOSE mode"); + LOGGER.info("Running on VERBOSE mode"); } // Register plugin commands @@ -88,13 +90,13 @@ public void onEnable() { try { storage.start(); } catch (Exception e) { - log(Level.SEVERE, "Failed to initialize image storage", e); + LOGGER.severe("Failed to initialize image storage", e); } // Create image renderer boolean animateImages = getConfig().getBoolean("animate-images", true); FakeImage.configure(animateImages); - info(animateImages ? "Enabled image animation support" : "Image animation support is disabled"); + LOGGER.info(animateImages ? "Enabled image animation support" : "Image animation support is disabled"); renderer = new ImageRenderer(basePath.resolve(dataPath).toString()); renderer.start(); @@ -106,12 +108,12 @@ public void onEnable() { scheduler = Executors.newScheduledThreadPool(6); // Warm-up plugin dependencies - fine("Waiting for ProtocolLib to be ready..."); + LOGGER.fine("Waiting for ProtocolLib to be ready..."); scheduler.execute(() -> { FakeEntity.waitForProtocolLib(); - fine("ProtocolLib is now ready"); + LOGGER.fine("ProtocolLib is now ready"); }); - fine("Triggered map color cache warm-up"); + LOGGER.fine("Triggered map color cache warm-up"); FakeMap.pixelToIndex(Color.RED.getRGB()); // Ask for a color index to force cache generation // Initialize bStats @@ -146,68 +148,6 @@ public void onDisable() { // Remove Bukkit listeners and tasks HandlerList.unregisterAll(this); Bukkit.getScheduler().cancelTasks(this); - } - - /** - * Log message - * @param level Record level - * @param message Message - * @param e Optional throwable to log - */ - private void log(@NotNull Level level, @NotNull String message, @Nullable Throwable e) { - // Fix log level - if (level.intValue() < Level.INFO.intValue()) { - if (!verbose) return; - level = Level.INFO; - } - - // Proxy record to real logger - if (e == null) { - getLogger().log(level, message); - } else { - getLogger().log(level, message, e); - } - } - - /** - * Log severe message - * @param message Message - * @param e Throwable to log - */ - public void severe(@NotNull String message, @NotNull Throwable e) { - log(Level.SEVERE, message, e); - } - - /** - * Log warning message - * @param message Message - * @param e Throwable to log - */ - public void warning(@NotNull String message, @NotNull Throwable e) { - log(Level.WARNING, message, e); - } - - /** - * Log warning message - * @param message Message - */ - public void warning(@NotNull String message) { - log(Level.WARNING, message, null); - } - /** - * Log info message - * @param message Message - */ - public void info(@NotNull String message) { - log(Level.INFO, message, null); - } - - /** - * Log fine message - * @param message Message - */ - public void fine(@NotNull String message) { - log(Level.FINE, message, null); } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java index 663fb5a..2bbcd76 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java @@ -5,6 +5,7 @@ import io.josemmo.bukkit.plugin.renderer.ImageRenderer; import io.josemmo.bukkit.plugin.renderer.ItemService; import io.josemmo.bukkit.plugin.storage.ImageFile; +import io.josemmo.bukkit.plugin.utils.Logger; import io.josemmo.bukkit.plugin.utils.Permissions; import io.josemmo.bukkit.plugin.utils.SelectBlockTask; import io.josemmo.bukkit.plugin.utils.ActionBar; @@ -29,6 +30,7 @@ public class ImageCommand { public static final int ITEMS_PER_PAGE = 9; + private static final Logger LOGGER = Logger.getLogger("ImageCommand"); public static void showHelp(@NotNull CommandSender s, @NotNull String commandName) { String cmd = "/" + commandName; @@ -159,10 +161,10 @@ public static void downloadImage(@NotNull CommandSender sender, @NotNull String sender.sendMessage(ChatColor.GREEN + "Done!"); } catch (IOException e) { sender.sendMessage(ChatColor.RED + "An error occurred trying to download the remote file"); - plugin.warning("Failed to download file from \"" + finalUrl + "\": " + e.getClass().getName()); + LOGGER.warning("Failed to download file from \"" + finalUrl + "\": " + e.getClass().getName()); } catch (IllegalArgumentException e) { if (Files.exists(destPath) && !destPath.toFile().delete()) { - plugin.warning("Failed to delete corrupted file \"" + destPath + "\""); + LOGGER.warning("Failed to delete corrupted file \"" + destPath + "\""); } sender.sendMessage(ChatColor.RED + e.getMessage()); } diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java index ea26427..dfe8fe6 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java @@ -7,6 +7,7 @@ import io.josemmo.bukkit.plugin.renderer.FakeImage; import io.josemmo.bukkit.plugin.storage.ImageFile; import io.josemmo.bukkit.plugin.utils.Internals; +import io.josemmo.bukkit.plugin.utils.Logger; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.OfflinePlayer; @@ -17,6 +18,7 @@ public class ImageCommandBridge { public static final String COMMAND_NAME = "yamipa"; public static final String[] COMMAND_ALIASES = new String[] {"image", "images"}; + private static final Logger LOGGER = Logger.getLogger("ImageCommandBridge"); /** * Register command @@ -42,7 +44,7 @@ public static void register(@NotNull YamipaPlugin plugin) { ); dispatcher.getRoot().addChild(aliasNode); } - plugin.fine("Registered plugin command and aliases"); + LOGGER.fine("Registered plugin command and aliases"); // Fix "minecraft.command.*" permissions Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> { @@ -50,7 +52,7 @@ public static void register(@NotNull YamipaPlugin plugin) { for (String alias : COMMAND_ALIASES) { fixPermissions(alias); } - plugin.fine("Fixed command permissions"); + LOGGER.fine("Fixed command permissions"); }); } diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java index 506f99d..a0cbdc6 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java @@ -8,11 +8,13 @@ import com.comphenix.protocol.wrappers.WrappedDataWatcher; import io.josemmo.bukkit.plugin.YamipaPlugin; import io.josemmo.bukkit.plugin.utils.Internals; +import io.josemmo.bukkit.plugin.utils.Logger; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; public abstract class FakeEntity { + private static final Logger LOGGER = Logger.getLogger("FakeEntity"); protected static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private static final ProtocolManager connection = ProtocolLibrary.getProtocolManager(); private static PlayerInjectionHandler playerInjectionHandler = null; @@ -31,7 +33,7 @@ public abstract class FakeEntity { throw new RuntimeException("No valid candidate field found in ProtocolManager"); } } catch (Exception e) { - plugin.severe("Failed to get PlayerInjectionHandler from ProtocolLib", e); + LOGGER.severe("Failed to get PlayerInjectionHandler from ProtocolLib", e); } } @@ -91,7 +93,7 @@ protected static void tryToSendPacket(@NotNull Player player, @NotNull PacketCon } catch (IllegalStateException e) { // Server is shutting down and cannot send the packet, ignore } catch (Exception e) { - plugin.severe("Failed to send FakeEntity packet", e); + LOGGER.severe("Failed to send FakeEntity packet", e); } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java index 7740de8..f526de2 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java @@ -3,6 +3,7 @@ import com.comphenix.protocol.events.PacketContainer; import io.josemmo.bukkit.plugin.storage.ImageFile; import io.josemmo.bukkit.plugin.utils.DirectionUtils; +import io.josemmo.bukkit.plugin.utils.Logger; import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.Rotation; @@ -19,6 +20,9 @@ import java.util.function.BiFunction; public class FakeImage extends FakeEntity { + private static final Logger LOGGER = Logger.getLogger("FakeImage"); + + // Image constants public static final int MAX_DIMENSION = 30; // In blocks public static final int MAX_STEPS = 500; // For animated images public static final int MIN_DELAY = 1; // Minimum step delay in 50ms intervals (50ms / 50ms) @@ -177,7 +181,7 @@ public FakeImage( } } - plugin.fine("Created FakeImage#(" + location + "," + face + ") from ImageFile#(" + filename + ")"); + LOGGER.fine("Created FakeImage#(" + location + "," + face + ") from ImageFile#(" + filename + ")"); } /** @@ -353,7 +357,7 @@ private void load() { FakeMapsContainer container; if (file == null) { container = FakeMap.getErrorMatrix(width, height); - plugin.warning("File \"" + filename + "\" does not exist"); + LOGGER.warning("File \"" + filename + "\" does not exist"); } else { container = file.getMapsAndSubscribe(this); } @@ -382,7 +386,7 @@ private void load() { delay*50L, TimeUnit.MILLISECONDS ); - plugin.fine("Spawned animation task for FakeImage#(" + location + "," + face + ")"); + LOGGER.fine("Spawned animation task for FakeImage#(" + location + "," + face + ")"); } // Notify listener @@ -397,7 +401,7 @@ private void load() { * @param player Player instance */ public void spawn(@NotNull Player player) { - plugin.fine("Received request to spawn FakeImage#(" + location + "," + face + ") for Player#" + player.getName()); + LOGGER.fine("Received request to spawn FakeImage#(" + location + "," + face + ") for Player#" + player.getName()); // Send pixels if instance is already loaded if (frames != null) { @@ -439,7 +443,7 @@ private void spawnOnceLoaded(@NotNull Player player) { for (FakeItemFrame frame : frames) { packets.add(frame.getSpawnPacket()); packets.addAll(frame.getRenderPackets(player, 0)); - plugin.fine("Spawned FakeItemFrame#" + frame.getId() + " for Player#" + playerName); + LOGGER.fine("Spawned FakeItemFrame#" + frame.getId() + " for Player#" + playerName); } // Send packets @@ -460,7 +464,7 @@ public void destroy() { * @param player Player instance or NULL for all observing players */ public void destroy(@Nullable Player player) { - plugin.fine( + LOGGER.fine( "Received request to destroy FakeImage#(" + location + "," + face + ") for " + (player == null ? "all players" : "Player#" + player.getName()) ); @@ -473,7 +477,7 @@ public void destroy(@Nullable Player player) { List packets = new ArrayList<>(); for (FakeItemFrame frame : frames) { packets.add(frame.getDestroyPacket()); - plugin.fine("Destroyed FakeItemFrame#" + frame.getId() + " for Player#" + targetName); + LOGGER.fine("Destroyed FakeItemFrame#" + frame.getId() + " for Player#" + targetName); } tryToSendPackets(target, packets); } @@ -514,12 +518,12 @@ private void invalidate() { task.cancel(true); task = null; currentStep = -1; - plugin.fine("Destroyed animation task for FakeImage#(" + location + "," + face + ")"); + LOGGER.fine("Destroyed animation task for FakeImage#(" + location + "," + face + ")"); } // Free array of fake item frames frames = null; - plugin.fine("Invalidated FakeImage#(" + location + "," + face + ")"); + LOGGER.fine("Invalidated FakeImage#(" + location + "," + face + ")"); // Notify invalidation to source ImageFile ImageFile file = getFile(); diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java index 0a6f4ec..0febbde 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java @@ -8,6 +8,7 @@ import io.josemmo.bukkit.plugin.packets.EntityMetadataPacket; import io.josemmo.bukkit.plugin.packets.SpawnEntityPacket; import io.josemmo.bukkit.plugin.utils.Internals; +import io.josemmo.bukkit.plugin.utils.Logger; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Rotation; @@ -24,6 +25,7 @@ public class FakeItemFrame extends FakeEntity { public static final int MIN_FRAME_ID = Integer.MAX_VALUE / 4; public static final int MAX_FRAME_ID = Integer.MAX_VALUE; private static final boolean SUPPORTS_GLOWING = Internals.MINECRAFT_VERSION >= 17; + private static final Logger LOGGER = Logger.getLogger("FakeItemFrame"); private static final AtomicInteger lastFrameId = new AtomicInteger(MAX_FRAME_ID); private final int id; private final Location location; @@ -66,7 +68,7 @@ public FakeItemFrame( this.rotation = rotation; this.glowing = glowing; this.maps = maps; - plugin.fine("Created FakeItemFrame#" + this.id + " using " + this.maps.length + " FakeMap(s)"); + LOGGER.fine("Created FakeItemFrame#" + this.id + " using " + this.maps.length + " FakeMap(s)"); } /** diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java index a063bb8..d02e82d 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java @@ -1,6 +1,7 @@ package io.josemmo.bukkit.plugin.renderer; import io.josemmo.bukkit.plugin.packets.MapDataPacket; +import io.josemmo.bukkit.plugin.utils.Logger; import org.bukkit.entity.Player; import org.bukkit.map.MapPalette; import org.jetbrains.annotations.NotNull; @@ -17,6 +18,7 @@ public class FakeMap extends FakeEntity { public static final int MIN_MAP_ID = Integer.MAX_VALUE / 4; public static final int MAX_MAP_ID = Integer.MAX_VALUE; public static final int RESEND_THRESHOLD = 60*5; // Seconds after sending pixels when resending should be avoided + private static final Logger LOGGER = Logger.getLogger("FakeMap"); private static final AtomicInteger lastMapId = new AtomicInteger(-1); private static FakeMap errorInstance; private final int id; @@ -90,7 +92,7 @@ public FakeMap(byte[] pixels, int scanSize, int startX, int startY) { System.arraycopy(pixels, startX+(startY+y)*scanSize, this.pixels, y*DIMENSION, DIMENSION); } - plugin.fine("Created FakeMap#" + this.id); + LOGGER.fine("Created FakeMap#" + this.id); } /** @@ -100,7 +102,7 @@ public FakeMap(byte[] pixels, int scanSize, int startX, int startY) { public FakeMap(byte[] pixels) { this.id = getNextId(); this.pixels = pixels; - plugin.fine("Created FakeMap#" + this.id); + LOGGER.fine("Created FakeMap#" + this.id); } /** @@ -136,7 +138,7 @@ public boolean requestResend(@NotNull Player player) { // Authorize re-send and update latest timestamp lastPlayerSendTime.put(uuid, now); - plugin.fine("Granted sending pixels for FakeMap#" + id + " to Player#" + player.getName()); + LOGGER.fine("Granted sending pixels for FakeMap#" + id + " to Player#" + player.getName()); return true; } diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java index 05c8f8a..5c1b197 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java @@ -2,6 +2,7 @@ import io.josemmo.bukkit.plugin.YamipaPlugin; import io.josemmo.bukkit.plugin.utils.CsvConfiguration; +import io.josemmo.bukkit.plugin.utils.Logger; import org.bukkit.*; import org.bukkit.block.BlockFace; import org.bukkit.entity.Entity; @@ -26,6 +27,7 @@ public class ImageRenderer implements Listener { public static final long SAVE_INTERVAL = 20L * 90; // In server ticks + private static final Logger LOGGER = Logger.getLogger("ImageRenderer"); private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private final String configPath; private BukkitTask saveTask; @@ -81,7 +83,7 @@ public void stop() { */ private void loadConfig() { if (!Files.isRegularFile(Paths.get(configPath))) { - plugin.info("No placed fake images configuration file found"); + LOGGER.info("No placed fake images configuration file found"); return; } @@ -90,7 +92,7 @@ private void loadConfig() { try { config.load(configPath); } catch (IOException e) { - plugin.severe("Failed to load placed fake images from disk", e); + LOGGER.severe("Failed to load placed fake images from disk", e); return; } @@ -121,7 +123,7 @@ private void loadConfig() { placedAt, placedBy, flags); addImage(fakeImage, true); } catch (Exception e) { - plugin.severe("Invalid fake image properties: " + String.join(";", row), e); + LOGGER.severe("Invalid fake image properties: " + String.join(";", row), e); } } } @@ -166,9 +168,9 @@ private void saveConfig() { // Write to disk try { config.save(configPath); - plugin.info("Saved placed fake images to disk"); + LOGGER.info("Saved placed fake images to disk"); } catch (IOException e) { - plugin.severe("Failed to save placed fake images to disk", e); + LOGGER.severe("Failed to save placed fake images to disk", e); } } @@ -183,7 +185,7 @@ public void addImage(@NotNull FakeImage image, boolean isInit) { // Add image to renderer for (WorldAreaId worldAreaId : imageWorldAreaIds) { images.computeIfAbsent(worldAreaId, __ -> { - plugin.fine("Created WorldArea#(" + worldAreaId + ")"); + LOGGER.fine("Created WorldArea#(" + worldAreaId + ")"); return ConcurrentHashMap.newKeySet(); }).add(image); } @@ -273,7 +275,7 @@ public void removeImage(@NotNull FakeImage image) { Set worldAreaImages = images.get(worldAreaId); worldAreaImages.remove(image); if (worldAreaImages.isEmpty()) { - plugin.fine("Destroyed WorldArea#(" + worldAreaId + ")"); + LOGGER.fine("Destroyed WorldArea#(" + worldAreaId + ")"); images.remove(worldAreaId); } } @@ -365,7 +367,7 @@ public int size() { private void onPlayerLocationChange(@NotNull Player player, @NotNull Location location) { // Ignore NPC events from other plugins if (player.hasMetadata("NPC")) { - plugin.fine("Ignored NPC event from Player#" + player.getName()); + LOGGER.fine("Ignored NPC event from Player#" + player.getName()); return; } @@ -376,7 +378,7 @@ private void onPlayerLocationChange(@NotNull Player player, @NotNull Location lo return; } playersLocation.put(player, worldAreaId); - plugin.fine("Player#" + player.getName() + " moved to WorldArea#(" + worldAreaId + ")"); + LOGGER.fine("Player#" + player.getName() + " moved to WorldArea#(" + worldAreaId + ")"); // Get images that should be spawned/destroyed Set desiredState = getImagesInViewDistance(worldAreaId); diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java index 0bbb94b..9b03a05 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java @@ -5,6 +5,7 @@ import io.josemmo.bukkit.plugin.storage.ImageFile; import io.josemmo.bukkit.plugin.utils.ActionBar; import io.josemmo.bukkit.plugin.utils.InteractWithEntityListener; +import io.josemmo.bukkit.plugin.utils.Logger; import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -27,6 +28,7 @@ import java.util.Objects; public class ItemService extends InteractWithEntityListener implements Listener { + private static final Logger LOGGER = Logger.getLogger("ItemService"); private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private static final NamespacedKey NSK_FILENAME = new NamespacedKey(plugin, "filename"); private static final NamespacedKey NSK_WIDTH = new NamespacedKey(plugin, "width"); @@ -132,14 +134,14 @@ public void onPlaceItem(@NotNull HangingPlaceEvent event) { return; } if (width == null || height == null || flags == null) { - plugin.warning(player + " tried to place corrupted image item (missing width/height/flags properties)"); + LOGGER.warning(player + " tried to place corrupted image item (missing width/height/flags properties)"); return; } // Validate filename ImageFile image = YamipaPlugin.getInstance().getStorage().get(filename); if (image == null) { - plugin.warning(player + " tried to place corrupted image item (\"" + filename + "\" no longer exists)"); + LOGGER.warning(player + " tried to place corrupted image item (\"" + filename + "\" no longer exists)"); ActionBar.send(player, ChatColor.RED + "Image file \"" + filename + "\" no longer exists"); return; } diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java index a065edb..6fa46d4 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java @@ -5,6 +5,7 @@ import io.josemmo.bukkit.plugin.renderer.FakeImage; import io.josemmo.bukkit.plugin.renderer.FakeMap; import io.josemmo.bukkit.plugin.renderer.FakeMapsContainer; +import io.josemmo.bukkit.plugin.utils.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.imageio.ImageIO; @@ -33,6 +34,7 @@ public class ImageFile { public static final String CACHE_EXT = "cache"; public static final byte[] CACHE_SIGNATURE = new byte[] {0x59, 0x4d, 0x50}; // "YMP" public static final int CACHE_VERSION = 1; + private static final Logger LOGGER = Logger.getLogger("ImageFile"); private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private final ConcurrentHashMap locks = new ConcurrentHashMap<>(); private final Map cache = new HashMap<>(); @@ -248,9 +250,9 @@ public long getLastModified() { cache.put(cacheKey, container); return container; } catch (IllegalArgumentException e) { - plugin.info("Cache file \"" + cacheFile.getAbsolutePath() + "\" is outdated and will be overwritten"); + LOGGER.info("Cache file \"" + cacheFile.getAbsolutePath() + "\" is outdated and will be overwritten"); } catch (Exception e) { - plugin.warning("Cache file \"" + cacheFile.getAbsolutePath() + "\" is corrupted", e); + LOGGER.warning("Cache file \"" + cacheFile.getAbsolutePath() + "\" is corrupted", e); } } @@ -284,11 +286,11 @@ public long getLastModified() { cacheFile.getParentFile().mkdirs(); writeMapsToCacheFile(container, cacheFile); } catch (IOException e) { - plugin.severe("Failed to write to cache file \"" + cacheFile.getAbsolutePath() + "\"", e); + LOGGER.severe("Failed to write to cache file \"" + cacheFile.getAbsolutePath() + "\"", e); } } catch (Exception e) { container = FakeMap.getErrorMatrix(width, height); - plugin.severe("Failed to render image(s) from file \"" + path + "\"", e); + LOGGER.severe("Failed to render image(s) from file \"" + path + "\"", e); } // Persist in memory cache and return @@ -401,7 +403,7 @@ public synchronized void unsubscribe(@NotNull FakeImage subscriber) { if (currentSubscribers.isEmpty()) { subscribers.remove(cacheKey); cache.remove(cacheKey); - plugin.fine("Invalidated cached maps \"" + cacheKey + "\" in ImageFile#(" + filename + ")"); + LOGGER.fine("Invalidated cached maps \"" + cacheKey + "\" in ImageFile#(" + filename + ")"); } } @@ -424,14 +426,14 @@ public synchronized void invalidate() { } File[] files = cacheDirectory.listFiles((__, item) -> item.matches(cachePattern)); if (files == null) { - plugin.warning("An error occurred when listing cache files for image \"" + filename + "\""); + LOGGER.warning("An error occurred when listing cache files for image \"" + filename + "\""); return; } // Delete disk cache files for (File file : files) { if (!file.delete()) { - plugin.warning("Failed to delete cache file \"" + file.getAbsolutePath() + "\""); + LOGGER.warning("Failed to delete cache file \"" + file.getAbsolutePath() + "\""); } } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index 2e7bfd6..7c961a5 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -1,7 +1,7 @@ package io.josemmo.bukkit.plugin.storage; import com.sun.nio.file.ExtendedWatchEventModifier; -import io.josemmo.bukkit.plugin.YamipaPlugin; +import io.josemmo.bukkit.plugin.utils.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; @@ -19,7 +19,7 @@ */ public class ImageStorage { private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); - private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); + private static final Logger LOGGER = Logger.getLogger("ImageStorage"); /** Map of registered files indexed by filename */ private final SortedMap files = new TreeMap<>(); private final Path basePath; @@ -60,10 +60,10 @@ public ImageStorage(@NotNull Path basePath, @NotNull Path cachePath) { public void start() throws IOException { // Create base directories if necessary if (basePath.toFile().mkdirs()) { - plugin.info("Created images directory as it did not exist"); + LOGGER.info("Created images directory as it did not exist"); } if (cachePath.toFile().mkdirs()) { - plugin.info("Created cache directory as it did not exist"); + LOGGER.info("Created cache directory as it did not exist"); } // Start watching files @@ -71,7 +71,7 @@ public void start() throws IOException { watchServiceThread = new WatcherThread(); watchServiceThread.start(); registerDirectory(basePath, true); - plugin.fine("Found " + files.size() + " file(s) in images directory"); + LOGGER.fine("Found " + files.size() + " file(s) in images directory"); } /** @@ -89,7 +89,7 @@ public void stop() { try { watchService.close(); } catch (IOException e) { - plugin.warning("Failed to close watch service", e); + LOGGER.warning("Failed to close watch service", e); } watchService = null; } @@ -128,7 +128,7 @@ public synchronized int size() { private synchronized void registerDirectory(@NotNull Path path, boolean isBase) { // Validate path if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { - plugin.warning("Cannot list files in \"" + path.toAbsolutePath() + "\" as it is not a valid directory"); + LOGGER.warning("Cannot list files in \"" + path.toAbsolutePath() + "\" as it is not a valid directory"); return; } @@ -153,9 +153,9 @@ private synchronized void registerDirectory(@NotNull Path path, boolean isBase) new WatchEvent.Modifier[]{ExtendedWatchEventModifier.FILE_TREE} : new WatchEvent.Modifier[0]; path.register(watchService, events, modifiers); - plugin.fine("Started watching directory at \"" + path.toAbsolutePath() + "\""); + LOGGER.fine("Started watching directory at \"" + path.toAbsolutePath() + "\""); } catch (IOException e) { - plugin.severe("Failed to register directory", e); + LOGGER.severe("Failed to register directory", e); } } } @@ -167,7 +167,7 @@ private synchronized void registerDirectory(@NotNull Path path, boolean isBase) private synchronized void registerFile(@NotNull Path path) { // Validate path if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { - plugin.warning("Cannot register \"" + path.toAbsolutePath() + "\" as it is not a valid file"); + LOGGER.warning("Cannot register \"" + path.toAbsolutePath() + "\" as it is not a valid file"); return; } @@ -175,7 +175,7 @@ private synchronized void registerFile(@NotNull Path path) { String filename = getFilename(path); ImageFile imageFile = new ImageFile(filename, path); if (files.putIfAbsent(filename, imageFile) == null) { - plugin.fine("Registered file \"" + filename + "\""); + LOGGER.fine("Registered file \"" + filename + "\""); } } @@ -191,7 +191,7 @@ private synchronized void unregisterDirectory(@NotNull String filename) { if (entryKey.startsWith(filename+"/")) { foundFirst = true; iter.remove(); - plugin.fine("Unregistered file \"" + entryKey + "\""); + LOGGER.fine("Unregistered file \"" + entryKey + "\""); } else if (foundFirst) { // We can break early because set is alphabetically sorted by key break; @@ -207,7 +207,7 @@ private synchronized void unregisterFile(@NotNull String filename) { ImageFile imageFile = files.remove(filename); if (imageFile != null) { imageFile.invalidate(); - plugin.fine("Unregistered file \"" + filename + "\""); + LOGGER.fine("Unregistered file \"" + filename + "\""); } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/ActionBar.java b/src/main/java/io/josemmo/bukkit/plugin/utils/ActionBar.java index b4e0e9f..b1f6c9c 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/ActionBar.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/ActionBar.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.NotNull; public class ActionBar { + private static final Logger LOGGER = Logger.getLogger("ActionBar"); private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private final Player player; private String message; @@ -71,7 +72,7 @@ public ActionBar sendOnce() { try { ProtocolLibrary.getProtocolManager().sendServerPacket(player, actionBarPacket); } catch (Exception e) { - plugin.severe("Failed to send ActionBar to " + player.getName(), e); + LOGGER.severe("Failed to send ActionBar to " + player.getName(), e); } return this; } diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java b/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java index 2f5d834..5445262 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java @@ -17,6 +17,7 @@ public abstract class InteractWithEntityListener implements PacketListener { public static final int MAX_BLOCK_DISTANCE = 5; // Server should only accept entities within a 4-block radius + private static final Logger LOGGER = Logger.getLogger("InteractWithEntityListener"); /** * On player attack listener @@ -90,7 +91,7 @@ public final void onPacketReceiving(@NotNull PacketEvent event) { allowEvent = onInteract(player, targetBlock, targetBlockFace); } } catch (Exception e) { - YamipaPlugin.getInstance().severe("Failed to notify entity listener handler", e); + LOGGER.severe("Failed to notify entity listener handler", e); } // Cancel event (if needed) diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/Logger.java b/src/main/java/io/josemmo/bukkit/plugin/utils/Logger.java new file mode 100644 index 0000000..0621142 --- /dev/null +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/Logger.java @@ -0,0 +1,110 @@ +package io.josemmo.bukkit.plugin.utils; + +import io.josemmo.bukkit.plugin.YamipaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.logging.Level; + +/** + * Custom logging class used by components from the plugin. + * It is instance agnostic, that is, the same logger instance will work regardless of whether the plugin + * instance has changed (for example, because of the plugin being restarted by a plugin manager). + */ +public class Logger { + private final @Nullable String name; + + /** + * Get logger instance + * @param name Logger name + * @return Logger instance + */ + public static @NotNull Logger getLogger(@NotNull String name) { + return new Logger(name); + } + + /** + * Get logger instance + * @return Logger instance + */ + public static @NotNull Logger getLogger() { + return new Logger(null); + } + + /** + * Class constructor + * @param name Optional logger name + */ + private Logger(@Nullable String name) { + this.name = name; + } + + /** + * Log message + * @param level Record level + * @param message Message + * @param e Optional throwable to log + */ + private void log(@NotNull Level level, @NotNull String message, @Nullable Throwable e) { + YamipaPlugin plugin = YamipaPlugin.getInstance(); + + // Handle verbose logging levels + if (level.intValue() < Level.INFO.intValue()) { + if (!plugin.isVerbose()) return; + level = Level.INFO; + } + + // Add logger name to message + if (name != null) { + message = "[" + name + "] " + message; + } + + // Proxy record to real logger + if (e == null) { + plugin.getLogger().log(level, message); + } else { + plugin.getLogger().log(level, message, e); + } + } + + /** + * Log severe message + * @param message Message + * @param e Throwable to log + */ + public void severe(@NotNull String message, @NotNull Throwable e) { + log(Level.SEVERE, message, e); + } + + /** + * Log warning message + * @param message Message + * @param e Throwable to log + */ + public void warning(@NotNull String message, @NotNull Throwable e) { + log(Level.WARNING, message, e); + } + + /** + * Log warning message + * @param message Message + */ + public void warning(@NotNull String message) { + log(Level.WARNING, message, null); + } + + /** + * Log info message + * @param message Message + */ + public void info(@NotNull String message) { + log(Level.INFO, message, null); + } + + /** + * Log fine message + * @param message Message + */ + public void fine(@NotNull String message) { + log(Level.FINE, message, null); + } +} diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java b/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java index 4d6ce0b..a8d5515 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java @@ -21,9 +21,10 @@ import java.util.concurrent.Callable; public class Permissions { - @Nullable private static WorldGuard worldGuard = null; - @Nullable private static GriefPrevention griefPrevention = null; - @Nullable private static TownyAPI townyApi = null; + private static final Logger LOGGER = Logger.getLogger(); + private static @Nullable WorldGuard worldGuard; + private static @Nullable GriefPrevention griefPrevention; + private static @Nullable TownyAPI townyApi; static { try { @@ -108,7 +109,7 @@ private static boolean queryGriefPrevention(@NotNull Player player, @NotNull Loc canEditCallable.call() : Bukkit.getScheduler().callSyncMethod(plugin, canEditCallable).get(); } catch (Exception e) { - plugin.severe("Failed to get player permissions from GriefPrevention", e); + LOGGER.severe("Failed to get player permissions from GriefPrevention", e); return false; } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java b/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java index d1fb601..a38d39a 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java @@ -22,6 +22,7 @@ import java.util.function.BiConsumer; public class SelectBlockTask { + private static final Logger LOGGER = Logger.getLogger("SelectBlockTask"); private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private static final Map instances = new HashMap<>(); private static SelectBlockTaskListener listener = null; @@ -71,7 +72,7 @@ public void run(@NotNull String helpMessage) { if (listener == null) { listener = new SelectBlockTaskListener(); listener.register(); - plugin.fine("Created SelectBlockTaskListener singleton"); + LOGGER.fine("Created SelectBlockTaskListener singleton"); } // Start task @@ -94,7 +95,7 @@ public void cancel() { if (instances.isEmpty()) { listener.unregister(); listener = null; - plugin.fine("Destroyed SelectBlockTaskListener singleton"); + LOGGER.fine("Destroyed SelectBlockTaskListener singleton"); } } From 6d4472cb0e3c7a6d0fe983d68aa158e10ecc777f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 14:34:05 +0200 Subject: [PATCH 07/30] Minor change in FakeMap properties - Updated FakeMap class --- .../java/io/josemmo/bukkit/plugin/renderer/FakeMap.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java index d02e82d..a10d4a1 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java @@ -15,9 +15,9 @@ public class FakeMap extends FakeEntity { public static final int DIMENSION = 128; - public static final int MIN_MAP_ID = Integer.MAX_VALUE / 4; - public static final int MAX_MAP_ID = Integer.MAX_VALUE; - public static final int RESEND_THRESHOLD = 60*5; // Seconds after sending pixels when resending should be avoided + private static final int MIN_MAP_ID = Integer.MAX_VALUE / 4; + private static final int MAX_MAP_ID = Integer.MAX_VALUE; + private static final int RESEND_THRESHOLD = 60*5; // Seconds after sending pixels when resending should be avoided private static final Logger LOGGER = Logger.getLogger("FakeMap"); private static final AtomicInteger lastMapId = new AtomicInteger(-1); private static FakeMap errorInstance; From 179cb691464639daabd1ac374caf891c60871c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 14:51:14 +0200 Subject: [PATCH 08/30] Improved support for plugin managers - Updated YamipaPlugin to free singleton instance on disable - Updated rest of classes to not keep dangling references --- .../josemmo/bukkit/plugin/YamipaPlugin.java | 54 +++++++++++++------ .../bukkit/plugin/renderer/FakeEntity.java | 3 +- .../bukkit/plugin/renderer/FakeImage.java | 5 +- .../bukkit/plugin/renderer/ImageRenderer.java | 13 +++-- .../bukkit/plugin/renderer/ItemService.java | 25 +++++---- .../bukkit/plugin/utils/SelectBlockTask.java | 2 +- 6 files changed, 65 insertions(+), 37 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index 1627485..a96b1de 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -13,27 +13,28 @@ import org.jetbrains.annotations.Nullable; import java.awt.Color; import java.nio.file.Path; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Function; -import java.util.logging.Level; public class YamipaPlugin extends JavaPlugin { public static final int BSTATS_PLUGIN_ID = 10243; - private static YamipaPlugin instance; private static final Logger LOGGER = Logger.getLogger(); + private static @Nullable YamipaPlugin INSTANCE; private boolean verbose; - private ImageStorage storage; - private ImageRenderer renderer; - private ItemService itemService; - private ScheduledExecutorService scheduler; + private @Nullable ImageStorage storage; + private @Nullable ImageRenderer renderer; + private @Nullable ItemService itemService; + private @Nullable ScheduledExecutorService scheduler; /** * Get plugin instance * @return Plugin instance */ public static @NotNull YamipaPlugin getInstance() { - return instance; + Objects.requireNonNull(INSTANCE, "Cannot get plugin instance if plugin is not running"); + return INSTANCE; } /** @@ -41,6 +42,7 @@ public class YamipaPlugin extends JavaPlugin { * @return Image storage instance */ public @NotNull ImageStorage getStorage() { + Objects.requireNonNull(storage, "Cannot get storage instance if plugin is not running"); return storage; } @@ -49,6 +51,7 @@ public class YamipaPlugin extends JavaPlugin { * @return Image renderer instance */ public @NotNull ImageRenderer getRenderer() { + Objects.requireNonNull(renderer, "Cannot get renderer instance if plugin is not running"); return renderer; } @@ -57,12 +60,21 @@ public class YamipaPlugin extends JavaPlugin { * @return Tasks scheduler */ public @NotNull ScheduledExecutorService getScheduler() { + Objects.requireNonNull(scheduler, "Cannot get scheduler instance if plugin is not running"); return scheduler; } + /** + * Is verbose + * @return Whether plugin is running in verbose mode + */ + public boolean isVerbose() { + return verbose; + } + @Override public void onLoad() { - instance = this; + INSTANCE = this; } @Override @@ -134,20 +146,30 @@ public void onEnable() { @Override public void onDisable() { // Stop plugin components - storage.stop(); - renderer.stop(); - itemService.stop(); - storage = null; - renderer = null; - itemService = null; + if (storage != null) { + storage.stop(); + storage = null; + } + if (renderer != null) { + renderer.stop(); + renderer = null; + } + if (itemService != null) { + itemService.stop(); + itemService = null; + } // Stop internal scheduler - scheduler.shutdownNow(); - scheduler = null; + if (scheduler != null) { + scheduler.shutdownNow(); + scheduler = null; + } // Remove Bukkit listeners and tasks HandlerList.unregisterAll(this); Bukkit.getScheduler().cancelTasks(this); + // Unlink reference to instance + INSTANCE = null; } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java index a0cbdc6..00878fe 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java @@ -15,7 +15,6 @@ public abstract class FakeEntity { private static final Logger LOGGER = Logger.getLogger("FakeEntity"); - protected static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private static final ProtocolManager connection = ProtocolLibrary.getProtocolManager(); private static PlayerInjectionHandler playerInjectionHandler = null; private static boolean ready = false; @@ -119,6 +118,6 @@ protected static void tryToSendPackets(@NotNull Player player, @NotNull Iterable * @param callback Callback to execute */ protected static void tryToRunAsyncTask(@NotNull Runnable callback) { - plugin.getScheduler().execute(callback); + YamipaPlugin.getInstance().getScheduler().execute(callback); } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java index f526de2..7ff859e 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java @@ -1,6 +1,7 @@ package io.josemmo.bukkit.plugin.renderer; import com.comphenix.protocol.events.PacketContainer; +import io.josemmo.bukkit.plugin.YamipaPlugin; import io.josemmo.bukkit.plugin.storage.ImageFile; import io.josemmo.bukkit.plugin.utils.DirectionUtils; import io.josemmo.bukkit.plugin.utils.Logger; @@ -197,7 +198,7 @@ public FakeImage( * @return Image file instance or NULL if not found */ public @Nullable ImageFile getFile() { - return plugin.getStorage().get(filename); + return YamipaPlugin.getInstance().getStorage().get(filename); } /** @@ -380,7 +381,7 @@ private void load() { // Start animation task (if needed) if (animateImages && task == null && hasFlag(FLAG_ANIMATABLE) && numOfSteps > 1) { - task = plugin.getScheduler().scheduleAtFixedRate( + task = YamipaPlugin.getInstance().getScheduler().scheduleAtFixedRate( this::nextStep, 0, delay*50L, diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java index 5c1b197..755ee5f 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java @@ -28,7 +28,6 @@ public class ImageRenderer implements Listener { public static final long SAVE_INTERVAL = 20L * 90; // In server ticks private static final Logger LOGGER = Logger.getLogger("ImageRenderer"); - private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private final String configPath; private BukkitTask saveTask; private final AtomicBoolean hasConfigChanged = new AtomicBoolean(false); @@ -49,6 +48,7 @@ public ImageRenderer(@NotNull String configPath) { */ public void start() { loadConfig(); + YamipaPlugin plugin = YamipaPlugin.getInstance(); plugin.getServer().getPluginManager().registerEvents(this, plugin); saveTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::saveConfig, SAVE_INTERVAL, SAVE_INTERVAL); } @@ -100,7 +100,7 @@ private void loadConfig() { for (String[] row : config.getRows()) { try { String filename = row[0]; - World world = Objects.requireNonNull(plugin.getServer().getWorld(row[1])); + World world = Objects.requireNonNull(YamipaPlugin.getInstance().getServer().getWorld(row[1])); double x = Integer.parseInt(row[2]); double y = Integer.parseInt(row[3]); double z = Integer.parseInt(row[4]); @@ -109,10 +109,10 @@ private void loadConfig() { Rotation rotation = Rotation.valueOf(row[6]); int width = Math.min(FakeImage.MAX_DIMENSION, Math.abs(Integer.parseInt(row[7]))); int height = Math.min(FakeImage.MAX_DIMENSION, Math.abs(Integer.parseInt(row[8]))); - Date placedAt = (row.length > 9 && !row[9].equals("")) ? + Date placedAt = (row.length > 9 && !row[9].isEmpty()) ? new Date(Long.parseLong(row[9])*1000L) : null; - UUID placedById = (row.length > 10 && !row[10].equals("")) ? + UUID placedById = (row.length > 10 && !row[10].isEmpty()) ? UUID.fromString(row[10]) : FakeImage.UNKNOWN_PLAYER_ID; OfflinePlayer placedBy = Bukkit.getOfflinePlayer(placedById); @@ -429,9 +429,8 @@ public void onPlayerTeleport(@NotNull PlayerTeleportEvent event) { // Wait until next server tick before handling location change // This is necessary as teleport events get fired *before* teleporting the player - Bukkit.getScheduler().runTask(plugin, () -> { - onPlayerLocationChange(event.getPlayer(), event.getTo()); - }); + YamipaPlugin plugin = YamipaPlugin.getInstance(); + Bukkit.getScheduler().runTask(plugin, () -> onPlayerLocationChange(event.getPlayer(), event.getTo())); } @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java index 9b03a05..5de2874 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java @@ -29,11 +29,18 @@ public class ItemService extends InteractWithEntityListener implements Listener { private static final Logger LOGGER = Logger.getLogger("ItemService"); - private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); - private static final NamespacedKey NSK_FILENAME = new NamespacedKey(plugin, "filename"); - private static final NamespacedKey NSK_WIDTH = new NamespacedKey(plugin, "width"); - private static final NamespacedKey NSK_HEIGHT = new NamespacedKey(plugin, "height"); - private static final NamespacedKey NSK_FLAGS = new NamespacedKey(plugin, "flags"); + private static final NamespacedKey NSK_FILENAME; + private static final NamespacedKey NSK_WIDTH; + private static final NamespacedKey NSK_HEIGHT; + private static final NamespacedKey NSK_FLAGS; + + static { + YamipaPlugin plugin = YamipaPlugin.getInstance(); // Only used for getting namespace, reference will be freed + NSK_FILENAME = new NamespacedKey(plugin, "filename"); + NSK_WIDTH = new NamespacedKey(plugin, "width"); + NSK_HEIGHT = new NamespacedKey(plugin, "height"); + NSK_FLAGS = new NamespacedKey(plugin, "flags"); + } /** * Get image item @@ -66,6 +73,7 @@ public class ItemService extends InteractWithEntityListener implements Listener */ public void start() { register(); + YamipaPlugin plugin = YamipaPlugin.getInstance(); plugin.getServer().getPluginManager().registerEvents(this, plugin); } @@ -178,7 +186,7 @@ public void onPlaceItem(@NotNull HangingPlaceEvent event) { @Override public boolean onAttack(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face) { - ImageRenderer renderer = plugin.getRenderer(); + ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer(); Location location = block.getLocation(); // Has the player clicked a removable placed image? @@ -209,9 +217,8 @@ public boolean onAttack(@NotNull Player player, @NotNull Block block, @NotNull B ImageFile imageFile = Objects.requireNonNull(image.getFile()); ItemStack imageItem = getImageItem(imageFile, 1, image.getWidth(), image.getHeight(), image.getFlags()); Location dropLocation = location.clone().add(0.5, -0.5, 0.5).add(face.getDirection()); - Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> { - block.getWorld().dropItem(dropLocation, imageItem); - }); + YamipaPlugin plugin = YamipaPlugin.getInstance(); + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> block.getWorld().dropItem(dropLocation, imageItem)); } return false; diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java b/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java index a38d39a..b97e9f7 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java @@ -23,7 +23,6 @@ public class SelectBlockTask { private static final Logger LOGGER = Logger.getLogger("SelectBlockTask"); - private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private static final Map instances = new HashMap<>(); private static SelectBlockTaskListener listener = null; private final Player player; @@ -106,6 +105,7 @@ private static class SelectBlockTaskListener extends InteractWithEntityListener @Override public void register() { super.register(); + YamipaPlugin plugin = YamipaPlugin.getInstance(); plugin.getServer().getPluginManager().registerEvents(this, plugin); } From 0eab4f7f4d2bc9b5a9b13be676fbd9ee776770f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 14:52:25 +0200 Subject: [PATCH 09/30] Minor improvements - Updated ImageCommand class - Updated ImageRenderer and ImageStorage classes --- .../io/josemmo/bukkit/plugin/commands/ImageCommand.java | 8 +++----- .../io/josemmo/bukkit/plugin/renderer/ImageRenderer.java | 2 +- .../io/josemmo/bukkit/plugin/storage/ImageStorage.java | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java index 2bbcd76..e8c623a 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java @@ -86,7 +86,7 @@ public static void listImages(@NotNull CommandSender sender, int page) { sender.sendMessage("=== Page " + page + " out of " + maxPage + " ==="); } for (int i=firstImageIndex; i { - placeImage(player, image, width, finalHeight, flags, location, face); - }); + task.onSuccess((location, face) -> placeImage(player, image, width, finalHeight, flags, location, face)); task.onFailure(() -> ActionBar.send(player, ChatColor.RED + "Image placing canceled")); task.run("Right click a block to continue"); } @@ -282,7 +280,7 @@ public static void clearImages( // Get images in area Set images = renderer.getImages( - origin.getWorld(), + Objects.requireNonNull(origin.getWorld()), origin.getBlockX()-radius+1, origin.getBlockX()+radius-1, origin.getBlockZ()-radius+1, diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java index 755ee5f..5f53ec1 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java @@ -244,7 +244,7 @@ public void addImage(@NotNull FakeImage image) { * @param maxX Maximum X coordinate * @param minZ Minimum Z coordinate * @param maxZ Maximum Z coordinate - * @return List of found images + * @return Set of found images */ public @NotNull Set getImages(@NotNull World world, int minX, int maxX, int minZ, int maxZ) { Set response = new HashSet<>(); diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index 7c961a5..0737fa4 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -18,7 +18,7 @@ * directory. */ public class ImageStorage { - private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); + private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"); private static final Logger LOGGER = Logger.getLogger("ImageStorage"); /** Map of registered files indexed by filename */ private final SortedMap files = new TreeMap<>(); From 599a7de7fc16f8e03bf72fb11786cbacf8c7a467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 15:05:49 +0200 Subject: [PATCH 10/30] Fixed remaining dangling plugin reference - Updated ImageFile class --- .../java/io/josemmo/bukkit/plugin/storage/ImageFile.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java index 6fa46d4..3e54c57 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java @@ -35,7 +35,6 @@ public class ImageFile { public static final byte[] CACHE_SIGNATURE = new byte[] {0x59, 0x4d, 0x50}; // "YMP" public static final int CACHE_VERSION = 1; private static final Logger LOGGER = Logger.getLogger("ImageFile"); - private static final YamipaPlugin plugin = YamipaPlugin.getInstance(); private final ConcurrentHashMap locks = new ConcurrentHashMap<>(); private final Map cache = new HashMap<>(); private final Map> subscribers = new HashMap<>(); @@ -243,7 +242,7 @@ public long getLastModified() { // Try to get maps from disk cache String cacheFilename = filename + "." + cacheKey + "." + CACHE_EXT; - File cacheFile = plugin.getStorage().getCachePath().resolve(cacheFilename).toFile(); + File cacheFile = YamipaPlugin.getInstance().getStorage().getCachePath().resolve(cacheFilename).toFile(); if (cacheFile.isFile() && cacheFile.lastModified() >= getLastModified()) { try { FakeMapsContainer container = readMapsFromCacheFile(cacheFile, width, height); @@ -417,7 +416,7 @@ public synchronized void invalidate() { cache.clear(); // Find cache files to delete - Path cachePath = plugin.getStorage().getCachePath(); + Path cachePath = YamipaPlugin.getInstance().getStorage().getCachePath(); String cachePattern = Pattern.quote(path.getFileName().toString()) + "\\.[0-9]+-[0-9]+\\." + CACHE_EXT; File cacheDirectory = cachePath.resolve(filename).getParent().toFile(); if (!cacheDirectory.exists()) { From a16cda7c22708d8fcec9218dc971f292b787c513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 15:08:35 +0200 Subject: [PATCH 11/30] Improved legibility of static properties - Made static properties UPPERCASE - Added missing nullable annotations - Improved legibility/performance of LAST_MAP_ID and LAST_FRAME_ID counters --- .../bukkit/plugin/commands/ImageCommand.java | 2 +- .../plugin/commands/ImageCommandBridge.java | 4 ++-- .../bukkit/plugin/renderer/FakeEntity.java | 23 ++++++++++--------- .../bukkit/plugin/renderer/FakeItemFrame.java | 6 ++--- .../bukkit/plugin/renderer/FakeMap.java | 15 ++++++------ .../bukkit/plugin/renderer/ImageRenderer.java | 2 +- .../bukkit/plugin/storage/ImageFile.java | 6 ++--- 7 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java index e8c623a..9a9ae31 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java @@ -29,7 +29,7 @@ import java.util.*; public class ImageCommand { - public static final int ITEMS_PER_PAGE = 9; + private static final int ITEMS_PER_PAGE = 9; private static final Logger LOGGER = Logger.getLogger("ImageCommand"); public static void showHelp(@NotNull CommandSender s, @NotNull String commandName) { diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java index dfe8fe6..4c173d4 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java @@ -16,8 +16,8 @@ import org.jetbrains.annotations.NotNull; public class ImageCommandBridge { - public static final String COMMAND_NAME = "yamipa"; - public static final String[] COMMAND_ALIASES = new String[] {"image", "images"}; + private static final String COMMAND_NAME = "yamipa"; + private static final String[] COMMAND_ALIASES = new String[] {"image", "images"}; private static final Logger LOGGER = Logger.getLogger("ImageCommandBridge"); /** diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java index 00878fe..906ae32 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java @@ -11,24 +11,25 @@ import io.josemmo.bukkit.plugin.utils.Logger; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.reflect.Field; public abstract class FakeEntity { private static final Logger LOGGER = Logger.getLogger("FakeEntity"); - private static final ProtocolManager connection = ProtocolLibrary.getProtocolManager(); - private static PlayerInjectionHandler playerInjectionHandler = null; - private static boolean ready = false; + private static final ProtocolManager CONNECTION = ProtocolLibrary.getProtocolManager(); + private static @Nullable PlayerInjectionHandler PLAYER_INJECTION_HANDLER; + private static boolean READY = false; static { try { - for (Field field : connection.getClass().getDeclaredFields()) { + for (Field field : CONNECTION.getClass().getDeclaredFields()) { if (field.getType().equals(PlayerInjectionHandler.class)) { field.setAccessible(true); - playerInjectionHandler = (PlayerInjectionHandler) field.get(connection); + PLAYER_INJECTION_HANDLER = (PlayerInjectionHandler) field.get(CONNECTION); break; } } - if (playerInjectionHandler == null) { + if (PLAYER_INJECTION_HANDLER == null) { throw new RuntimeException("No valid candidate field found in ProtocolManager"); } } catch (Exception e) { @@ -42,7 +43,7 @@ public abstract class FakeEntity { * NOTE: Will wait synchronously, blocking the invoker thread */ public static synchronized void waitForProtocolLib() { - if (ready) { + if (READY) { // ProtocolLib is ready return; } @@ -51,7 +52,7 @@ public static synchronized void waitForProtocolLib() { while (true) { try { WrappedDataWatcher.Registry.get(Byte.class); - ready = true; + READY = true; break; } catch (Exception e) { if (++retry > 20) { @@ -84,10 +85,10 @@ protected static void tryToSleep(long ms) { */ protected static void tryToSendPacket(@NotNull Player player, @NotNull PacketContainer packet) { try { - if (playerInjectionHandler == null) { // Use single-threaded packet sending if reflection failed - connection.sendServerPacket(player, packet); + if (PLAYER_INJECTION_HANDLER == null) { // Use single-threaded packet sending if reflection failed + CONNECTION.sendServerPacket(player, packet); } else { // Use non-blocking packet sending if available (faster, the expected case) - playerInjectionHandler.sendServerPacket(player, packet, null, false); + PLAYER_INJECTION_HANDLER.sendServerPacket(player, packet, null, false); } } catch (IllegalStateException e) { // Server is shutting down and cannot send the packet, ignore diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java index 0febbde..377bbfc 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java @@ -26,7 +26,7 @@ public class FakeItemFrame extends FakeEntity { public static final int MAX_FRAME_ID = Integer.MAX_VALUE; private static final boolean SUPPORTS_GLOWING = Internals.MINECRAFT_VERSION >= 17; private static final Logger LOGGER = Logger.getLogger("FakeItemFrame"); - private static final AtomicInteger lastFrameId = new AtomicInteger(MAX_FRAME_ID); + private static final AtomicInteger LAST_FRAME_ID = new AtomicInteger(MAX_FRAME_ID); private final int id; private final Location location; private final BlockFace face; @@ -39,8 +39,8 @@ public class FakeItemFrame extends FakeEntity { * @return Next unused item frame ID */ private static int getNextId() { - return lastFrameId.updateAndGet(lastId -> { - if (lastId >= MAX_FRAME_ID) { + return LAST_FRAME_ID.updateAndGet(lastId -> { + if (lastId == MAX_FRAME_ID) { return MIN_FRAME_ID; } return lastId + 1; diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java index a10d4a1..3d506c8 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java @@ -5,6 +5,7 @@ import org.bukkit.entity.Player; import org.bukkit.map.MapPalette; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.awt.*; import java.time.Instant; import java.util.Arrays; @@ -19,8 +20,8 @@ public class FakeMap extends FakeEntity { private static final int MAX_MAP_ID = Integer.MAX_VALUE; private static final int RESEND_THRESHOLD = 60*5; // Seconds after sending pixels when resending should be avoided private static final Logger LOGGER = Logger.getLogger("FakeMap"); - private static final AtomicInteger lastMapId = new AtomicInteger(-1); - private static FakeMap errorInstance; + private static final AtomicInteger LAST_MAP_ID = new AtomicInteger(MIN_MAP_ID); + private static @Nullable FakeMap ERROR_INSTANCE; private final int id; private final byte[] pixels; private final ConcurrentMap lastPlayerSendTime = new ConcurrentHashMap<>(); @@ -30,8 +31,8 @@ public class FakeMap extends FakeEntity { * @return Next unused map ID */ private static int getNextId() { - return lastMapId.updateAndGet(lastId -> { - if (lastId <= MIN_MAP_ID) { + return LAST_MAP_ID.updateAndGet(lastId -> { + if (lastId == MIN_MAP_ID) { return MAX_MAP_ID; } return lastId - 1; @@ -53,12 +54,12 @@ public static byte pixelToIndex(int pixel) { * @return Error instance */ private static @NotNull FakeMap getErrorInstance() { - if (errorInstance == null) { + if (ERROR_INSTANCE == null) { byte[] pixels = new byte[DIMENSION * DIMENSION]; Arrays.fill(pixels, pixelToIndex(Color.RED.getRGB())); - errorInstance = new FakeMap(pixels); + ERROR_INSTANCE = new FakeMap(pixels); } - return errorInstance; + return ERROR_INSTANCE; } /** diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java index 5f53ec1..f9d2948 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java @@ -26,7 +26,7 @@ import java.util.stream.Collectors; public class ImageRenderer implements Listener { - public static final long SAVE_INTERVAL = 20L * 90; // In server ticks + private static final long SAVE_INTERVAL = 20L * 90; // In server ticks private static final Logger LOGGER = Logger.getLogger("ImageRenderer"); private final String configPath; private BukkitTask saveTask; diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java index 3e54c57..b3c0d36 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageFile.java @@ -31,9 +31,9 @@ import java.util.stream.IntStream; public class ImageFile { - public static final String CACHE_EXT = "cache"; - public static final byte[] CACHE_SIGNATURE = new byte[] {0x59, 0x4d, 0x50}; // "YMP" - public static final int CACHE_VERSION = 1; + private static final String CACHE_EXT = "cache"; + private static final byte[] CACHE_SIGNATURE = new byte[] {0x59, 0x4d, 0x50}; // "YMP" + private static final int CACHE_VERSION = 1; private static final Logger LOGGER = Logger.getLogger("ImageFile"); private final ConcurrentHashMap locks = new ConcurrentHashMap<>(); private final Map cache = new HashMap<>(); From a5d698a2661ab458a14ca4200015dbaef08d1f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 16:38:30 +0200 Subject: [PATCH 12/30] Fixed component stop order - Updated YamipaPlugin#onDisable() --- .../io/josemmo/bukkit/plugin/YamipaPlugin.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index a96b1de..c30d611 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -145,18 +145,22 @@ public void onEnable() { @Override public void onDisable() { - // Stop plugin components - if (storage != null) { - storage.stop(); - storage = null; + // Stop item service + if (itemService != null) { + itemService.stop(); + itemService = null; } + + // Stop image renderer if (renderer != null) { renderer.stop(); renderer = null; } - if (itemService != null) { - itemService.stop(); - itemService = null; + + // Stop image storage + if (storage != null) { + storage.stop(); + storage = null; } // Stop internal scheduler From 16f04aab1af6604cccd822b4569f4931fab96495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Mon, 31 Jul 2023 16:42:52 +0200 Subject: [PATCH 13/30] Updated CI to work with new logger - Updated tests.yml workflow --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 545c825..60ae7cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -109,19 +109,19 @@ jobs: echo "Plugin did not enabled verbose mode" exit 1 fi - if ! grep -Fq '[YamipaPlugin] Found 5 file(s) in images directory' server.log; then + if ! grep -Fq '[YamipaPlugin] [ImageStorage] Found 5 file(s) in images directory' server.log; then echo "Plugin did not read image directory" exit 1 fi - if ! grep -Fq '[YamipaPlugin] Fixed command permissions' server.log; then + if ! grep -Fq '[YamipaPlugin] [ImageCommandBridge] Fixed command permissions' server.log; then echo "Plugin did not fixed command permissions" exit 1 fi - if ! grep -Fq '[YamipaPlugin] Created FakeImage' server.log; then + if ! grep -Fq '[YamipaPlugin] [FakeImage] Created FakeImage' server.log; then echo "Plugin did not place the fake image" exit 1 fi - if ! grep -Fq '[YamipaPlugin] Invalidated FakeImage' server.log; then + if ! grep -Fq '[YamipaPlugin] [FakeImage] Invalidated FakeImage' server.log; then echo "Plugin did not remove the fake image" exit 1 fi From 14ac8ee5262a20af6dfbf6af10b5e6a246618274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sat, 14 Oct 2023 17:53:20 +0200 Subject: [PATCH 14/30] Improving handling of metrics reference - Updated YamipaPlugin class --- src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index c30d611..42222b5 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -27,6 +27,7 @@ public class YamipaPlugin extends JavaPlugin { private @Nullable ImageRenderer renderer; private @Nullable ItemService itemService; private @Nullable ScheduledExecutorService scheduler; + private @Nullable Metrics metrics; /** * Get plugin instance @@ -137,7 +138,7 @@ public void onEnable() { if (number >= 10) return "10-49"; return "0-9"; }; - Metrics metrics = new Metrics(this, BSTATS_PLUGIN_ID); + metrics = new Metrics(this, BSTATS_PLUGIN_ID); metrics.addCustomChart(new SimplePie("animate_images", () -> FakeImage.isAnimationEnabled() ? "true" : "false")); metrics.addCustomChart(new SimplePie("number_of_image_files", () -> toStats.apply(storage.size()))); metrics.addCustomChart(new SimplePie("number_of_placed_images", () -> toStats.apply(renderer.size()))); @@ -145,6 +146,12 @@ public void onEnable() { @Override public void onDisable() { + // Stop metrics + if (metrics != null) { + metrics.shutdown(); + metrics = null; + } + // Stop item service if (itemService != null) { itemService.stop(); From 6a48a178737da142ac3e5862d386dd6dfd5031ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sat, 14 Oct 2023 18:02:18 +0200 Subject: [PATCH 15/30] Minor annotation changes in commands - Updated Command class - Updated ImageCommandBridge class --- .../java/io/josemmo/bukkit/plugin/commands/Command.java | 6 ++++-- .../josemmo/bukkit/plugin/commands/ImageCommandBridge.java | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/Command.java b/src/main/java/io/josemmo/bukkit/plugin/commands/Command.java index afaebae..1b457e1 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/Command.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/Command.java @@ -10,6 +10,7 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.function.BiConsumer; @@ -19,8 +20,8 @@ public class Command { private final String name; private final List arguments = new ArrayList<>(); private Predicate requirementHandler = __ -> true; - private BiConsumer executesHandler = null; - private BiConsumer executesPlayerHandler = null; + private @Nullable BiConsumer executesHandler; + private @Nullable BiConsumer executesPlayerHandler; private final List subcommands = new ArrayList<>(); /** @@ -82,6 +83,7 @@ public Command(@NotNull String name) { * @param handler Command handler * @return This instance */ + @SuppressWarnings("UnusedReturnValue") public @NotNull Command executesPlayer(@NotNull BiConsumer handler) { executesPlayerHandler = handler; return this; diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java index 4c173d4..73bfd5b 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java @@ -56,6 +56,7 @@ public static void register(@NotNull YamipaPlugin plugin) { }); } + @SuppressWarnings("CodeBlock2Expr") private static @NotNull Command getRootCommand() { Command root = new Command(COMMAND_NAME); From 0edd1f28e50eddd9e328965e4e3ff65cff418292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Fri, 29 Dec 2023 17:25:07 +0100 Subject: [PATCH 16/30] Heavy reworking of storage and renderer components - Created SynchronizedFile class and their dependencies - Updated ImageFile class - Migrated from FakeMapsContainer to CachedMapsFile class - Updated renderer classes > Related to #91 --- .../bukkit/plugin/renderer/FakeImage.java | 29 +- .../bukkit/plugin/renderer/FakeMap.java | 6 +- .../plugin/renderer/FakeMapsContainer.java | 27 -- .../bukkit/plugin/renderer/ImageRenderer.java | 1 + .../bukkit/plugin/renderer/WorldAreaId.java | 2 +- .../bukkit/plugin/storage/CachedMapsFile.java | 341 +++++++++++++++ .../bukkit/plugin/storage/ImageFile.java | 392 ++---------------- .../bukkit/plugin/storage/ImageStorage.java | 20 +- .../plugin/storage/SynchronizedFile.java | 77 ++++ .../storage/SynchronizedFileStream.java | 26 ++ 10 files changed, 525 insertions(+), 396 deletions(-) delete mode 100644 src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMapsContainer.java create mode 100644 src/main/java/io/josemmo/bukkit/plugin/storage/CachedMapsFile.java create mode 100644 src/main/java/io/josemmo/bukkit/plugin/storage/SynchronizedFile.java create mode 100644 src/main/java/io/josemmo/bukkit/plugin/storage/SynchronizedFileStream.java diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java index 7ff859e..8b7cba4 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java @@ -2,6 +2,7 @@ import com.comphenix.protocol.events.PacketContainer; import io.josemmo.bukkit.plugin.YamipaPlugin; +import io.josemmo.bukkit.plugin.storage.CachedMapsFile; import io.josemmo.bukkit.plugin.storage.ImageFile; import io.josemmo.bukkit.plugin.utils.DirectionUtils; import io.josemmo.bukkit.plugin.utils.Logger; @@ -50,7 +51,7 @@ public class FakeImage extends FakeEntity { private final int flags; private final BiFunction getLocationVector; private final Set observingPlayers = new HashSet<>(); - private Runnable onLoadedListener = null; + private @Nullable Runnable onLoadedListener = null; // Generated values private boolean loading = false; @@ -59,8 +60,8 @@ public class FakeImage extends FakeEntity { private int numOfSteps = -1; // Total number of animation steps // Animation task attributes - private static boolean animateImages = false; - private ScheduledFuture task; + private static boolean ANIMATE_IMAGES = false; + private @Nullable ScheduledFuture task; private int currentStep = -1; // Current animation step /** @@ -68,7 +69,7 @@ public class FakeImage extends FakeEntity { * @param animImages Animate images */ public static void configure(boolean animImages) { - animateImages = animImages; + ANIMATE_IMAGES = animImages; } /** @@ -76,7 +77,7 @@ public static void configure(boolean animImages) { * @return Is animation enabled */ public static boolean isAnimationEnabled() { - return animateImages; + return ANIMATE_IMAGES; } /** @@ -315,6 +316,7 @@ public int getDelay() { * @param face Block face * @return TRUE for contained, FALSE otherwise */ + @SuppressWarnings("RedundantIfStatement") public boolean contains(@NotNull Location location, @NotNull BlockFace face) { // Is point facing the same plane as the image? if (face != this.face) { @@ -355,18 +357,18 @@ public void setOnLoadedListener(@NotNull Runnable onLoadedListener) { */ private void load() { ImageFile file = getFile(); - FakeMapsContainer container; + + // Get maps to use + FakeMap[][][] maps; if (file == null) { - container = FakeMap.getErrorMatrix(width, height); + maps = FakeMap.getErrorMatrix(width, height); LOGGER.warning("File \"" + filename + "\" does not exist"); } else { - container = file.getMapsAndSubscribe(this); + CachedMapsFile cachedMapsFile = file.getMapsAndSubscribe(this); + maps = cachedMapsFile.getMaps(); + delay = cachedMapsFile.getDelay(); } - - // Extract data from container - FakeMap[][][] maps = container.getFakeMaps(); numOfSteps = maps[0][0].length; - delay = container.getDelay(); // Generate frames FakeItemFrame[] newFrames = new FakeItemFrame[width*height]; @@ -380,7 +382,7 @@ private void load() { frames = newFrames; // Start animation task (if needed) - if (animateImages && task == null && hasFlag(FLAG_ANIMATABLE) && numOfSteps > 1) { + if (ANIMATE_IMAGES && task == null && hasFlag(FLAG_ANIMATABLE) && numOfSteps > 1) { task = YamipaPlugin.getInstance().getScheduler().scheduleAtFixedRate( this::nextStep, 0, @@ -524,6 +526,7 @@ private void invalidate() { // Free array of fake item frames frames = null; + loading = false; LOGGER.fine("Invalidated FakeImage#(" + location + "," + face + ")"); // Notify invalidation to source ImageFile diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java index 3d506c8..dd56684 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java @@ -66,15 +66,15 @@ public static byte pixelToIndex(int pixel) { * Get matrix of error maps * @param width Width in blocks * @param height Height in blocks - * @return Fake maps container + * @return Fake maps */ - public static @NotNull FakeMapsContainer getErrorMatrix(int width, int height) { + public static @NotNull FakeMap[][][] getErrorMatrix(int width, int height) { FakeMap[] errorMaps = new FakeMap[] {getErrorInstance()}; FakeMap[][][] matrix = new FakeMap[width][height][1]; for (FakeMap[][] column : matrix) { Arrays.fill(column, errorMaps); } - return new FakeMapsContainer(matrix, 0); + return matrix; } /** diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMapsContainer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMapsContainer.java deleted file mode 100644 index 8bb07b8..0000000 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMapsContainer.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.josemmo.bukkit.plugin.renderer; - -public class FakeMapsContainer { - private final FakeMap[][][] fakeMaps; - private final int delay; - - public FakeMapsContainer(FakeMap[][][] fakeMaps, int delay) { - this.fakeMaps = fakeMaps; - this.delay = delay; - } - - /** - * Get fake maps - * @return Tri-dimensional array of maps (column, row, step) - */ - public FakeMap[][][] getFakeMaps() { - return fakeMaps; - } - - /** - * Get delay between steps - * @return Delay in 50ms intervals or 0 if not applicable - */ - public int getDelay() { - return delay; - } -} diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java index f9d2948..5d6d85c 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java @@ -131,6 +131,7 @@ private void loadConfig() { /** * Save configuration to disk */ + @SuppressWarnings("ExtractMethodRecommender") private void saveConfig() { if (!hasConfigChanged.get()) return; diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/WorldAreaId.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/WorldAreaId.java index ccaf88a..9c44a86 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/WorldAreaId.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/WorldAreaId.java @@ -61,7 +61,7 @@ public WorldAreaId(@NotNull World world, int x, int z) { /** * Get nearby world area IDs in view distance (plus this one) - * @return List of neighbors + * @return Array of neighbors */ public @NotNull WorldAreaId[] getNeighborhood() { // Get value from cache (if available) diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/CachedMapsFile.java b/src/main/java/io/josemmo/bukkit/plugin/storage/CachedMapsFile.java new file mode 100644 index 0000000..8207f14 --- /dev/null +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/CachedMapsFile.java @@ -0,0 +1,341 @@ +package io.josemmo.bukkit.plugin.storage; + +import io.josemmo.bukkit.plugin.YamipaPlugin; +import io.josemmo.bukkit.plugin.renderer.FakeImage; +import io.josemmo.bukkit.plugin.renderer.FakeMap; +import io.josemmo.bukkit.plugin.utils.Logger; +import org.jetbrains.annotations.NotNull; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.ImageInputStream; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +public class CachedMapsFile extends SynchronizedFile { + private static final String CACHE_EXT = "cache"; + private static final byte[] CACHE_SIGNATURE = new byte[] {0x59, 0x4d, 0x50}; // "YMP" + private static final int CACHE_VERSION = 1; + private static final Logger LOGGER = Logger.getLogger("CachedMapsFile"); + private final ImageFile imageFile; + private final int width; + private final int height; + private FakeMap[][][] maps; + private int delay; + + /** + * Create instance from image file + * @param imageFile Image file instance + * @param width Width in blocks + * @param height Height in blocks + * @return Cached maps instance + */ + public static @NotNull CachedMapsFile from(@NotNull ImageFile imageFile, int width, int height) { + Path cachePath = YamipaPlugin.getInstance().getStorage().getCachePath(); + Path path = cachePath.resolve(imageFile.getFilename() + "." + width + "-" + height + "." + CACHE_EXT); + return new CachedMapsFile(path, imageFile, width, height); + } + + /** + * Delete all cached maps files associated to an image file + * @param imageFile Image file instance + */ + public static void deleteAll(@NotNull ImageFile imageFile) { + String relativeFilename = imageFile.getFilename(); + Path cachePath = YamipaPlugin.getInstance().getStorage().getCachePath(); + File baseDirectory = cachePath.resolve(relativeFilename).getParent().toFile(); + String cachePattern = Pattern.quote(Paths.get(relativeFilename).getFileName().toString()) + + "\\.[0-9]+-[0-9]+\\." + CACHE_EXT; + + // Find cache files to delete + if (!baseDirectory.exists()) { + // Cache subdirectory does not exist, no need to delete files + return; + } + File[] files = baseDirectory.listFiles((__, item) -> item.matches(cachePattern)); + if (files == null) { + LOGGER.warning("An error occurred when listing cache files for image \"" + relativeFilename + "\""); + return; + } + + // Delete disk cache files + for (File file : files) { + if (!file.delete()) { + LOGGER.warning("Failed to delete cache file \"" + file.getAbsolutePath() + "\""); + } + } + } + + /** + * Class constructor + * @param path Path to cached maps file in disk + * @param imageFile Image file associated to these maps + * @param width Width in blocks + * @param height Height blocks + */ + private CachedMapsFile(@NotNull Path path, @NotNull ImageFile imageFile, int width, int height) { + super(path); + this.imageFile = imageFile; + this.width = width; + this.height = height; + load(); + } + + /** + * Get maps + * @return Tri-dimensional array of maps (column, row, step) + */ + public @NotNull FakeMap[][][] getMaps() { + return maps; + } + + /** + * Get delay between steps + * @return Delay in 50ms intervals or 0 if not applicable + */ + public int getDelay() { + return delay; + } + + /** + * Load maps + */ + private void load() { + // Try to load maps from disk + if (exists() && getLastModified() > imageFile.getLastModified()) { + try { + loadFromDisk(); + return; + } catch (IllegalArgumentException e) { + LOGGER.info("Cache file \"" + path + "\" is outdated and will be overwritten"); + } catch (Exception e) { + LOGGER.warning("Cache file \"" + path + "\" is corrupted", e); + } + } + + // Generate maps from image file + try { + generateFromImage(); + tryToWriteToDisk(); + return; + } catch (Exception e) { + LOGGER.severe("Failed to render image step(s) from file \"" + path + "\"", e); + } + + // Fallback to error matrix + maps = FakeMap.getErrorMatrix(width, height); + delay = 0; + } + + /** + * Load data from disk + * @throws IllegalArgumentException if cache file is outdated + * @throws IOException if cache file is corrupted + */ + private void loadFromDisk() throws IllegalArgumentException, IOException { + try (RandomAccessFile stream = read()) { + // Validate file signature + for (byte expectedByte : CACHE_SIGNATURE) { + if ((byte) stream.read() != expectedByte) { + throw new IllegalArgumentException("Invalid file signature"); + } + } + + // Validate version number + if ((byte) stream.read() != CACHE_VERSION) { + throw new IllegalArgumentException("Incompatible file format version"); + } + + // Get number of animation steps + int numOfSteps = stream.read() | (stream.read() << 8); + if (numOfSteps < 1 || numOfSteps > FakeImage.MAX_STEPS) { + throw new IOException("Invalid number of animation steps: " + numOfSteps); + } + + // Get delay between steps + int delay = 0; + if (numOfSteps > 1) { + delay = stream.read(); + if (delay < FakeImage.MIN_DELAY || delay > FakeImage.MAX_DELAY) { + throw new IOException("Invalid delay between steps: " + delay); + } + } + + // Read pixels + FakeMap[][][] maps = new FakeMap[width][height][numOfSteps]; + for (int col=0; col renderedImages = new ArrayList<>(); + Map delays = new HashMap<>(); + try (ImageInputStream inputStream = ImageIO.createImageInputStream(imageFile.read())) { + ImageReader reader = ImageIO.getImageReaders(inputStream).next(); + reader.setInput(inputStream); + String format = reader.getFormatName().toLowerCase(); + + // Create temporary canvas + int originalWidth = reader.getWidth(0); + int originalHeight = reader.getHeight(0); + BufferedImage tmpImage = new BufferedImage(originalWidth, originalHeight, BufferedImage.TYPE_4BYTE_ABGR); + Graphics2D tmpGraphics = tmpImage.createGraphics(); + tmpGraphics.setBackground(new Color(0, 0, 0, 0)); + + // Create temporary scaled canvas (for resizing) + BufferedImage tmpScaledImage = new BufferedImage(widthInPixels, heightInPixels, BufferedImage.TYPE_INT_ARGB); + Graphics2D tmpScaledGraphics = tmpScaledImage.createGraphics(); + tmpScaledGraphics.setBackground(new Color(0, 0, 0, 0)); + + // Read images from file + for (int step=0; step (count == null) ? 1 : count + 1); + disposePrevious = controlExtensionNode.getAttribute("disposalMethod").startsWith("restore"); + } + } + } + + // Clear temporary canvases (if needed) + if (disposePrevious) { + tmpGraphics.clearRect(0, 0, originalWidth, originalHeight); + tmpScaledGraphics.clearRect(0, 0, widthInPixels, heightInPixels); + } + + // Paint step image over temporary canvas + BufferedImage image = reader.read(step); + tmpGraphics.drawImage(image, imageLeft, imageTop, null); + image.flush(); + + // Resize image and get pixels + tmpScaledGraphics.drawImage(tmpImage, 0, 0, widthInPixels, heightInPixels, null); + int[] rgbaPixels = ((DataBufferInt) tmpScaledImage.getRaster().getDataBuffer()).getData(); + + // Convert RGBA pixels to Minecraft color indexes + byte[] renderedImage = new byte[widthInPixels * heightInPixels]; + IntStream.range(0, rgbaPixels.length) + .parallel() + .forEach(pixelIndex -> renderedImage[pixelIndex] = FakeMap.pixelToIndex(rgbaPixels[pixelIndex])); + renderedImages.add(renderedImage); + } catch (IndexOutOfBoundsException __) { + // No more steps to read + break; + } + } + + // Free resources + reader.dispose(); + tmpGraphics.dispose(); + tmpImage.flush(); + tmpScaledGraphics.dispose(); + tmpScaledImage.flush(); + } + + // Get most occurring delay (mode) + int delay = 0; + if (renderedImages.size() > 1) { + delay = Collections.max(delays.entrySet(), Map.Entry.comparingByValue()).getKey(); + delay = Math.round(delay * 0.2f); // (delay * 10) / 50 + delay = Math.min(Math.max(delay, FakeImage.MIN_DELAY), FakeImage.MAX_DELAY); + } + + // Instantiate fake maps from image steps + FakeMap[][][] maps = new FakeMap[width][height][renderedImages.size()]; + IntStream.range(0, renderedImages.size()).forEach(i -> { + for (int col=0; col> 8) & 0xff); // Number of animation steps (second byte) + if (numOfSteps > 1) { + stream.write(delay); + } + + // Add pixels + //noinspection ForLoopReplaceableByForEach + for (int col=0; col locks = new ConcurrentHashMap<>(); - private final Map cache = new HashMap<>(); + private final Map cache = new HashMap<>(); private final Map> subscribers = new HashMap<>(); private final String filename; - private final Path path; + private @Nullable Dimension size; /** * Class constructor @@ -47,8 +34,8 @@ public class ImageFile { * @param path Path to image file */ protected ImageFile(@NotNull String filename, @NotNull Path path) { + super(path); this.filename = filename; - this.path = path; } /** @@ -59,146 +46,32 @@ protected ImageFile(@NotNull String filename, @NotNull Path path) { return filename; } - /** - * Get image reader - * @return Image reader instance - * @throws IOException if failed to get suitable image reader - */ - private @NotNull ImageReader getImageReader() throws IOException { - ImageInputStream inputStream = ImageIO.createImageInputStream(path.toFile()); - ImageReader reader = ImageIO.getImageReaders(inputStream).next(); - reader.setInput(inputStream); - return reader; - } - - /** - * Render images using Minecraft palette - * @param width New width in pixels - * @param height New height in pixels - * @return Pair of bi-dimensional array of Minecraft images (step, pixel index) and delay between steps - * @throws IOException if failed to render images from file - */ - private @NotNull Pair renderImages(int width, int height) throws IOException { - ImageReader reader = getImageReader(); - String format = reader.getFormatName().toLowerCase(); - - // Create temporary canvas - int originalWidth = reader.getWidth(0); - int originalHeight = reader.getHeight(0); - BufferedImage tmpImage = new BufferedImage(originalWidth, originalHeight, BufferedImage.TYPE_4BYTE_ABGR); - Graphics2D tmpGraphics = tmpImage.createGraphics(); - tmpGraphics.setBackground(new Color(0, 0, 0, 0)); - - // Create temporary scaled canvas (for resizing) - BufferedImage tmpScaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D tmpScaledGraphics = tmpScaledImage.createGraphics(); - tmpScaledGraphics.setBackground(new Color(0, 0, 0, 0)); - - // Read images from file - List renderedImages = new ArrayList<>(); - Map delays = new HashMap<>(); - for (int step=0; step (count == null) ? 1 : count + 1); - disposePrevious = controlExtensionNode.getAttribute("disposalMethod").startsWith("restore"); - } - } - } - - // Clear temporary canvases (if needed) - if (disposePrevious) { - tmpGraphics.clearRect(0, 0, originalWidth, originalHeight); - tmpScaledGraphics.clearRect(0, 0, width, height); - } - - // Paint step image over temporary canvas - BufferedImage image = reader.read(step); - tmpGraphics.drawImage(image, imageLeft, imageTop, null); - image.flush(); - - // Resize image and get pixels - tmpScaledGraphics.drawImage(tmpImage, 0, 0, width, height, null); - int[] rgbaPixels = ((DataBufferInt) tmpScaledImage.getRaster().getDataBuffer()).getData(); - - // Convert RGBA pixels to Minecraft color indexes - byte[] renderedImage = new byte[width*height]; - IntStream.range(0, rgbaPixels.length).parallel().forEach(pixelIndex -> { - renderedImage[pixelIndex] = FakeMap.pixelToIndex(rgbaPixels[pixelIndex]); - }); - renderedImages.add(renderedImage); - } catch (IndexOutOfBoundsException __) { - // No more steps to read - break; - } - } - - // Get most occurring delay (mode) - int delay = 0; - if (renderedImages.size() > 1) { - delay = Collections.max(delays.entrySet(), Map.Entry.comparingByValue()).getKey(); - delay = Math.round(delay * 0.2f); // (delay * 10) / 50 - delay = Math.min(Math.max(delay, FakeImage.MIN_DELAY), FakeImage.MAX_DELAY); - } - - // Free resources - reader.dispose(); - tmpGraphics.dispose(); - tmpImage.flush(); - tmpScaledGraphics.dispose(); - tmpScaledImage.flush(); - - return new Pair<>(renderedImages.toArray(new byte[0][0]), delay); - } - /** * Get original size in pixels * @return Dimension instance or NULL if not a valid image file */ - public @Nullable Dimension getSize() { - try { - ImageReader reader = getImageReader(); - Dimension dimension = new Dimension(reader.getWidth(0), reader.getHeight(0)); + public synchronized @Nullable Dimension getSize() { + if (size != null) { + return size; + } + try (ImageInputStream inputStream = ImageIO.createImageInputStream(read())) { + ImageReader reader = ImageIO.getImageReaders(inputStream).next(); + reader.setInput(inputStream); + size = new Dimension(reader.getWidth(0), reader.getHeight(0)); reader.dispose(); - return dimension; - } catch (Exception e) { + return size; + } catch (IOException | RuntimeException e) { return null; } } /** - * Get image last modified time - * @return Last modified time in milliseconds, zero in case of error - */ - public long getLastModified() { - try { - return Files.getLastModifiedTime(path).toMillis(); - } catch (Exception __) { - return 0L; - } - } - - /** - * Get maps and subscribe to maps cache + * Get maps and subscribe to them * @param subscriber Fake image instance requesting the maps - * @return Fake maps container + * @return Cached maps */ - public @NotNull FakeMapsContainer getMapsAndSubscribe(@NotNull FakeImage subscriber) { + @Blocking + public @NotNull CachedMapsFile getMapsAndSubscribe(@NotNull FakeImage subscriber) { int width = subscriber.getWidth(); int height = subscriber.getHeight(); String cacheKey = width + "-" + height; @@ -207,192 +80,38 @@ public long getLastModified() { Lock lock = locks.computeIfAbsent(cacheKey, __ -> new ReentrantLock()); lock.lock(); - // Execute code - FakeMapsContainer container; - try { - container = getMapsAndSubscribe(subscriber, cacheKey, width, height); - } finally { - lock.unlock(); + // Get cached maps without locking this instance + CachedMapsFile maps = cache.get(cacheKey); + if (maps == null) { + maps = CachedMapsFile.from(this, width, height); } - return container; - } - - /** - * Get maps and subscribe to maps cache (internal) - * @param subscriber Fake image instance requesting the maps - * @param cacheKey Maps cache key - * @param width Desired image width in pixels - * @param height Desired image height in pixels - * @return Fake maps container - */ - private @NotNull FakeMapsContainer getMapsAndSubscribe( - @NotNull FakeImage subscriber, - @NotNull String cacheKey, - int width, - int height - ) { - // Update subscribers for given cached maps - subscribers.computeIfAbsent(cacheKey, __ -> new HashSet<>()).add(subscriber); - - // Try to get maps from memory cache - if (cache.containsKey(cacheKey)) { - return cache.get(cacheKey); - } - - // Try to get maps from disk cache - String cacheFilename = filename + "." + cacheKey + "." + CACHE_EXT; - File cacheFile = YamipaPlugin.getInstance().getStorage().getCachePath().resolve(cacheFilename).toFile(); - if (cacheFile.isFile() && cacheFile.lastModified() >= getLastModified()) { - try { - FakeMapsContainer container = readMapsFromCacheFile(cacheFile, width, height); - cache.put(cacheKey, container); - return container; - } catch (IllegalArgumentException e) { - LOGGER.info("Cache file \"" + cacheFile.getAbsolutePath() + "\" is outdated and will be overwritten"); - } catch (Exception e) { - LOGGER.warning("Cache file \"" + cacheFile.getAbsolutePath() + "\" is corrupted", e); - } - } - - // Generate maps from original image - FakeMapsContainer container; - try { - int widthInPixels = width*FakeMap.DIMENSION; - int heightInPixels = height*FakeMap.DIMENSION; - Pair res = renderImages(widthInPixels, heightInPixels); - byte[][] images = res.getFirst(); - int delay = res.getSecond(); - - // Instantiate fake maps - FakeMap[][][] matrix = new FakeMap[width][height][images.length]; - IntStream.range(0, images.length).forEach(i -> { - for (int col=0; col FakeImage.MAX_STEPS) { - throw new IOException("Invalid number of animation steps: " + numOfSteps); - } - - // Get delay between steps - int delay = 0; - if (numOfSteps > 1) { - delay = stream.read(); - if (delay < FakeImage.MIN_DELAY || delay > FakeImage.MAX_DELAY) { - throw new IOException("Invalid delay between steps: " + delay); - } - } - - // Read pixels - FakeMap[][][] maps = new FakeMap[width][height][numOfSteps]; - for (int col=0; col> 8) & 0xff); // Number of animation steps (second byte) - if (numOfSteps > 1) { - stream.write(container.getDelay()); - } - - // Add pixels - for (int col=0; col new HashSet<>()).add(subscriber); + lock.unlock(); + locks.remove(cacheKey); + return maps; } } /** * Unsubscribe from memory cache *

- * This method is called by FakeImage instances when they get invalidated by a WorldArea. - * By notifying their respective source ImageFile, the latter can clear cached maps from memory when no more - * FakeItemFrames are using them. + * This method is called by {@link FakeImage} instances when they get invalidated by a world area change. + * By notifying their respective source {@link ImageFile}, the latter can clear cached maps from memory when no + * more {@link FakeItemFrame}s are using them. * @param subscriber Fake image instance */ public synchronized void unsubscribe(@NotNull FakeImage subscriber) { String cacheKey = subscriber.getWidth() + "-" + subscriber.getHeight(); - if (!subscribers.containsKey(cacheKey)) return; + if (!subscribers.containsKey(cacheKey)) { + // Not subscribed to this image file + return; + } // Remove subscriber Set currentSubscribers = subscribers.get(cacheKey); @@ -409,31 +128,12 @@ public synchronized void unsubscribe(@NotNull FakeImage subscriber) { /** * Invalidate cache *

- * Removes all references to pre-generated map sets. + * Removes all references to cached map instances. * This way, next time an image is requested to be rendered, maps will be regenerated. */ public synchronized void invalidate() { + size = null; cache.clear(); - - // Find cache files to delete - Path cachePath = YamipaPlugin.getInstance().getStorage().getCachePath(); - String cachePattern = Pattern.quote(path.getFileName().toString()) + "\\.[0-9]+-[0-9]+\\." + CACHE_EXT; - File cacheDirectory = cachePath.resolve(filename).getParent().toFile(); - if (!cacheDirectory.exists()) { - // Cache (sub)directory does not exist, no need to delete files - return; - } - File[] files = cacheDirectory.listFiles((__, item) -> item.matches(cachePattern)); - if (files == null) { - LOGGER.warning("An error occurred when listing cache files for image \"" + filename + "\""); - return; - } - - // Delete disk cache files - for (File file : files) { - if (!file.delete()) { - LOGGER.warning("Failed to delete cache file \"" + file.getAbsolutePath() + "\""); - } - } + CachedMapsFile.deleteAll(this); } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index 0737fa4..ad5702f 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -24,8 +24,8 @@ public class ImageStorage { private final SortedMap files = new TreeMap<>(); private final Path basePath; private final Path cachePath; - private WatchService watchService; - private Thread watchServiceThread; + private @Nullable WatchService watchService; + private @Nullable Thread watchServiceThread; /** * Class constructor @@ -56,8 +56,14 @@ public ImageStorage(@NotNull Path basePath, @NotNull Path cachePath) { /** * Start service * @throws IOException if failed to start watch service + * @throws RuntimeException if already running */ - public void start() throws IOException { + public void start() throws IOException, RuntimeException { + // Prevent initializing more than once + if (watchService != null || watchServiceThread != null) { + throw new RuntimeException("Service is already running"); + } + // Create base directories if necessary if (basePath.toFile().mkdirs()) { LOGGER.info("Created images directory as it did not exist"); @@ -152,9 +158,9 @@ private synchronized void registerDirectory(@NotNull Path path, boolean isBase) WatchEvent.Modifier[] modifiers = IS_WINDOWS ? new WatchEvent.Modifier[]{ExtendedWatchEventModifier.FILE_TREE} : new WatchEvent.Modifier[0]; - path.register(watchService, events, modifiers); + path.register(Objects.requireNonNull(watchService), events, modifiers); LOGGER.fine("Started watching directory at \"" + path.toAbsolutePath() + "\""); - } catch (IOException e) { + } catch (IOException | NullPointerException e) { LOGGER.severe("Failed to register directory", e); } } @@ -273,7 +279,7 @@ private class WatcherThread extends Thread { public void run() { try { WatchKey key; - while ((key = watchService.take()) != null) { + while ((key = Objects.requireNonNull(watchService).take()) != null) { for (WatchEvent event : key.pollEvents()) { WatchEvent.Kind kind = event.kind(); Path keyPath = (Path) key.watchable(); @@ -284,6 +290,8 @@ public void run() { } } catch (InterruptedException __) { // Silently ignore exception, this is expected when service shuts down + } catch (NullPointerException e) { + LOGGER.severe("Watch service was stopped before watcher thread", e); } } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/SynchronizedFile.java b/src/main/java/io/josemmo/bukkit/plugin/storage/SynchronizedFile.java new file mode 100644 index 0000000..c679032 --- /dev/null +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/SynchronizedFile.java @@ -0,0 +1,77 @@ +package io.josemmo.bukkit.plugin.storage; + +import org.jetbrains.annotations.NotNull; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * An instance of a file stored in a shared filesystem (e.g., a NFS drive). + * All operations are performed in a blocking way. + * That is, the instance will acquire a read or write lock before reading or modifying the file. + * If the file is already locked by another resource, it will wait until the lock is released + * while blocking the thread. + */ +public class SynchronizedFile { + protected final Path path; + + /** + * Class constructor + * @param path Path to file + */ + public SynchronizedFile(@NotNull Path path) { + this.path = path; + } + + /** + * Check file exists + * @return Whether file exists or not + */ + public boolean exists() { + return Files.exists(path); + } + + /** + * Get image last modified time + * @return Last modified time in milliseconds or 0 in case of error + */ + public long getLastModified() { + try { + return Files.getLastModifiedTime(path).toMillis(); + } catch (Exception __) { + return 0L; + } + } + + /** + * Make directories + *

+ * Creates the directory containing this file and its parents if they don't exist. + */ + public void mkdirs() { + try { + Files.createDirectories(path.getParent()); + } catch (IOException __) { + // Silently ignore exception + } + } + + /** + * Get file reader + * @return Readable stream of the file + * @throws IOException if failed to acquire lock + */ + public RandomAccessFile read() throws IOException { + return new SynchronizedFileStream(path, true); + } + + /** + * Get file writer + * @return Writable stream of the file + * @throws IOException if failed to acquire lock + */ + public RandomAccessFile write() throws IOException { + return new SynchronizedFileStream(path, false); + } +} diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/SynchronizedFileStream.java b/src/main/java/io/josemmo/bukkit/plugin/storage/SynchronizedFileStream.java new file mode 100644 index 0000000..7410d16 --- /dev/null +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/SynchronizedFileStream.java @@ -0,0 +1,26 @@ +package io.josemmo.bukkit.plugin.storage; + +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.NotNull; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; + +/** + * Blocking file stream implementing {@link RandomAccessFile} for reading and writing data. + * It will lock the file for reading/writing when opened, and release the lock when closed. + */ +public class SynchronizedFileStream extends RandomAccessFile { + /** + * Class constructor + * @param path Path to file + * @param readOnly Whether to open stream in read-only mode + * @throws IOException if failed to acquire lock + */ + @Blocking + public SynchronizedFileStream(@NotNull Path path, boolean readOnly) throws IOException { + super(path.toFile(), readOnly ? "r" : "rw"); + //noinspection ResultOfMethodCallIgnored + getChannel().lock(0L, Long.MAX_VALUE, readOnly); + } +} From d25fcb3a1fa1d1b05aac48c24578eafeaa4a86f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Fri, 29 Dec 2023 17:46:05 +0100 Subject: [PATCH 17/30] Allowed downloading images in subdirectories - Updated ImageCommand class - Updated ImageStorage class - Updated YamipaPlugin class --- src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java | 4 ++-- .../io/josemmo/bukkit/plugin/commands/ImageCommand.java | 5 +++-- .../java/io/josemmo/bukkit/plugin/storage/ImageStorage.java | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index 42222b5..bce1d2a 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -97,8 +97,8 @@ public void onEnable() { // Create image storage storage = new ImageStorage( - basePath.resolve(imagesPath).toAbsolutePath(), - basePath.resolve(cachePath).toAbsolutePath() + basePath.resolve(imagesPath).toAbsolutePath().normalize(), + basePath.resolve(cachePath).toAbsolutePath().normalize() ); try { storage.start(); diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java index 9a9ae31..2ff24ca 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java @@ -95,8 +95,8 @@ public static void downloadImage(@NotNull CommandSender sender, @NotNull String // Validate destination file Path basePath = plugin.getStorage().getBasePath(); - Path destPath = basePath.resolve(filename); - if (!destPath.getParent().equals(basePath)) { + Path destPath = basePath.resolve(filename).normalize(); + if (!destPath.startsWith(basePath)) { sender.sendMessage(ChatColor.RED + "Not a valid destination filename"); return; } @@ -150,6 +150,7 @@ public static void downloadImage(@NotNull CommandSender sender, @NotNull String // Download file sender.sendMessage("Downloading file..."); + Files.createDirectories(destPath.getParent()); Files.copy(conn.getInputStream(), destPath); // Validate downloaded file diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index ad5702f..f59fcaf 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -134,7 +134,7 @@ public synchronized int size() { private synchronized void registerDirectory(@NotNull Path path, boolean isBase) { // Validate path if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { - LOGGER.warning("Cannot list files in \"" + path.toAbsolutePath() + "\" as it is not a valid directory"); + LOGGER.warning("Cannot list files in \"" + path + "\" as it is not a valid directory"); return; } @@ -159,7 +159,7 @@ private synchronized void registerDirectory(@NotNull Path path, boolean isBase) new WatchEvent.Modifier[]{ExtendedWatchEventModifier.FILE_TREE} : new WatchEvent.Modifier[0]; path.register(Objects.requireNonNull(watchService), events, modifiers); - LOGGER.fine("Started watching directory at \"" + path.toAbsolutePath() + "\""); + LOGGER.fine("Started watching directory at \"" + path + "\""); } catch (IOException | NullPointerException e) { LOGGER.severe("Failed to register directory", e); } @@ -173,7 +173,7 @@ private synchronized void registerDirectory(@NotNull Path path, boolean isBase) private synchronized void registerFile(@NotNull Path path) { // Validate path if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { - LOGGER.warning("Cannot register \"" + path.toAbsolutePath() + "\" as it is not a valid file"); + LOGGER.warning("Cannot register \"" + path + "\" as it is not a valid file"); return; } From 0d8dbaf9d7f1cf044dd7262707b949c1def9a01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Fri, 29 Dec 2023 17:50:22 +0100 Subject: [PATCH 18/30] Updated configuration path type - Updated ImageRenderer class - Updated CsvConfiguration class - Updated YamipaPlugin class --- .../java/io/josemmo/bukkit/plugin/YamipaPlugin.java | 2 +- .../josemmo/bukkit/plugin/renderer/ImageRenderer.java | 8 ++++---- .../josemmo/bukkit/plugin/utils/CsvConfiguration.java | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index bce1d2a..ea9061e 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -110,7 +110,7 @@ public void onEnable() { boolean animateImages = getConfig().getBoolean("animate-images", true); FakeImage.configure(animateImages); LOGGER.info(animateImages ? "Enabled image animation support" : "Image animation support is disabled"); - renderer = new ImageRenderer(basePath.resolve(dataPath).toString()); + renderer = new ImageRenderer(basePath.resolve(dataPath)); renderer.start(); // Create image item service diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java index 5d6d85c..bdeab40 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java @@ -18,7 +18,7 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -28,7 +28,7 @@ public class ImageRenderer implements Listener { private static final long SAVE_INTERVAL = 20L * 90; // In server ticks private static final Logger LOGGER = Logger.getLogger("ImageRenderer"); - private final String configPath; + private final Path configPath; private BukkitTask saveTask; private final AtomicBoolean hasConfigChanged = new AtomicBoolean(false); private final ConcurrentMap> images = new ConcurrentHashMap<>(); @@ -39,7 +39,7 @@ public class ImageRenderer implements Listener { * Class constructor * @param configPath Path to configuration file */ - public ImageRenderer(@NotNull String configPath) { + public ImageRenderer(@NotNull Path configPath) { this.configPath = configPath; } @@ -82,7 +82,7 @@ public void stop() { * Load configuration from disk */ private void loadConfig() { - if (!Files.isRegularFile(Paths.get(configPath))) { + if (!Files.isRegularFile(configPath)) { LOGGER.info("No placed fake images configuration file found"); return; } diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java b/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java index d0c1e29..32fdfe1 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java @@ -6,7 +6,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -42,8 +42,8 @@ public void addRow(@NotNull String[] row) { * @param path File path * @throws IOException if failed to read file */ - public void load(@NotNull String path) throws IOException { - try (Stream stream = Files.lines(Paths.get(path), CHARSET)) { + public void load(@NotNull Path path) throws IOException { + try (Stream stream = Files.lines(path, CHARSET)) { stream.forEach(line -> { line = line.trim(); @@ -68,8 +68,8 @@ public void load(@NotNull String path) throws IOException { * @param path File path * @throws IOException if failed to write file */ - public void save(@NotNull String path) throws IOException { - try (OutputStreamWriter writer = new OutputStreamWriter(Files.newOutputStream(Paths.get(path)), CHARSET)) { + public void save(@NotNull Path path) throws IOException { + try (OutputStreamWriter writer = new OutputStreamWriter(Files.newOutputStream(path), CHARSET)) { for (String[] row : getRows()) { writer.write(String.join(COLUMN_DELIMITER, row) + "\n"); } From 0926bcc942b8cb5bd02c1ac2dc9340b2359b31b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sun, 31 Dec 2023 14:10:53 +0100 Subject: [PATCH 19/30] Limit number of directories when downloading files - Updated ImageCommand class --- .../bukkit/plugin/commands/ImageCommand.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java index 2ff24ca..1f85f00 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java @@ -24,12 +24,14 @@ import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.*; public class ImageCommand { private static final int ITEMS_PER_PAGE = 9; + private static final int MAX_PATH_DEPTH = 10; private static final Logger LOGGER = Logger.getLogger("ImageCommand"); public static void showHelp(@NotNull CommandSender s, @NotNull String commandName) { @@ -92,10 +94,18 @@ public static void listImages(@NotNull CommandSender sender, int page) { public static void downloadImage(@NotNull CommandSender sender, @NotNull String rawUrl, @NotNull String filename) { YamipaPlugin plugin = YamipaPlugin.getInstance(); + Path basePath = plugin.getStorage().getBasePath(); + + // Resolve destination path + Path destPath; + try { + destPath = basePath.resolve(filename).normalize(); + } catch (InvalidPathException e) { + sender.sendMessage(ChatColor.RED + "Malformed destination path"); + return; + } // Validate destination file - Path basePath = plugin.getStorage().getBasePath(); - Path destPath = basePath.resolve(filename).normalize(); if (!destPath.startsWith(basePath)) { sender.sendMessage(ChatColor.RED + "Not a valid destination filename"); return; @@ -104,6 +114,11 @@ public static void downloadImage(@NotNull CommandSender sender, @NotNull String sender.sendMessage(ChatColor.RED + "There's already a file with that name"); return; } + int depth = destPath.getNameCount() - basePath.getNameCount(); + if (depth > MAX_PATH_DEPTH) { + sender.sendMessage(ChatColor.RED + "Destination path has too many directories"); + return; + } // Validate and fix remote URL URL url; From 4a303c1768d4730d2e7d9f2800297388021799b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sun, 31 Dec 2023 14:11:55 +0100 Subject: [PATCH 20/30] Updated compiler target version - Updated pom.xml --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3ce577d..a599296 100644 --- a/pom.xml +++ b/pom.xml @@ -9,8 +9,7 @@ 1.2.13 - 8 - 8 + 9 UTF-8 From d87a97bb08be925187401756507c258eced9adb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sun, 31 Dec 2023 14:21:45 +0100 Subject: [PATCH 21/30] Updated configuration file separator - Updated CsvConfiguration class --- .../java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java b/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java index 32fdfe1..bd809f1 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/CsvConfiguration.java @@ -18,7 +18,7 @@ */ public class CsvConfiguration { public static final Charset CHARSET = StandardCharsets.UTF_8; - public static final String COLUMN_DELIMITER = ";"; + public static final String COLUMN_DELIMITER = "\t"; private final List data = new ArrayList<>(); /** From 5c18ee6e209c89f940f8561fc1d9e23f96ff4b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sun, 31 Dec 2023 14:34:42 +0100 Subject: [PATCH 22/30] v1.3.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a599296..f984a54 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.josemmo.bukkit.plugin YamipaPlugin - 1.2.13 + 1.3.0-SNAPSHOT 9 From 3b5a27e71b23365dbf9584a4faf3c616a9a29231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sun, 31 Dec 2023 14:44:56 +0100 Subject: [PATCH 23/30] Revert "Updated compiler target version" This reverts commit 4a303c1768d4730d2e7d9f2800297388021799b7. --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f984a54..e139c09 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,8 @@ 1.3.0-SNAPSHOT - 9 + 8 + 8 UTF-8 From 85c075ad4d63eac298336322c5aa99e4926b568e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Wed, 3 Jan 2024 12:48:12 +0100 Subject: [PATCH 24/30] Improved animate images property - Updated FakeImage and ImageRenderer classes - Updated YamipaPlugin class --- .../josemmo/bukkit/plugin/YamipaPlugin.java | 5 ++-- .../bukkit/plugin/renderer/FakeImage.java | 23 ++++--------------- .../bukkit/plugin/renderer/ImageRenderer.java | 15 ++++++++++-- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index ea9061e..b4c8edd 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -108,9 +108,8 @@ public void onEnable() { // Create image renderer boolean animateImages = getConfig().getBoolean("animate-images", true); - FakeImage.configure(animateImages); LOGGER.info(animateImages ? "Enabled image animation support" : "Image animation support is disabled"); - renderer = new ImageRenderer(basePath.resolve(dataPath)); + renderer = new ImageRenderer(basePath.resolve(dataPath), animateImages); renderer.start(); // Create image item service @@ -139,7 +138,7 @@ public void onEnable() { return "0-9"; }; metrics = new Metrics(this, BSTATS_PLUGIN_ID); - metrics.addCustomChart(new SimplePie("animate_images", () -> FakeImage.isAnimationEnabled() ? "true" : "false")); + metrics.addCustomChart(new SimplePie("animate_images", () -> animateImages ? "true" : "false")); metrics.addCustomChart(new SimplePie("number_of_image_files", () -> toStats.apply(storage.size()))); metrics.addCustomChart(new SimplePie("number_of_placed_images", () -> toStats.apply(renderer.size()))); } diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java index 8b7cba4..b3b0e25 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java @@ -60,26 +60,9 @@ public class FakeImage extends FakeEntity { private int numOfSteps = -1; // Total number of animation steps // Animation task attributes - private static boolean ANIMATE_IMAGES = false; private @Nullable ScheduledFuture task; private int currentStep = -1; // Current animation step - /** - * Configure class - * @param animImages Animate images - */ - public static void configure(boolean animImages) { - ANIMATE_IMAGES = animImages; - } - - /** - * Is animation enabled - * @return Is animation enabled - */ - public static boolean isAnimationEnabled() { - return ANIMATE_IMAGES; - } - /** * Get image rotation from player eyesight * @param face Image block face @@ -382,8 +365,10 @@ private void load() { frames = newFrames; // Start animation task (if needed) - if (ANIMATE_IMAGES && task == null && hasFlag(FLAG_ANIMATABLE) && numOfSteps > 1) { - task = YamipaPlugin.getInstance().getScheduler().scheduleAtFixedRate( + YamipaPlugin plugin = YamipaPlugin.getInstance(); + boolean isAnimationEnabled = plugin.getRenderer().isAnimationEnabled(); + if (isAnimationEnabled && task == null && hasFlag(FLAG_ANIMATABLE) && numOfSteps > 1) { + task = plugin.getScheduler().scheduleAtFixedRate( this::nextStep, 0, delay*50L, diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java index bdeab40..1b2af5f 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java @@ -29,6 +29,7 @@ public class ImageRenderer implements Listener { private static final long SAVE_INTERVAL = 20L * 90; // In server ticks private static final Logger LOGGER = Logger.getLogger("ImageRenderer"); private final Path configPath; + private final boolean animateImages; private BukkitTask saveTask; private final AtomicBoolean hasConfigChanged = new AtomicBoolean(false); private final ConcurrentMap> images = new ConcurrentHashMap<>(); @@ -37,10 +38,20 @@ public class ImageRenderer implements Listener { /** * Class constructor - * @param configPath Path to configuration file + * @param configPath Path to configuration file + * @param animateImages Whether to animate images or not */ - public ImageRenderer(@NotNull Path configPath) { + public ImageRenderer(@NotNull Path configPath, boolean animateImages) { this.configPath = configPath; + this.animateImages = animateImages; + } + + /** + * Is animation enabled + * @return Is animation enabled + */ + public boolean isAnimationEnabled() { + return animateImages; } /** From 2a8cd059b8717a34977444c5768c4a79e84844b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Wed, 3 Jan 2024 13:31:56 +0100 Subject: [PATCH 25/30] Added support for custom max. image dimension - Updated image commands - Updated image renderer - Updated Permissions class - Updated YamipaPlugin class - Created ImageDimensionArgument class - Updated documentation - Updated dependencies --- README.md | 35 +++++++++++++--- pom.xml | 20 +++++++++- .../josemmo/bukkit/plugin/YamipaPlugin.java | 3 +- .../bukkit/plugin/commands/ImageCommand.java | 4 +- .../plugin/commands/ImageCommandBridge.java | 20 +++++----- .../arguments/ImageDimensionArgument.java | 26 ++++++++++++ .../bukkit/plugin/renderer/FakeImage.java | 29 ++++++++++++-- .../bukkit/plugin/renderer/ImageRenderer.java | 21 +++++++--- .../bukkit/plugin/utils/Permissions.java | 40 ++++++++++++++++++- src/main/resources/plugin.yml | 2 + 10 files changed, 169 insertions(+), 31 deletions(-) create mode 100644 src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageDimensionArgument.java diff --git a/README.md b/README.md index 8f770bf..920a389 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,15 @@ Yamipa is ready-to-go right out of the box. By default, it creates the following - `images.dat`: A file holding the list and properties (e.g. coordinates) of all placed images in your server. You shouldn't modify its contents. -You can change the default path of these files by creating a `config.yml` file in the plugin configuration directory: +You can change the path of these files by creating a `config.yml` file in the plugin configuration directory. +Here are the default configuration values if you don't specify them: ```yaml -verbose: false # Set to "true" to enable more verbose logging -animate-images: true # Set to "false" to disable GIF support -images-path: images # Path to images directory -cache-path: cache # Path to cache directory -data-path: images.dat # Path to placed images database file +verbose: false # Set to "true" to enable more verbose logging +animate-images: true # Set to "false" to disable GIF support +images-path: images # Path to images directory +cache-path: cache # Path to cache directory +data-path: images.dat # Path to placed images database file +max-image-dimension: 30 # Maximum width or height in blocks allowed in images ``` This library uses bStats to anonymously report the number of installs. If you don't like this, feel free to @@ -123,6 +125,27 @@ You can change which roles or players are granted these commands by using a perm such as [LuckPerms](https://luckperms.net/) or [GroupManager](https://elgarl.github.io/GroupManager/). Both these plugins have been tested to work with Yamipa, although any similar one should work just fine. +## Player variables +Some permission plugins like LuckPerms allow server operators to assign +[key-value pairs](https://luckperms.net/wiki/Meta-Commands) to entities as if they were permissions. +This is useful for granting different capabilities to different players or groups. + +Yamipa looks for the following variables which, if found, override the default configuration value that applies to all +players: + +| Variable (key) | Overrides | Description | +|:-----------------------------|:----------------------|:---------------------------------------------------------------------------------| +| `yamipa-max-image-dimension` | `max-image-dimension` | Maximum width or height of images and image items issued by this player or group | + +For example, if you want to limit the image size to 5x5 blocks just for the "test" player, you can run this command: +```sh +# Using LuckPerms +/lp user test meta set yamipa-max-image-dimension 5 + +# Using GroupManager +/manuaddv test yamipa-max-image-dimension 5 +``` + ## Protecting areas In large servers, letting your players place and remove images wherever they want might not be the most sensible idea. For those cases, Yamipa is compatible with other Bukkit plugins that allow creating and managing world areas. diff --git a/pom.xml b/pom.xml index e139c09..76e3d52 100644 --- a/pom.xml +++ b/pom.xml @@ -62,13 +62,29 @@ provided - + org.bstats bstats-bukkit 3.0.2 + + + net.luckperms + api + 5.4 + provided + + + + + com.github.ElgarL + groupmanager + 3.2 + provided + + com.sk89q.worldguard @@ -101,7 +117,7 @@ provided - + org.jetbrains annotations diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index b4c8edd..356e220 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -109,7 +109,8 @@ public void onEnable() { // Create image renderer boolean animateImages = getConfig().getBoolean("animate-images", true); LOGGER.info(animateImages ? "Enabled image animation support" : "Image animation support is disabled"); - renderer = new ImageRenderer(basePath.resolve(dataPath), animateImages); + int maxImageDimension = getConfig().getInt("max-image-dimension", 30); + renderer = new ImageRenderer(basePath.resolve(dataPath), animateImages, maxImageDimension); renderer.start(); // Create image item service diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java index 1f85f00..f93ffa9 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java @@ -200,7 +200,7 @@ public static void placeImage( player.sendMessage(ChatColor.RED + "The requested file is not a valid image"); return; } - final int finalHeight = (height == 0) ? FakeImage.getProportionalHeight(sizeInPixels, width) : height; + final int finalHeight = (height == 0) ? FakeImage.getProportionalHeight(sizeInPixels, player, width) : height; // Ask player where to place image SelectBlockTask task = new SelectBlockTask(player); @@ -461,7 +461,7 @@ public static void giveImageItems( return; } if (height == 0) { - height = FakeImage.getProportionalHeight(sizeInPixels, width); + height = FakeImage.getProportionalHeight(sizeInPixels, sender, width); } // Create item stack diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java index 73bfd5b..f9182db 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java @@ -120,8 +120,8 @@ public static void register(@NotNull YamipaPlugin plugin) { .withArgument(new OnlinePlayerArgument("player")) .withArgument(new ImageFileArgument("filename")) .withArgument(new IntegerArgument("amount", 1, 64)) - .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION)) - .withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION)) + .withArgument(new ImageDimensionArgument("width")) + .withArgument(new ImageDimensionArgument("height")) .withArgument(new ImageFlagsArgument("flags", FakeImage.DEFAULT_GIVE_FLAGS)) .executes((sender, args) -> { ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3], @@ -132,8 +132,8 @@ public static void register(@NotNull YamipaPlugin plugin) { .withArgument(new OnlinePlayerArgument("player")) .withArgument(new ImageFileArgument("filename")) .withArgument(new IntegerArgument("amount", 1, 64)) - .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION)) - .withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION)) + .withArgument(new ImageDimensionArgument("width")) + .withArgument(new ImageDimensionArgument("height")) .executes((sender, args) -> { ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3], (int) args[4], (int) args[5], FakeImage.DEFAULT_GIVE_FLAGS); @@ -143,7 +143,7 @@ public static void register(@NotNull YamipaPlugin plugin) { .withArgument(new OnlinePlayerArgument("player")) .withArgument(new ImageFileArgument("filename")) .withArgument(new IntegerArgument("amount", 1, 64)) - .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION)) + .withArgument(new ImageDimensionArgument("width")) .executes((sender, args) -> { ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3], (int) args[4], 0, FakeImage.DEFAULT_GIVE_FLAGS); @@ -167,8 +167,8 @@ public static void register(@NotNull YamipaPlugin plugin) { root.addSubcommand("place") .withPermission("yamipa.command.place", "yamipa.place") .withArgument(new ImageFileArgument("filename")) - .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION)) - .withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION)) + .withArgument(new ImageDimensionArgument("width")) + .withArgument(new ImageDimensionArgument("height")) .withArgument(new ImageFlagsArgument("flags", FakeImage.DEFAULT_PLACE_FLAGS)) .executesPlayer((player, args) -> { ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], (int) args[3], (int) args[4]); @@ -176,8 +176,8 @@ public static void register(@NotNull YamipaPlugin plugin) { root.addSubcommand("place") .withPermission("yamipa.command.place", "yamipa.place") .withArgument(new ImageFileArgument("filename")) - .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION)) - .withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION)) + .withArgument(new ImageDimensionArgument("width")) + .withArgument(new ImageDimensionArgument("height")) .executesPlayer((player, args) -> { ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], (int) args[3], FakeImage.DEFAULT_PLACE_FLAGS); @@ -185,7 +185,7 @@ public static void register(@NotNull YamipaPlugin plugin) { root.addSubcommand("place") .withPermission("yamipa.command.place", "yamipa.place") .withArgument(new ImageFileArgument("filename")) - .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION)) + .withArgument(new ImageDimensionArgument("width")) .executesPlayer((player, args) -> { ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], 0, FakeImage.DEFAULT_PLACE_FLAGS); diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageDimensionArgument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageDimensionArgument.java new file mode 100644 index 0000000..5434d60 --- /dev/null +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageDimensionArgument.java @@ -0,0 +1,26 @@ +package io.josemmo.bukkit.plugin.commands.arguments; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.josemmo.bukkit.plugin.renderer.FakeImage; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class ImageDimensionArgument extends IntegerArgument { + /** + * Dimension Argument constructor + * @param name Argument name + */ + public ImageDimensionArgument(@NotNull String name) { + super(name, 1); + } + + @Override + public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException { + int maxDimension = FakeImage.getMaxImageDimension(sender); + int value = (int) rawValue; + if (value > maxDimension) { + throw newException("Image cannot be larger than " + maxDimension + "x" + maxDimension); + } + return value; + } +} diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java index b3b0e25..8fe4adb 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java @@ -6,10 +6,12 @@ import io.josemmo.bukkit.plugin.storage.ImageFile; import io.josemmo.bukkit.plugin.utils.DirectionUtils; import io.josemmo.bukkit.plugin.utils.Logger; +import io.josemmo.bukkit.plugin.utils.Permissions; import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.Rotation; import org.bukkit.block.BlockFace; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; @@ -25,7 +27,6 @@ public class FakeImage extends FakeEntity { private static final Logger LOGGER = Logger.getLogger("FakeImage"); // Image constants - public static final int MAX_DIMENSION = 30; // In blocks public static final int MAX_STEPS = 500; // For animated images public static final int MIN_DELAY = 1; // Minimum step delay in 50ms intervals (50ms / 50ms) public static final int MAX_DELAY = 50; // Maximum step delay in 50ms intervals (5000ms / 50ms) @@ -89,16 +90,36 @@ public class FakeImage extends FakeEntity { } } + /** + * Get maximum image dimension + * @param sender Sender instance + * @return Maximum image dimension in blocks + */ + public static int getMaxImageDimension(@NotNull CommandSender sender) { + if (sender instanceof Player) { + String rawValue = Permissions.getVariable("yamipa-max-image-dimension", (Player) sender); + if (rawValue != null) { + try { + return Integer.parseInt(rawValue); + } catch (NumberFormatException __) { + LOGGER.warning("Max. image dimension for " + sender + " is not a valid integer: \"" + rawValue + "\""); + } + } + } + return YamipaPlugin.getInstance().getRenderer().getMaxImageDimension(); + } + /** * Get proportional height * @param sizeInPixels Image file dimension in pixels + * @param sender Sender instance * @param width Desired width in blocks - * @return Height in blocks (capped at FakeImage.MAX_DIMENSION) + * @return Height in blocks (capped at maximum image dimension for sender) */ - public static int getProportionalHeight(@NotNull Dimension sizeInPixels, int width) { + public static int getProportionalHeight(@NotNull Dimension sizeInPixels, @NotNull CommandSender sender, int width) { float imageRatio = (float) sizeInPixels.height / sizeInPixels.width; int height = Math.round(width * imageRatio); - height = Math.min(height, MAX_DIMENSION); + height = Math.min(height, getMaxImageDimension(sender)); return height; } diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java index 1b2af5f..7685460 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java +++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java @@ -30,6 +30,7 @@ public class ImageRenderer implements Listener { private static final Logger LOGGER = Logger.getLogger("ImageRenderer"); private final Path configPath; private final boolean animateImages; + private final int maxImageDimension; private BukkitTask saveTask; private final AtomicBoolean hasConfigChanged = new AtomicBoolean(false); private final ConcurrentMap> images = new ConcurrentHashMap<>(); @@ -38,12 +39,14 @@ public class ImageRenderer implements Listener { /** * Class constructor - * @param configPath Path to configuration file - * @param animateImages Whether to animate images or not + * @param configPath Path to configuration file + * @param animateImages Whether to animate images or not + * @param maxImageDimension Maximum image dimension in blocks */ - public ImageRenderer(@NotNull Path configPath, boolean animateImages) { + public ImageRenderer(@NotNull Path configPath, boolean animateImages, int maxImageDimension) { this.configPath = configPath; this.animateImages = animateImages; + this.maxImageDimension = maxImageDimension; } /** @@ -54,6 +57,14 @@ public boolean isAnimationEnabled() { return animateImages; } + /** + * Get maximum image dimension + * @return Maximum image dimension in blocks + */ + public int getMaxImageDimension() { + return maxImageDimension; + } + /** * Start instance */ @@ -118,8 +129,8 @@ private void loadConfig() { Location location = new Location(world, x, y, z); BlockFace face = BlockFace.valueOf(row[5]); Rotation rotation = Rotation.valueOf(row[6]); - int width = Math.min(FakeImage.MAX_DIMENSION, Math.abs(Integer.parseInt(row[7]))); - int height = Math.min(FakeImage.MAX_DIMENSION, Math.abs(Integer.parseInt(row[8]))); + int width = Math.abs(Integer.parseInt(row[7])); + int height = Math.abs(Integer.parseInt(row[8])); Date placedAt = (row.length > 9 && !row[9].isEmpty()) ? new Date(Long.parseLong(row[9])*1000L) : null; diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java b/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java index 515e3ef..58a0741 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java +++ b/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java @@ -16,6 +16,9 @@ import me.angeschossen.lands.api.land.LandWorld; import me.angeschossen.lands.api.player.LandPlayer; import me.ryanhamshire.GriefPrevention.GriefPrevention; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import org.anjocaido.groupmanager.GroupManager; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -27,12 +30,26 @@ public class Permissions { private static final Logger LOGGER = Logger.getLogger(); + private static @Nullable LuckPerms luckPerms; + private static @Nullable GroupManager groupManager; private static @Nullable WorldGuard worldGuard; private static @Nullable GriefPrevention griefPrevention; private static @Nullable TownyAPI townyApi; - private static @Nullable LandsIntegration landsApi = null; + private static @Nullable LandsIntegration landsApi; static { + try { + luckPerms = LuckPermsProvider.get(); + } catch (NoClassDefFoundError | IllegalStateException __) { + // LuckPerms is not installed + } + + try { + groupManager = (GroupManager) YamipaPlugin.getInstance().getServer().getPluginManager().getPlugin("GroupManager"); + } catch (NoClassDefFoundError __) { + // GroupManager is not installed + } + try { worldGuard = WorldGuard.getInstance(); } catch (NoClassDefFoundError __) { @@ -58,6 +75,27 @@ public class Permissions { } } + /** + * Get player variable + * @param variable Variable name (key) + * @param player Player instance + * @return Variable value or NULL if not found + */ + public static @Nullable String getVariable(@NotNull String variable, @NotNull Player player) { + if (luckPerms != null) { + return luckPerms.getPlayerAdapter(Player.class).getUser(player).getCachedData().getMetaData() + .getMetaValue(variable); + } + + if (groupManager != null) { + String rawValue = groupManager.getWorldsHolder().getWorldPermissions(player) + .getPermissionString(player.getName(), variable); + return rawValue.isEmpty() ? null : rawValue; + } + + return null; + } + /** * Can build at this block * @param player Player instance diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bb95eb3..70faeba 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -5,7 +5,9 @@ api-version: 1.16 depend: [ProtocolLib] softdepend: - GriefPrevention + - GroupManager - Hyperverse + - LuckPerms - Multiverse-Core - My_Worlds - Towny From d99e037f9abe0c7986460d34076245d0973d892d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Thu, 4 Jan 2024 18:27:52 +0100 Subject: [PATCH 26/30] Improved arguments builder - Added Argument#suggest() method - Updated child argument classes - Updated Command class --- .../bukkit/plugin/commands/Command.java | 7 ++++++- .../plugin/commands/arguments/Argument.java | 20 +++++++++++++++++- .../commands/arguments/ImageFileArgument.java | 21 ++++++------------- .../arguments/ImageFlagsArgument.java | 9 +++----- .../arguments/OnlinePlayerArgument.java | 15 +++---------- .../commands/arguments/PlacedByArgument.java | 15 +++---------- .../commands/arguments/WorldArgument.java | 19 +++++------------ 7 files changed, 45 insertions(+), 61 deletions(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/Command.java b/src/main/java/io/josemmo/bukkit/plugin/commands/Command.java index 1b457e1..bbcd959 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/Command.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/Command.java @@ -129,7 +129,12 @@ public Command(@NotNull String name) { // Chain command elements from the bottom-up if (argIndex < arguments.size()) { - parent.then(buildElement(arguments.get(argIndex).build(), argIndex+1)).executes(ctx -> { + Argument argument = arguments.get(argIndex); + ArgumentBuilder argumentBuilder = argument.build().suggests((ctx, builder) -> { + CommandSender sender = Internals.getBukkitSender(ctx.getSource()); + return argument.suggest(sender, builder); + }); + parent.then(buildElement(argumentBuilder, argIndex+1)).executes(ctx -> { CommandSender sender = Internals.getBukkitSender(ctx.getSource()); sender.sendMessage(ChatColor.RED + "Missing required arguments"); return 1; diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/Argument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/Argument.java index f5bda89..5a840fe 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/Argument.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/Argument.java @@ -4,8 +4,11 @@ import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; +import java.util.concurrent.CompletableFuture; public abstract class Argument { protected final String name; @@ -28,10 +31,20 @@ public Argument(@NotNull String name) { /** * Build argument - * @return Required Argument Builder instance + * @return Argument builder instance */ public abstract @NotNull RequiredArgumentBuilder build(); + /** + * Suggest argument values + * @param sender Command sender + * @param builder Suggestions builder instance + * @return Suggestions + */ + public @NotNull CompletableFuture suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) { + return builder.buildFuture(); + } + /** * Parse argument value * @param sender Command sender @@ -42,6 +55,11 @@ public Argument(@NotNull String name) { return rawValue; } + /** + * Create new syntax exception + * @param message Message to show + * @return Syntax exception + */ protected @NotNull CommandSyntaxException newException(@NotNull String message) { return new SimpleCommandExceptionType(new LiteralMessage(message)).create(); } diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFileArgument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFileArgument.java index a2dfd9f..263a5fb 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFileArgument.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFileArgument.java @@ -1,7 +1,6 @@ package io.josemmo.bukkit.plugin.commands.arguments; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; @@ -21,8 +20,11 @@ public ImageFileArgument(@NotNull String name) { } @Override - public @NotNull RequiredArgumentBuilder build() { - return super.build().suggests(this::getSuggestions); + public @NotNull CompletableFuture suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) { + for (String filename : YamipaPlugin.getInstance().getStorage().getAllFilenames()) { + builder.suggest(StringArgumentType.escapeIfRequired(filename)); + } + return builder.buildFuture(); } @Override @@ -33,15 +35,4 @@ public ImageFileArgument(@NotNull String name) { } return imageFile; } - - private @NotNull CompletableFuture getSuggestions( - @NotNull CommandContext ctx, - @NotNull SuggestionsBuilder builder - ) { - for (String filename : YamipaPlugin.getInstance().getStorage().getAllFilenames()) { - String suggestion = "\"" + filename.replaceAll("\"","\\\\\"") + "\""; - builder.suggest(suggestion); - } - return builder.buildFuture(); - } } diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFlagsArgument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFlagsArgument.java index cfe4576..48cd985 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFlagsArgument.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFlagsArgument.java @@ -2,7 +2,6 @@ import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.RequiredArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; @@ -28,13 +27,11 @@ public ImageFlagsArgument(@NotNull String name, int defaultFlags) { @Override public @NotNull RequiredArgumentBuilder build() { - return RequiredArgumentBuilder.argument(name, StringArgumentType.greedyString()).suggests(this::getSuggestions); + return RequiredArgumentBuilder.argument(name, StringArgumentType.greedyString()); } - private @NotNull CompletableFuture getSuggestions( - @NotNull CommandContext ctx, - @NotNull SuggestionsBuilder builder - ) { + @Override + public @NotNull CompletableFuture suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) { String input = builder.getRemaining().replaceAll("[^A-Z+\\-,]", ""); int lastIndex = Collections.max( Arrays.asList(input.lastIndexOf(","), input.lastIndexOf("+"), input.lastIndexOf("-")) diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/OnlinePlayerArgument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/OnlinePlayerArgument.java index 51350f2..6957534 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/OnlinePlayerArgument.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/OnlinePlayerArgument.java @@ -1,7 +1,5 @@ package io.josemmo.bukkit.plugin.commands.arguments; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; @@ -23,8 +21,9 @@ public OnlinePlayerArgument(@NotNull String name) { } @Override - public @NotNull RequiredArgumentBuilder build() { - return super.build().suggests(this::getSuggestions); + public @NotNull CompletableFuture suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) { + getAllowedValues().keySet().forEach(builder::suggest); + return builder.buildFuture(); } @Override @@ -36,14 +35,6 @@ public OnlinePlayerArgument(@NotNull String name) { return player; } - private @NotNull CompletableFuture getSuggestions( - @NotNull CommandContext ctx, - @NotNull SuggestionsBuilder builder - ) { - getAllowedValues().keySet().forEach(builder::suggest); - return builder.buildFuture(); - } - private @NotNull Map getAllowedValues() { Map values = new HashMap<>(); for (Player player : Bukkit.getOnlinePlayers()) { diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/PlacedByArgument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/PlacedByArgument.java index 45f87a8..a575bf1 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/PlacedByArgument.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/PlacedByArgument.java @@ -1,7 +1,5 @@ package io.josemmo.bukkit.plugin.commands.arguments; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; @@ -24,8 +22,9 @@ public PlacedByArgument(@NotNull String name) { } @Override - public @NotNull RequiredArgumentBuilder build() { - return super.build().suggests(this::getSuggestions); + public @NotNull CompletableFuture suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) { + getAllowedValues().keySet().forEach(builder::suggest); + return builder.buildFuture(); } @Override @@ -37,14 +36,6 @@ public PlacedByArgument(@NotNull String name) { return player; } - private @NotNull CompletableFuture getSuggestions( - @NotNull CommandContext ctx, - @NotNull SuggestionsBuilder builder - ) { - getAllowedValues().keySet().forEach(builder::suggest); - return builder.buildFuture(); - } - private @NotNull Map getAllowedValues() { Map values = new HashMap<>(); ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer(); diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/WorldArgument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/WorldArgument.java index 388cf95..523a79f 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/WorldArgument.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/WorldArgument.java @@ -1,7 +1,5 @@ package io.josemmo.bukkit.plugin.commands.arguments; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; @@ -21,8 +19,11 @@ public WorldArgument(@NotNull String name) { } @Override - public @NotNull RequiredArgumentBuilder build() { - return super.build().suggests(this::getSuggestions); + public @NotNull CompletableFuture suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) { + for (World world : Bukkit.getWorlds()) { + builder.suggest(world.getName()); + } + return builder.buildFuture(); } @Override @@ -33,14 +34,4 @@ public WorldArgument(@NotNull String name) { } return world; } - - private @NotNull CompletableFuture getSuggestions( - @NotNull CommandContext ctx, - @NotNull SuggestionsBuilder builder - ) { - for (World world : Bukkit.getWorlds()) { - builder.suggest(world.getName()); - } - return builder.buildFuture(); - } } From 9ea2cdb3788c0f890348bd44fb5bbe51c406ab07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Fri, 5 Jan 2024 14:24:13 +0100 Subject: [PATCH 27/30] Initial support for image files authorization - Updated ImageFileArgument and ImageCommand classes - Updated ImageStorage class - Updated YamipaPlugin class --- README.md | 40 ++++++++++- .../josemmo/bukkit/plugin/YamipaPlugin.java | 4 +- .../bukkit/plugin/commands/ImageCommand.java | 8 ++- .../commands/arguments/ImageFileArgument.java | 9 ++- .../bukkit/plugin/storage/ImageStorage.java | 68 +++++++++++++++++-- 5 files changed, 112 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 920a389..80e6e41 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,42 @@ animate-images: true # Set to "false" to disable GIF support images-path: images # Path to images directory cache-path: cache # Path to cache directory data-path: images.dat # Path to placed images database file +allowed-paths: null # Set to a RegExp to limit accessible images to players max-image-dimension: 30 # Maximum width or height in blocks allowed in images ``` +For more information on how to set a different `allowed-paths` or `max-image-dimension` value per player, see the +[Player variables](#player-variables) section. + +### Allowed paths +The variable `allowed-paths` is a regular expression that determines whether a player is allowed to see or download +an image file. If the desired path relative to the images directory matches this expression, then the player is allowed +to continue. + +If `allowed-paths` is an empty string ("") or null, then the player can read any image file or download to any path +inside the images directory. + +This regular expression must follow +[the syntax used by Java](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html). You can test your +expression beforehand using an online tool like [regex101](https://regex101.com/). +In addition, you can make use of the following special tokens: + +- `#player#`: Player name +- `#uuid#`: Player UUID (with hyphens) + +For example, if you want every player in your server to have their own subdirectory for storing files that only they +can access, plus a shared public directory, you can use the following `allowed-paths` value: +```regexp +^(private/#player#|public)/ +``` + +That way, the player "john" can see the image file at "private/john/something.jpg", but "jane" cannot. + +> **IMPORTANT!**\ +> Note that these restrictions **also apply to other entities** like NPCs, command blocks or the server console. +> However, special tokens will always match in non-player contexts (e.g., "#player#" will be interpreted as ".+"). + +### bStats This library uses bStats to anonymously report the number of installs. If you don't like this, feel free to disable it at any time by adding `enabled: false` to the [bStats configuration file](https://bstats.org/getting-started#:~:text=Disabling%20bStats) (it's ok, no hard feelings). @@ -133,9 +166,10 @@ This is useful for granting different capabilities to different players or group Yamipa looks for the following variables which, if found, override the default configuration value that applies to all players: -| Variable (key) | Overrides | Description | -|:-----------------------------|:----------------------|:---------------------------------------------------------------------------------| -| `yamipa-max-image-dimension` | `max-image-dimension` | Maximum width or height of images and image items issued by this player or group | +| Variable (key) | Overrides | Description | +|:-----------------------------|:----------------------|:----------------------------------------------------------------------------------| +| `yamipa-allowed-paths` | `allowed-paths` | Regular expression that limits which paths in the images directory are accessible | +| `yamipa-max-image-dimension` | `max-image-dimension` | Maximum width or height of images and image items issued by this player or group | For example, if you want to limit the image size to 5x5 blocks just for the "test" player, you can run this command: ```sh diff --git a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java index 356e220..934b36e 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java +++ b/src/main/java/io/josemmo/bukkit/plugin/YamipaPlugin.java @@ -96,9 +96,11 @@ public void onEnable() { String dataPath = getConfig().getString("data-path", "images.dat"); // Create image storage + String allowedPaths = getConfig().getString("allowed-paths", ""); storage = new ImageStorage( basePath.resolve(imagesPath).toAbsolutePath().normalize(), - basePath.resolve(cachePath).toAbsolutePath().normalize() + basePath.resolve(cachePath).toAbsolutePath().normalize(), + allowedPaths ); try { storage.start(); diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java index f93ffa9..7f5ab37 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java @@ -5,6 +5,7 @@ import io.josemmo.bukkit.plugin.renderer.ImageRenderer; import io.josemmo.bukkit.plugin.renderer.ItemService; import io.josemmo.bukkit.plugin.storage.ImageFile; +import io.josemmo.bukkit.plugin.storage.ImageStorage; import io.josemmo.bukkit.plugin.utils.Logger; import io.josemmo.bukkit.plugin.utils.Permissions; import io.josemmo.bukkit.plugin.utils.SelectBlockTask; @@ -65,8 +66,9 @@ public static void showHelp(@NotNull CommandSender s, @NotNull String commandNam } public static void listImages(@NotNull CommandSender sender, int page) { - String[] filenames = YamipaPlugin.getInstance().getStorage().getAllFilenames(); - int numOfImages = filenames.length; + ImageStorage storage = YamipaPlugin.getInstance().getStorage(); + List filenames = storage.getFilenames(sender); + int numOfImages = filenames.size(); // Are there any images available? if (numOfImages == 0) { @@ -88,7 +90,7 @@ public static void listImages(@NotNull CommandSender sender, int page) { sender.sendMessage("=== Page " + page + " out of " + maxPage + " ==="); } for (int i=firstImageIndex; i suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) { - for (String filename : YamipaPlugin.getInstance().getStorage().getAllFilenames()) { + for (String filename : YamipaPlugin.getInstance().getStorage().getFilenames(sender)) { builder.suggest(StringArgumentType.escapeIfRequired(filename)); } return builder.buildFuture(); @@ -29,8 +30,10 @@ public ImageFileArgument(@NotNull String name) { @Override public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException { - ImageFile imageFile = YamipaPlugin.getInstance().getStorage().get((String) rawValue); - if (imageFile == null) { + String filename = (String) rawValue; + ImageStorage storage = YamipaPlugin.getInstance().getStorage(); + ImageFile imageFile = storage.get(filename); + if (imageFile == null || !storage.isPathAllowed(filename, sender)) { throw newException("Image file does not exist"); } return imageFile; diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index f59fcaf..7729e93 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -2,12 +2,18 @@ import com.sun.nio.file.ExtendedWatchEventModifier; import io.josemmo.bukkit.plugin.utils.Logger; +import io.josemmo.bukkit.plugin.utils.Permissions; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.nio.file.*; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; /** * A service whose purpose is to keep track of all available image files in a given directory. @@ -24,17 +30,20 @@ public class ImageStorage { private final SortedMap files = new TreeMap<>(); private final Path basePath; private final Path cachePath; + private final String allowedPaths; private @Nullable WatchService watchService; private @Nullable Thread watchServiceThread; /** * Class constructor - * @param basePath Path to directory containing the images - * @param cachePath Path to directory containing the cached image maps + * @param basePath Path to directory containing the images + * @param cachePath Path to directory containing the cached image maps + * @param allowedPaths Allowed paths pattern */ - public ImageStorage(@NotNull Path basePath, @NotNull Path cachePath) { + public ImageStorage(@NotNull Path basePath, @NotNull Path cachePath, @NotNull String allowedPaths) { this.basePath = basePath; this.cachePath = cachePath; + this.allowedPaths = allowedPaths; } /** @@ -110,11 +119,56 @@ public synchronized int size() { } /** - * Get all image filenames - * @return Sorted array of filenames + * Get image filenames + * @param sender Sender instance to filter only allowed images + * @return Allowed images */ - public synchronized @NotNull String[] getAllFilenames() { - return files.keySet().toArray(new String[0]); + public synchronized @NotNull List getFilenames(@NotNull CommandSender sender) { + List response = new ArrayList<>(); + for (String filename : files.keySet()) { + if (isPathAllowed(filename, sender)) { + response.add(filename); + } + } + return response; + } + + /** + * Is path allowed + * @param path Path relative to {@link ImageStorage#basePath} + * @param sender Sender instance + * @return Whether sender is allowed to access path + */ + public boolean isPathAllowed(@NotNull String path, @NotNull CommandSender sender) { + // Find allowed paths pattern + String rawPattern = null; + if (sender instanceof Player) { + rawPattern = Permissions.getVariable("yamipa-allowed-paths", (Player) sender); + } + if (rawPattern == null) { + rawPattern = allowedPaths; + } + if (rawPattern.isEmpty()) { + return true; + } + + // Replace special tokens in pattern + if (sender instanceof Player) { + Player player = (Player) sender; + rawPattern = rawPattern.replaceAll("#player#", Matcher.quoteReplacement(Pattern.quote(player.getName()))); + rawPattern = rawPattern.replaceAll("#uuid#", player.getUniqueId().toString()); + } else { + rawPattern = rawPattern.replaceAll("#player#", ".+"); + rawPattern = rawPattern.replaceAll("#uuid#", ".+"); + } + + // Perform partial match against pattern + try { + return Pattern.compile(rawPattern).matcher(path).find(); + } catch (PatternSyntaxException __) { + LOGGER.warning("Invalid allowed paths pattern: " + rawPattern); + return false; + } } /** From 7cd2664ed4414a5904f4b4ddb964f44a75d9298d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Fri, 5 Jan 2024 14:33:24 +0100 Subject: [PATCH 28/30] Restricted download paths - Updated ImageCommand class - Updated ImageStorage class --- .../josemmo/bukkit/plugin/commands/ImageCommand.java | 7 ++++++- .../io/josemmo/bukkit/plugin/storage/ImageStorage.java | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java index 7f5ab37..7bb9cb1 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java +++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java @@ -96,7 +96,8 @@ public static void listImages(@NotNull CommandSender sender, int page) { public static void downloadImage(@NotNull CommandSender sender, @NotNull String rawUrl, @NotNull String filename) { YamipaPlugin plugin = YamipaPlugin.getInstance(); - Path basePath = plugin.getStorage().getBasePath(); + ImageStorage storage = plugin.getStorage(); + Path basePath = storage.getBasePath(); // Resolve destination path Path destPath; @@ -112,6 +113,10 @@ public static void downloadImage(@NotNull CommandSender sender, @NotNull String sender.sendMessage(ChatColor.RED + "Not a valid destination filename"); return; } + if (!storage.isPathAllowed(destPath, sender)) { + sender.sendMessage(ChatColor.RED + "Not allowed to download a file here"); + return; + } if (destPath.toFile().exists()) { sender.sendMessage(ChatColor.RED + "There's already a file with that name"); return; diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index 7729e93..643613d 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -133,6 +133,16 @@ public synchronized int size() { return response; } + /** + * Is path allowed + * @param path Path instance + * @param sender Sender instance + * @return Whether sender is allowed to access path + */ + public boolean isPathAllowed(@NotNull Path path, @NotNull CommandSender sender) { + return isPathAllowed(getFilename(path), sender); + } + /** * Is path allowed * @param path Path relative to {@link ImageStorage#basePath} From ffbacd296290ed1716fe9ba27f701640e774240a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Fri, 5 Jan 2024 15:27:14 +0100 Subject: [PATCH 29/30] Fixed exception when stopping storage - Updated ImageStorage class --- .../java/io/josemmo/bukkit/plugin/storage/ImageStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java index 643613d..e6085d4 100644 --- a/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java +++ b/src/main/java/io/josemmo/bukkit/plugin/storage/ImageStorage.java @@ -352,7 +352,7 @@ public void run() { } key.reset(); } - } catch (InterruptedException __) { + } catch (ClosedWatchServiceException | InterruptedException __) { // Silently ignore exception, this is expected when service shuts down } catch (NullPointerException e) { LOGGER.severe("Watch service was stopped before watcher thread", e); From b0f6330d6ea47654d52eee86577ea5cbccb9ad05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20Moreno?= Date: Sat, 6 Jan 2024 12:07:37 +0100 Subject: [PATCH 30/30] v1.3.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 76e3d52..f992871 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.josemmo.bukkit.plugin YamipaPlugin - 1.3.0-SNAPSHOT + 1.3.0 8