refactor: drop lolib, add own scheduler, inventory GUI and void generator

This commit is contained in:
loki5512344 2026-10-06 13:41:25 +02:00
parent cc521dc284
commit 7e227e39a4
Signed by: boba
GPG key ID: 253067914055423B
47 changed files with 708 additions and 113 deletions

View file

@ -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

4
.gitignore vendored
View file

@ -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

View file

@ -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") {

Binary file not shown.

View file

@ -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 + " &#404040» &#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);
}

View file

@ -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);

View file

@ -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;
*
* <p>Bugs fixed vs original:
* <ul>
* <li>Duplicate import of {@code dev.lolib.scheduler.Scheduler} — removed.</li>
* <li>Duplicate field declaration {@code dev.lolib.scheduler.ScheduledTask task}
* shadowed the already-imported type — removed the fully-qualified duplicate.</li>
* <li>Duplicate scheduler import — removed.</li>
* <li>Duplicate {@code ScheduledTask task} field shadowed the already-imported
* type — removed the fully-qualified duplicate.</li>
* </ul>
*/
public class GhostPlayer {

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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++) {

View file

@ -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

View file

@ -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&lt;InventoryClickEvent&gt;).open(player)
* ScrollableGUI.create().title(String).rows(n).addItem(item, Consumer&lt;InventoryClickEvent&gt;)
* .scrollUpButton(item,slot).scrollDownButton(item,slot).open(player)
* Menu API ({@link InventoryGui}):
* InventoryGui.create().title(String).rows(n).fill(item)
* .setItem(slot, item, Consumer&lt;InventoryClickEvent&gt;).open(player)
*
* cancelAllClicks = true by default — items cannot be taken out automatically.
* Handlers receive Consumer&lt;InventoryClickEvent&gt;, 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());

View file

@ -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() {}

View file

@ -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);

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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<UUID, OpenGui> 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) {
}
}

View file

@ -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.
*
* <p>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<Integer, PlacedItem> 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<InventoryClickEvent> 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<Integer, PlacedItem> items() {
return Map.copyOf(items);
}
/** An item and its optional click handler. */
record PlacedItem(ItemStack item, Consumer<InventoryClickEvent> onClick) {
}
}

View file

@ -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"));

View file

@ -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"),

View file

@ -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++) {

View file

@ -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);

View file

@ -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);

View file

@ -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<String> 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);

View file

@ -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()) {

View file

@ -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 -> {

View file

@ -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);

View file

@ -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;

View file

@ -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.

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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.
*
* <p>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<Object> 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);
}
}

View file

@ -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();
}

View file

@ -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.
*
* <p>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<Plugin, Scheduler> 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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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]

View file

@ -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<Integer, InventoryGui.PlacedItem> 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);
}
}

View file

@ -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");
}
}

View file

@ -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");
}
}