diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4faa735..60a216a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: include: - # 1.19.2 / 1.20.1 — Java 21 (plugin requires Java 21+; lolib jar is 21 bytecode) + # 1.19.2 / 1.20.1 — Java 21 (plugin requires Java 21+) - { mc: "1.19.2", coordinate: "io.papermc.paper:paper-api:1.19.2-R0.1-SNAPSHOT", java: 21, release: 21, api: "1.19" } - { mc: "1.20.1", coordinate: "io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT", java: 21, release: 21, api: "1.20" } # 1.21 — Java 21 diff --git a/.gitignore b/.gitignore index 47399f1..57646d9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ # Package Files *.jar -!libs/*.jar *.war *.nar *.ear @@ -64,9 +63,6 @@ test-output/ # Modrinth files (local only) modrinth/ -# LoLib API Documentation (не нужно в репозитории) -api_docs/ - # Project documentation (local only) TODO.md diff --git a/build.gradle.kts b/build.gradle.kts index 30b5586..331ef3a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -182,9 +182,6 @@ dependencies { compileOnly("net.kyori:adventure-api:4.14.0") compileOnly("net.kyori:adventure-text-minimessage:4.14.0") - // LoLib Core - implementation(files("libs/lolib-3.0.0.jar")) - // Shaded dependencies implementation("io.papermc:paperlib:1.0.7") implementation("com.google.code.gson:gson:2.11.0") @@ -222,18 +219,10 @@ tasks.shadowJar { archiveFileName.set("LoParkour-${project.version}-mc${mcVersion}.jar") // Relocate shaded dependencies - relocate("dev.lolib", "dev.loki.loparkour.lib.lolib") relocate("io.papermc.lib", "dev.loki.loparkour.lib.paperlib") relocate("com.google.gson", "dev.loki.loparkour.lib.gson") relocate("org.bstats", "dev.loki.loparkour.lib.bstats") relocate("org.apache.commons.math3", "dev.loki.loparkour.lib.commonsmath3") - - // Don't minimize - causes issues with Caffeine's dynamically generated classes - // minimize { - // exclude(dependency("com.zaxxer:HikariCP:.*")) - // exclude(dependency("com.mysql:mysql-connector-j:.*")) - // exclude(dependency(files("libs/lolib-3.0.0.jar"))) - // } } tasks.named("build") { diff --git a/libs/lolib-3.0.0.jar b/libs/lolib-3.0.0.jar deleted file mode 100644 index 3400b76..0000000 Binary files a/libs/lolib-3.0.0.jar and /dev/null differ diff --git a/src/main/java/dev/loki/loparkour/LoParkour.java b/src/main/java/dev/loki/loparkour/LoParkour.java index 53f3fd9..890f279 100644 --- a/src/main/java/dev/loki/loparkour/LoParkour.java +++ b/src/main/java/dev/loki/loparkour/LoParkour.java @@ -11,9 +11,9 @@ import dev.loki.loparkour.schematic.core.SchematicManager; import dev.loki.loparkour.storage.Storage; import dev.loki.loparkour.world.core.Divider; import dev.loki.loparkour.world.core.World; -import dev.lolib.core.LoPlugin; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,7 +26,7 @@ import java.util.logging.Level; * * @author loki */ -public final class LoParkour extends LoPlugin { +public final class LoParkour extends JavaPlugin { public static final String NAME = "&#FF6464&lLoParkour&r"; public static final String PREFIX = NAME + " 񢩈» &#A0A0A0"; @@ -87,13 +87,13 @@ public final class LoParkour extends LoPlugin { } @Override - public void enable() { + public void onEnable() { instance = this; PluginBootstrap.enable(this); } @Override - public void disable() { + public void onDisable() { PluginBootstrap.disable(this); } diff --git a/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java b/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java index 7eebebd..33018bc 100644 --- a/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java +++ b/src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java @@ -36,8 +36,10 @@ import org.bstats.charts.SimplePie; import org.bstats.charts.SingleLineChart; import org.jetbrains.annotations.NotNull; +import java.util.logging.Level; + /** - * Wires plugin subsystems during {@link LoParkour#enable()} and {@link LoParkour#disable()}. + * Wires plugin subsystems during {@link LoParkour#onEnable()} and {@link LoParkour#onDisable()}. */ public final class PluginBootstrap { @@ -117,16 +119,16 @@ public final class PluginBootstrap { return; } try { - plugin.loLogger().info("Creating parkour world..."); + plugin.getLogger().info("Creating parkour world..."); World.create(); - plugin.loLogger().info("Parkour world created successfully!"); + plugin.getLogger().info("Parkour world created successfully!"); } catch (Exception ex) { - plugin.loLogger().error("Failed to create parkour world", ex); + plugin.getLogger().log(Level.SEVERE, "Failed to create parkour world", ex); } } private static void registerEventsAndCommands(@NotNull LoParkour plugin) { - dev.lolib.gui.GUIManager.init(plugin); + dev.loki.loparkour.menu.gui.inventory.GuiManager.init(plugin); final var pm = plugin.getServer().getPluginManager(); pm.registerEvents(new PlayerConnectionListener(), plugin); diff --git a/src/main/java/dev/loki/loparkour/ghost/core/GhostPlayer.java b/src/main/java/dev/loki/loparkour/ghost/core/GhostPlayer.java index 367cb88..ff1239e 100644 --- a/src/main/java/dev/loki/loparkour/ghost/core/GhostPlayer.java +++ b/src/main/java/dev/loki/loparkour/ghost/core/GhostPlayer.java @@ -3,8 +3,8 @@ package dev.loki.loparkour.ghost.core; import dev.loki.loparkour.ghost.model.GhostFrame; import dev.loki.loparkour.ghost.model.GhostData; -import dev.lolib.scheduler.Scheduler; -import dev.lolib.scheduler.ScheduledTask; +import dev.loki.loparkour.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.ScheduledTask; import dev.loki.loparkour.LoParkour; import org.bukkit.Location; import org.bukkit.World; @@ -17,9 +17,9 @@ import org.jetbrains.annotations.NotNull; * *

Bugs fixed vs original: *

*/ public class GhostPlayer { diff --git a/src/main/java/dev/loki/loparkour/ghost/service/GhostReplay.java b/src/main/java/dev/loki/loparkour/ghost/service/GhostReplay.java index 5fdbc6f..8768fb0 100644 --- a/src/main/java/dev/loki/loparkour/ghost/service/GhostReplay.java +++ b/src/main/java/dev/loki/loparkour/ghost/service/GhostReplay.java @@ -5,8 +5,8 @@ import dev.loki.loparkour.ghost.model.GhostData; import dev.loki.loparkour.ghost.model.GhostFrame; import dev.loki.loparkour.ghost.replay.PlayerGhost; import dev.loki.loparkour.player.core.ParkourUser; -import dev.lolib.scheduler.ScheduledTask; -import dev.lolib.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.ScheduledTask; +import dev.loki.loparkour.scheduler.Scheduler; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; diff --git a/src/main/java/dev/loki/loparkour/leaderboard/persistence/LeaderboardStorage.java b/src/main/java/dev/loki/loparkour/leaderboard/persistence/LeaderboardStorage.java index e6fb551..ac392c5 100644 --- a/src/main/java/dev/loki/loparkour/leaderboard/persistence/LeaderboardStorage.java +++ b/src/main/java/dev/loki/loparkour/leaderboard/persistence/LeaderboardStorage.java @@ -6,7 +6,7 @@ import dev.loki.loparkour.leaderboard.core.LeaderboardSorter; import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.config.core.Config; import dev.loki.loparkour.storage.Storage; -import dev.lolib.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.Scheduler; import org.jetbrains.annotations.NotNull; import java.util.Map; diff --git a/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java b/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java index aa7c4c3..3ae8a2e 100644 --- a/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java +++ b/src/main/java/dev/loki/loparkour/listener/gameplay/ParkourRestrictionListener.java @@ -142,7 +142,7 @@ public class ParkourRestrictionListener implements Listener { return; } if (event.getInventory().getType() == InventoryType.CHEST) { - return; // LoLib GUI + return; // menu GUI } if (event.getInventory().getType() == InventoryType.CRAFTING) { return; // own inv diff --git a/src/main/java/dev/loki/loparkour/menu/community/LeaderboardsMenu.java b/src/main/java/dev/loki/loparkour/menu/community/LeaderboardsMenu.java index 93a386f..9bae19b 100644 --- a/src/main/java/dev/loki/loparkour/menu/community/LeaderboardsMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/community/LeaderboardsMenu.java @@ -7,7 +7,7 @@ import dev.loki.loparkour.menu.core.LPMenu; import dev.loki.loparkour.menu.core.Menus; import dev.loki.loparkour.menu.core.screen.ParkourOption; import dev.loki.loparkour.mode.base.Mode; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -58,7 +58,7 @@ public class LeaderboardsMenu extends LPMenu { final int startIndex = currentPage * ITEMS_PER_PAGE; final int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, items.size()); - InventoryGUI gui = baseGui(title, ROWS); + InventoryGui gui = baseGui(title, ROWS); // Add items for current page for (int i = startIndex; i < endIndex; i++) { diff --git a/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java b/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java index 06a47c2..1d9d177 100644 --- a/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/community/SingleLeaderboardMenu.java @@ -7,7 +7,7 @@ import dev.loki.loparkour.menu.core.LPMenu; import dev.loki.loparkour.menu.core.screen.ParkourOption; import dev.loki.loparkour.mode.base.Mode; import dev.loki.loparkour.player.core.ParkourUser; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; @@ -49,7 +49,7 @@ public class SingleLeaderboardMenu extends LPMenu { final int startIndex = currentPage * ITEMS_PER_PAGE; final int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, items.size()); - InventoryGUI gui = baseGui(title, ROWS); + InventoryGui gui = baseGui(title, ROWS); for (int i = startIndex; i < endIndex; i++) { final int slot = FIRST_SLOT + (i - startIndex); // Slots 10-16 diff --git a/src/main/java/dev/loki/loparkour/menu/core/LPMenu.java b/src/main/java/dev/loki/loparkour/menu/core/LPMenu.java index 68e92ff..f6d02a9 100644 --- a/src/main/java/dev/loki/loparkour/menu/core/LPMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/core/LPMenu.java @@ -3,7 +3,7 @@ package dev.loki.loparkour.menu.core; import dev.loki.loparkour.config.locale.Locales; import dev.loki.loparkour.config.options.Option; import dev.loki.loparkour.util.text.ColorUtil; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -14,13 +14,11 @@ import org.jetbrains.annotations.NotNull; /** * Base class for all LoParkour menus. * - * Real LoLib API: - * InventoryGUI.create().title(String).rows(n).fill(item).setItem(slot, item, Consumer<InventoryClickEvent>).open(player) - * ScrollableGUI.create().title(String).rows(n).addItem(item, Consumer<InventoryClickEvent>) - * .scrollUpButton(item,slot).scrollDownButton(item,slot).open(player) + * Menu API ({@link InventoryGui}): + * InventoryGui.create().title(String).rows(n).fill(item) + * .setItem(slot, item, Consumer<InventoryClickEvent>).open(player) * - * cancelAllClicks = true by default — items cannot be taken out automatically. - * Handlers receive Consumer<InventoryClickEvent>, event is already cancelled before handler runs. + * Every click is cancelled before the handler runs — items cannot be taken out. */ public abstract class LPMenu { @@ -74,8 +72,8 @@ public abstract class LPMenu { * Create a base GUI filled with filler items. * Uses .fill() — only fills empty slots, so setItem calls after this override correctly. */ - protected InventoryGUI baseGui(String title, int rows) { - return InventoryGUI.create() + protected InventoryGui baseGui(String title, int rows) { + return InventoryGui.create() .title(ColorUtil.color(title)) .rows(rows) .fill(filler()); diff --git a/src/main/java/dev/loki/loparkour/menu/core/MenuStub.java b/src/main/java/dev/loki/loparkour/menu/core/MenuStub.java index a61b8e3..665740c 100644 --- a/src/main/java/dev/loki/loparkour/menu/core/MenuStub.java +++ b/src/main/java/dev/loki/loparkour/menu/core/MenuStub.java @@ -1,7 +1,7 @@ package dev.loki.loparkour.menu.core; /** - * Placeholder during menu migration; real flows use {@link Menus} / LoLib GUI. + * Placeholder during menu migration; real flows use {@link Menus} / InventoryGui. */ public final class MenuStub { private MenuStub() {} diff --git a/src/main/java/dev/loki/loparkour/menu/core/screen/DynamicMenu.java b/src/main/java/dev/loki/loparkour/menu/core/screen/DynamicMenu.java index 5e82849..db24984 100644 --- a/src/main/java/dev/loki/loparkour/menu/core/screen/DynamicMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/core/screen/DynamicMenu.java @@ -29,7 +29,7 @@ public abstract class DynamicMenu { * @param item The item * @param shouldDisplay Whether this item should be displayed right now. */ - // Stub: migrate to LoLib GUI when dynamic rows are wired up. + // Stub: migrate to InventoryGui when dynamic rows are wired up. /* public void registerMainItem( int row, int id, @@ -55,7 +55,7 @@ public abstract class DynamicMenu { * @param player The player * @param menu The menu */ - // Stub: migrate to LoLib GUI when dynamic rows are wired up. + // Stub: migrate to InventoryGui when dynamic rows are wired up. /* public void display(Player player, Menu menu) { ParkourUser user = ParkourUser.getUser(player); diff --git a/src/main/java/dev/loki/loparkour/menu/core/screen/MainMenu.java b/src/main/java/dev/loki/loparkour/menu/core/screen/MainMenu.java index 18ff89a..6372588 100644 --- a/src/main/java/dev/loki/loparkour/menu/core/screen/MainMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/core/screen/MainMenu.java @@ -5,7 +5,7 @@ import dev.loki.loparkour.config.locale.Locales; import dev.loki.loparkour.menu.core.LPMenu; import dev.loki.loparkour.menu.core.Menus; import dev.loki.loparkour.player.core.ParkourUser; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -51,7 +51,7 @@ public class MainMenu extends LPMenu { final int[] slots = distributeEvenly(count, SLOT_START, SLOT_END); - InventoryGUI gui = baseGui(title, ROWS); + InventoryGui gui = baseGui(title, ROWS); for (int i = 0; i < count; i++) { final int idx = i; diff --git a/src/main/java/dev/loki/loparkour/menu/gui/Menu.java b/src/main/java/dev/loki/loparkour/menu/gui/Menu.java deleted file mode 100644 index df26abc..0000000 --- a/src/main/java/dev/loki/loparkour/menu/gui/Menu.java +++ /dev/null @@ -1,30 +0,0 @@ -package dev.loki.loparkour.menu.gui; - -import dev.loki.loparkour.config.locale.Locales; -import dev.loki.loparkour.util.item.Item; -import org.bukkit.entity.Player; - -/** - * Legacy menu stub — real menus use LoLib {@link dev.lolib.gui.InventoryGUI}. - */ -public class Menu { - private final int rows; - private final String title; - - public Menu(int rows, String title) { - this.rows = rows; - this.title = title; - } - - public Menu item(int slot, Item item) { - return this; - } - - public Menu distributeRowsEvenly() { - return this; - } - - public void open(Player player) { - Locales.send(player, "other.menu_unavailable", title, rows); - } -} diff --git a/src/main/java/dev/loki/loparkour/menu/gui/PagedMenu.java b/src/main/java/dev/loki/loparkour/menu/gui/PagedMenu.java index 326b3b5..ccc7fc5 100644 --- a/src/main/java/dev/loki/loparkour/menu/gui/PagedMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/gui/PagedMenu.java @@ -7,7 +7,7 @@ import org.bukkit.entity.Player; import java.util.List; /** - * Legacy paged menu stub — real menus use LoLib GUI. + * Legacy paged menu stub — real menus use InventoryGui. */ public class PagedMenu { private final int rows; diff --git a/src/main/java/dev/loki/loparkour/menu/gui/inventory/GuiManager.java b/src/main/java/dev/loki/loparkour/menu/gui/inventory/GuiManager.java new file mode 100644 index 0000000..e67dcd1 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/menu/gui/inventory/GuiManager.java @@ -0,0 +1,72 @@ +package dev.loki.loparkour.menu.gui.inventory; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.plugin.Plugin; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Routes inventory clicks and closes to the {@link InventoryGui} opened for a player. + * One GUI per player UUID; entries are dropped on close and on quit. + */ +public final class GuiManager implements Listener { + + private static final Map ACTIVE = new ConcurrentHashMap<>(); + + private GuiManager() { + } + + /** + * Registers the click/close listener. Safe to call once during plugin enable. + * + * @param plugin the owning plugin + */ + public static void init(Plugin plugin) { + plugin.getServer().getPluginManager().registerEvents(new GuiManager(), plugin); + } + + static void register(Player player, InventoryGui gui, Inventory inventory) { + ACTIVE.put(player.getUniqueId(), new OpenGui(gui, inventory)); + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + final OpenGui open = ACTIVE.get(player.getUniqueId()); + if (open == null || !open.inventory().equals(event.getInventory())) { + return; + } + open.gui().handleClick(event); + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (!(event.getPlayer() instanceof Player player)) { + return; + } + final OpenGui open = ACTIVE.get(player.getUniqueId()); + if (open == null || !open.inventory().equals(event.getInventory())) { + return; + } + ACTIVE.remove(player.getUniqueId()); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + ACTIVE.remove(event.getPlayer().getUniqueId()); + } + + /** The GUI a player currently has open, together with its inventory. */ + private record OpenGui(InventoryGui gui, Inventory inventory) { + } +} diff --git a/src/main/java/dev/loki/loparkour/menu/gui/inventory/InventoryGui.java b/src/main/java/dev/loki/loparkour/menu/gui/inventory/InventoryGui.java new file mode 100644 index 0000000..8772268 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/menu/gui/inventory/InventoryGui.java @@ -0,0 +1,135 @@ +package dev.loki.loparkour.menu.gui.inventory; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Inventory menu built fluently: fill background, place items with click handlers, + * then open it for a player. Clicks are always cancelled, so menu items cannot be + * taken out; only the registered handlers run. + * + *

Replaces the LoLib {@code InventoryGUI} (TODO, Задача 1). Handler dispatch lives + * in {@link GuiManager}, which tracks the inventory opened for each player. + */ +public final class InventoryGui { + + private static final int SLOTS_PER_ROW = 9; + private static final int MIN_ROWS = 1; + private static final int MAX_ROWS = 6; + private static final String DEFAULT_TITLE = "GUI"; + + private final Map items = new HashMap<>(); + + private String title = DEFAULT_TITLE; + private int rows = MIN_ROWS; + + private InventoryGui() { + } + + /** + * @return a new empty menu + */ + public static InventoryGui create() { + return new InventoryGui(); + } + + /** + * @param newTitle the (already colored) inventory title + * @return this menu + */ + public InventoryGui title(String newTitle) { + this.title = newTitle; + return this; + } + + /** + * @param newRows inventory rows, 1..6 + * @return this menu + */ + public InventoryGui rows(int newRows) { + if (newRows < MIN_ROWS || newRows > MAX_ROWS) { + throw new IllegalArgumentException("Rows must be between " + MIN_ROWS + " and " + MAX_ROWS); + } + this.rows = newRows; + return this; + } + + /** + * Puts the item into every slot that does not have one yet. + * + * @param item background item + * @return this menu + */ + public InventoryGui fill(ItemStack item) { + for (int slot = 0; slot < rows * SLOTS_PER_ROW; slot++) { + items.putIfAbsent(slot, new PlacedItem(item, null)); + } + return this; + } + + /** + * @param slot inventory slot + * @param item item to show + * @return this menu + */ + public InventoryGui setItem(int slot, ItemStack item) { + items.put(slot, new PlacedItem(item, null)); + return this; + } + + /** + * @param slot inventory slot + * @param item item to show + * @param onClick click handler, invoked after the event was cancelled + * @return this menu + */ + public InventoryGui setItem(int slot, ItemStack item, Consumer onClick) { + items.put(slot, new PlacedItem(item, onClick)); + return this; + } + + /** + * Builds the inventory, registers it for the player and opens it. + * + * @param player the player to show this menu to + */ + public void open(Player player) { + final Inventory inventory = Bukkit.createInventory(null, rows * SLOTS_PER_ROW, title); + items.forEach((slot, placed) -> { + if (slot < inventory.getSize() && placed.item() != null) { + inventory.setItem(slot, placed.item()); + } + }); + GuiManager.register(player, this, inventory); + player.openInventory(inventory); + } + + /** + * Cancels the click and runs the handler registered for the clicked slot. + * + * @param event the click event + */ + void handleClick(InventoryClickEvent event) { + event.setCancelled(true); + final PlacedItem placed = items.get(event.getRawSlot()); + if (placed != null && placed.onClick() != null) { + placed.onClick().accept(event); + } + } + + /** Snapshot of the placed items — package-private seam for tests. */ + Map items() { + return Map.copyOf(items); + } + + /** An item and its optional click handler. */ + record PlacedItem(ItemStack item, Consumer onClick) { + } +} diff --git a/src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java b/src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java index 4258250..3e1b091 100644 --- a/src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/lobby/DuelsMenu.java @@ -8,7 +8,7 @@ import dev.loki.loparkour.player.core.ParkourUser; import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.util.text.ColorUtil; import dev.loki.loparkour.world.core.Divider; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -50,7 +50,7 @@ public class DuelsMenu extends LPMenu { final int startIndex = currentPage * ITEMS_PER_PAGE; final int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, sessions.size()); - InventoryGUI gui = baseGui(title, ROWS); + InventoryGui gui = baseGui(title, ROWS); if (sessions.isEmpty()) { gui = gui.setItem(SLOT_EMPTY, localeItem(player, "play.multi.duels.menu_empty")); diff --git a/src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java b/src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java index 11cd630..9f8c3d1 100644 --- a/src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/lobby/LobbyMenu.java @@ -7,7 +7,7 @@ import dev.loki.loparkour.menu.core.screen.ParkourOption; import dev.loki.loparkour.player.core.ParkourUser; import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.util.text.ColorUtil; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -36,7 +36,7 @@ public class LobbyMenu extends LPMenu { final boolean isHost = !user.session.getPlayers().isEmpty() && user.session.getPlayers().get(0) == user; - InventoryGUI gui = baseGui(title, ROWS); + InventoryGui gui = baseGui(title, ROWS); if (ParkourOption.PLAYER_MANAGEMENT.mayPerform(player) && isHost) { gui = gui.setItem(SLOT_PLAYER_MANAGEMENT, localeItem(player, "lobby.player_management.item"), diff --git a/src/main/java/dev/loki/loparkour/menu/lobby/PlayerManagementMenu.java b/src/main/java/dev/loki/loparkour/menu/lobby/PlayerManagementMenu.java index a7d4443..4707b99 100644 --- a/src/main/java/dev/loki/loparkour/menu/lobby/PlayerManagementMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/lobby/PlayerManagementMenu.java @@ -8,7 +8,7 @@ import dev.loki.loparkour.player.core.ParkourPlayer; import dev.loki.loparkour.player.core.ParkourUser; import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.util.text.ColorUtil; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; @@ -42,7 +42,7 @@ public class PlayerManagementMenu extends LPMenu { others.addAll(session.getPlayers().stream().filter(p -> p != viewer).toList()); others.addAll(session.getSpectators()); - InventoryGUI gui = baseGui(title, ROWS) + InventoryGui gui = baseGui(title, ROWS) .setItem(SLOT_CLOSE, closeButton(player), e -> Menus.LOBBY.open(player)); for (int i = 0; i < Math.min(others.size(), MAX_PLAYERS); i++) { diff --git a/src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java b/src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java index ed6cde9..67d08e0 100644 --- a/src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/play/SingleMenu.java @@ -6,7 +6,7 @@ import dev.loki.loparkour.config.locale.Locales; import dev.loki.loparkour.menu.core.LPMenu; import dev.loki.loparkour.mode.base.Mode; import dev.loki.loparkour.player.core.ParkourUser; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -46,7 +46,7 @@ public class SingleMenu extends LPMenu { final int startIndex = currentPage * ITEMS_PER_PAGE; final int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, items.size()); - InventoryGUI gui = baseGui(title, ROWS); + InventoryGui gui = baseGui(title, ROWS); for (int i = startIndex; i < endIndex; i++) { final Mode mode = availableModes.get(i); diff --git a/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java b/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java index 9cfbe12..9436e84 100644 --- a/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/play/SpectatorMenu.java @@ -6,7 +6,7 @@ import dev.loki.loparkour.mode.base.Modes; import dev.loki.loparkour.player.core.ParkourUser; import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.world.core.Divider; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -43,7 +43,7 @@ public class SpectatorMenu extends LPMenu { final int startIndex = currentPage * ITEMS_PER_PAGE; final int endIndex = Math.min(startIndex + ITEMS_PER_PAGE, items.size()); - InventoryGUI gui = baseGui(title, ROWS); + InventoryGui gui = baseGui(title, ROWS); for (int i = startIndex; i < endIndex; i++) { final Session session = sessions.get(i); diff --git a/src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java b/src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java index daabd68..9e1a073 100644 --- a/src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/settings/LangMenu.java @@ -5,7 +5,7 @@ import dev.loki.loparkour.menu.core.LPMenu; import dev.loki.loparkour.menu.core.Menus; import dev.loki.loparkour.player.core.ParkourPlayer; import dev.loki.loparkour.util.text.ColorUtil; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -40,7 +40,7 @@ public class LangMenu extends LPMenu { final List langs = new ArrayList<>(Locales.getLocaleKeys()); - InventoryGUI gui = baseGui(title, ROWS); + InventoryGui gui = baseGui(title, ROWS); for (int i = 0; i < Math.min(langs.size(), MAX_LANGS); i++) { final String lang = langs.get(i); diff --git a/src/main/java/dev/loki/loparkour/menu/settings/ParkourSettingsMenu.java b/src/main/java/dev/loki/loparkour/menu/settings/ParkourSettingsMenu.java index 23591e2..85a2d66 100644 --- a/src/main/java/dev/loki/loparkour/menu/settings/ParkourSettingsMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/settings/ParkourSettingsMenu.java @@ -9,7 +9,7 @@ import dev.loki.loparkour.menu.core.screen.ParkourOption; import dev.loki.loparkour.player.core.ParkourPlayer; import dev.loki.loparkour.style.core.Style; import dev.loki.loparkour.util.text.ColorUtil; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -168,7 +168,7 @@ public class ParkourSettingsMenu extends LPMenu { final String locale = pp.locale; final String title = Locales.getString(locale, "settings.parkour_settings.styles.name"); - InventoryGUI gui = baseGui(title, ROWS_STYLE); + InventoryGui gui = baseGui(title, ROWS_STYLE); int slot = FIRST_STYLE_SLOT; for (final Style style : Registry.getStyles()) { diff --git a/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java b/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java index 9fb5f5d..338c610 100644 --- a/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java +++ b/src/main/java/dev/loki/loparkour/menu/settings/SettingsMenu.java @@ -6,7 +6,7 @@ import dev.loki.loparkour.menu.core.Menus; import dev.loki.loparkour.menu.core.screen.ParkourOption; import dev.loki.loparkour.player.core.ParkourPlayer; import dev.loki.loparkour.player.core.ParkourUser; -import dev.lolib.gui.InventoryGUI; +import dev.loki.loparkour.menu.gui.inventory.InventoryGui; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -22,7 +22,7 @@ public class SettingsMenu extends LPMenu { final String locale = locale(player); final String title = Locales.getString(locale, "settings.name"); - InventoryGUI gui = baseGui(title, ROWS); + InventoryGui gui = baseGui(title, ROWS); if (ParkourOption.PARKOUR_SETTINGS.mayPerform(player) && ParkourPlayer.isPlayer(player)) { gui = gui.setItem(SLOT_PARKOUR_SETTINGS, localeItem(player, "settings.parkour_settings.item"), e -> { diff --git a/src/main/java/dev/loki/loparkour/mode/impl/multi/RaceMode.java b/src/main/java/dev/loki/loparkour/mode/impl/multi/RaceMode.java index 6292a5b..7e25e02 100644 --- a/src/main/java/dev/loki/loparkour/mode/impl/multi/RaceMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/multi/RaceMode.java @@ -146,7 +146,7 @@ public class RaceMode implements Mode { registerScore(time, "1.0", targetScore); // Return to lobby after 5 seconds — guarded against null/closed session - dev.lolib.scheduler.Scheduler.get(LoParkour.getPlugin()) + dev.loki.loparkour.scheduler.Scheduler.get(LoParkour.getPlugin()) .runLater(() -> { if (session != null && !state.stopped) { reset(false); diff --git a/src/main/java/dev/loki/loparkour/mode/impl/solo/SpeedrunMode.java b/src/main/java/dev/loki/loparkour/mode/impl/solo/SpeedrunMode.java index 0b81820..062d714 100644 --- a/src/main/java/dev/loki/loparkour/mode/impl/solo/SpeedrunMode.java +++ b/src/main/java/dev/loki/loparkour/mode/impl/solo/SpeedrunMode.java @@ -5,8 +5,8 @@ import dev.loki.loparkour.mode.base.Mode; import dev.loki.loparkour.mode.base.Modes; import dev.loki.loparkour.util.item.Item; -import dev.lolib.scheduler.Scheduler; -import dev.lolib.scheduler.ScheduledTask; +import dev.loki.loparkour.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.ScheduledTask; import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.config.core.Config; import dev.loki.loparkour.config.locale.Locales; diff --git a/src/main/java/dev/loki/loparkour/player/core/PlayerPersistence.java b/src/main/java/dev/loki/loparkour/player/core/PlayerPersistence.java index 7e32ec6..4c25fc5 100644 --- a/src/main/java/dev/loki/loparkour/player/core/PlayerPersistence.java +++ b/src/main/java/dev/loki/loparkour/player/core/PlayerPersistence.java @@ -2,7 +2,7 @@ package dev.loki.loparkour.player.core; import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.storage.Storage; -import dev.lolib.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.Scheduler; /** * Handles persistence of player data to the configured storage backend. diff --git a/src/main/java/dev/loki/loparkour/player/core/PlayerSpawnSetup.java b/src/main/java/dev/loki/loparkour/player/core/PlayerSpawnSetup.java index 5fa4eee..8c0f97e 100644 --- a/src/main/java/dev/loki/loparkour/player/core/PlayerSpawnSetup.java +++ b/src/main/java/dev/loki/loparkour/player/core/PlayerSpawnSetup.java @@ -3,7 +3,7 @@ package dev.loki.loparkour.player.core; import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.config.core.Config; import dev.loki.loparkour.player.service.ui.ParkourHotbar; -import dev.lolib.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.Scheduler; import org.bukkit.GameMode; import org.bukkit.Location; diff --git a/src/main/java/dev/loki/loparkour/player/data/InventoryData.java b/src/main/java/dev/loki/loparkour/player/data/InventoryData.java index 5d72052..5499caf 100644 --- a/src/main/java/dev/loki/loparkour/player/data/InventoryData.java +++ b/src/main/java/dev/loki/loparkour/player/data/InventoryData.java @@ -2,7 +2,7 @@ package dev.loki.loparkour.player.data; import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.config.core.Config; -import dev.lolib.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.Scheduler; import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; diff --git a/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java b/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java index 7a5f26d..6127508 100644 --- a/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java +++ b/src/main/java/dev/loki/loparkour/player/spectator/ParkourSpectator.java @@ -9,8 +9,8 @@ import dev.loki.loparkour.config.locale.Locales; import dev.loki.loparkour.player.data.PreviousData; import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.util.text.ColorUtil; -import dev.lolib.scheduler.Scheduler; -import dev.lolib.scheduler.ScheduledTask; +import dev.loki.loparkour.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.ScheduledTask; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.GameMode; diff --git a/src/main/java/dev/loki/loparkour/scheduler/BukkitBackend.java b/src/main/java/dev/loki/loparkour/scheduler/BukkitBackend.java new file mode 100644 index 0000000..8230d98 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/scheduler/BukkitBackend.java @@ -0,0 +1,36 @@ +package dev.loki.loparkour.scheduler; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +/** + * Scheduler backend for Bukkit/Paper servers. + */ +final class BukkitBackend implements Scheduler.Backend { + + private final Plugin plugin; + + BukkitBackend(Plugin plugin) { + this.plugin = plugin; + } + + @Override + public ScheduledTask run(Runnable task) { + return Bukkit.getScheduler().runTask(plugin, task)::cancel; + } + + @Override + public ScheduledTask runAsync(Runnable task) { + return Bukkit.getScheduler().runTaskAsynchronously(plugin, task)::cancel; + } + + @Override + public ScheduledTask runLater(Runnable task, long delayTicks) { + return Bukkit.getScheduler().runTaskLater(plugin, task, delayTicks)::cancel; + } + + @Override + public ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks) { + return Bukkit.getScheduler().runTaskTimer(plugin, task, delayTicks, periodTicks)::cancel; + } +} diff --git a/src/main/java/dev/loki/loparkour/scheduler/FoliaBackend.java b/src/main/java/dev/loki/loparkour/scheduler/FoliaBackend.java new file mode 100644 index 0000000..5495159 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/scheduler/FoliaBackend.java @@ -0,0 +1,92 @@ +package dev.loki.loparkour.scheduler; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Objects; +import java.util.function.Consumer; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +/** + * Scheduler backend for Folia servers: the global region scheduler drives sync tasks + * and the async scheduler drives worker tasks, mirroring the Bukkit semantics above. + * + *

The Folia API is resolved reflectively because the build matrix compiles against + * Spigot/Paper API artifacts that do not ship it (see TODO, Задача 1 / п.143). + */ +final class FoliaBackend implements Scheduler.Backend { + + private static final String SCHEDULER_PACKAGE = "io.papermc.paper.threadedregions.scheduler."; + + private static final String GLOBAL_SCHEDULER_CLASS = "GlobalRegionScheduler"; + private static final String ASYNC_SCHEDULER_CLASS = "AsyncScheduler"; + + private final Plugin plugin; + private final Object globalScheduler; + private final Object asyncScheduler; + private final Method globalRun; + private final Method globalRunDelayed; + private final Method globalRunAtFixedRate; + private final Method asyncRunNow; + private final Method taskCancel; + + FoliaBackend(Plugin plugin) throws ReflectiveOperationException { + this.plugin = plugin; + this.globalScheduler = scheduler("getGlobalRegionScheduler"); + this.asyncScheduler = scheduler("getAsyncScheduler"); + this.globalRun = method(GLOBAL_SCHEDULER_CLASS, "run", Plugin.class, Consumer.class); + this.globalRunDelayed = method(GLOBAL_SCHEDULER_CLASS, "runDelayed", + Plugin.class, Consumer.class, long.class); + this.globalRunAtFixedRate = method(GLOBAL_SCHEDULER_CLASS, "runAtFixedRate", + Plugin.class, Consumer.class, long.class, long.class); + this.asyncRunNow = method(ASYNC_SCHEDULER_CLASS, "runNow", Plugin.class, Consumer.class); + this.taskCancel = Class.forName(SCHEDULER_PACKAGE + "ScheduledTask").getMethod("cancel"); + } + + @Override + public ScheduledTask run(Runnable task) { + return wrap(invoke(globalRun, globalScheduler, plugin, consumer(task))); + } + + @Override + public ScheduledTask runAsync(Runnable task) { + return wrap(invoke(asyncRunNow, asyncScheduler, plugin, consumer(task))); + } + + @Override + public ScheduledTask runLater(Runnable task, long delayTicks) { + return wrap(invoke(globalRunDelayed, globalScheduler, plugin, consumer(task), delayTicks)); + } + + @Override + public ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks) { + return wrap(invoke(globalRunAtFixedRate, globalScheduler, plugin, + consumer(task), delayTicks, periodTicks)); + } + + private static Object scheduler(String bukkitGetter) throws ReflectiveOperationException { + return Bukkit.class.getMethod(bukkitGetter).invoke(null); + } + + private static Method method(String owner, String name, Class... params) + throws ReflectiveOperationException { + return Class.forName(SCHEDULER_PACKAGE + owner).getMethod(name, params); + } + + private static Consumer consumer(Runnable task) { + return ignored -> task.run(); + } + + private static Object invoke(Method method, Object target, Object... args) { + try { + return method.invoke(target, args); + } catch (IllegalAccessException | InvocationTargetException cause) { + throw new IllegalStateException("Folia scheduler call failed: " + method.getName(), cause); + } + } + + private ScheduledTask wrap(Object foliaTask) { + final Object handle = Objects.requireNonNull(foliaTask, "Folia scheduler returned no task"); + return () -> invoke(taskCancel, handle); + } +} diff --git a/src/main/java/dev/loki/loparkour/scheduler/ScheduledTask.java b/src/main/java/dev/loki/loparkour/scheduler/ScheduledTask.java new file mode 100644 index 0000000..8a1ac3e --- /dev/null +++ b/src/main/java/dev/loki/loparkour/scheduler/ScheduledTask.java @@ -0,0 +1,13 @@ +package dev.loki.loparkour.scheduler; + +/** + * Handle for a scheduled task. + */ +@FunctionalInterface +public interface ScheduledTask { + + /** + * Cancels the task if it has not run yet (repeating tasks stop after the current run). + */ + void cancel(); +} diff --git a/src/main/java/dev/loki/loparkour/scheduler/Scheduler.java b/src/main/java/dev/loki/loparkour/scheduler/Scheduler.java new file mode 100644 index 0000000..b65706a --- /dev/null +++ b/src/main/java/dev/loki/loparkour/scheduler/Scheduler.java @@ -0,0 +1,122 @@ +package dev.loki.loparkour.scheduler; + +import org.bukkit.plugin.Plugin; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; + +/** + * Thin task scheduler facade over the platform scheduler. + * + *

Call sites keep the {@code Scheduler.get(plugin).runLater(...)} shape, so the + * backend (Bukkit today, Folia region/async schedulers) can change without touching + * game code. The Folia backend is selected automatically when the Folia server + * classes are present; the Folia scheduler API itself is resolved reflectively + * because it is absent from the Spigot/Paper API artifacts used by the build matrix. + */ +public final class Scheduler { + + private static final String FOLIA_SERVER_CLASS = "io.papermc.paper.threadedregions.RegionizedServer"; + + private static final Map INSTANCES = new ConcurrentHashMap<>(); + + private static final boolean FOLIA = hasFoliaServer(); + + private final Backend backend; + + private Scheduler(Plugin plugin) { + this.backend = createBackend(plugin); + } + + /** + * @param plugin the owning plugin + * @return the scheduler bound to that plugin + */ + public static Scheduler get(Plugin plugin) { + return INSTANCES.computeIfAbsent(plugin, Scheduler::new); + } + + /** + * @return true when the server runs Folia (or a Folia fork) + */ + public static boolean isFolia() { + return FOLIA; + } + + /** + * Runs the task on the next global tick. + * + * @param task the task to run + * @return a handle used to cancel the task + */ + public ScheduledTask run(Runnable task) { + return backend.run(task); + } + + /** + * Runs the task on a worker thread. + * + * @param task the task to run + * @return a handle used to cancel the task + */ + public ScheduledTask runAsync(Runnable task) { + return backend.runAsync(task); + } + + /** + * Runs the task once after the given delay. + * + * @param task the task to run + * @param delayTicks delay in ticks + * @return a handle used to cancel the task + */ + public ScheduledTask runLater(Runnable task, long delayTicks) { + return backend.runLater(task, delayTicks); + } + + /** + * Runs the task repeatedly after the given delay. + * + * @param task the task to run + * @param delayTicks first run delay in ticks + * @param periodTicks period between runs in ticks + * @return a handle used to cancel the task + */ + public ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks) { + return backend.runTimer(task, delayTicks, periodTicks); + } + + private static Backend createBackend(Plugin plugin) { + if (FOLIA) { + try { + return new FoliaBackend(plugin); + } catch (ReflectiveOperationException expected) { + plugin.getLogger().log(Level.WARNING, + "Folia scheduler API not found - falling back to the Bukkit scheduler", expected); + } + } + return new BukkitBackend(plugin); + } + + private static boolean hasFoliaServer() { + try { + Class.forName(FOLIA_SERVER_CLASS, false, Scheduler.class.getClassLoader()); + return true; + } catch (ClassNotFoundException expected) { + return false; + } + } + + /** The four task operations shared by the Bukkit and Folia backends. */ + interface Backend { + + ScheduledTask run(Runnable task); + + ScheduledTask runAsync(Runnable task); + + ScheduledTask runLater(Runnable task, long delayTicks); + + ScheduledTask runTimer(Runnable task, long delayTicks, long periodTicks); + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/registry/Schematics.java b/src/main/java/dev/loki/loparkour/schematic/registry/Schematics.java index e14699e..af51f6c 100644 --- a/src/main/java/dev/loki/loparkour/schematic/registry/Schematics.java +++ b/src/main/java/dev/loki/loparkour/schematic/registry/Schematics.java @@ -2,7 +2,7 @@ package dev.loki.loparkour.schematic.registry; import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.schematic.core.SchematicManager; -import dev.lolib.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.Scheduler; import java.io.File; diff --git a/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java b/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java index 4208827..e89fb7e 100644 --- a/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java +++ b/src/main/java/dev/loki/loparkour/session/manager/SessionStateManager.java @@ -7,8 +7,8 @@ import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices; import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator; import dev.loki.loparkour.player.core.ParkourUser; import dev.loki.loparkour.world.core.Divider; -import dev.lolib.scheduler.Scheduler; -import dev.lolib.scheduler.ScheduledTask; +import dev.loki.loparkour.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.ScheduledTask; import org.bukkit.Location; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/dev/loki/loparkour/storage/sql/StorageSQL.java b/src/main/java/dev/loki/loparkour/storage/sql/StorageSQL.java index 979e257..7bc36d7 100644 --- a/src/main/java/dev/loki/loparkour/storage/sql/StorageSQL.java +++ b/src/main/java/dev/loki/loparkour/storage/sql/StorageSQL.java @@ -6,7 +6,7 @@ import dev.loki.loparkour.player.core.ParkourPlayer; import dev.loki.loparkour.storage.sql.repo.SQLPlayerRepository; import dev.loki.loparkour.storage.sql.repo.SQLScoreRepository; import dev.loki.loparkour.storage.sql.schema.SQLSchemaManager; -import dev.lolib.scheduler.Scheduler; +import dev.loki.loparkour.scheduler.Scheduler; import org.jetbrains.annotations.NotNull; import java.sql.SQLException; diff --git a/src/main/java/dev/loki/loparkour/world/core/VoidGenerator.java b/src/main/java/dev/loki/loparkour/world/core/VoidGenerator.java new file mode 100644 index 0000000..252b996 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/world/core/VoidGenerator.java @@ -0,0 +1,52 @@ +package dev.loki.loparkour.world.core; + +import org.bukkit.generator.ChunkGenerator; + +/** + * Generates an empty chunk grid — the parkour world must not contain terrain + * (vanilla map layers also break flat void worlds: "No keys in MapLayer"). + */ +public final class VoidGenerator extends ChunkGenerator { + + private static final VoidGenerator INSTANCE = new VoidGenerator(); + + private VoidGenerator() { + } + + /** + * @return the shared void generator + */ + public static VoidGenerator getGenerator() { + return INSTANCE; + } + + @Override + public boolean shouldGenerateNoise() { + return false; + } + + @Override + public boolean shouldGenerateSurface() { + return false; + } + + @Override + public boolean shouldGenerateCaves() { + return false; + } + + @Override + public boolean shouldGenerateDecorations() { + return false; + } + + @Override + public boolean shouldGenerateMobs() { + return false; + } + + @Override + public boolean shouldGenerateStructures() { + return false; + } +} diff --git a/src/main/java/dev/loki/loparkour/world/core/World.java b/src/main/java/dev/loki/loparkour/world/core/World.java index b6632f0..0d9ec68 100644 --- a/src/main/java/dev/loki/loparkour/world/core/World.java +++ b/src/main/java/dev/loki/loparkour/world/core/World.java @@ -2,7 +2,6 @@ package dev.loki.loparkour.world.core; import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.config.core.Config; -import dev.lolib.worlds.VoidGenerator; import org.bukkit.Bukkit; import org.bukkit.Difficulty; import org.bukkit.GameRule; diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f1bb873..3168329 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -4,7 +4,7 @@ author: loki version: ${version} api-version: ${apiVersion} main: dev.loki.loparkour.LoParkour -softdepend: [floodgate, Vault, PlaceholderAPI, Multiverse-Core, VoidGen] +softdepend: [floodgate, Vault, PlaceholderAPI, Multiverse-Core] commands: loparkour: aliases: [parkour, lp] diff --git a/src/test/java/dev/loki/loparkour/menu/gui/inventory/InventoryGuiTest.java b/src/test/java/dev/loki/loparkour/menu/gui/inventory/InventoryGuiTest.java new file mode 100644 index 0000000..fe3ce04 --- /dev/null +++ b/src/test/java/dev/loki/loparkour/menu/gui/inventory/InventoryGuiTest.java @@ -0,0 +1,72 @@ +package dev.loki.loparkour.menu.gui.inventory; + +import org.bukkit.event.inventory.InventoryClickEvent; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class InventoryGuiTest { + + private static final int ROWS = 2; + private static final int SLOTS_PER_ROW = 9; + private static final int SLOT_WITH_HANDLER = 5; + private static final int PLAIN_SLOT = 7; + private static final int OUT_OF_RANGE_SLOT = 40; + + @Test + void testRowsBoundsAreEnforced() { + final InventoryGui gui = InventoryGui.create(); + assertThrows(IllegalArgumentException.class, () -> gui.rows(0), "zero rows are rejected"); + assertThrows(IllegalArgumentException.class, () -> gui.rows(7), "seven rows are rejected"); + assertSame(gui, gui.rows(6), "rows() is fluent"); + } + + @Test + void testFillKeepsItemsPlacedBeforeIt() { + final InventoryGui gui = InventoryGui.create().rows(ROWS); + gui.setItem(SLOT_WITH_HANDLER, null, event -> { + }); + gui.fill(null); + + final Map items = gui.items(); + assertEquals(ROWS * SLOTS_PER_ROW, items.size(), "fill covers every slot exactly once"); + assertNotNull(items.get(SLOT_WITH_HANDLER).onClick(), "fill must not overwrite placed items"); + assertNull(items.get(PLAIN_SLOT).onClick(), "a plain filler slot has no handler"); + } + + @Test + void testClickRunsHandlerAndCancelsTheEvent() { + final AtomicBoolean clicked = new AtomicBoolean(); + final InventoryGui gui = InventoryGui.create().rows(ROWS); + gui.setItem(SLOT_WITH_HANDLER, null, event -> clicked.set(true)); + + final InventoryClickEvent event = Mockito.mock(InventoryClickEvent.class); + when(event.getRawSlot()).thenReturn(SLOT_WITH_HANDLER); + gui.handleClick(event); + + verify(event).setCancelled(true); + assertTrue(clicked.get(), "the registered handler must run"); + } + + @Test + void testClickOnUnknownSlotOnlyCancels() { + final InventoryGui gui = InventoryGui.create().rows(ROWS); + + final InventoryClickEvent event = Mockito.mock(InventoryClickEvent.class); + when(event.getRawSlot()).thenReturn(OUT_OF_RANGE_SLOT); + gui.handleClick(event); + + verify(event).setCancelled(true); + } +} diff --git a/src/test/java/dev/loki/loparkour/scheduler/SchedulerTest.java b/src/test/java/dev/loki/loparkour/scheduler/SchedulerTest.java new file mode 100644 index 0000000..ca685e0 --- /dev/null +++ b/src/test/java/dev/loki/loparkour/scheduler/SchedulerTest.java @@ -0,0 +1,22 @@ +package dev.loki.loparkour.scheduler; + +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; + +class SchedulerTest { + + @Test + void testNoFoliaOnPlainJvm() { + assertFalse(Scheduler.isFolia(), "Folia server classes are absent in the test JVM"); + } + + @Test + void testInstancesAreCachedPerPlugin() { + final Plugin plugin = Mockito.mock(Plugin.class); + assertSame(Scheduler.get(plugin), Scheduler.get(plugin), "the same plugin must reuse one scheduler"); + } +} diff --git a/src/test/java/dev/loki/loparkour/world/core/VoidGeneratorTest.java b/src/test/java/dev/loki/loparkour/world/core/VoidGeneratorTest.java new file mode 100644 index 0000000..5934ac0 --- /dev/null +++ b/src/test/java/dev/loki/loparkour/world/core/VoidGeneratorTest.java @@ -0,0 +1,25 @@ +package dev.loki.loparkour.world.core; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; + +class VoidGeneratorTest { + + @Test + void testGeneratorIsShared() { + assertSame(VoidGenerator.getGenerator(), VoidGenerator.getGenerator(), "the generator is a singleton"); + } + + @Test + void testGeneratesNothing() { + final VoidGenerator generator = VoidGenerator.getGenerator(); + assertFalse(generator.shouldGenerateNoise(), "noise off"); + assertFalse(generator.shouldGenerateSurface(), "surface off"); + assertFalse(generator.shouldGenerateCaves(), "caves off"); + assertFalse(generator.shouldGenerateDecorations(), "decorations off"); + assertFalse(generator.shouldGenerateMobs(), "mobs off"); + assertFalse(generator.shouldGenerateStructures(), "structures off"); + } +}