From 3e11654286a7d9c911b479a40916782919fcd099 Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 15:22:36 +0200 Subject: [PATCH 01/18] Refactor CoreScheduler and ScheduledTask to remove Bukkit dependencies and implement a standalone, thread-safe scheduling system --- .../gg/nextforge/scheduler/CoreScheduler.java | 216 ++++-------------- .../gg/nextforge/scheduler/ScheduledTask.java | 63 ++--- 2 files changed, 58 insertions(+), 221 deletions(-) diff --git a/src/main/java/gg/nextforge/scheduler/CoreScheduler.java b/src/main/java/gg/nextforge/scheduler/CoreScheduler.java index bc1f513..051c1b0 100644 --- a/src/main/java/gg/nextforge/scheduler/CoreScheduler.java +++ b/src/main/java/gg/nextforge/scheduler/CoreScheduler.java @@ -1,220 +1,94 @@ package gg.nextforge.scheduler; import lombok.Getter; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; -import org.bukkit.scheduler.BukkitTask; -import java.util.Collection; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; /** - * A wrapper around Bukkit's scheduler to simplify task management. - *

- * Features: - * - Static access for convenience (e.g., CoreScheduler.runLater()) - * - Automatic task cleanup - * - Cancel tokens for better task control - * - Avoids manual tracking of task IDs + * A standalone, thread-safe scheduler implementation. + * No Bukkit dependencies, suitable for general Java use. */ public class CoreScheduler { - private static CoreScheduler instance; // Singleton instance of CoreScheduler - private final Plugin plugin; // Plugin instance for scheduling tasks + private static CoreScheduler instance; + @Getter - private static final Map activeTasks = new ConcurrentHashMap<>(); // Active tasks map - private final AtomicInteger taskCounter = new AtomicInteger(0); // Counter for task IDs - - /** - * Constructs a CoreScheduler instance. - * - * @param plugin The plugin instance. - */ - public CoreScheduler(Plugin plugin) { - this.plugin = plugin; + private static final Map activeTasks = new ConcurrentHashMap<>(); + + private final ScheduledExecutorService executorService; + private final AtomicInteger taskCounter = new AtomicInteger(0); + + public CoreScheduler() { + this.executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); instance = this; } - /** - * Runs a task on the next tick. - * - * @param task The task to run. - * @return A ScheduledTask representing the scheduled task. - */ public static ScheduledTask run(Runnable task) { - return instance.runTask(task); + return instance.schedule(task, 0, false, false); } - // Static convenience methods for scheduling tasks - - /** - * Runs a task after a specified delay. - * - * @param task The task to run. - * @param delay The delay in ticks (20 ticks = 1 second). - * @return A ScheduledTask representing the scheduled task. - */ - public static ScheduledTask runLater(Runnable task, long delay) { - return instance.runTaskLater(task, delay); + public static ScheduledTask runLater(Runnable task, long delayTicks) { + return instance.schedule(task, delayTicks, false, false); } - /** - * Runs a task repeatedly with a specified delay and interval. - * - * @param task The task to run. - * @param delay The initial delay in ticks. - * @param period The interval between executions in ticks. - * @return A ScheduledTask representing the scheduled task. - */ - public static ScheduledTask runTimer(Runnable task, long delay, long period) { - return instance.runTaskTimer(task, delay, period); + public static ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks) { + return instance.scheduleRepeating(task, delayTicks, periodTicks, false); } - /** - * Runs a task asynchronously. - * - * @param task The task to run. - * @return A ScheduledTask representing the scheduled task. - */ public static ScheduledTask runAsync(Runnable task) { - return instance.runTaskAsync(task); + return instance.schedule(task, 0, true, false); } - /** - * Runs a task asynchronously after a specified delay. - * - * @param task The task to run. - * @param delay The delay in ticks. - * @return A ScheduledTask representing the scheduled task. - */ - public static ScheduledTask runAsyncLater(Runnable task, long delay) { - return instance.runTaskAsyncLater(task, delay); + public static ScheduledTask runAsyncLater(Runnable task, long delayTicks) { + return instance.schedule(task, delayTicks, true, false); } - /** - * Runs a task asynchronously and repeatedly. - * - * @param task The task to run. - * @param delay The initial delay in ticks. - * @param period The interval between executions in ticks. - * @return A ScheduledTask representing the scheduled task. - */ - public static ScheduledTask runAsyncTimer(Runnable task, long delay, long period) { - return instance.runTaskAsyncTimer(task, delay, period); + public static ScheduledTask runAsyncTimer(Runnable task, long delayTicks, long periodTicks) { + return instance.scheduleRepeating(task, delayTicks, periodTicks, true); } - /** - * Cleans up all active tasks. - * Should be called when the plugin is disabled. - */ public void shutdown() { activeTasks.values().forEach(ScheduledTask::cancel); activeTasks.clear(); + executorService.shutdownNow(); } - // Instance methods for scheduling tasks - - private ScheduledTask runTask(Runnable task) { - int id = taskCounter.incrementAndGet(); - ScheduledTask scheduled = new ScheduledTask(id); - - BukkitTask bukkitTask = Bukkit.getScheduler().runTask(plugin, () -> { - try { - task.run(); - } finally { - activeTasks.remove(id); - } - }); - - scheduled.setBukkitTask(bukkitTask); - activeTasks.put(id, scheduled); - return scheduled; - } - - private ScheduledTask runTaskLater(Runnable task, long delay) { - int id = taskCounter.incrementAndGet(); - ScheduledTask scheduled = new ScheduledTask(id); - - BukkitTask bukkitTask = Bukkit.getScheduler().runTaskLater(plugin, () -> { - try { - task.run(); - } finally { - activeTasks.remove(id); - } - }, delay); - - scheduled.setBukkitTask(bukkitTask); - activeTasks.put(id, scheduled); - return scheduled; - } - - private ScheduledTask runTaskTimer(Runnable task, long delay, long period) { - int id = taskCounter.incrementAndGet(); - ScheduledTask scheduled = new ScheduledTask(id); - - BukkitTask bukkitTask = Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period); - - scheduled.setBukkitTask(bukkitTask); - activeTasks.put(id, scheduled); - return scheduled; - } - - private ScheduledTask runTaskAsync(Runnable task) { - int id = taskCounter.incrementAndGet(); - ScheduledTask scheduled = new ScheduledTask(id); - - BukkitTask bukkitTask = Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - try { - task.run(); - } finally { - activeTasks.remove(id); - } - }); - - scheduled.setBukkitTask(bukkitTask); - activeTasks.put(id, scheduled); - return scheduled; - } - - private ScheduledTask runTaskAsyncLater(Runnable task, long delay) { + private ScheduledTask schedule(Runnable task, long delayTicks, boolean async, boolean repeating) { int id = taskCounter.incrementAndGet(); - ScheduledTask scheduled = new ScheduledTask(id); + ScheduledTask scheduledTask = new ScheduledTask(id); + long delayMillis = ticksToMillis(delayTicks); - BukkitTask bukkitTask = Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> { + ScheduledFuture future = executorService.schedule(() -> { try { task.run(); } finally { - activeTasks.remove(id); + if (!repeating) { + activeTasks.remove(id); + } } - }, delay); + }, delayMillis, TimeUnit.MILLISECONDS); - scheduled.setBukkitTask(bukkitTask); - activeTasks.put(id, scheduled); - return scheduled; + scheduledTask.setFuture(future); + activeTasks.put(id, scheduledTask); + return scheduledTask; } - private ScheduledTask runTaskAsyncTimer(Runnable task, long delay, long period) { + private ScheduledTask scheduleRepeating(Runnable task, long delayTicks, long periodTicks, boolean async) { int id = taskCounter.incrementAndGet(); - ScheduledTask scheduled = new ScheduledTask(id); + ScheduledTask scheduledTask = new ScheduledTask(id); + long delayMillis = ticksToMillis(delayTicks); + long periodMillis = ticksToMillis(periodTicks); - BukkitTask bukkitTask = Bukkit.getScheduler().runTaskTimerAsynchronously( - plugin, task, delay, period - ); + ScheduledFuture future = executorService.scheduleAtFixedRate(task, delayMillis, periodMillis, TimeUnit.MILLISECONDS); - scheduled.setBukkitTask(bukkitTask); - activeTasks.put(id, scheduled); - return scheduled; + scheduledTask.setFuture(future); + activeTasks.put(id, scheduledTask); + return scheduledTask; } - /** - * Runs a task on the main thread. - * Useful for accessing Bukkit API from asynchronous tasks. - * - * @param task The task to run. - */ - public void runSync(Runnable task) { - Bukkit.getScheduler().runTask(plugin, task); + private long ticksToMillis(long ticks) { + return ticks * 50L; // 20 ticks = 1 second } -} \ No newline at end of file +} diff --git a/src/main/java/gg/nextforge/scheduler/ScheduledTask.java b/src/main/java/gg/nextforge/scheduler/ScheduledTask.java index 957a8b7..485bfd6 100644 --- a/src/main/java/gg/nextforge/scheduler/ScheduledTask.java +++ b/src/main/java/gg/nextforge/scheduler/ScheduledTask.java @@ -1,62 +1,25 @@ package gg.nextforge.scheduler; -import org.bukkit.Bukkit; -import org.bukkit.scheduler.BukkitTask; +import lombok.Getter; +import lombok.Setter; + +import java.util.concurrent.ScheduledFuture; -/** - * Represents a scheduled task that can be cancelled. - */ public class ScheduledTask { - private final int id; // Unique ID for the task - private BukkitTask bukkitTask; // Bukkit task instance - private volatile boolean cancelled = false; // Flag indicating if the task is cancelled - /** - * Constructs a ScheduledTask instance. - * - * @param id The unique ID for the task. - */ - ScheduledTask(int id) { - this.id = id; - } + @Getter + private final int id; - /** - * Sets the BukkitTask instance for this ScheduledTask. - * - * @param task The BukkitTask instance. - */ - void setBukkitTask(BukkitTask task) { - this.bukkitTask = task; + @Setter + private ScheduledFuture future; + + public ScheduledTask(int id) { + this.id = id; } - /** - * Cancels this task. - * Can be called multiple times safely. - */ public void cancel() { - if (!cancelled && bukkitTask != null) { - bukkitTask.cancel(); - cancelled = true; - CoreScheduler.getActiveTasks().remove(id); + if (future != null) { + future.cancel(false); } } - - /** - * Checks if this task is still active. - * - * @return True if the task is running, false otherwise. - */ - public boolean isActive() { - return !cancelled && bukkitTask != null && - Bukkit.getScheduler().isCurrentlyRunning(bukkitTask.getTaskId()); - } - - /** - * Retrieves the task ID of this ScheduledTask. - * - * @return The task ID, or -1 if the task is not active. - */ - public int getTaskId() { - return bukkitTask != null ? bukkitTask.getTaskId() : -1; - } } From bc0c95067589c4e963c9c2c2d02803fc4bb75ff5 Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 15:26:46 +0200 Subject: [PATCH 02/18] Fix logging format in ConsoleHeader to remove unnecessary spaces --- .../java/gg/nextforge/console/ConsoleHeader.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/gg/nextforge/console/ConsoleHeader.java b/src/main/java/gg/nextforge/console/ConsoleHeader.java index b6d7731..328b01c 100644 --- a/src/main/java/gg/nextforge/console/ConsoleHeader.java +++ b/src/main/java/gg/nextforge/console/ConsoleHeader.java @@ -11,12 +11,12 @@ public static void send(NextForgePlugin nextForgePlugin) { String HEADER_LINE_4 = "██║╚██╗██║██╔══╝ ██╔██╗ ██║ ██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝ "; String HEADER_LINE_5 = "██║ ╚████║███████╗██╔╝ ██╗ ██║ ██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗"; String HEADER_LINE_6 = "╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝"; - nextForgePlugin.getSLF4JLogger().info(" {}", HEADER_LINE_1); - nextForgePlugin.getSLF4JLogger().info(" {}", HEADER_LINE_2); - nextForgePlugin.getSLF4JLogger().info(" {}", HEADER_LINE_3); - nextForgePlugin.getSLF4JLogger().info(" {}", HEADER_LINE_4); - nextForgePlugin.getSLF4JLogger().info(" {}", HEADER_LINE_5); - nextForgePlugin.getSLF4JLogger().info(" {}", HEADER_LINE_6); + nextForgePlugin.getSLF4JLogger().info("{}", HEADER_LINE_1); + nextForgePlugin.getSLF4JLogger().info("{}", HEADER_LINE_2); + nextForgePlugin.getSLF4JLogger().info("{}", HEADER_LINE_3); + nextForgePlugin.getSLF4JLogger().info("{}", HEADER_LINE_4); + nextForgePlugin.getSLF4JLogger().info("{}", HEADER_LINE_5); + nextForgePlugin.getSLF4JLogger().info("{}", HEADER_LINE_6); } } From b1f4cfa9eb06bc0f4992dfb75f5d2a2b50f22dcf Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 15:32:44 +0200 Subject: [PATCH 03/18] Add event handler registration methods with execution limits and timeouts --- .../java/gg/nextforge/event/EventBus.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/main/java/gg/nextforge/event/EventBus.java b/src/main/java/gg/nextforge/event/EventBus.java index a5a16e3..6a44b2b 100644 --- a/src/main/java/gg/nextforge/event/EventBus.java +++ b/src/main/java/gg/nextforge/event/EventBus.java @@ -1,5 +1,7 @@ package gg.nextforge.event; +import gg.nextforge.scheduler.CoreScheduler; +import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.bukkit.event.EventPriority; @@ -12,6 +14,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; @@ -95,6 +99,73 @@ public static EventSubscription on(Class eventClass, Consum }, EventPriority.NORMAL, false); } + /** + * Registers an event handler that will be executed a specified number of times. + * After the maximum number of executions, the handler will automatically unregister itself. + * + * @param eventClass The class of the event to listen for. + * @param handler The lambda function to handle the event. + * @param maxExecutions The maximum number of times the handler should be executed. + * @param The type of the event. + * @return An EventSubscription object for managing the handler. + */ + public static EventSubscription onTimes(Class eventClass, Consumer handler, int maxExecutions) { + AtomicInteger counter = new AtomicInteger(); + EventSubscription[] sub = new EventSubscription[1]; // dirty, but functional + sub[0] = on(eventClass, e -> { + handler.accept(e); + if (counter.incrementAndGet() >= maxExecutions) { + sub[0].unregister(); + } + }); + return sub[0]; + } + + /** + * Registers an event handler that will be executed after a specified timeout. + * The handler will automatically unregister itself after the timeout. + * + * @param eventClass The class of the event to listen for. + * @param handler The lambda function to handle the event. + * @param duration The duration to wait before executing the handler. + * @param unit The time unit of the duration. + * @param The type of the event. + * @return An EventSubscription object for managing the handler. + */ + public static EventSubscription onTimeout(Class eventClass, Consumer handler, long duration, TimeUnit unit) { + EventSubscription subscription = on(eventClass, handler); + CoreScheduler.runLater(subscription::unregister, unit.toMillis(duration) / 50); // Convert ms to ticks + return subscription; + } + + /** + * Registers an event handler that will be executed a specified number of times + * within a timeout period. After the maximum number of executions or the timeout, + * the handler will automatically unregister itself. + * + * @param eventClass The class of the event to listen for. + * @param handler The lambda function to handle the event. + * @param maxExecutions The maximum number of times the handler should be executed. + * @param timeout The timeout duration after which the handler will be unregistered. + * @param unit The time unit of the timeout duration. + * @param The type of the event. + * @return An EventSubscription object for managing the handler. + */ + public static EventSubscription onLimit(Class eventClass, Consumer handler, int maxExecutions, long timeout, TimeUnit unit) { + AtomicInteger counter = new AtomicInteger(); + EventSubscription[] sub = new EventSubscription[1]; + sub[0] = on(eventClass, e -> { + handler.accept(e); + if (counter.incrementAndGet() >= maxExecutions) { + sub[0].unregister(); + } + }); + CoreScheduler.runLater(() -> { + if (!sub[0].isCancelled()) sub[0].unregister(); + }, unit.toMillis(timeout) / 50); + return sub[0]; + } + /** * Fires a custom event. * @@ -210,6 +281,7 @@ public class EventSubscription { private final Class eventClass; private final RegisteredHandler handler; private final Listener listener; + @Getter private boolean cancelled = false; EventSubscription(Class eventClass, RegisteredHandler handler, Listener listener) { From 8f58b81094f1d973da066026a8f821dc4b848c4c Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 15:32:49 +0200 Subject: [PATCH 04/18] Remove plugin reference from CoreScheduler instantiation for improved modularity --- src/main/java/gg/nextforge/plugin/NextForgePlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/gg/nextforge/plugin/NextForgePlugin.java b/src/main/java/gg/nextforge/plugin/NextForgePlugin.java index 5efc684..5965f77 100644 --- a/src/main/java/gg/nextforge/plugin/NextForgePlugin.java +++ b/src/main/java/gg/nextforge/plugin/NextForgePlugin.java @@ -56,7 +56,7 @@ public void onEnable() { this.metrics = new Metrics(this, getMetricsId()); this.configManager = new ConfigManager(this); - this.scheduler = new CoreScheduler(this); + this.scheduler = new CoreScheduler(); this.commandManager = new CommandManager(this); this.textManager = new TextManager(this); this.npcManager = new NPCManager(this); From 526aab8eb75b97a737b72acd8659692326b92a4c Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 15:45:31 +0200 Subject: [PATCH 05/18] Add chat pagination management classes for improved user experience --- .../gg/nextforge/pagination/Pagination.java | 98 +++++++++++++++++++ .../pagination/chat/ChatPaginatorManager.java | 56 +++++++++++ .../chat/DefaultChatPageRenderer.java | 46 +++++++++ .../gg/nextforge/pagination/model/Page.java | 21 ++++ .../pagination/renderer/PageRenderer.java | 19 ++++ .../pagination/session/PaginationSession.java | 81 +++++++++++++++ .../gg/nextforge/utility/ChatPagination.java | 37 ------- 7 files changed, 321 insertions(+), 37 deletions(-) create mode 100644 src/main/java/gg/nextforge/pagination/Pagination.java create mode 100644 src/main/java/gg/nextforge/pagination/chat/ChatPaginatorManager.java create mode 100644 src/main/java/gg/nextforge/pagination/chat/DefaultChatPageRenderer.java create mode 100644 src/main/java/gg/nextforge/pagination/model/Page.java create mode 100644 src/main/java/gg/nextforge/pagination/renderer/PageRenderer.java create mode 100644 src/main/java/gg/nextforge/pagination/session/PaginationSession.java delete mode 100644 src/main/java/gg/nextforge/utility/ChatPagination.java diff --git a/src/main/java/gg/nextforge/pagination/Pagination.java b/src/main/java/gg/nextforge/pagination/Pagination.java new file mode 100644 index 0000000..0715a45 --- /dev/null +++ b/src/main/java/gg/nextforge/pagination/Pagination.java @@ -0,0 +1,98 @@ +package gg.nextforge.pagination; + +import gg.nextforge.pagination.model.Page; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A utility class for managing pagination of a list of items. + * This class provides methods to divide a list of items into pages + * and retrieve specific pages based on the index. + * + * @param the type of items to be paginated + */ +public class Pagination { + + // The list of items to be paginated + private final List items = new ArrayList<>(); + // The number of items per page + @Getter + private final int itemsPerPage; + + /** + * Constructs a Pagination object with the specified number of items per page. + * + * @param itemsPerPage the number of items per page; must be greater than 0 + * @throws IllegalArgumentException if itemsPerPage is less than or equal to 0 + */ + public Pagination(int itemsPerPage) { + if (itemsPerPage <= 0) throw new IllegalArgumentException("itemsPerPage must be > 0"); + this.itemsPerPage = itemsPerPage; + } + + /** + * Adds a single item to the pagination. + * + * @param item the item to add + */ + public void addItem(T item) { + items.add(item); + } + + /** + * Adds a list of items to the pagination. + * + * @param elements the list of items to add + */ + public void addAll(List elements) { + items.addAll(elements); + } + + /** + * Retrieves a specific page of items based on the given index. + * + * @param index the index of the page to retrieve (0-based) + * @return a {@link Page} object containing the items for the specified page + * or an empty page if the index is out of bounds + */ + public Page getPage(int index) { + int totalPages = getTotalPages(); + if (index < 0 || index >= totalPages) return new Page<>(index, totalPages, Collections.emptyList()); + + int start = index * itemsPerPage; + int end = Math.min(start + itemsPerPage, items.size()); + + List sublist = items.subList(start, end); + return new Page<>(index, totalPages, new ArrayList<>(sublist)); + } + + /** + * Calculates the total number of pages based on the number of items and items per page. + * + * @return the total number of pages + */ + public int getTotalPages() { + return (int) Math.ceil((double) items.size() / itemsPerPage); + } + + /** + * Returns the total number of items in the pagination. + * + * @return the total number of items + */ + public int getTotalItems() { + return items.size(); + } + + /** + * Checks if the pagination contains no items. + * + * @return true if there are no items, false otherwise + */ + public boolean isEmpty() { + return items.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/pagination/chat/ChatPaginatorManager.java b/src/main/java/gg/nextforge/pagination/chat/ChatPaginatorManager.java new file mode 100644 index 0000000..36588a1 --- /dev/null +++ b/src/main/java/gg/nextforge/pagination/chat/ChatPaginatorManager.java @@ -0,0 +1,56 @@ +package gg.nextforge.pagination.chat; + +import gg.nextforge.pagination.session.PaginationSession; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages chat pagination sessions for players. + * This class provides methods to set, get, remove, and check the existence of pagination sessions. + */ +public class ChatPaginatorManager { + + // A thread-safe map to store pagination sessions keyed by player UUID. + private static final Map> sessions = new ConcurrentHashMap<>(); + + /** + * Sets a pagination session for a player. + * + * @param playerId The UUID of the player. + * @param session The pagination session to set for the player. + */ + public static void setSession(UUID playerId, PaginationSession session) { + sessions.put(playerId, session); + } + + /** + * Retrieves the pagination session for a player. + * + * @param playerId The UUID of the player. + * @return The pagination session for the player, or null if no session exists. + */ + public static PaginationSession getSession(UUID playerId) { + return sessions.get(playerId); + } + + /** + * Removes the pagination session for a player. + * + * @param playerId The UUID of the player. + */ + public static void removeSession(UUID playerId) { + sessions.remove(playerId); + } + + /** + * Checks if a pagination session exists for a player. + * + * @param playerId The UUID of the player. + * @return true if a session exists for the player, false otherwise. + */ + public static boolean hasSession(UUID playerId) { + return sessions.containsKey(playerId); + } +} diff --git a/src/main/java/gg/nextforge/pagination/chat/DefaultChatPageRenderer.java b/src/main/java/gg/nextforge/pagination/chat/DefaultChatPageRenderer.java new file mode 100644 index 0000000..c405800 --- /dev/null +++ b/src/main/java/gg/nextforge/pagination/chat/DefaultChatPageRenderer.java @@ -0,0 +1,46 @@ +package gg.nextforge.pagination.chat; + +import gg.nextforge.pagination.model.Page; +import gg.nextforge.pagination.renderer.PageRenderer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation of {@link PageRenderer} for rendering chat pages. + * @param the type of items in the page + */ +public class DefaultChatPageRenderer implements PageRenderer { + + // Header and footer templates for the chat page + private final String header; + private final String footer; + + /** + * Creates a new DefaultChatPageRenderer with the specified header and footer. + * @param header the header template, which should contain placeholders for page index and total pages + * @param footer the footer template + */ + public DefaultChatPageRenderer(String header, String footer) { + this.header = header; + this.footer = footer; + } + + /** + * Renders the given page into a list of strings suitable for chat display. + * @param page the page to render + * @return a list of strings representing the rendered page + */ + @Override + public List render(Page page) { + List lines = new ArrayList<>(); + lines.add(String.format(header, page.index() + 1, page.totalPages())); + + for (T item : page.content()) { + lines.add(item.toString()); + } + + lines.add(footer); + return lines; + } +} diff --git a/src/main/java/gg/nextforge/pagination/model/Page.java b/src/main/java/gg/nextforge/pagination/model/Page.java new file mode 100644 index 0000000..67b34c5 --- /dev/null +++ b/src/main/java/gg/nextforge/pagination/model/Page.java @@ -0,0 +1,21 @@ +package gg.nextforge.pagination.model; + +import java.util.List; + +/** + * Represents a paginated result set. + * @param index the current page index (0-based) + * @param totalPages the total number of pages available + * @param content the list of items on the current page + * @param the type of items in the page + */ +public record Page(int index, int totalPages, List content) { + + /** + * Checks if the page is empty. + * @return true if the page has no content, false otherwise + */ + public boolean isEmpty() { + return content.isEmpty(); + } +} diff --git a/src/main/java/gg/nextforge/pagination/renderer/PageRenderer.java b/src/main/java/gg/nextforge/pagination/renderer/PageRenderer.java new file mode 100644 index 0000000..9a370b4 --- /dev/null +++ b/src/main/java/gg/nextforge/pagination/renderer/PageRenderer.java @@ -0,0 +1,19 @@ +package gg.nextforge.pagination.renderer; + +import gg.nextforge.pagination.model.Page; + +import java.util.List; + +/** + * Functional interface for rendering a page of items. + * @param the type of items in the page + */ +@FunctionalInterface +public interface PageRenderer { + /** + * Render a page of items. + * @param page the page to render + * @return a list of strings representing the rendered items + */ + List render(Page page); +} diff --git a/src/main/java/gg/nextforge/pagination/session/PaginationSession.java b/src/main/java/gg/nextforge/pagination/session/PaginationSession.java new file mode 100644 index 0000000..27b1873 --- /dev/null +++ b/src/main/java/gg/nextforge/pagination/session/PaginationSession.java @@ -0,0 +1,81 @@ +package gg.nextforge.pagination.session; + +import gg.nextforge.pagination.Pagination; +import gg.nextforge.pagination.model.Page; + +/** + * A session for managing pagination state. + * This class allows navigating through pages of a {@link Pagination} object. + * + * @param the type of items in the pagination + */ +public class PaginationSession { + + // The Pagination object that holds the items and pagination logic + private final Pagination pagination; + // The current page index, starting from 0 + private int currentPage = 0; + + /** + * Constructs a PaginationSession with the given Pagination object. + * + * @param pagination the Pagination object to manage + */ + public PaginationSession(Pagination pagination) { + this.pagination = pagination; + } + + /** + * Moves to the next page if available and returns it. + * + * @return the next {@link Page} of items + */ + public Page next() { + if (currentPage + 1 < pagination.getTotalPages()) currentPage++; + return pagination.getPage(currentPage); + } + + /** + * Moves to the previous page if available and returns it. + * + * @return the previous {@link Page} of items + */ + public Page previous() { + if (currentPage > 0) currentPage--; + return pagination.getPage(currentPage); + } + + /** + * Returns the current page without changing the state. + * + * @return the current {@link Page} of items + */ + public Page current() { + return pagination.getPage(currentPage); + } + + /** + * Checks if there is a next page available. + * + * @return true if a next page exists, false otherwise + */ + public boolean hasNext() { + return currentPage + 1 < pagination.getTotalPages(); + } + + /** + * Checks if there is a previous page available. + * + * @return true if a previous page exists, false otherwise + */ + public boolean hasPrevious() { + return currentPage > 0; + } + + /** + * Resets the session to the first page. + */ + public void reset() { + currentPage = 0; + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/utility/ChatPagination.java b/src/main/java/gg/nextforge/utility/ChatPagination.java deleted file mode 100644 index 653ea99..0000000 --- a/src/main/java/gg/nextforge/utility/ChatPagination.java +++ /dev/null @@ -1,37 +0,0 @@ -package gg.nextforge.utility; - -import lombok.Getter; - -import java.util.ArrayList; -import java.util.List; - -@Getter -public class ChatPagination { - - int currentPage; - int totalPages; - int linesPerPage; - List lines; - - public ChatPagination(int linesPerPage) { - this.lines = new ArrayList<>(); - this.linesPerPage = linesPerPage; - this.totalPages = 0; - this.currentPage = 0; - } - - public void addLine(String line) { - lines.add(line); - totalPages = (int) Math.ceil((double) lines.size() / linesPerPage); - } - - public List getPage(int page) { - if (page < 0 || page >= totalPages) { - throw new IndexOutOfBoundsException("Page number out of range"); - } - int start = page * linesPerPage; - int end = Math.min(start + linesPerPage, lines.size()); - return lines.subList(start, end); - } - -} From 1635d044438434f807c418241ac86c5476d4922d Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 18:59:44 +0200 Subject: [PATCH 06/18] Implement database management system with support for multiple databases and entity management --- build.gradle | 6 + .../nextforge/database/DatabaseManager.java | 112 ++++++++++++++++++ .../database/annotations/Column.java | 13 ++ .../nextforge/database/annotations/Index.java | 9 ++ .../nextforge/database/annotations/Table.java | 9 ++ .../bootstrap/EntityBootstrapper.java | 42 +++++++ .../database/entity/EntityManager.java | 70 +++++++++++ .../database/entity/EntityScanner.java | 34 ++++++ .../database/query/QueryBuilder.java | 49 ++++++++ .../database/registry/RepositoryRegistry.java | 47 ++++++++ .../database/repository/MongoRepository.java | 72 +++++++++++ .../database/repository/RedisRepository.java | 54 +++++++++ .../database/repository/Repository.java | 90 ++++++++++++++ .../repository/RepositoryProvider.java | 5 + .../database/transaction/UnitOfWork.java | 39 ++++++ 15 files changed, 651 insertions(+) create mode 100644 src/main/java/gg/nextforge/database/DatabaseManager.java create mode 100644 src/main/java/gg/nextforge/database/annotations/Column.java create mode 100644 src/main/java/gg/nextforge/database/annotations/Index.java create mode 100644 src/main/java/gg/nextforge/database/annotations/Table.java create mode 100644 src/main/java/gg/nextforge/database/bootstrap/EntityBootstrapper.java create mode 100644 src/main/java/gg/nextforge/database/entity/EntityManager.java create mode 100644 src/main/java/gg/nextforge/database/entity/EntityScanner.java create mode 100644 src/main/java/gg/nextforge/database/query/QueryBuilder.java create mode 100644 src/main/java/gg/nextforge/database/registry/RepositoryRegistry.java create mode 100644 src/main/java/gg/nextforge/database/repository/MongoRepository.java create mode 100644 src/main/java/gg/nextforge/database/repository/RedisRepository.java create mode 100644 src/main/java/gg/nextforge/database/repository/Repository.java create mode 100644 src/main/java/gg/nextforge/database/repository/RepositoryProvider.java create mode 100644 src/main/java/gg/nextforge/database/transaction/UnitOfWork.java diff --git a/build.gradle b/build.gradle index 65d0298..dabe516 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,12 @@ dependencies { implementation "org.mozilla:rhino:1.7.14" implementation "com.google.code.gson:gson:2.10.1" implementation "org.bstats:bstats-bukkit:3.0.2" + + implementation 'org.redisson:redisson:3.50.0' + implementation 'com.mysql:mysql-connector-j:9.3.0' + implementation 'org.xerial:sqlite-jdbc:3.50.3.0' + implementation 'com.h2database:h2:2.3.232' + implementation 'org.mongodb:mongodb-driver-sync:5.5.1' } subprojects { diff --git a/src/main/java/gg/nextforge/database/DatabaseManager.java b/src/main/java/gg/nextforge/database/DatabaseManager.java new file mode 100644 index 0000000..f992c70 --- /dev/null +++ b/src/main/java/gg/nextforge/database/DatabaseManager.java @@ -0,0 +1,112 @@ +package gg.nextforge.database; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import lombok.Getter; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DatabaseManager { + + private static final Logger LOGGER = Logger.getLogger("Database"); + + public enum DatabaseType { + MYSQL, H2, SQLITE, MONGODB, REDISSON + } + + @Getter + private final DatabaseType type; + private final String connectionString; + private Connection sqlConnection; + private MongoClient mongoClient; + private MongoDatabase mongoDatabase; + private RedissonClient redissonClient; + + public DatabaseManager(DatabaseType type, String connectionString) { + this.type = type; + this.connectionString = connectionString; + initialize(); + } + + private void initialize() { + try { + switch (type) { + case MYSQL: + case H2: + case SQLITE: + sqlConnection = DriverManager.getConnection(connectionString); + break; + case MONGODB: + String uri = (connectionString); + String databaseName = uri.substring(uri.lastIndexOf("/") + 1); + mongoClient = MongoClients.create(uri); + mongoDatabase = mongoClient.getDatabase(databaseName); + break; + case REDISSON: + Config config = new Config(); + config.useSingleServer().setAddress(connectionString); + redissonClient = Redisson.create(config); + break; + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to initialize database connection", e); + } + } + + public Connection getSqlConnection() { + return sqlConnection; + } + + public MongoDatabase getMongoDatabase() { + return mongoDatabase; + } + + public RedissonClient getRedissonClient() { + return redissonClient; + } + + public void close() { + try { + if (sqlConnection != null && !sqlConnection.isClosed()) { + sqlConnection.close(); + } + } catch (SQLException e) { + LOGGER.log(Level.WARNING, "Failed to close SQL connection", e); + } + + if (mongoClient != null) { + mongoClient.close(); + } + + if (redissonClient != null) { + redissonClient.shutdown(); + } + } + + public boolean isConnected() { + switch (type) { + case MYSQL: + case H2: + case SQLITE: + try { + return sqlConnection != null && !sqlConnection.isClosed(); + } catch (SQLException e) { + return false; + } + case MONGODB: + return mongoClient != null; + case REDISSON: + return redissonClient != null && !redissonClient.isShutdown(); + default: + return false; + } + } +} diff --git a/src/main/java/gg/nextforge/database/annotations/Column.java b/src/main/java/gg/nextforge/database/annotations/Column.java new file mode 100644 index 0000000..386c4f8 --- /dev/null +++ b/src/main/java/gg/nextforge/database/annotations/Column.java @@ -0,0 +1,13 @@ +package gg.nextforge.database.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Column { + String value(); + boolean id() default false; +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/database/annotations/Index.java b/src/main/java/gg/nextforge/database/annotations/Index.java new file mode 100644 index 0000000..e5cb18f --- /dev/null +++ b/src/main/java/gg/nextforge/database/annotations/Index.java @@ -0,0 +1,9 @@ +package gg.nextforge.database.annotations; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Index { + boolean unique() default false; +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/database/annotations/Table.java b/src/main/java/gg/nextforge/database/annotations/Table.java new file mode 100644 index 0000000..8517514 --- /dev/null +++ b/src/main/java/gg/nextforge/database/annotations/Table.java @@ -0,0 +1,9 @@ +package gg.nextforge.database.annotations; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Table { + String value(); +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/database/bootstrap/EntityBootstrapper.java b/src/main/java/gg/nextforge/database/bootstrap/EntityBootstrapper.java new file mode 100644 index 0000000..ae47d27 --- /dev/null +++ b/src/main/java/gg/nextforge/database/bootstrap/EntityBootstrapper.java @@ -0,0 +1,42 @@ +package gg.nextforge.database.bootstrap; + +import gg.nextforge.database.entity.EntityManager; +import gg.nextforge.database.entity.EntityScanner; + +import java.sql.Connection; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class EntityBootstrapper { + + private static final Logger LOGGER = Logger.getLogger("EntityBootstrapper"); + + private final Connection connection; + private final String basePackage; + + public EntityBootstrapper(Connection connection, String basePackage) { + this.connection = connection; + this.basePackage = basePackage; + } + + public void initialize() { + try { + EntityScanner scanner = new EntityScanner(); + List> entities = scanner.findEntities(basePackage); + EntityManager manager = new EntityManager(connection); + + for (Class entity : entities) { + try { + manager.createSchema(entity); + LOGGER.info("Schema created for entity: " + entity.getSimpleName()); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to create schema for " + entity.getSimpleName(), e); + } + } + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to bootstrap entities", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/database/entity/EntityManager.java b/src/main/java/gg/nextforge/database/entity/EntityManager.java new file mode 100644 index 0000000..2a65c45 --- /dev/null +++ b/src/main/java/gg/nextforge/database/entity/EntityManager.java @@ -0,0 +1,70 @@ +package gg.nextforge.database.entity; + +import gg.nextforge.database.annotations.Column; +import gg.nextforge.database.annotations.Table; + +import java.lang.reflect.Field; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +public class EntityManager { + + private final Connection connection; + + public EntityManager(Connection connection) { + this.connection = connection; + } + + public void createSchema(Class clazz) throws SQLException { + if (!clazz.isAnnotationPresent(Table.class)) { + throw new IllegalArgumentException("Class must be annotated with @Table"); + } + + Table tableAnnotation = clazz.getAnnotation(Table.class); + String tableName = tableAnnotation.value(); + + List columns = new ArrayList<>(); + String primaryKey = null; + + for (Field field : clazz.getDeclaredFields()) { + if (!field.isAnnotationPresent(Column.class)) continue; + + Column column = field.getAnnotation(Column.class); + String name = column.value(); + String type = mapJavaTypeToSql(field.getType()); + + columns.add(name + " " + type); + + if (column.id()) { + primaryKey = name; + } + } + + StringBuilder builder = new StringBuilder("CREATE TABLE IF NOT EXISTS ") + .append(tableName) + .append(" (") + .append(String.join(", ", columns)); + + if (primaryKey != null) { + builder.append(", PRIMARY KEY (").append(primaryKey).append(")"); + } + + builder.append(")"); + + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate(builder.toString()); + } + } + + private String mapJavaTypeToSql(Class type) { + if (type == String.class) return "VARCHAR(255)"; + if (type == int.class || type == Integer.class) return "INT"; + if (type == long.class || type == Long.class) return "BIGINT"; + if (type == boolean.class || type == Boolean.class) return "BOOLEAN"; + if (type == double.class || type == Double.class) return "DOUBLE"; + return "TEXT"; + } +} diff --git a/src/main/java/gg/nextforge/database/entity/EntityScanner.java b/src/main/java/gg/nextforge/database/entity/EntityScanner.java new file mode 100644 index 0000000..c331f76 --- /dev/null +++ b/src/main/java/gg/nextforge/database/entity/EntityScanner.java @@ -0,0 +1,34 @@ +package gg.nextforge.database.entity; + +import gg.nextforge.database.annotations.Table; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +public class EntityScanner { + + public List> findEntities(String basePackage) throws IOException, ClassNotFoundException { + List> classes = new ArrayList<>(); + String path = basePackage.replace('.', '/'); + Enumeration resources = Thread.currentThread().getContextClassLoader().getResources(path); + + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + File dir = new File(resource.getFile()); + for (File file : dir.listFiles()) { + if (file.getName().endsWith(".class")) { + String className = basePackage + '.' + file.getName().replace(".class", ""); + Class clazz = Class.forName(className); + if (clazz.isAnnotationPresent(Table.class)) { + classes.add(clazz); + } + } + } + } + return classes; + } +} diff --git a/src/main/java/gg/nextforge/database/query/QueryBuilder.java b/src/main/java/gg/nextforge/database/query/QueryBuilder.java new file mode 100644 index 0000000..6ebde99 --- /dev/null +++ b/src/main/java/gg/nextforge/database/query/QueryBuilder.java @@ -0,0 +1,49 @@ +package gg.nextforge.database.query; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +public class QueryBuilder { + + private final String table; + private final List where = new ArrayList<>(); + private final List params = new ArrayList<>(); + private String orderBy = ""; + private String limit = ""; + + public QueryBuilder(String table) { + this.table = table; + } + + public QueryBuilder where(String column, String operator, Object value) { + where.add(column + " " + operator + " ?"); + params.add(value); + return this; + } + + public QueryBuilder orderBy(String column, boolean ascending) { + this.orderBy = "ORDER BY " + column + (ascending ? " ASC" : " DESC"); + return this; + } + + public QueryBuilder limit(int limit) { + this.limit = "LIMIT " + limit; + return this; + } + + public String build() { + StringBuilder sb = new StringBuilder("SELECT * FROM ").append(table); + if (!where.isEmpty()) { + sb.append(" WHERE "); + sb.append(String.join(" AND ", where)); + } + if (!orderBy.isEmpty()) sb.append(" ").append(orderBy); + if (!limit.isEmpty()) sb.append(" ").append(limit); + return sb.toString(); + } + + public Object[] getParameters() { + return params.toArray(); + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/database/registry/RepositoryRegistry.java b/src/main/java/gg/nextforge/database/registry/RepositoryRegistry.java new file mode 100644 index 0000000..391b42d --- /dev/null +++ b/src/main/java/gg/nextforge/database/registry/RepositoryRegistry.java @@ -0,0 +1,47 @@ +package gg.nextforge.database.registry; + +import gg.nextforge.database.repository.Repository; +import gg.nextforge.database.repository.RepositoryProvider; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class RepositoryRegistry { + + private static final Logger LOGGER = Logger.getLogger("RepositoryRegistry"); + private final Map, RepositoryProvider> registry = new HashMap<>(); + + public void registerProvider(RepositoryProvider provider) { + Class entityType = provider.getEntityType(); + if (entityType != null) { + registry.put(entityType, provider); + LOGGER.info("Registered repository provider for: " + entityType.getSimpleName()); + } else { + LOGGER.warning("Repository provider has null entity type: " + provider.getClass().getSimpleName()); + } + } + + public RepositoryProvider getProvider(Class entityType) { + return (RepositoryProvider) registry.get(entityType); + } + + public Repository getSqlRepository(Class entityType) { + RepositoryProvider provider = registry.get(entityType); + if (provider instanceof Repository) { + return (Repository) provider; + } + return null; + } + + + public Set> getRegisteredTypes() { + return registry.keySet(); + } + + public void clear() { + registry.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/database/repository/MongoRepository.java b/src/main/java/gg/nextforge/database/repository/MongoRepository.java new file mode 100644 index 0000000..4b30462 --- /dev/null +++ b/src/main/java/gg/nextforge/database/repository/MongoRepository.java @@ -0,0 +1,72 @@ +package gg.nextforge.database.repository; + +import com.mongodb.client.MongoCollection; +import org.bson.Document; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public abstract class MongoRepository implements RepositoryProvider { + + protected final MongoCollection collection; + private final Class entityType; + + public MongoRepository(MongoCollection collection, Class entityType) { + this.collection = collection; + this.entityType = entityType; + } + + protected abstract T fromDocument(Document doc); + protected abstract Document toDocument(T entity); + + public CompletableFuture> findAllAsync() { + return CompletableFuture.supplyAsync(() -> { + List results = new ArrayList<>(); + for (Document doc : collection.find()) { + results.add(fromDocument(doc)); + } + return results; + }); + } + + public CompletableFuture findOneAsync(String key, Object value) { + return CompletableFuture.supplyAsync(() -> { + Document doc = collection.find(new Document(key, value)).first(); + return doc != null ? fromDocument(doc) : null; + }); + } + + public CompletableFuture insertOneAsync(T entity) { + return CompletableFuture.runAsync(() -> collection.insertOne(toDocument(entity))); + } + + public CompletableFuture deleteAsync(String key, Object value) { + return CompletableFuture.runAsync(() -> collection.deleteOne(new Document(key, value))); + } + + public CompletableFuture updateOneAsync(String key, Object value, T entity) { + return CompletableFuture.runAsync(() -> collection.updateOne( + new Document(key, value), new Document("$set", toDocument(entity)))); + } + + public CompletableFuture> findByFilterAsync(Document filter, Function mapper) { + return CompletableFuture.supplyAsync(() -> { + List results = new ArrayList<>(); + for (Document doc : collection.find(filter)) { + results.add(mapper.apply(doc)); + } + return results; + }); + } + + @Override + public Class getEntityType() { + return entityType; + } + + public MongoCollection getCollection() { + return collection; + } +} diff --git a/src/main/java/gg/nextforge/database/repository/RedisRepository.java b/src/main/java/gg/nextforge/database/repository/RedisRepository.java new file mode 100644 index 0000000..a8cc94c --- /dev/null +++ b/src/main/java/gg/nextforge/database/repository/RedisRepository.java @@ -0,0 +1,54 @@ +package gg.nextforge.database.repository; + +import org.redisson.api.RMap; +import org.redisson.api.RedissonClient; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public abstract class RedisRepository implements RepositoryProvider { + + protected final RedissonClient redisson; + protected final String mapName; + private final Class entityType; + + public RedisRepository(RedissonClient redisson, String mapName, Class entityType) { + this.redisson = redisson; + this.mapName = mapName; + this.entityType = entityType; + } + + protected RMap getMap() { + return redisson.getMap(mapName); + } + + public CompletableFuture saveAsync(K key, V value) { + return CompletableFuture.runAsync(() -> getMap().put(key, value)); + } + + public CompletableFuture findAsync(K key) { + return CompletableFuture.supplyAsync(() -> getMap().get(key)); + } + + public CompletableFuture> findAllAsync() { + return CompletableFuture.supplyAsync(() -> getMap().values().stream().collect(Collectors.toList())); + } + + public CompletableFuture deleteAsync(K key) { + return CompletableFuture.runAsync(() -> getMap().remove(key)); + } + + public CompletableFuture containsKeyAsync(K key) { + return CompletableFuture.supplyAsync(() -> getMap().containsKey(key)); + } + + public CompletableFuture clearAsync() { + return CompletableFuture.runAsync(() -> getMap().clear()); + } + + @Override + public Class getEntityType() { + return entityType; + } +} diff --git a/src/main/java/gg/nextforge/database/repository/Repository.java b/src/main/java/gg/nextforge/database/repository/Repository.java new file mode 100644 index 0000000..e789604 --- /dev/null +++ b/src/main/java/gg/nextforge/database/repository/Repository.java @@ -0,0 +1,90 @@ +package gg.nextforge.database.repository; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class Repository { + + private final Connection connection; + private final Logger logger = Logger.getLogger(getClass().getSimpleName()); + + public Repository(Connection connection) { + this.connection = connection; + } + + protected abstract String getTableName(); + protected abstract T map(ResultSet rs) throws SQLException; + + public CompletableFuture> findAllAsync() { + return CompletableFuture.supplyAsync(() -> { + List results = new ArrayList<>(); + try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM " + getTableName()); + ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + results.add(map(rs)); + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to fetch all from " + getTableName(), e); + } + return results; + }); + } + + public CompletableFuture> findByIdAsync(Object id, String idColumn) { + return CompletableFuture.supplyAsync(() -> { + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT * FROM " + getTableName() + " WHERE " + idColumn + " = ?")) { + stmt.setObject(1, id); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return Optional.of(map(rs)); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to fetch by id from " + getTableName(), e); + } + return Optional.empty(); + }); + } + + public CompletableFuture executeUpdateAsync(String sql, Object... params) { + return CompletableFuture.runAsync(() -> { + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + for (int i = 0; i < params.length; i++) { + stmt.setObject(i + 1, params[i]); + } + stmt.executeUpdate(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to execute update", e); + } + }); + } + + public CompletableFuture> executeQueryAsync(String sql, Function mapper, Object... params) { + return CompletableFuture.supplyAsync(() -> { + List results = new ArrayList<>(); + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + for (int i = 0; i < params.length; i++) { + stmt.setObject(i + 1, params[i]); + } + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + results.add(mapper.apply(rs)); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to execute query", e); + } + return results; + }); + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/database/repository/RepositoryProvider.java b/src/main/java/gg/nextforge/database/repository/RepositoryProvider.java new file mode 100644 index 0000000..02f98ef --- /dev/null +++ b/src/main/java/gg/nextforge/database/repository/RepositoryProvider.java @@ -0,0 +1,5 @@ +package gg.nextforge.database.repository; + +public interface RepositoryProvider { + Class getEntityType(); +} diff --git a/src/main/java/gg/nextforge/database/transaction/UnitOfWork.java b/src/main/java/gg/nextforge/database/transaction/UnitOfWork.java new file mode 100644 index 0000000..3ebd0ec --- /dev/null +++ b/src/main/java/gg/nextforge/database/transaction/UnitOfWork.java @@ -0,0 +1,39 @@ +package gg.nextforge.database.transaction; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class UnitOfWork { + + private static final Logger LOGGER = Logger.getLogger("UnitOfWork"); + + private final Connection connection; + + public UnitOfWork(Connection connection) { + this.connection = connection; + } + + public void execute(Consumer action) { + try { + connection.setAutoCommit(false); + action.accept(connection); + connection.commit(); + } catch (Exception e) { + try { + connection.rollback(); + } catch (SQLException rollbackEx) { + LOGGER.log(Level.SEVERE, "Failed to rollback transaction", rollbackEx); + } + LOGGER.log(Level.SEVERE, "Transaction failed", e); + } finally { + try { + connection.setAutoCommit(true); + } catch (SQLException e) { + LOGGER.log(Level.SEVERE, "Failed to reset auto-commit", e); + } + } + } +} \ No newline at end of file From 46c2383844eca2e5ec03a9887ea3d69068a711ac Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 19:00:04 +0200 Subject: [PATCH 07/18] Bump version to 3.0-SNAPSHOT in build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dabe516..05786dc 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { } group = 'gg.nextforge' -version = '2.0' +version = '3.0-SNAPSHOT' repositories { mavenCentral() From 4da0a207b11e33ece6eaa23e16d347d36d4ec7b3 Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 19:34:13 +0200 Subject: [PATCH 08/18] Add UI components and layout management for enhanced user interface --- .../gg/nextforge/plugin/NextForgePlugin.java | 13 +++++ src/main/java/gg/nextforge/ui/UIManager.java | 28 ++++++++++ .../gg/nextforge/ui/action/ActionContext.java | 54 ++++++++++++++++++ .../gg/nextforge/ui/action/ClickAction.java | 21 +++++++ .../gg/nextforge/ui/action/HoverAction.java | 20 +++++++ .../ui/builder/InventoryBuilder.java | 56 +++++++++++++++++++ .../gg/nextforge/ui/component/UIButton.java | 31 ++++++++++ .../nextforge/ui/component/UIComponent.java | 13 +++++ .../gg/nextforge/ui/component/UIItemView.java | 33 +++++++++++ .../gg/nextforge/ui/component/UIPanel.java | 49 ++++++++++++++++ .../nextforge/ui/component/UIPlaceholder.java | 39 +++++++++++++ .../nextforge/ui/context/PlayerUIContext.java | 52 +++++++++++++++++ .../ui/context/UIContextManager.java | 37 ++++++++++++ .../nextforge/ui/inventory/InventoryUI.java | 43 ++++++++++++++ .../nextforge/ui/inventory/NavigationBar.java | 50 +++++++++++++++++ .../ui/inventory/StaticInventory.java | 40 +++++++++++++ .../gg/nextforge/ui/layout/BorderLayout.java | 38 +++++++++++++ .../gg/nextforge/ui/layout/GridLayout.java | 40 +++++++++++++ .../nextforge/ui/layout/PaginationLayout.java | 43 ++++++++++++++ .../java/gg/nextforge/ui/layout/UILayout.java | 19 +++++++ .../gg/nextforge/ui/support/AnvilInputUI.java | 55 ++++++++++++++++++ .../gg/nextforge/ui/support/ChatPromptUI.java | 47 ++++++++++++++++ .../gg/nextforge/ui/support/HotbarUI.java | 54 ++++++++++++++++++ 23 files changed, 875 insertions(+) create mode 100644 src/main/java/gg/nextforge/ui/UIManager.java create mode 100644 src/main/java/gg/nextforge/ui/action/ActionContext.java create mode 100644 src/main/java/gg/nextforge/ui/action/ClickAction.java create mode 100644 src/main/java/gg/nextforge/ui/action/HoverAction.java create mode 100644 src/main/java/gg/nextforge/ui/builder/InventoryBuilder.java create mode 100644 src/main/java/gg/nextforge/ui/component/UIButton.java create mode 100644 src/main/java/gg/nextforge/ui/component/UIComponent.java create mode 100644 src/main/java/gg/nextforge/ui/component/UIItemView.java create mode 100644 src/main/java/gg/nextforge/ui/component/UIPanel.java create mode 100644 src/main/java/gg/nextforge/ui/component/UIPlaceholder.java create mode 100644 src/main/java/gg/nextforge/ui/context/PlayerUIContext.java create mode 100644 src/main/java/gg/nextforge/ui/context/UIContextManager.java create mode 100644 src/main/java/gg/nextforge/ui/inventory/InventoryUI.java create mode 100644 src/main/java/gg/nextforge/ui/inventory/NavigationBar.java create mode 100644 src/main/java/gg/nextforge/ui/inventory/StaticInventory.java create mode 100644 src/main/java/gg/nextforge/ui/layout/BorderLayout.java create mode 100644 src/main/java/gg/nextforge/ui/layout/GridLayout.java create mode 100644 src/main/java/gg/nextforge/ui/layout/PaginationLayout.java create mode 100644 src/main/java/gg/nextforge/ui/layout/UILayout.java create mode 100644 src/main/java/gg/nextforge/ui/support/AnvilInputUI.java create mode 100644 src/main/java/gg/nextforge/ui/support/ChatPromptUI.java create mode 100644 src/main/java/gg/nextforge/ui/support/HotbarUI.java diff --git a/src/main/java/gg/nextforge/plugin/NextForgePlugin.java b/src/main/java/gg/nextforge/plugin/NextForgePlugin.java index 5965f77..dae31a9 100644 --- a/src/main/java/gg/nextforge/plugin/NextForgePlugin.java +++ b/src/main/java/gg/nextforge/plugin/NextForgePlugin.java @@ -3,10 +3,12 @@ import gg.nextforge.NextCorePlugin; import gg.nextforge.command.CommandManager; import gg.nextforge.config.ConfigManager; +import gg.nextforge.database.DatabaseManager; import gg.nextforge.npc.NPCManager; import gg.nextforge.protocol.ProtocolManager; import gg.nextforge.scheduler.CoreScheduler; import gg.nextforge.text.TextManager; +import gg.nextforge.ui.UIManager; import lombok.Getter; import org.bstats.bukkit.Metrics; import org.bukkit.plugin.Plugin; @@ -26,6 +28,8 @@ public abstract class NextForgePlugin extends JavaPlugin { TextManager textManager; NPCManager npcManager; ProtocolManager protocolManager; + UIManager uiManager; + DatabaseManager databaseManager; Metrics metrics; public abstract int getMetricsId(); @@ -61,6 +65,9 @@ public void onEnable() { this.textManager = new TextManager(this); this.npcManager = new NPCManager(this); this.protocolManager = new ProtocolManager(this); + this.uiManager = new UIManager(); + + this.uiManager.init(this); boolean isReload = getServer().getPluginManager().isPluginEnabled("NextForge"); enable(isReload); @@ -68,6 +75,12 @@ public void onEnable() { @Override public void onDisable() { + if (instance != this) { + getLogger().warning("Plugin instance mismatch! This should not happen."); + return; + } + instance = null; + this.uiManager.shutdown(); disable(); } } diff --git a/src/main/java/gg/nextforge/ui/UIManager.java b/src/main/java/gg/nextforge/ui/UIManager.java new file mode 100644 index 0000000..9e1b1e9 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/UIManager.java @@ -0,0 +1,28 @@ +package gg.nextforge.ui; + +import gg.nextforge.ui.context.UIContextManager; +import gg.nextforge.ui.support.ChatPromptUI; +import gg.nextforge.ui.support.HotbarUI; +import org.bukkit.plugin.Plugin; + +/** + * Central UI bootstrapper and manager. + */ +public class UIManager { + + private Plugin plugin; + + public void init(Plugin pl) { + plugin = pl; + + ChatPromptUI.registerListener(plugin); + HotbarUI.registerListener(plugin); + + // Additional: Register Inventory click listeners etc. + // e.g. InventoryInteractionHandler.register(plugin); + } + + public void shutdown() { + UIContextManager.clearAll(); + } +} diff --git a/src/main/java/gg/nextforge/ui/action/ActionContext.java b/src/main/java/gg/nextforge/ui/action/ActionContext.java new file mode 100644 index 0000000..9df5d12 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/action/ActionContext.java @@ -0,0 +1,54 @@ +package gg.nextforge.ui.action; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; + +/** + * Encapsulates relevant data for a UI action event. + */ +public class ActionContext { + + private final Player player; + private final int slot; + private final ClickType clickType; + + /** + * Constructs an ActionContext with the specified player, slot, and click type. + * + * @param player The player who performed the action. + * @param slot The inventory slot that was interacted with. + * @param clickType The type of click that triggered the action. + */ + public ActionContext(Player player, int slot, ClickType clickType) { + this.player = player; + this.slot = slot; + this.clickType = clickType; + } + + /** + * Gets the player who performed the action. + * + * @return The player associated with this action context. + */ + public Player getPlayer() { + return player; + } + + /** + * Gets the inventory slot that was interacted with. + * + * @return The slot number associated with this action context. + */ + public int getSlot() { + return slot; + } + + /** + * Gets the type of click that triggered the action. + * + * @return The ClickType associated with this action context. + */ + public ClickType getClickType() { + return clickType; + } +} diff --git a/src/main/java/gg/nextforge/ui/action/ClickAction.java b/src/main/java/gg/nextforge/ui/action/ClickAction.java new file mode 100644 index 0000000..6933449 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/action/ClickAction.java @@ -0,0 +1,21 @@ +package gg.nextforge.ui.action; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; + +/** + * Represents an action that can be executed when a player clicks on a UI element. + * This interface defines a single method to handle click events with the player and the type of click. + */ +@FunctionalInterface +public interface ClickAction { + + /** + * Executes the action when a player clicks on a UI element. + * + * @param player the player who clicked + * @param clickType the type of click (e.g., LEFT, RIGHT) + */ + void execute(Player player, ClickType clickType); +} + diff --git a/src/main/java/gg/nextforge/ui/action/HoverAction.java b/src/main/java/gg/nextforge/ui/action/HoverAction.java new file mode 100644 index 0000000..78fbe59 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/action/HoverAction.java @@ -0,0 +1,20 @@ +package gg.nextforge.ui.action; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +/** + * Optional interface for implementing item hover behavior, + * such as tooltip updates based on player state or localization. + */ +@FunctionalInterface +public interface HoverAction { + + /** + * Render the item stack for the given player. + * @param viewer the player viewing the item + * @return the ItemStack to be displayed + */ + ItemStack render(Player viewer); +} + diff --git a/src/main/java/gg/nextforge/ui/builder/InventoryBuilder.java b/src/main/java/gg/nextforge/ui/builder/InventoryBuilder.java new file mode 100644 index 0000000..bdb7619 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/builder/InventoryBuilder.java @@ -0,0 +1,56 @@ +package gg.nextforge.ui.builder; + +import gg.nextforge.ui.component.UIComponent; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public class InventoryBuilder { + + private final int size; + private String title = "Inventory"; + private final Map components = new HashMap<>(); + private Function dynamicTitle = null; + + public InventoryBuilder(int size) { + this.size = size; + } + + public InventoryBuilder title(String title) { + this.title = title; + return this; + } + + public InventoryBuilder title(Function titleFunction) { + this.dynamicTitle = titleFunction; + return this; + } + + public InventoryBuilder set(int slot, UIComponent component) { + components.put(slot, component); + return this; + } + + public Inventory build(Player player) { + String finalTitle = dynamicTitle != null ? dynamicTitle.apply(player) : title; + Inventory inventory = Bukkit.createInventory(null, size, finalTitle); + + for (Map.Entry entry : components.entrySet()) { + inventory.setItem(entry.getKey(), entry.getValue().render(player)); + } + + return inventory; + } + + public void open(Player player) { + player.openInventory(build(player)); + } + + public Map getComponents() { + return components; + } +} diff --git a/src/main/java/gg/nextforge/ui/component/UIButton.java b/src/main/java/gg/nextforge/ui/component/UIButton.java new file mode 100644 index 0000000..64876eb --- /dev/null +++ b/src/main/java/gg/nextforge/ui/component/UIButton.java @@ -0,0 +1,31 @@ +package gg.nextforge.ui.component; + +import gg.nextforge.ui.action.ClickAction; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; + +import java.util.function.Supplier; + +public class UIButton implements UIComponent { + + private final Supplier itemSupplier; + private final ClickAction clickAction; + + public UIButton(Supplier itemSupplier, ClickAction clickAction) { + this.itemSupplier = itemSupplier; + this.clickAction = clickAction; + } + + @Override + public ItemStack render(Player player) { + return itemSupplier.get(); + } + + @Override + public void onClick(Player player, ClickType clickType) { + if (clickAction != null) { + clickAction.execute(player, clickType); + } + } +} diff --git a/src/main/java/gg/nextforge/ui/component/UIComponent.java b/src/main/java/gg/nextforge/ui/component/UIComponent.java new file mode 100644 index 0000000..506b51d --- /dev/null +++ b/src/main/java/gg/nextforge/ui/component/UIComponent.java @@ -0,0 +1,13 @@ +package gg.nextforge.ui.component; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; + +public interface UIComponent { + + ItemStack render(Player viewer); + + void onClick(Player viewer, ClickType click); + +} diff --git a/src/main/java/gg/nextforge/ui/component/UIItemView.java b/src/main/java/gg/nextforge/ui/component/UIItemView.java new file mode 100644 index 0000000..fdf4764 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/component/UIItemView.java @@ -0,0 +1,33 @@ +package gg.nextforge.ui.component; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; + +import java.util.function.Function; + +/** + * A non-interactive UI component used to display static or dynamic items. + */ +public class UIItemView implements UIComponent { + + private final Function itemProvider; + + public UIItemView(ItemStack item) { + this(p -> item); + } + + public UIItemView(Function itemProvider) { + this.itemProvider = itemProvider; + } + + @Override + public ItemStack render(Player viewer) { + return itemProvider.apply(viewer); + } + + @Override + public void onClick(Player viewer, ClickType click) { + // Intentionally no-op (passive component) + } +} diff --git a/src/main/java/gg/nextforge/ui/component/UIPanel.java b/src/main/java/gg/nextforge/ui/component/UIPanel.java new file mode 100644 index 0000000..593d36e --- /dev/null +++ b/src/main/java/gg/nextforge/ui/component/UIPanel.java @@ -0,0 +1,49 @@ +package gg.nextforge.ui.component; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.Map; + +/** + * A composite component that holds other components in specific slots. + */ +public class UIPanel implements UIComponent { + + private final Map children = new HashMap<>(); + + public void setComponent(int slot, UIComponent component) { + children.put(slot, component); + } + + public UIComponent getComponent(int slot) { + return children.get(slot); + } + + public Map getChildren() { + return children; + } + + @Override + public ItemStack render(Player viewer) { + return null; // Panels don't render to a single item + } + + @Override + public void onClick(Player viewer, ClickType click) { + // Panels don't handle clicks directly + } + + public void renderToInventory(org.bukkit.inventory.Inventory inventory, Player viewer) { + children.forEach((slot, component) -> { + inventory.setItem(slot, component.render(viewer)); + }); + } + + public void handleClick(Player player, int slot, ClickType clickType) { + UIComponent comp = children.get(slot); + if (comp != null) comp.onClick(player, clickType); + } +} diff --git a/src/main/java/gg/nextforge/ui/component/UIPlaceholder.java b/src/main/java/gg/nextforge/ui/component/UIPlaceholder.java new file mode 100644 index 0000000..0146697 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/component/UIPlaceholder.java @@ -0,0 +1,39 @@ +package gg.nextforge.ui.component; + +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +/** + * A static, non-interactive filler slot. + */ +public class UIPlaceholder implements UIComponent { + + private final ItemStack placeholder; + + public UIPlaceholder(Material material) { + this.placeholder = new ItemStack(material); + ItemMeta meta = this.placeholder.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(" ")); + this.placeholder.setItemMeta(meta); + } + } + + public UIPlaceholder(ItemStack customItem) { + this.placeholder = customItem; + } + + @Override + public ItemStack render(Player viewer) { + return placeholder; + } + + @Override + public void onClick(Player viewer, ClickType click) { + // No-op + } +} diff --git a/src/main/java/gg/nextforge/ui/context/PlayerUIContext.java b/src/main/java/gg/nextforge/ui/context/PlayerUIContext.java new file mode 100644 index 0000000..4cd40d9 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/context/PlayerUIContext.java @@ -0,0 +1,52 @@ +package gg.nextforge.ui.context; + +import gg.nextforge.ui.component.UIComponent; +import gg.nextforge.ui.inventory.InventoryUI; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Represents the UI state/session for a player. + */ +public class PlayerUIContext { + + private final UUID playerId; + private InventoryUI currentInventory; + private final Map activeComponents = new HashMap<>(); + + public PlayerUIContext(Player player) { + this.playerId = player.getUniqueId(); + } + + public UUID getPlayerId() { + return playerId; + } + + public InventoryUI getCurrentInventory() { + return currentInventory; + } + + public void setCurrentInventory(InventoryUI inventory) { + this.currentInventory = inventory; + } + + public void setComponent(int slot, UIComponent component) { + activeComponents.put(slot, component); + } + + public UIComponent getComponent(int slot) { + return activeComponents.get(slot); + } + + public Map getActiveComponents() { + return activeComponents; + } + + public void clear() { + currentInventory = null; + activeComponents.clear(); + } +} diff --git a/src/main/java/gg/nextforge/ui/context/UIContextManager.java b/src/main/java/gg/nextforge/ui/context/UIContextManager.java new file mode 100644 index 0000000..3275f00 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/context/UIContextManager.java @@ -0,0 +1,37 @@ +package gg.nextforge.ui.context; + +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages active UI contexts for all players. + */ +public class UIContextManager { + + private static final Map contextMap = new ConcurrentHashMap<>(); + + public static PlayerUIContext get(Player player) { + return contextMap.computeIfAbsent(player.getUniqueId(), id -> new PlayerUIContext(player)); + } + + public static void clear(Player player) { + PlayerUIContext ctx = contextMap.remove(player.getUniqueId()); + if (ctx != null) ctx.clear(); + } + + public static void clearAll() { + contextMap.values().forEach(PlayerUIContext::clear); + contextMap.clear(); + } + + public static boolean has(Player player) { + return contextMap.containsKey(player.getUniqueId()); + } + + public static Map getAll() { + return contextMap; + } +} diff --git a/src/main/java/gg/nextforge/ui/inventory/InventoryUI.java b/src/main/java/gg/nextforge/ui/inventory/InventoryUI.java new file mode 100644 index 0000000..fb02e90 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/inventory/InventoryUI.java @@ -0,0 +1,43 @@ +package gg.nextforge.ui.inventory; + +import gg.nextforge.ui.context.UIContextManager; +import gg.nextforge.ui.component.UIComponent; +import gg.nextforge.ui.context.PlayerUIContext; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +public abstract class InventoryUI { + + private final int size; + private final String title; + + public InventoryUI(int size, String title) { + this.size = size; + this.title = title; + } + + /** + * Called when the UI is opened. Use this to fill items. + */ + protected abstract void render(Player player, Inventory inventory, PlayerUIContext context); + + public void open(Player player) { + Inventory inventory = Bukkit.createInventory(null, size, MiniMessage.miniMessage().deserialize(title)); + PlayerUIContext context = UIContextManager.get(player); + context.setCurrentInventory(this); + context.getActiveComponents().clear(); + + render(player, inventory, context); + player.openInventory(inventory); + } + + public int getSize() { + return size; + } + + public String getTitle() { + return title; + } +} diff --git a/src/main/java/gg/nextforge/ui/inventory/NavigationBar.java b/src/main/java/gg/nextforge/ui/inventory/NavigationBar.java new file mode 100644 index 0000000..ff51236 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/inventory/NavigationBar.java @@ -0,0 +1,50 @@ +package gg.nextforge.ui.inventory; + +import gg.nextforge.ui.component.UIButton; +import gg.nextforge.ui.component.UIComponent; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +/** + * Utility class to build navigation bars (prev/next/back). + */ +public class NavigationBar { + + private final Map buttons = new HashMap<>(); + + public NavigationBar backButton(int slot, BiConsumer handler) { + buttons.put(slot, new UIButton(() -> createItem(Material.ARROW, "§cBack"), handler)); + return this; + } + + public NavigationBar nextButton(int slot, BiConsumer handler) { + buttons.put(slot, new UIButton(() -> createItem(Material.ARROW, "§aNext"), handler)); + return this; + } + + public NavigationBar closeButton(int slot, BiConsumer handler) { + buttons.put(slot, new UIButton(() -> createItem(Material.BARRIER, "§4Close"), handler)); + return this; + } + + public Map build() { + return buttons; + } + + private ItemStack createItem(Material material, String name) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(name); + item.setItemMeta(meta); + } + return item; + } +} diff --git a/src/main/java/gg/nextforge/ui/inventory/StaticInventory.java b/src/main/java/gg/nextforge/ui/inventory/StaticInventory.java new file mode 100644 index 0000000..706d669 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/inventory/StaticInventory.java @@ -0,0 +1,40 @@ +package gg.nextforge.ui.inventory; + +import gg.nextforge.ui.component.UIComponent; +import gg.nextforge.ui.context.PlayerUIContext; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +import java.util.HashMap; +import java.util.Map; + +/** + * A simple inventory with fixed slot-to-component mapping. + */ +public class StaticInventory extends InventoryUI { + + private final Map components = new HashMap<>(); + + public StaticInventory(int size, String title) { + super(size, title); + } + + public StaticInventory set(int slot, UIComponent component) { + components.put(slot, component); + return this; + } + + public Map getComponents() { + return components; + } + + @Override + protected void render(Player player, Inventory inventory, PlayerUIContext context) { + for (Map.Entry entry : components.entrySet()) { + int slot = entry.getKey(); + UIComponent comp = entry.getValue(); + inventory.setItem(slot, comp.render(player)); + context.setComponent(slot, comp); + } + } +} diff --git a/src/main/java/gg/nextforge/ui/layout/BorderLayout.java b/src/main/java/gg/nextforge/ui/layout/BorderLayout.java new file mode 100644 index 0000000..d4b323f --- /dev/null +++ b/src/main/java/gg/nextforge/ui/layout/BorderLayout.java @@ -0,0 +1,38 @@ +package gg.nextforge.ui.layout; + +import gg.nextforge.ui.component.UIComponent; + +import java.util.HashMap; +import java.util.Map; + +/** + * Simple layout to fill border slots in an inventory. + */ +public class BorderLayout implements UILayout { + + private final int rows; + private final UIComponent borderComponent; + + public BorderLayout(int rows, UIComponent borderComponent) { + this.rows = rows; + this.borderComponent = borderComponent; + } + + @Override + public Map layout() { + Map result = new HashMap<>(); + int columns = 9; + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < columns; col++) { + boolean isBorder = row == 0 || row == rows - 1 || col == 0 || col == columns - 1; + if (isBorder) { + int slot = row * columns + col; + result.put(slot, borderComponent); + } + } + } + + return result; + } +} diff --git a/src/main/java/gg/nextforge/ui/layout/GridLayout.java b/src/main/java/gg/nextforge/ui/layout/GridLayout.java new file mode 100644 index 0000000..937dca1 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/layout/GridLayout.java @@ -0,0 +1,40 @@ +package gg.nextforge.ui.layout; + +import gg.nextforge.ui.component.UIComponent; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Places components in a grid layout row-by-row. + */ +public class GridLayout implements UILayout { + + private final int startSlot; + private final int columns; + private final List components; + + public GridLayout(int startSlot, int columns, List components) { + this.startSlot = startSlot; + this.columns = columns; + this.components = components; + } + + @Override + public Map layout() { + Map result = new HashMap<>(); + int index = 0; + + for (UIComponent component : components) { + int row = index / columns; + int col = index % columns; + int slot = startSlot + row * 9 + col; + + result.put(slot, component); + index++; + } + + return result; + } +} diff --git a/src/main/java/gg/nextforge/ui/layout/PaginationLayout.java b/src/main/java/gg/nextforge/ui/layout/PaginationLayout.java new file mode 100644 index 0000000..9de90b9 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/layout/PaginationLayout.java @@ -0,0 +1,43 @@ +package gg.nextforge.ui.layout; + +import gg.nextforge.ui.component.UIComponent; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Layout for paginating components across multiple pages. + */ +public class PaginationLayout implements UILayout { + + private final List slotPositions; + private final List components; + private final int page; + + public PaginationLayout(List slotPositions, List components, int page) { + this.slotPositions = slotPositions; + this.components = components; + this.page = page; + } + + @Override + public Map layout() { + Map result = new HashMap<>(); + + int itemsPerPage = slotPositions.size(); + int startIndex = page * itemsPerPage; + + for (int i = 0; i < itemsPerPage; i++) { + int componentIndex = startIndex + i; + if (componentIndex >= components.size()) break; + + int slot = slotPositions.get(i); + UIComponent component = components.get(componentIndex); + + result.put(slot, component); + } + + return result; + } +} diff --git a/src/main/java/gg/nextforge/ui/layout/UILayout.java b/src/main/java/gg/nextforge/ui/layout/UILayout.java new file mode 100644 index 0000000..f1a0747 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/layout/UILayout.java @@ -0,0 +1,19 @@ +package gg.nextforge.ui.layout; + +import gg.nextforge.ui.component.UIComponent; + +import java.util.Map; + +/** + * Defines a contract for arranging components into a slot map. + */ +public interface UILayout { + + /** + * Produces a map of slot index to UIComponent. + * + * @return layout mapping + */ + Map layout(); + +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/ui/support/AnvilInputUI.java b/src/main/java/gg/nextforge/ui/support/AnvilInputUI.java new file mode 100644 index 0000000..a345965 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/support/AnvilInputUI.java @@ -0,0 +1,55 @@ +package gg.nextforge.ui.support; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; + +import java.util.function.BiConsumer; + +public class AnvilInputUI { + + private final Plugin plugin; + private final String title; + private final ItemStack insertItem; + private final BiConsumer onComplete; + + public AnvilInputUI(Plugin plugin, String title, ItemStack insertItem, BiConsumer onComplete) { + this.plugin = plugin; + this.title = title; + this.insertItem = insertItem; + this.onComplete = onComplete; + } + + public void open(Player player) { + Inventory inventory = Bukkit.createInventory(null, InventoryType.ANVIL, title); + if (insertItem != null) { + inventory.setItem(0, insertItem); + } + + player.openInventory(inventory); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + Bukkit.getPluginManager().registerEvents(new org.bukkit.event.Listener() { + @org.bukkit.event.EventHandler + public void onClose(InventoryCloseEvent event) { + if (event.getPlayer().equals(player) && event.getInventory().getType() == InventoryType.ANVIL) { + String text = null; + if (event.getInventory() instanceof AnvilInventory anvil) { + ItemStack result = anvil.getItem(2); + if (result != null && result.hasItemMeta() && result.getItemMeta().hasDisplayName()) { + text = result.getItemMeta().getDisplayName(); + } + } + onComplete.accept(player, text); + InventoryCloseEvent.getHandlerList().unregister(this); + } + } + }, plugin); + }, 2L); + } +} diff --git a/src/main/java/gg/nextforge/ui/support/ChatPromptUI.java b/src/main/java/gg/nextforge/ui/support/ChatPromptUI.java new file mode 100644 index 0000000..9b63290 --- /dev/null +++ b/src/main/java/gg/nextforge/ui/support/ChatPromptUI.java @@ -0,0 +1,47 @@ +package gg.nextforge.ui.support; + +import io.papermc.paper.event.player.AsyncChatEvent; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.Plugin; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; + +public class ChatPromptUI { + + private static final Map> prompts = new ConcurrentHashMap<>(); + + public static void requestInput(Plugin plugin, Player player, String prompt, BiConsumer onInput) { + player.sendMessage(prompt); + prompts.put(player.getUniqueId(), onInput); + } + + public static void registerListener(Plugin plugin) { + Bukkit.getPluginManager().registerEvents(new org.bukkit.event.Listener() { + + @org.bukkit.event.EventHandler + public void onChat(AsyncChatEvent event) { + Player player = event.getPlayer(); + if (prompts.containsKey(player.getUniqueId())) { + event.setCancelled(true); + BiConsumer handler = prompts.remove(player.getUniqueId()); + if (handler != null) { + handler.accept(player, PlainTextComponentSerializer.plainText().serialize(event.message())); + } + } + } + + @org.bukkit.event.EventHandler + public void onQuit(PlayerQuitEvent event) { + prompts.remove(event.getPlayer().getUniqueId()); + } + + }, plugin); + } +} diff --git a/src/main/java/gg/nextforge/ui/support/HotbarUI.java b/src/main/java/gg/nextforge/ui/support/HotbarUI.java new file mode 100644 index 0000000..7183e2a --- /dev/null +++ b/src/main/java/gg/nextforge/ui/support/HotbarUI.java @@ -0,0 +1,54 @@ +package gg.nextforge.ui.support; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerItemHeldEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiConsumer; + +/** + * Provides temporary hotbar-based interaction. + */ +public class HotbarUI { + + private static final Map>> hotbarActions = new HashMap<>(); + + public static void setSlot(Player player, int slot, ItemStack item, BiConsumer action) { + player.getInventory().setItem(slot, item); + hotbarActions.computeIfAbsent(player.getUniqueId(), id -> new HashMap<>()) + .put(slot, action); + } + + public static void registerListener(Plugin plugin) { + Bukkit.getPluginManager().registerEvents(new org.bukkit.event.Listener() { + + @org.bukkit.event.EventHandler + public void onInteract(PlayerInteractEvent event) { + Player player = event.getPlayer(); + int slot = player.getInventory().getHeldItemSlot(); + Map> actions = hotbarActions.get(player.getUniqueId()); + if (actions != null && actions.containsKey(slot)) { + event.setCancelled(true); + ItemStack item = player.getInventory().getItem(slot); + actions.get(slot).accept(player, item); + } + } + + @org.bukkit.event.EventHandler + public void onSwitch(PlayerItemHeldEvent event) { + // Optional: clear hotbar on slot switch if desired + } + + }, plugin); + } + + public static void clear(Player player) { + hotbarActions.remove(player.getUniqueId()); + } +} From cffbed7293917f24720263e0b3c32a7bf406950e Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 19:34:40 +0200 Subject: [PATCH 09/18] Bump version to 3.0.1-SNAPSHOT in build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 05786dc..0a90cd0 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { } group = 'gg.nextforge' -version = '3.0-SNAPSHOT' +version = '3.0.1-SNAPSHOT' repositories { mavenCentral() From af283de617abb99d973d28f93bdba12d59e63161 Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 19:41:47 +0200 Subject: [PATCH 10/18] Add DataStore and DataStoreRegistry classes for managing key-value pairs with optional expiration --- .../gg/nextforge/datastore/DataStore.java | 87 +++++++++++++++++++ .../datastore/DataStoreRegistry.java | 41 +++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/main/java/gg/nextforge/datastore/DataStore.java create mode 100644 src/main/java/gg/nextforge/datastore/DataStoreRegistry.java diff --git a/src/main/java/gg/nextforge/datastore/DataStore.java b/src/main/java/gg/nextforge/datastore/DataStore.java new file mode 100644 index 0000000..a8d8a4f --- /dev/null +++ b/src/main/java/gg/nextforge/datastore/DataStore.java @@ -0,0 +1,87 @@ +package gg.nextforge.datastore; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.*; +import java.util.function.Function; +import java.util.function.Supplier; + +public class DataStore { + + private final Map store = new ConcurrentHashMap<>(); + private final Map expiryMap = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final long expiryMillis; + + private DataStore(long expiryMillis) { + this.expiryMillis = expiryMillis; + startCleaner(); + } + + public static DataStore expiring(long duration, TimeUnit unit) { + return new DataStore<>(unit.toMillis(duration)); + } + + public static DataStore persistent() { + return new DataStore<>(-1); + } + + public void put(K key, V value) { + store.put(key, value); + if (expiryMillis > 0) { + expiryMap.put(key, System.currentTimeMillis() + expiryMillis); + } + } + + public Optional get(K key) { + if (isExpired(key)) { + remove(key); + return Optional.empty(); + } + return Optional.ofNullable(store.get(key)); + } + + public V getOrCreate(K key, Supplier supplier) { + return get(key).orElseGet(() -> { + V value = supplier.get(); + put(key, value); + return value; + }); + } + + public void remove(K key) { + store.remove(key); + expiryMap.remove(key); + } + + public boolean contains(K key) { + return get(key).isPresent(); + } + + public int size() { + return store.size(); + } + + private boolean isExpired(K key) { + if (expiryMillis < 0) return false; + return Optional.ofNullable(expiryMap.get(key)) + .map(expiry -> System.currentTimeMillis() > expiry) + .orElse(false); + } + + private void startCleaner() { + if (expiryMillis < 0) return; + scheduler.scheduleAtFixedRate(() -> { + long now = System.currentTimeMillis(); + expiryMap.entrySet().removeIf(entry -> { + boolean expired = now > entry.getValue(); + if (expired) store.remove(entry.getKey()); + return expired; + }); + }, expiryMillis, expiryMillis, TimeUnit.MILLISECONDS); + } + + public void shutdown() { + scheduler.shutdown(); + } +} diff --git a/src/main/java/gg/nextforge/datastore/DataStoreRegistry.java b/src/main/java/gg/nextforge/datastore/DataStoreRegistry.java new file mode 100644 index 0000000..86fecf9 --- /dev/null +++ b/src/main/java/gg/nextforge/datastore/DataStoreRegistry.java @@ -0,0 +1,41 @@ +package gg.nextforge.datastore; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * Central registry for reusable data stores. + */ +public class DataStoreRegistry { + + private static final Map> stores = new ConcurrentHashMap<>(); + + public static DataStore createExpiring(String name, long duration, TimeUnit unit) { + DataStore store = DataStore.expiring(duration, unit); + stores.put(name, store); + return store; + } + + public static DataStore createPersistent(String name) { + DataStore store = DataStore.persistent(); + stores.put(name, store); + return store; + } + + @SuppressWarnings("unchecked") + public static DataStore get(String name) { + return (DataStore) stores.get(name); + } + + public static void shutdownAll() { + stores.values().forEach(DataStore::shutdown); + stores.clear(); + } + + // Helper for UUID-keyed stores + public static DataStore createUUIDStore(String name, long duration, TimeUnit unit) { + return createExpiring(name, duration, unit); + } +} From 0647d465f9f66f2cc88a6069bea80bd4a245b470 Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 19:55:34 +0200 Subject: [PATCH 11/18] Add performance monitoring components including LagGuard, PerformanceCollector, and TickProfiler --- .../gg/nextforge/performance/LagGuard.java | 26 ++++++++++ .../performance/PerformanceBootstrap.java | 27 ++++++++++ .../performance/PerformanceCollector.java | 51 +++++++++++++++++++ .../performance/PerformanceContext.java | 39 ++++++++++++++ .../performance/PerformanceRegistry.java | 32 ++++++++++++ .../performance/PerformanceReport.java | 39 ++++++++++++++ .../performance/PerformanceSnapshot.java | 38 ++++++++++++++ .../nextforge/performance/TaskProfiler.java | 39 ++++++++++++++ .../nextforge/performance/TickListener.java | 39 ++++++++++++++ .../nextforge/performance/TickProfiler.java | 35 +++++++++++++ .../gg/nextforge/plugin/NextForgePlugin.java | 5 ++ 11 files changed, 370 insertions(+) create mode 100644 src/main/java/gg/nextforge/performance/LagGuard.java create mode 100644 src/main/java/gg/nextforge/performance/PerformanceBootstrap.java create mode 100644 src/main/java/gg/nextforge/performance/PerformanceCollector.java create mode 100644 src/main/java/gg/nextforge/performance/PerformanceContext.java create mode 100644 src/main/java/gg/nextforge/performance/PerformanceRegistry.java create mode 100644 src/main/java/gg/nextforge/performance/PerformanceReport.java create mode 100644 src/main/java/gg/nextforge/performance/PerformanceSnapshot.java create mode 100644 src/main/java/gg/nextforge/performance/TaskProfiler.java create mode 100644 src/main/java/gg/nextforge/performance/TickListener.java create mode 100644 src/main/java/gg/nextforge/performance/TickProfiler.java diff --git a/src/main/java/gg/nextforge/performance/LagGuard.java b/src/main/java/gg/nextforge/performance/LagGuard.java new file mode 100644 index 0000000..1e69ed8 --- /dev/null +++ b/src/main/java/gg/nextforge/performance/LagGuard.java @@ -0,0 +1,26 @@ +package gg.nextforge.performance; + +import java.util.function.Supplier; + +/** + * Utility to skip or throttle execution if lag threshold is exceeded. + */ +public class LagGuard { + + private static final long LAG_THRESHOLD_MS = 50; + + public static boolean isLagging() { + TickProfiler profiler = PerformanceRegistry.getTickProfiler("main"); + return profiler.getLastTickDurationMillis() > LAG_THRESHOLD_MS; + } + + public static void ifNotLagging(Runnable action) { + if (!isLagging()) { + action.run(); + } + } + + public static T supplyIfNotLagging(Supplier supplier, T fallback) { + return isLagging() ? fallback : supplier.get(); + } +} diff --git a/src/main/java/gg/nextforge/performance/PerformanceBootstrap.java b/src/main/java/gg/nextforge/performance/PerformanceBootstrap.java new file mode 100644 index 0000000..434c604 --- /dev/null +++ b/src/main/java/gg/nextforge/performance/PerformanceBootstrap.java @@ -0,0 +1,27 @@ +package gg.nextforge.performance; + +import gg.nextforge.performance.listener.TickListener; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +/** + * Initializes and wires up performance monitoring components. + */ +public class PerformanceBootstrap { + + private final Plugin plugin; + + public PerformanceBootstrap(Plugin plugin) { + this.plugin = plugin; + } + + public void enable() { + Bukkit.getPluginManager().registerEvents(new TickListener(plugin), plugin); + plugin.getLogger().info("[Performance] TickListener enabled."); + } + + public void disable() { + PerformanceRegistry.clearAll(); + plugin.getLogger().info("[Performance] Registry cleared."); + } +} diff --git a/src/main/java/gg/nextforge/performance/PerformanceCollector.java b/src/main/java/gg/nextforge/performance/PerformanceCollector.java new file mode 100644 index 0000000..d78b726 --- /dev/null +++ b/src/main/java/gg/nextforge/performance/PerformanceCollector.java @@ -0,0 +1,51 @@ +package gg.nextforge.performance; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Collects performance snapshots and aggregates statistics per key. + */ +public class PerformanceCollector { + + private final ConcurrentMap> data = new ConcurrentHashMap<>(); + + public void record(PerformanceSnapshot snapshot) { + data.computeIfAbsent(snapshot.getKey(), k -> Collections.synchronizedList(new ArrayList<>())) + .add(snapshot); + } + + public List getSnapshots(String key) { + return data.getOrDefault(key, List.of()); + } + + public long getAverageMillis(String key) { + List snapshots = getSnapshots(key); + if (snapshots.isEmpty()) return -1; + + long total = 0; + for (PerformanceSnapshot snapshot : snapshots) { + total += snapshot.getDurationMillis(); + } + return total / snapshots.size(); + } + + public int getCount(String key) { + return getSnapshots(key).size(); + } + + public void clear(String key) { + data.remove(key); + } + + public void clearAll() { + data.clear(); + } + + public List getTrackedKeys() { + return new ArrayList<>(data.keySet()); + } +} diff --git a/src/main/java/gg/nextforge/performance/PerformanceContext.java b/src/main/java/gg/nextforge/performance/PerformanceContext.java new file mode 100644 index 0000000..62182f0 --- /dev/null +++ b/src/main/java/gg/nextforge/performance/PerformanceContext.java @@ -0,0 +1,39 @@ +package gg.nextforge.performance; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Represents a performance context (e.g., per player, per world). + */ +public class PerformanceContext { + + @Getter + private final String name; + private final Map scopedCollectors = new HashMap<>(); + + public PerformanceContext(String name) { + this.name = name; + } + + public PerformanceCollector getCollector(String key) { + return scopedCollectors.computeIfAbsent(key, k -> new PerformanceCollector()); + } + + public Map getAllCollectors() { + return scopedCollectors; + } + + public String generateReport() { + StringBuilder sb = new StringBuilder(); + sb.append("=== Performance Context: ").append(name).append(" ===\n"); + for (Map.Entry entry : scopedCollectors.entrySet()) { + sb.append("-- ").append(entry.getKey()).append(" --\n"); + sb.append(PerformanceReport.generate(entry.getValue())).append("\n"); + } + return sb.toString(); + } +} diff --git a/src/main/java/gg/nextforge/performance/PerformanceRegistry.java b/src/main/java/gg/nextforge/performance/PerformanceRegistry.java new file mode 100644 index 0000000..77d5390 --- /dev/null +++ b/src/main/java/gg/nextforge/performance/PerformanceRegistry.java @@ -0,0 +1,32 @@ +package gg.nextforge.performance; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Central registry for all performance tracking components. + */ +public class PerformanceRegistry { + + private static final Map collectors = new ConcurrentHashMap<>(); + private static final Map tickProfilers = new ConcurrentHashMap<>(); + private static final Map taskProfilers = new ConcurrentHashMap<>(); + + public static PerformanceCollector getCollector(String key) { + return collectors.computeIfAbsent(key, k -> new PerformanceCollector()); + } + + public static TickProfiler getTickProfiler(String key) { + return tickProfilers.computeIfAbsent(key, k -> new TickProfiler()); + } + + public static TaskProfiler getTaskProfiler(String key) { + return taskProfilers.computeIfAbsent(key, k -> new TaskProfiler(getCollector(key))); + } + + public static void clearAll() { + collectors.clear(); + tickProfilers.clear(); + taskProfilers.clear(); + } +} diff --git a/src/main/java/gg/nextforge/performance/PerformanceReport.java b/src/main/java/gg/nextforge/performance/PerformanceReport.java new file mode 100644 index 0000000..493b264 --- /dev/null +++ b/src/main/java/gg/nextforge/performance/PerformanceReport.java @@ -0,0 +1,39 @@ +package gg.nextforge.performance; + +import java.util.List; +import java.util.StringJoiner; + +/** + * Utility to format and present performance data. + */ +public class PerformanceReport { + + public static String generate(PerformanceCollector collector) { + StringBuilder sb = new StringBuilder(); + sb.append("=== Performance Report ===\n"); + + List keys = collector.getTrackedKeys(); + if (keys.isEmpty()) { + sb.append("No data collected.\n"); + return sb.toString(); + } + + for (String key : keys) { + int count = collector.getCount(key); + long avg = collector.getAverageMillis(key); + sb.append("- ").append(key) + .append(": ").append(count).append(" runs, ") + .append("avg ").append(avg).append(" ms\n"); + } + + return sb.toString(); + } + + public static String json(PerformanceCollector collector) { + StringJoiner joiner = new StringJoiner(",", "{", "}"); + for (String key : collector.getTrackedKeys()) { + joiner.add("\"" + key + "\":" + collector.getAverageMillis(key)); + } + return joiner.toString(); + } +} diff --git a/src/main/java/gg/nextforge/performance/PerformanceSnapshot.java b/src/main/java/gg/nextforge/performance/PerformanceSnapshot.java new file mode 100644 index 0000000..0f042ef --- /dev/null +++ b/src/main/java/gg/nextforge/performance/PerformanceSnapshot.java @@ -0,0 +1,38 @@ +package gg.nextforge.performance; + +/** + * Immutable data holder representing a single measurement. + */ +public class PerformanceSnapshot { + + private final String key; + private final long startTimeNanos; + private final long durationNanos; + + public PerformanceSnapshot(String key, long startTimeNanos, long durationNanos) { + this.key = key; + this.startTimeNanos = startTimeNanos; + this.durationNanos = durationNanos; + } + + public String getKey() { + return key; + } + + public long getStartTimeNanos() { + return startTimeNanos; + } + + public long getDurationNanos() { + return durationNanos; + } + + public long getDurationMillis() { + return durationNanos / 1_000_000; + } + + @Override + public String toString() { + return key + " - " + getDurationMillis() + " ms"; + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/performance/TaskProfiler.java b/src/main/java/gg/nextforge/performance/TaskProfiler.java new file mode 100644 index 0000000..18f8652 --- /dev/null +++ b/src/main/java/gg/nextforge/performance/TaskProfiler.java @@ -0,0 +1,39 @@ +package gg.nextforge.performance; + +import java.util.concurrent.Callable; + +/** + * Wraps a Runnable or Callable to automatically record performance. + */ +public class TaskProfiler { + + private final PerformanceCollector collector; + + public TaskProfiler(PerformanceCollector collector) { + this.collector = collector; + } + + public Runnable wrap(String key, Runnable runnable) { + return () -> { + long start = System.nanoTime(); + try { + runnable.run(); + } finally { + long duration = System.nanoTime() - start; + collector.record(new PerformanceSnapshot(key, start, duration)); + } + }; + } + + public Callable wrap(String key, Callable callable) { + return () -> { + long start = System.nanoTime(); + try { + return callable.call(); + } finally { + long duration = System.nanoTime() - start; + collector.record(new PerformanceSnapshot(key, start, duration)); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/performance/TickListener.java b/src/main/java/gg/nextforge/performance/TickListener.java new file mode 100644 index 0000000..adfda52 --- /dev/null +++ b/src/main/java/gg/nextforge/performance/TickListener.java @@ -0,0 +1,39 @@ +package gg.nextforge.performance.listener; + +import gg.nextforge.performance.PerformanceRegistry; +import gg.nextforge.performance.TickProfiler; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.plugin.Plugin; + +/** + * A fallback tick monitor for platforms without ServerTickEvents. + */ +public class TickListener implements Listener { + + private final TickProfiler profiler; + private final Plugin plugin; + + public TickListener(Plugin plugin) { + this.plugin = plugin; + this.profiler = PerformanceRegistry.getTickProfiler("main"); + startLoop(); + } + + private void startLoop() { + new BukkitRunnable() { + long lastTick = System.nanoTime(); + + @Override + public void run() { + long now = System.nanoTime(); + profiler.onTickStart(); + profiler.onTickEnd(); + + long duration = now - lastTick; + lastTick = now; + } + }.runTaskTimer(plugin, 1L, 1L); + } +} diff --git a/src/main/java/gg/nextforge/performance/TickProfiler.java b/src/main/java/gg/nextforge/performance/TickProfiler.java new file mode 100644 index 0000000..498069a --- /dev/null +++ b/src/main/java/gg/nextforge/performance/TickProfiler.java @@ -0,0 +1,35 @@ +package gg.nextforge.performance; + +public class TickProfiler { + + private long lastTickTime = -1; + private long lastTickDuration = -1; + + public void onTickStart() { + lastTickTime = System.nanoTime(); + } + + public void onTickEnd() { + if (lastTickTime > 0) { + lastTickDuration = System.nanoTime() - lastTickTime; + } + } + + public long getLastTickDurationNanos() { + return lastTickDuration; + } + + public long getLastTickDurationMillis() { + return lastTickDuration / 1_000_000; + } + + public double getApproximateTPS() { + if (lastTickDuration <= 0) return 20.0; + double tickTimeMs = getLastTickDurationMillis(); + return Math.min(20.0, 1000.0 / tickTimeMs); + } + + public String format() { + return "Tick: " + getLastTickDurationMillis() + " ms | TPS: " + String.format("%.2f", getApproximateTPS()); + } +} diff --git a/src/main/java/gg/nextforge/plugin/NextForgePlugin.java b/src/main/java/gg/nextforge/plugin/NextForgePlugin.java index dae31a9..9796df7 100644 --- a/src/main/java/gg/nextforge/plugin/NextForgePlugin.java +++ b/src/main/java/gg/nextforge/plugin/NextForgePlugin.java @@ -5,14 +5,17 @@ import gg.nextforge.config.ConfigManager; import gg.nextforge.database.DatabaseManager; import gg.nextforge.npc.NPCManager; +import gg.nextforge.performance.listener.TickListener; import gg.nextforge.protocol.ProtocolManager; import gg.nextforge.scheduler.CoreScheduler; import gg.nextforge.text.TextManager; import gg.nextforge.ui.UIManager; import lombok.Getter; import org.bstats.bukkit.Metrics; +import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; import java.util.UUID; @@ -69,6 +72,8 @@ public void onEnable() { this.uiManager.init(this); + Bukkit.getPluginManager().registerEvents(new TickListener(this), this); + boolean isReload = getServer().getPluginManager().isPluginEnabled("NextForge"); enable(isReload); } From 14d6c60c12b8d461f115984725337a1f9ccc2830 Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 20:05:22 +0200 Subject: [PATCH 12/18] Add debug logging framework with DebugContext, DebugFlagStore, DebugManager, DebugPrinter, DebugScope, and Tracer classes --- .../java/gg/nextforge/debug/DebugContext.java | 39 ++++++++++++++ .../gg/nextforge/debug/DebugFlagStore.java | 41 +++++++++++++++ .../java/gg/nextforge/debug/DebugManager.java | 52 +++++++++++++++++++ .../java/gg/nextforge/debug/DebugPrinter.java | 46 ++++++++++++++++ .../java/gg/nextforge/debug/DebugScope.java | 23 ++++++++ src/main/java/gg/nextforge/debug/Tracer.java | 47 +++++++++++++++++ 6 files changed, 248 insertions(+) create mode 100644 src/main/java/gg/nextforge/debug/DebugContext.java create mode 100644 src/main/java/gg/nextforge/debug/DebugFlagStore.java create mode 100644 src/main/java/gg/nextforge/debug/DebugManager.java create mode 100644 src/main/java/gg/nextforge/debug/DebugPrinter.java create mode 100644 src/main/java/gg/nextforge/debug/DebugScope.java create mode 100644 src/main/java/gg/nextforge/debug/Tracer.java diff --git a/src/main/java/gg/nextforge/debug/DebugContext.java b/src/main/java/gg/nextforge/debug/DebugContext.java new file mode 100644 index 0000000..a445d98 --- /dev/null +++ b/src/main/java/gg/nextforge/debug/DebugContext.java @@ -0,0 +1,39 @@ +package gg.nextforge.debug; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a debug logging session for a specific topic or action. + */ +public class DebugContext { + + private final DebugScope scope; + private final String label; + private final List lines = new ArrayList<>(); + + public DebugContext(DebugScope scope, String label) { + this.scope = scope; + this.label = label; + } + + public void add(String message) { + lines.add(message); + } + + public void logNow() { + for (String line : lines) { + DebugManager.log(scope, label + ": " + line); + } + } + + public void clear() { + lines.clear(); + } + + public void logWithPrefix(String prefix) { + for (String line : lines) { + DebugManager.log(scope, prefix + " :: " + label + ": " + line); + } + } +} diff --git a/src/main/java/gg/nextforge/debug/DebugFlagStore.java b/src/main/java/gg/nextforge/debug/DebugFlagStore.java new file mode 100644 index 0000000..6910d6e --- /dev/null +++ b/src/main/java/gg/nextforge/debug/DebugFlagStore.java @@ -0,0 +1,41 @@ +package gg.nextforge.debug; + +import org.bukkit.entity.Player; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Manages which players or scopes have debugging enabled. + */ +public class DebugFlagStore { + + private final Set enabledPlayers = new HashSet<>(); + private boolean globalDebugEnabled = false; + + public boolean isEnabled(Player player) { + return globalDebugEnabled || enabledPlayers.contains(player.getUniqueId()); + } + + public void enableFor(Player player) { + enabledPlayers.add(player.getUniqueId()); + } + + public void disableFor(Player player) { + enabledPlayers.remove(player.getUniqueId()); + } + + public void setGlobal(boolean enabled) { + globalDebugEnabled = enabled; + } + + public boolean isGlobalEnabled() { + return globalDebugEnabled; + } + + public void clear() { + enabledPlayers.clear(); + globalDebugEnabled = false; + } +} diff --git a/src/main/java/gg/nextforge/debug/DebugManager.java b/src/main/java/gg/nextforge/debug/DebugManager.java new file mode 100644 index 0000000..36e35f8 --- /dev/null +++ b/src/main/java/gg/nextforge/debug/DebugManager.java @@ -0,0 +1,52 @@ +package gg.nextforge.debug; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.EnumMap; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Central entry point for debug logging and state management. + */ +public class DebugManager { + + private static final DebugFlagStore flagStore = new DebugFlagStore(); + private static final Map> scopeLoggers = new EnumMap<>(DebugScope.class); + + static { + for (DebugScope scope : DebugScope.values()) { + scopeLoggers.put(scope, msg -> Bukkit.getLogger().info("[Debug:" + scope.displayName() + "] " + msg)); + } + } + + public static void log(DebugScope scope, String message) { + Consumer logger = scopeLoggers.getOrDefault(scope, System.out::println); + logger.accept(message); + } + + public static boolean isEnabled(Player player) { + return flagStore.isEnabled(player); + } + + public static void enable(Player player) { + flagStore.enableFor(player); + } + + public static void disable(Player player) { + flagStore.disableFor(player); + } + + public static void setGlobal(boolean enabled) { + flagStore.setGlobal(enabled); + } + + public static boolean isGlobalEnabled() { + return flagStore.isGlobalEnabled(); + } + + public static DebugFlagStore getFlagStore() { + return flagStore; + } +} diff --git a/src/main/java/gg/nextforge/debug/DebugPrinter.java b/src/main/java/gg/nextforge/debug/DebugPrinter.java new file mode 100644 index 0000000..26c066a --- /dev/null +++ b/src/main/java/gg/nextforge/debug/DebugPrinter.java @@ -0,0 +1,46 @@ +package gg.nextforge.debug; + +import gg.nextforge.NextCorePlugin; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +/** + * Utility for printing debug messages to players or console. + */ +public class DebugPrinter { + + public static void print(Player player, String message) { + if (DebugManager.isEnabled(player)) { + player.sendMessage(format(message)); + } + } + + public static void console(String message) { + NextCorePlugin.getInstance().getComponentLogger().info(format(message)); + } + + public static void broadcast(String message) { + Component msg = format(message); + for (Player player : Bukkit.getOnlinePlayers()) { + if (DebugManager.isEnabled(player)) { + player.sendMessage(msg); + } + } + } + + public static Component format(String message) { + return Component.text() + .append(Component.text("[Debug] ", NamedTextColor.GRAY)) + .append(Component.text(message, NamedTextColor.YELLOW)) + .build(); + } + + public static void printRaw(Player player, Component component) { + if (DebugManager.isEnabled(player)) { + player.sendMessage(component); + } + } +} diff --git a/src/main/java/gg/nextforge/debug/DebugScope.java b/src/main/java/gg/nextforge/debug/DebugScope.java new file mode 100644 index 0000000..724c673 --- /dev/null +++ b/src/main/java/gg/nextforge/debug/DebugScope.java @@ -0,0 +1,23 @@ +package gg.nextforge.debug; + +/** + * Represents a logical debug category or module. + */ +public enum DebugScope { + PERFORMANCE, + DATABASE, + UI, + NETWORK, + SCHEDULER, + COMMAND, + EVENT, + REDIS, + MONGO, + INVENTORY, + SECURITY, + OTHER; + + public String displayName() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/gg/nextforge/debug/Tracer.java b/src/main/java/gg/nextforge/debug/Tracer.java new file mode 100644 index 0000000..76af32b --- /dev/null +++ b/src/main/java/gg/nextforge/debug/Tracer.java @@ -0,0 +1,47 @@ +package gg.nextforge.debug; + +import java.util.HashMap; +import java.util.Map; + +/** + * Lightweight tracer to measure and log elapsed time between steps. + */ +public class Tracer { + + private final DebugScope scope; + private final String label; + private final Map timestamps = new HashMap<>(); + + public Tracer(DebugScope scope, String label) { + this.scope = scope; + this.label = label; + mark("start"); + } + + public void mark(String step) { + timestamps.put(step, System.nanoTime()); + } + + public void log(String step) { + if (!timestamps.containsKey("start")) return; + long now = System.nanoTime(); + long duration = now - timestamps.get("start"); + DebugManager.log(scope, label + " [" + step + "] took " + (duration / 1_000_000.0) + "ms"); + } + + public void logStep(String from, String to) { + if (!timestamps.containsKey(from) || !timestamps.containsKey(to)) return; + long duration = timestamps.get(to) - timestamps.get(from); + DebugManager.log(scope, label + " [" + from + " -> " + to + "] " + (duration / 1_000_000.0) + "ms"); + } + + public void logAll() { + String lastKey = null; + for (String key : timestamps.keySet()) { + if (lastKey != null) { + logStep(lastKey, key); + } + lastKey = key; + } + } +} \ No newline at end of file From 64e0cd12a01b5a2920fd9527a0cb6642e7b9b79a Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 20:16:43 +0200 Subject: [PATCH 13/18] Add messaging framework with MessageBus, MessageChannel, MessageEnvelope, and related classes --- .../messaging/InternalMessageEvent.java | 32 ++++++++++ .../gg/nextforge/messaging/MessageBus.java | 38 +++++++++++ .../nextforge/messaging/MessageChannel.java | 35 ++++++++++ .../nextforge/messaging/MessageEnvelope.java | 37 +++++++++++ .../nextforge/messaging/MessageHandler.java | 11 ++++ .../messaging/MessageSubscription.java | 25 ++++++++ .../nextforge/messaging/MessagingService.java | 64 +++++++++++++++++++ .../messaging/queue/MessageQueue.java | 29 +++++++++ .../queue/QueuedMessageDispatcher.java | 44 +++++++++++++ 9 files changed, 315 insertions(+) create mode 100644 src/main/java/gg/nextforge/messaging/InternalMessageEvent.java create mode 100644 src/main/java/gg/nextforge/messaging/MessageBus.java create mode 100644 src/main/java/gg/nextforge/messaging/MessageChannel.java create mode 100644 src/main/java/gg/nextforge/messaging/MessageEnvelope.java create mode 100644 src/main/java/gg/nextforge/messaging/MessageHandler.java create mode 100644 src/main/java/gg/nextforge/messaging/MessageSubscription.java create mode 100644 src/main/java/gg/nextforge/messaging/MessagingService.java create mode 100644 src/main/java/gg/nextforge/messaging/queue/MessageQueue.java create mode 100644 src/main/java/gg/nextforge/messaging/queue/QueuedMessageDispatcher.java diff --git a/src/main/java/gg/nextforge/messaging/InternalMessageEvent.java b/src/main/java/gg/nextforge/messaging/InternalMessageEvent.java new file mode 100644 index 0000000..4dd8ede --- /dev/null +++ b/src/main/java/gg/nextforge/messaging/InternalMessageEvent.java @@ -0,0 +1,32 @@ +package gg.nextforge.messaging; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * A Bukkit-compatible event for internal messaging. + * This enables forwarding messages into the Bukkit event system. + */ +public class InternalMessageEvent extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final MessageEnvelope envelope; + + public InternalMessageEvent(MessageEnvelope envelope) { + this.envelope = envelope; + } + + public MessageEnvelope getEnvelope() { + return envelope; + } + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/src/main/java/gg/nextforge/messaging/MessageBus.java b/src/main/java/gg/nextforge/messaging/MessageBus.java new file mode 100644 index 0000000..d146598 --- /dev/null +++ b/src/main/java/gg/nextforge/messaging/MessageBus.java @@ -0,0 +1,38 @@ +package gg.nextforge.messaging; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Central dispatcher for publishing and subscribing to messages. + */ +public class MessageBus { + + private final Map, List>> handlerMap = new ConcurrentHashMap<>(); + + public MessageSubscription subscribe(Class type, MessageHandler handler) { + handlerMap.computeIfAbsent(type, k -> new ArrayList<>()).add(handler); + return new MessageSubscription(() -> unsubscribe(type, handler)); + } + + @SuppressWarnings("unchecked") + public void publish(T payload) { + Class type = (Class) payload.getClass(); + List> handlers = handlerMap.get(type); + if (handlers != null) { + for (MessageHandler handler : new ArrayList<>(handlers)) { + ((MessageHandler) handler).handle(payload); + } + } + } + + private void unsubscribe(Class type, MessageHandler handler) { + List> handlers = handlerMap.get(type); + if (handlers != null) { + handlers.remove(handler); + if (handlers.isEmpty()) { + handlerMap.remove(type); + } + } + } +} diff --git a/src/main/java/gg/nextforge/messaging/MessageChannel.java b/src/main/java/gg/nextforge/messaging/MessageChannel.java new file mode 100644 index 0000000..b464f2e --- /dev/null +++ b/src/main/java/gg/nextforge/messaging/MessageChannel.java @@ -0,0 +1,35 @@ +package gg.nextforge.messaging; + +import java.util.function.Consumer; + +/** + * A named logical channel for message segregation. + */ +public class MessageChannel { + + private final String name; + private final Class type; + private final MessageBus messageBus; + + public MessageChannel(String name, Class type, MessageBus messageBus) { + this.name = name; + this.type = type; + this.messageBus = messageBus; + } + + public String getName() { + return name; + } + + public Class getType() { + return type; + } + + public void publish(T message) { + messageBus.publish(message); + } + + public MessageSubscription subscribe(Consumer handler) { + return messageBus.subscribe(type, handler::accept); + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/messaging/MessageEnvelope.java b/src/main/java/gg/nextforge/messaging/MessageEnvelope.java new file mode 100644 index 0000000..ac74e40 --- /dev/null +++ b/src/main/java/gg/nextforge/messaging/MessageEnvelope.java @@ -0,0 +1,37 @@ +package gg.nextforge.messaging; + +import java.util.UUID; + +/** + * A message wrapper that includes metadata and a payload. + */ +public class MessageEnvelope { + + private final UUID messageId; + private final long timestamp; + private final Class type; + private final T payload; + + public MessageEnvelope(Class type, T payload) { + this.messageId = UUID.randomUUID(); + this.timestamp = System.currentTimeMillis(); + this.type = type; + this.payload = payload; + } + + public UUID getMessageId() { + return messageId; + } + + public long getTimestamp() { + return timestamp; + } + + public Class getType() { + return type; + } + + public T getPayload() { + return payload; + } +} diff --git a/src/main/java/gg/nextforge/messaging/MessageHandler.java b/src/main/java/gg/nextforge/messaging/MessageHandler.java new file mode 100644 index 0000000..d0fa129 --- /dev/null +++ b/src/main/java/gg/nextforge/messaging/MessageHandler.java @@ -0,0 +1,11 @@ +package gg.nextforge.messaging; + +/** + * Functional interface for handling typed message payloads. + * + * @param The type of payload this handler supports. + */ +@FunctionalInterface +public interface MessageHandler { + void handle(T payload); +} diff --git a/src/main/java/gg/nextforge/messaging/MessageSubscription.java b/src/main/java/gg/nextforge/messaging/MessageSubscription.java new file mode 100644 index 0000000..90d4257 --- /dev/null +++ b/src/main/java/gg/nextforge/messaging/MessageSubscription.java @@ -0,0 +1,25 @@ +package gg.nextforge.messaging; + +/** + * A handle to cancel a message subscription. + */ +public class MessageSubscription { + + private final Runnable cancelAction; + private boolean active = true; + + public MessageSubscription(Runnable cancelAction) { + this.cancelAction = cancelAction; + } + + public void unsubscribe() { + if (active) { + cancelAction.run(); + active = false; + } + } + + public boolean isActive() { + return active; + } +} diff --git a/src/main/java/gg/nextforge/messaging/MessagingService.java b/src/main/java/gg/nextforge/messaging/MessagingService.java new file mode 100644 index 0000000..3ae7752 --- /dev/null +++ b/src/main/java/gg/nextforge/messaging/MessagingService.java @@ -0,0 +1,64 @@ +package gg.nextforge.messaging; + +import gg.nextforge.messaging.queue.MessageQueue; +import gg.nextforge.messaging.queue.QueuedMessageDispatcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Entry point for plugin-wide message communication. + */ +public class MessagingService { + + private final MessageBus messageBus; + private final Map> channels = new HashMap<>(); + private final Map, QueuedMessageDispatcher> asyncDispatchers = new ConcurrentHashMap<>(); + + public MessagingService() { + this.messageBus = new MessageBus(); + } + + public MessageBus getBus() { + return messageBus; + } + + public MessageChannel createChannel(String name, Class type) { + MessageChannel channel = new MessageChannel<>(name, type, messageBus); + channels.put(name, channel); + return channel; + } + + @SuppressWarnings("unchecked") + public MessageChannel getChannel(String name, Class type) { + return (MessageChannel) channels.get(name); + } + + public boolean hasChannel(String name) { + return channels.containsKey(name); + } + + public void clearChannels() { + channels.clear(); + } + + public void registerAsyncHandler(Class type, MessageHandler handler) { + asyncDispatchers.put(type, new QueuedMessageDispatcher<>(handler)); + } + + @SuppressWarnings("unchecked") + public void publishAsync(T message) { + QueuedMessageDispatcher dispatcher = (QueuedMessageDispatcher) asyncDispatchers.get(message.getClass()); + if (dispatcher != null) { + dispatcher.dispatchLater(message); + } else { + throw new IllegalStateException("No async dispatcher registered for type: " + message.getClass()); + } + } + + public void shutdownAsync() { + asyncDispatchers.values().forEach(QueuedMessageDispatcher::stop); + asyncDispatchers.clear(); + } +} diff --git a/src/main/java/gg/nextforge/messaging/queue/MessageQueue.java b/src/main/java/gg/nextforge/messaging/queue/MessageQueue.java new file mode 100644 index 0000000..8241ba5 --- /dev/null +++ b/src/main/java/gg/nextforge/messaging/queue/MessageQueue.java @@ -0,0 +1,29 @@ +package gg.nextforge.messaging.queue; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public class MessageQueue { + + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + public void enqueue(T message) { + queue.add(message); + } + + public T poll() { + return queue.poll(); + } + + public boolean isEmpty() { + return queue.isEmpty(); + } + + public int size() { + return queue.size(); + } + + public void clear() { + queue.clear(); + } +} diff --git a/src/main/java/gg/nextforge/messaging/queue/QueuedMessageDispatcher.java b/src/main/java/gg/nextforge/messaging/queue/QueuedMessageDispatcher.java new file mode 100644 index 0000000..972141f --- /dev/null +++ b/src/main/java/gg/nextforge/messaging/queue/QueuedMessageDispatcher.java @@ -0,0 +1,44 @@ +package gg.nextforge.messaging.queue; + +import gg.nextforge.messaging.MessageHandler; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class QueuedMessageDispatcher { + + private final MessageQueue queue = new MessageQueue<>(); + private final MessageHandler handler; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private volatile boolean running = true; + + public QueuedMessageDispatcher(MessageHandler handler) { + this.handler = handler; + start(); + } + + private void start() { + executor.submit(() -> { + while (running) { + try { + T message = queue.poll(); + if (message != null) { + handler.handle(message); + } + Thread.sleep(5); // fine-tune for your environment + } catch (Exception ex) { + ex.printStackTrace(); // TODO: Logging + } + } + }); + } + + public void dispatchLater(T message) { + queue.enqueue(message); + } + + public void stop() { + running = false; + executor.shutdownNow(); + } +} From 31610ca30f3be2bb3b88f84d7fbff473571d56c5 Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 20:28:35 +0200 Subject: [PATCH 14/18] Add AdvancedTaskScheduler and TaskBuilder for enhanced task scheduling capabilities --- .../gg/nextforge/scheduler/CoreScheduler.java | 5 + .../advanced/AdvancedTaskScheduler.java | 56 +++++++++++ .../scheduler/advanced/TaskBuilder.java | 94 +++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/main/java/gg/nextforge/scheduler/advanced/AdvancedTaskScheduler.java create mode 100644 src/main/java/gg/nextforge/scheduler/advanced/TaskBuilder.java diff --git a/src/main/java/gg/nextforge/scheduler/CoreScheduler.java b/src/main/java/gg/nextforge/scheduler/CoreScheduler.java index 051c1b0..149c640 100644 --- a/src/main/java/gg/nextforge/scheduler/CoreScheduler.java +++ b/src/main/java/gg/nextforge/scheduler/CoreScheduler.java @@ -1,5 +1,6 @@ package gg.nextforge.scheduler; +import gg.nextforge.scheduler.advanced.AdvancedTaskScheduler; import lombok.Getter; import java.util.Map; @@ -13,6 +14,8 @@ public class CoreScheduler { private static CoreScheduler instance; + @Getter + private static AdvancedTaskScheduler advancedScheduler; @Getter private static final Map activeTasks = new ConcurrentHashMap<>(); @@ -22,6 +25,7 @@ public class CoreScheduler { public CoreScheduler() { this.executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); + this.advancedScheduler = new AdvancedTaskScheduler(); instance = this; } @@ -91,4 +95,5 @@ private ScheduledTask scheduleRepeating(Runnable task, long delayTicks, long per private long ticksToMillis(long ticks) { return ticks * 50L; // 20 ticks = 1 second } + } diff --git a/src/main/java/gg/nextforge/scheduler/advanced/AdvancedTaskScheduler.java b/src/main/java/gg/nextforge/scheduler/advanced/AdvancedTaskScheduler.java new file mode 100644 index 0000000..bb69d6c --- /dev/null +++ b/src/main/java/gg/nextforge/scheduler/advanced/AdvancedTaskScheduler.java @@ -0,0 +1,56 @@ +package gg.nextforge.scheduler.advanced; + +import java.util.concurrent.*; +import java.util.function.Consumer; +import java.util.logging.Logger; + +public class AdvancedTaskScheduler { + + private static final Logger LOGGER = Logger.getLogger("AdvancedTaskScheduler"); + + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4); + private final ExecutorService asyncExecutor = Executors.newCachedThreadPool(); + + public ScheduledFuture runLater(Runnable task, long delayMillis) { + return executor.schedule(safe(task), delayMillis, TimeUnit.MILLISECONDS); + } + + public ScheduledFuture runRepeating(Runnable task, long initialDelay, long period) { + return executor.scheduleAtFixedRate(safe(task), initialDelay, period, TimeUnit.MILLISECONDS); + } + + public Future runAsync(Runnable task) { + return asyncExecutor.submit(safe(task)); + } + + public Future supplyAsync(Callable task) { + return asyncExecutor.submit(task); + } + + public void supplyAsync(Callable task, Consumer resultHandler, Consumer errorHandler) { + asyncExecutor.submit(() -> { + try { + T result = task.call(); + resultHandler.accept(result); + } catch (Throwable t) { + errorHandler.accept(t); + } + }); + } + + public void shutdown() { + executor.shutdownNow(); + asyncExecutor.shutdownNow(); + } + + private Runnable safe(Runnable task) { + return () -> { + try { + task.run(); + } catch (Throwable t) { + LOGGER.severe("Task execution failed: " + t.getMessage()); + t.printStackTrace(); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/scheduler/advanced/TaskBuilder.java b/src/main/java/gg/nextforge/scheduler/advanced/TaskBuilder.java new file mode 100644 index 0000000..dd973ef --- /dev/null +++ b/src/main/java/gg/nextforge/scheduler/advanced/TaskBuilder.java @@ -0,0 +1,94 @@ +package gg.nextforge.scheduler.advanced; + +import java.util.concurrent.*; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class TaskBuilder { + + private boolean async = true; + private boolean repeating = false; + + private long delay = 0; + private long period = 0; + + private Supplier supplier; + private Consumer callback; + private Consumer errorHandler; + + private AdvancedTaskScheduler scheduler; + + private TaskBuilder(AdvancedTaskScheduler scheduler) { + this.scheduler = scheduler; + } + + public static TaskBuilder create(AdvancedTaskScheduler scheduler) { + return new TaskBuilder<>(scheduler); + } + + public TaskBuilder async(boolean async) { + this.async = async; + return this; + } + + public TaskBuilder delay(long millis) { + this.delay = millis; + return this; + } + + public TaskBuilder period(long millis) { + this.repeating = true; + this.period = millis; + return this; + } + + public TaskBuilder supplier(Supplier supplier) { + this.supplier = supplier; + return this; + } + + public TaskBuilder onResult(Consumer callback) { + this.callback = callback; + return this; + } + + public TaskBuilder onError(Consumer errorHandler) { + this.errorHandler = errorHandler; + return this; + } + + public Future schedule() { + if (supplier == null) throw new IllegalStateException("No task supplier defined!"); + + Callable callable = () -> { + try { + return supplier.get(); + } catch (Throwable t) { + if (errorHandler != null) errorHandler.accept(t); + else t.printStackTrace(); + return null; + } + }; + + Runnable task = () -> { + try { + T result = callable.call(); + if (callback != null && result != null) callback.accept(result); + } catch (Exception e) { + if (errorHandler != null) errorHandler.accept(e); + else e.printStackTrace(); + } + }; + + if (repeating) { + return scheduler.runRepeating(task, delay, period); + } else if (delay > 0) { + return scheduler.runLater(task, delay); + } else if (async) { + return scheduler.runAsync(task); + } else { + task.run(); + return null; + } + } +} From 199738b8e3840eed69e9c00d1db6b2ea12238f1b Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 20:29:03 +0200 Subject: [PATCH 15/18] Update version to 2.2 in build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0a90cd0..fd494a9 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { } group = 'gg.nextforge' -version = '3.0.1-SNAPSHOT' +version = '2.2' repositories { mavenCentral() From 4a5c6ec1c9239857c5a417c98f8544e737caa67f Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 21:02:39 +0200 Subject: [PATCH 16/18] Add backup and file management utilities with BackupHandler, DirectoryWatcher, FileCache, and FileService --- build.gradle | 15 ++++ .../nextforge/bridge/BridgeBootstrapper.java | 52 ++++++++++++++ .../gg/nextforge/bridge/BridgeManager.java | 69 ++++++++++++++++++ .../gg/nextforge/bridge/BridgeStatus.java | 27 +++++++ .../gg/nextforge/bridge/PluginBridge.java | 45 ++++++++++++ .../nextforge/bridge/annotations/Bridge.java | 24 +++++++ .../bridge/impl/LuckPermsBridge.java | 55 +++++++++++++++ .../bridge/impl/PlaceholderAPIBridge.java | 33 +++++++++ .../gg/nextforge/bridge/impl/VaultBridge.java | 62 ++++++++++++++++ .../java/gg/nextforge/io/BackupHandler.java | 42 +++++++++++ .../gg/nextforge/io/DirectoryWatcher.java | 69 ++++++++++++++++++ src/main/java/gg/nextforge/io/FileCache.java | 62 ++++++++++++++++ .../java/gg/nextforge/io/FileService.java | 70 +++++++++++++++++++ .../java/gg/nextforge/io/YamlFileHandler.java | 51 ++++++++++++++ src/main/resources/plugin.yml | 1 + 15 files changed, 677 insertions(+) create mode 100644 src/main/java/gg/nextforge/bridge/BridgeBootstrapper.java create mode 100644 src/main/java/gg/nextforge/bridge/BridgeManager.java create mode 100644 src/main/java/gg/nextforge/bridge/BridgeStatus.java create mode 100644 src/main/java/gg/nextforge/bridge/PluginBridge.java create mode 100644 src/main/java/gg/nextforge/bridge/annotations/Bridge.java create mode 100644 src/main/java/gg/nextforge/bridge/impl/LuckPermsBridge.java create mode 100644 src/main/java/gg/nextforge/bridge/impl/PlaceholderAPIBridge.java create mode 100644 src/main/java/gg/nextforge/bridge/impl/VaultBridge.java create mode 100644 src/main/java/gg/nextforge/io/BackupHandler.java create mode 100644 src/main/java/gg/nextforge/io/DirectoryWatcher.java create mode 100644 src/main/java/gg/nextforge/io/FileCache.java create mode 100644 src/main/java/gg/nextforge/io/FileService.java create mode 100644 src/main/java/gg/nextforge/io/YamlFileHandler.java diff --git a/build.gradle b/build.gradle index fd494a9..e749525 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,15 @@ sourceSets { } } +repositories { + maven { + url = 'https://repo.extendedclip.com/releases/' + } + maven { + url 'https://jitpack.io' + } +} + dependencies { paperweight.paperDevBundle("1.19.1-R0.1-SNAPSHOT") @@ -43,6 +52,12 @@ dependencies { implementation 'org.xerial:sqlite-jdbc:3.50.3.0' implementation 'com.h2database:h2:2.3.232' implementation 'org.mongodb:mongodb-driver-sync:5.5.1' + + implementation 'org.reflections:reflections:0.10.2' + + compileOnly 'me.clip:placeholderapi:2.11.6' + compileOnly "com.github.MilkBowl:VaultAPI:1.7" + compileOnly 'net.luckperms:api:5.4' } subprojects { diff --git a/src/main/java/gg/nextforge/bridge/BridgeBootstrapper.java b/src/main/java/gg/nextforge/bridge/BridgeBootstrapper.java new file mode 100644 index 0000000..e4217a9 --- /dev/null +++ b/src/main/java/gg/nextforge/bridge/BridgeBootstrapper.java @@ -0,0 +1,52 @@ +package gg.nextforge.bridge; + +import gg.nextforge.bridge.annotations.Bridge; +import org.reflections.Reflections; + +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Automatically discovers and registers all PluginBridges annotated with @Bridge. + */ +public class BridgeBootstrapper { + + private static final Logger LOGGER = Logger.getLogger("BridgeBootstrapper"); + + private final BridgeManager bridgeManager; + + public BridgeBootstrapper(BridgeManager bridgeManager) { + this.bridgeManager = bridgeManager; + } + + /** + * Scans and registers all bridges annotated with @Bridge. + */ + public void loadBridges(String basePackage) { + Reflections reflections = new Reflections(basePackage); + Set> bridgeClasses = reflections.getTypesAnnotatedWith(Bridge.class); + + for (Class clazz : bridgeClasses) { + if (!PluginBridge.class.isAssignableFrom(clazz)) { + LOGGER.warning("Invalid @Bridge class (not a PluginBridge): " + clazz.getName()); + continue; + } + + try { + PluginBridge instance = (PluginBridge) clazz.getDeclaredConstructor().newInstance(); + Bridge annotation = clazz.getAnnotation(Bridge.class); + + if (annotation.enabled()) { + bridgeManager.register(instance); + LOGGER.info("Registered bridge: " + clazz.getSimpleName()); + } else { + LOGGER.info("Bridge disabled (annotation): " + clazz.getSimpleName()); + } + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to initialize bridge: " + clazz.getName(), e); + } + } + } +} diff --git a/src/main/java/gg/nextforge/bridge/BridgeManager.java b/src/main/java/gg/nextforge/bridge/BridgeManager.java new file mode 100644 index 0000000..5e726a0 --- /dev/null +++ b/src/main/java/gg/nextforge/bridge/BridgeManager.java @@ -0,0 +1,69 @@ +package gg.nextforge.bridge; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import java.util.*; +import java.util.logging.Logger; + +/** + * Central manager for all plugin bridges. + */ +public class BridgeManager { + + private static final Logger LOGGER = Logger.getLogger("BridgeManager"); + + private final Map, PluginBridge> bridges = new HashMap<>(); + + /** + * Registers and tries to initialize the bridge. + */ + public void register(PluginBridge bridge) { + String pluginName = bridge.getPluginName(); + + Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginName); + if (plugin != null && plugin.isEnabled()) { + try { + bridge.onEnable(plugin); + bridge.setStatus(BridgeStatus.ENABLED); + LOGGER.info("Bridge enabled: " + pluginName); + } catch (Exception e) { + bridge.setStatus(BridgeStatus.FAILED); + LOGGER.warning("Failed to enable bridge for " + pluginName + ": " + e.getMessage()); + } + } else { + bridge.setStatus(BridgeStatus.MISSING); + LOGGER.info("Bridge missing: " + pluginName); + } + + bridges.put(bridge.getClass(), bridge); + } + + /** + * Returns true if a bridge is available and enabled. + */ + public boolean isAvailable(String pluginName) { + return bridges.values().stream() + .anyMatch(b -> b.getPluginName().equalsIgnoreCase(pluginName) && b.isEnabled()); + } + + /** + * Gets the bridge by class if registered. + */ + @SuppressWarnings("unchecked") + public Optional get(Class type) { + return Optional.ofNullable((T) bridges.get(type)); + } + + /** + * Calls onDisable on all loaded bridges. + */ + public void shutdown() { + for (PluginBridge bridge : bridges.values()) { + if (bridge.isEnabled()) { + bridge.onDisable(); + } + } + bridges.clear(); + } +} diff --git a/src/main/java/gg/nextforge/bridge/BridgeStatus.java b/src/main/java/gg/nextforge/bridge/BridgeStatus.java new file mode 100644 index 0000000..97c99f8 --- /dev/null +++ b/src/main/java/gg/nextforge/bridge/BridgeStatus.java @@ -0,0 +1,27 @@ +package gg.nextforge.bridge; + +/** + * Represents the current status of a plugin bridge. + */ +public enum BridgeStatus { + + /** Plugin not found or not supported */ + MISSING, + + /** Plugin found but initialization failed */ + FAILED, + + /** Plugin found and bridge initialized successfully */ + ENABLED, + + /** Bridge not yet initialized */ + UNINITIALIZED; + + public boolean isReady() { + return this == ENABLED; + } + + public boolean isMissingOrFailed() { + return this == MISSING || this == FAILED; + } +} diff --git a/src/main/java/gg/nextforge/bridge/PluginBridge.java b/src/main/java/gg/nextforge/bridge/PluginBridge.java new file mode 100644 index 0000000..5d9d16d --- /dev/null +++ b/src/main/java/gg/nextforge/bridge/PluginBridge.java @@ -0,0 +1,45 @@ +package gg.nextforge.bridge; + +import gg.nextforge.bridge.BridgeStatus; +import org.bukkit.plugin.Plugin; + +/** + * Base class for defining plugin bridges. + * Handles lifecycle and dependency check. + */ +public abstract class PluginBridge { + + private BridgeStatus status = BridgeStatus.UNINITIALIZED; + + /** + * Name of the external plugin this bridge supports. + */ + public abstract String getPluginName(); + + /** + * Called if the plugin is available and the bridge is loaded. + * @param plugin the detected plugin instance + */ + public abstract void onEnable(Plugin plugin); + + /** + * Called when the bridge should shut down or disconnect. + */ + public void onDisable() {} + + public final void setStatus(BridgeStatus status) { + this.status = status; + } + + public final BridgeStatus getStatus() { + return status; + } + + public final boolean isEnabled() { + return status == BridgeStatus.ENABLED; + } + + public final boolean isFailed() { + return status == BridgeStatus.FAILED; + } +} diff --git a/src/main/java/gg/nextforge/bridge/annotations/Bridge.java b/src/main/java/gg/nextforge/bridge/annotations/Bridge.java new file mode 100644 index 0000000..2b8b60e --- /dev/null +++ b/src/main/java/gg/nextforge/bridge/annotations/Bridge.java @@ -0,0 +1,24 @@ +package gg.nextforge.bridge.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a PluginBridge for automatic discovery and registration. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Bridge { + + /** + * Whether to enable this bridge by default. + */ + boolean enabled() default true; + + /** + * Optional description of the bridge. + */ + String description() default ""; +} diff --git a/src/main/java/gg/nextforge/bridge/impl/LuckPermsBridge.java b/src/main/java/gg/nextforge/bridge/impl/LuckPermsBridge.java new file mode 100644 index 0000000..94e436b --- /dev/null +++ b/src/main/java/gg/nextforge/bridge/impl/LuckPermsBridge.java @@ -0,0 +1,55 @@ +package gg.nextforge.bridge; + +import gg.nextforge.bridge.annotations.Bridge; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.Node; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +/** + * Integration bridge for LuckPerms. + */ +@Bridge(description = "Integration with LuckPerms") +public class LuckPermsBridge extends PluginBridge { + + private LuckPerms api; + + @Override + public String getPluginName() { + return "LuckPerms"; + } + + @Override + public void onEnable(Plugin plugin) { + this.api = LuckPermsProvider.get(); + } + + public LuckPerms getApi() { + return api; + } + + public boolean hasPermission(Player player, String permissionNode) { + User user = api.getUserManager().getUser(player.getUniqueId()); + if (user == null) return false; + + return user.getCachedData().getPermissionData().checkPermission(permissionNode).asBoolean(); + } + + public void addPermission(Player player, String permissionNode) { + User user = api.getUserManager().getUser(player.getUniqueId()); + if (user != null) { + user.data().add(Node.builder(permissionNode).build()); + api.getUserManager().saveUser(user); + } + } + + public void removePermission(Player player, String permissionNode) { + User user = api.getUserManager().getUser(player.getUniqueId()); + if (user != null) { + user.data().remove(Node.builder(permissionNode).build()); + api.getUserManager().saveUser(user); + } + } +} diff --git a/src/main/java/gg/nextforge/bridge/impl/PlaceholderAPIBridge.java b/src/main/java/gg/nextforge/bridge/impl/PlaceholderAPIBridge.java new file mode 100644 index 0000000..59c7c6c --- /dev/null +++ b/src/main/java/gg/nextforge/bridge/impl/PlaceholderAPIBridge.java @@ -0,0 +1,33 @@ +package gg.nextforge.bridge.impl; + +import gg.nextforge.bridge.PluginBridge; +import gg.nextforge.bridge.annotations.Bridge; +import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +/** + * Integration bridge for PlaceholderAPI. + */ +@Bridge(description = "Integration with PlaceholderAPI") +public class PlaceholderAPIBridge extends PluginBridge { + + @Override + public String getPluginName() { + return "PlaceholderAPI"; + } + + @Override + public void onEnable(Plugin plugin) { + // Optional setup if needed + } + + public String setPlaceholder(Player player, String placeholder) { + return PlaceholderAPI.setPlaceholders(player, placeholder); + } + + public String setPlaceholder(OfflinePlayer offlinePlayer, String placeholder) { + return PlaceholderAPI.setPlaceholders(offlinePlayer, placeholder); + } +} diff --git a/src/main/java/gg/nextforge/bridge/impl/VaultBridge.java b/src/main/java/gg/nextforge/bridge/impl/VaultBridge.java new file mode 100644 index 0000000..9b83054 --- /dev/null +++ b/src/main/java/gg/nextforge/bridge/impl/VaultBridge.java @@ -0,0 +1,62 @@ +package gg.nextforge.bridge.impl; + +import gg.nextforge.bridge.PluginBridge; +import gg.nextforge.bridge.annotations.Bridge; +import net.milkbowl.vault.chat.Chat; +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.permission.Permission; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredServiceProvider; + +/** + * Integration bridge for Vault (Economy, Chat, Permissions). + */ +@Bridge(description = "Integration with Vault") +public class VaultBridge extends PluginBridge { + + private Economy economy; + private Permission permission; + private Chat chat; + + @Override + public String getPluginName() { + return "Vault"; + } + + @Override + public void onEnable(Plugin plugin) { + this.economy = getProvider(Economy.class); + this.permission = getProvider(Permission.class); + this.chat = getProvider(Chat.class); + } + + private T getProvider(Class clazz) { + RegisteredServiceProvider provider = Bukkit.getServicesManager().getRegistration(clazz); + return provider != null ? provider.getProvider() : null; + } + + public Economy getEconomy() { + return economy; + } + + public Permission getPermission() { + return permission; + } + + public Chat getChat() { + return chat; + } + + public boolean isEconomyAvailable() { + return economy != null; + } + + public boolean isPermissionAvailable() { + return permission != null; + } + + public boolean isChatAvailable() { + return chat != null; + } +} diff --git a/src/main/java/gg/nextforge/io/BackupHandler.java b/src/main/java/gg/nextforge/io/BackupHandler.java new file mode 100644 index 0000000..11b6e5e --- /dev/null +++ b/src/main/java/gg/nextforge/io/BackupHandler.java @@ -0,0 +1,42 @@ +package gg.nextforge.io; + +import java.io.IOException; +import java.nio.file.*; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BackupHandler { + + private static final Logger LOGGER = Logger.getLogger("BackupHandler"); + private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss"); + + private final Path backupDirectory; + + public BackupHandler(Path backupDirectory) { + this.backupDirectory = backupDirectory; + FileService.ensureDirectoryExists(backupDirectory); + } + + public boolean backupFile(Path originalFile) { + if (!Files.exists(originalFile)) { + LOGGER.warning("Cannot backup missing file: " + originalFile); + return false; + } + + String timestamp = TIMESTAMP_FORMAT.format(new Date()); + String filename = originalFile.getFileName().toString(); + String backupFilename = filename + "." + timestamp + ".bak"; + Path target = backupDirectory.resolve(backupFilename); + + try { + Files.copy(originalFile, target, StandardCopyOption.REPLACE_EXISTING); + LOGGER.info("Backup created: " + target); + return true; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to backup file: " + originalFile, e); + return false; + } + } +} diff --git a/src/main/java/gg/nextforge/io/DirectoryWatcher.java b/src/main/java/gg/nextforge/io/DirectoryWatcher.java new file mode 100644 index 0000000..15d42fe --- /dev/null +++ b/src/main/java/gg/nextforge/io/DirectoryWatcher.java @@ -0,0 +1,69 @@ +package gg.nextforge.io; + +import java.io.IOException; +import java.nio.file.*; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DirectoryWatcher { + + private static final Logger LOGGER = Logger.getLogger("DirectoryWatcher"); + + private final Path directory; + private final WatchService watchService; + private Thread watcherThread; + private boolean running = false; + + public DirectoryWatcher(Path directory) throws IOException { + this.directory = directory; + this.watchService = FileSystems.getDefault().newWatchService(); + } + + public void startWatching(Consumer> onEvent) { + if (running) return; + + running = true; + + try { + directory.register( + watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE + ); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to register directory watcher for: " + directory, e); + return; + } + + watcherThread = new Thread(() -> { + while (running) { + try { + WatchKey key = watchService.take(); + for (WatchEvent event : key.pollEvents()) { + if (event.context() instanceof Path path) { + onEvent.accept((WatchEvent) event); + } + } + key.reset(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.warning("Directory watcher interrupted."); + } + } + }, "DirectoryWatcher-" + directory.getFileName()); + + watcherThread.setDaemon(true); + watcherThread.start(); + } + + public void stopWatching() { + running = false; + try { + watchService.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to stop directory watcher.", e); + } + } +} diff --git a/src/main/java/gg/nextforge/io/FileCache.java b/src/main/java/gg/nextforge/io/FileCache.java new file mode 100644 index 0000000..e24890b --- /dev/null +++ b/src/main/java/gg/nextforge/io/FileCache.java @@ -0,0 +1,62 @@ +package gg.nextforge.io; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class FileCache { + + private static final Logger LOGGER = Logger.getLogger("FileCache"); + + private final Map cache = new ConcurrentHashMap<>(); + private final Function loader; + private final DirectoryWatcher watcher; + + public FileCache(Path directory, Function loader) throws IOException { + this.loader = loader; + this.watcher = new DirectoryWatcher(directory); + + // Automatically refresh cache on file change + this.watcher.startWatching(event -> { + Path path = directory.resolve(event.context()); + switch (event.kind().name()) { + case "ENTRY_DELETE" -> cache.remove(path); + case "ENTRY_MODIFY", "ENTRY_CREATE" -> loadFile(path); + } + }); + } + + public T get(Path path) { + return cache.computeIfAbsent(path, this::loadFile); + } + + public boolean isCached(Path path) { + return cache.containsKey(path); + } + + public void clear() { + cache.clear(); + } + + public void shutdown() { + watcher.stopWatching(); + } + + private T loadFile(Path path) { + try { + T result = loader.apply(path); + if (result != null) { + cache.put(path, result); + LOGGER.info("FileCache: Loaded " + path); + } + return result; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to load file into cache: " + path, e); + return null; + } + } +} diff --git a/src/main/java/gg/nextforge/io/FileService.java b/src/main/java/gg/nextforge/io/FileService.java new file mode 100644 index 0000000..a09eda2 --- /dev/null +++ b/src/main/java/gg/nextforge/io/FileService.java @@ -0,0 +1,70 @@ +package gg.nextforge.io; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class FileService { + + private static final Logger LOGGER = Logger.getLogger("FileService"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public static boolean ensureDirectoryExists(Path dir) { + if (!Files.exists(dir)) { + try { + Files.createDirectories(dir); + return true; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to create directory: " + dir, e); + } + } + return false; + } + + public static boolean writeJson(Path file, Object data) { + try (Writer writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { + GSON.toJson(data, writer); + return true; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to write JSON: " + file, e); + return false; + } + } + + public static Optional readJson(Path file, Class type) { + if (!Files.exists(file)) return Optional.empty(); + try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + return Optional.ofNullable(GSON.fromJson(reader, type)); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to read JSON: " + file, e); + return Optional.empty(); + } + } + + public static boolean copyFile(Path source, Path target, boolean overwrite) { + try { + if (overwrite || !Files.exists(target)) { + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); + return true; + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to copy file: " + source + " -> " + target, e); + } + return false; + } + + public static boolean deleteFile(Path file) { + try { + return Files.deleteIfExists(file); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to delete file: " + file, e); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/gg/nextforge/io/YamlFileHandler.java b/src/main/java/gg/nextforge/io/YamlFileHandler.java new file mode 100644 index 0000000..d0b5214 --- /dev/null +++ b/src/main/java/gg/nextforge/io/YamlFileHandler.java @@ -0,0 +1,51 @@ +package gg.nextforge.io; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class YamlFileHandler { + + private static final Logger LOGGER = Logger.getLogger("YamlFileHandler"); + + private static final DumperOptions OPTIONS = new DumperOptions(); + private static final Yaml YAML; + + static { + OPTIONS.setIndent(2); + OPTIONS.setPrettyFlow(true); + OPTIONS.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + YAML = new Yaml(OPTIONS); + } + + public static Optional load(Path file, Class type) { + if (!Files.exists(file)) return Optional.empty(); + + try (InputStream input = Files.newInputStream(file)) { + Yaml yaml = new Yaml(new Constructor(type, new LoaderOptions())); + T obj = yaml.load(input); + return Optional.ofNullable(obj); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to load YAML: " + file, e); + return Optional.empty(); + } + } + + public static boolean save(Path file, Object data) { + try (Writer writer = Files.newBufferedWriter(file)) { + YAML.dump(data, writer); + return true; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to save YAML: " + file, e); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index ca76e8b..738ec90 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -6,4 +6,5 @@ api-version: 1.19 depend: - PlaceholderAPI softdepend: + - Vault - LuckPerms From 65178d076669d4099e4845f4067c251f01b59ed9 Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 21:08:55 +0200 Subject: [PATCH 17/18] Add backup and file management utilities with BackupHandler, DirectoryWatcher, FileCache, and FileService --- .../discord/DiscordEmbedBuilder.java | 102 ++++++++++++++++++ .../discord/DiscordWebhookSender.java | 92 ++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/main/java/gg/nextforge/discord/DiscordEmbedBuilder.java create mode 100644 src/main/java/gg/nextforge/discord/DiscordWebhookSender.java diff --git a/src/main/java/gg/nextforge/discord/DiscordEmbedBuilder.java b/src/main/java/gg/nextforge/discord/DiscordEmbedBuilder.java new file mode 100644 index 0000000..4be2142 --- /dev/null +++ b/src/main/java/gg/nextforge/discord/DiscordEmbedBuilder.java @@ -0,0 +1,102 @@ +package gg.nextforge.discord; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Builder for Discord embed objects. + */ +public class DiscordEmbedBuilder { + + private final Map fields = new LinkedHashMap<>(); + + public DiscordEmbedBuilder setTitle(String title) { + fields.put("title", title); + return this; + } + + public DiscordEmbedBuilder setDescription(String description) { + fields.put("description", description); + return this; + } + + public DiscordEmbedBuilder setUrl(String url) { + fields.put("url", url); + return this; + } + + public DiscordEmbedBuilder setColor(int rgb) { + fields.put("color", rgb); + return this; + } + + public DiscordEmbedBuilder setFooter(String text, String iconUrl) { + Map footer = new LinkedHashMap<>(); + footer.put("text", text); + if (iconUrl != null) footer.put("icon_url", iconUrl); + fields.put("footer", footer); + return this; + } + + public DiscordEmbedBuilder setImage(String imageUrl) { + Map image = new LinkedHashMap<>(); + image.put("url", imageUrl); + fields.put("image", image); + return this; + } + + public DiscordEmbedBuilder setThumbnail(String url) { + Map thumbnail = new LinkedHashMap<>(); + thumbnail.put("url", url); + fields.put("thumbnail", thumbnail); + return this; + } + + public DiscordEmbedBuilder setAuthor(String name, String url, String iconUrl) { + Map author = new LinkedHashMap<>(); + author.put("name", name); + if (url != null) author.put("url", url); + if (iconUrl != null) author.put("icon_url", iconUrl); + fields.put("author", author); + return this; + } + + public String build() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + + boolean first = true; + for (Map.Entry entry : fields.entrySet()) { + if (!first) sb.append(","); + sb.append("\"").append(entry.getKey()).append("\":"); + sb.append(toJson(entry.getValue())); + first = false; + } + + sb.append("}"); + return sb.toString(); + } + + @SuppressWarnings("unchecked") + private String toJson(Object value) { + if (value instanceof String) { + return "\"" + escape((String) value) + "\""; + } else if (value instanceof Map) { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : ((Map) value).entrySet()) { + if (!first) sb.append(","); + sb.append("\"").append(entry.getKey()).append("\":\"").append(escape(entry.getValue())).append("\""); + first = false; + } + sb.append("}"); + return sb.toString(); + } else { + return String.valueOf(value); + } + } + + private String escape(String input) { + return input.replace("\"", "\\\"").replace("\n", "\\n"); + } +} diff --git a/src/main/java/gg/nextforge/discord/DiscordWebhookSender.java b/src/main/java/gg/nextforge/discord/DiscordWebhookSender.java new file mode 100644 index 0000000..6a4389f --- /dev/null +++ b/src/main/java/gg/nextforge/discord/DiscordWebhookSender.java @@ -0,0 +1,92 @@ +package gg.nextforge.discord; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A simple Discord webhook sender with message builder support. + */ +public class DiscordWebhookSender { + + private static final Logger LOGGER = Logger.getLogger("DiscordWebhookSender"); + + private final String webhookUrl; + + public DiscordWebhookSender(String webhookUrl) { + this.webhookUrl = webhookUrl; + } + + public void send(DiscordMessage message) { + sendPayload(message.toJson()); + } + + private void sendPayload(String json) { + try { + URL url = new URL(webhookUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + + try (OutputStream os = connection.getOutputStream()) { + os.write(json.getBytes(StandardCharsets.UTF_8)); + } + + int responseCode = connection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_NO_CONTENT && responseCode != HttpURLConnection.HTTP_OK) { + LOGGER.warning("Discord webhook failed with response code: " + responseCode); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to send Discord webhook", e); + } + } + + // Builder class for messages + public static class DiscordMessage { + private String content; + private final List embeds = new ArrayList<>(); + + public DiscordMessage setContent(String content) { + this.content = content; + return this; + } + + public DiscordMessage addEmbed(String embedJson) { + this.embeds.add(embedJson); + return this; + } + + public String toJson() { + StringBuilder builder = new StringBuilder(); + builder.append("{"); + + if (content != null) { + builder.append("\"content\":\"").append(escape(content)).append("\""); + } + + if (!embeds.isEmpty()) { + if (content != null) builder.append(","); + builder.append("\"embeds\":["); + for (int i = 0; i < embeds.size(); i++) { + builder.append(embeds.get(i)); + if (i < embeds.size() - 1) builder.append(","); + } + builder.append("]"); + } + + builder.append("}"); + return builder.toString(); + } + + private String escape(String input) { + return input.replace("\"", "\\\"").replace("\n", "\\n"); + } + } +} From 5b3b832dcb684dbec9618da6644047b20d61b4d3 Mon Sep 17 00:00:00 2001 From: maxim Date: Tue, 22 Jul 2025 21:09:21 +0200 Subject: [PATCH 18/18] Update version to 2.3 in build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e749525..b87f2a0 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { } group = 'gg.nextforge' -version = '2.2' +version = '2.3' repositories { mavenCentral()