diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml new file mode 100644 index 0000000..48b7c72 --- /dev/null +++ b/.github/workflows/dev-build.yml @@ -0,0 +1,48 @@ +name: Dev Build + +on: + push: + branches: + - main + - dev + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'maven' + + - name: Build with Maven + run: mvn clean package + + - name: Update Dev-Build Tag + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git tag -d dev-build || true + git push origin :refs/tags/dev-build || true + git tag dev-build + git push origin dev-build --force + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish Dev-Build Release + run: | + gh release delete dev-build --yes || true + gh release create dev-build oumlib-plugin/target/oumlib-plugin-*.jar --title "Development Build" --notes "Automated pre-release build of OumLib." + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 9853625..2639685 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ /example-plugin/example-plugin.iml /example-plugin/target /oumlib-core/oumlib-core.iml -/oumlib-core/target \ No newline at end of file +/oumlib-core/target +/oumlib-plugin/target +/oumlib-plugin/oumlib-plugin.iml \ No newline at end of file diff --git a/docs/events.md b/docs/events.md index f329e7b..5ab8a92 100644 --- a/docs/events.md +++ b/docs/events.md @@ -23,22 +23,42 @@ Events.listen(PlayerInteractEvent.class) }); ``` -### Player-Specific Filtering (Paper-only) -If you need to listen for events triggered specifically by a particular player (e.g. during a minigame, a chat prompt session, or an active GUI menu context), use `BukkitEvents.listenFor(Player, Class)`: +### Player-Specific Filtering (Multi-Platform) +If you need to listen for events triggered specifically by a particular player (e.g. during a minigame, a chat prompt session, or an active GUI menu context), you can use `.playerFilter(extractor, condition)` on `EventBuilder`. This is fully platform-independent and type-safe: ```java -import dev.oum.oumlib.event.BukkitEvents; +import dev.oum.oumlib.event.Events; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.entity.Player; Player targetPlayer = ...; -BukkitEvents.listenFor(targetPlayer, PlayerInteractEvent.class) +Events.listen(PlayerInteractEvent.class) + .playerFilter(PlayerInteractEvent::getPlayer, player -> player.equals(targetPlayer)) .handler(event -> { - // This handler will only execute if event.getPlayer() matches the targetPlayer event.getPlayer().sendMessage("You interacted!"); }); ``` +On Velocity, this works exactly the same with Velocity's `Player` class and proxy events: + +```java +import dev.oum.oumlib.event.Events; +import com.velocitypowered.api.event.player.PlayerChatEvent; +import com.velocitypowered.api.proxy.Player; + +Player targetPlayer = ...; + +Events.listen(PlayerChatEvent.class) + .playerFilter(PlayerChatEvent::getPlayer, player -> player.equals(targetPlayer)) + .handler(event -> { + // This only fires for targetPlayer + }); +``` + +> [!NOTE] +> The legacy class `BukkitEvents` (e.g. `BukkitEvents.listenFor(...)`) has been deprecated since version `1.0.7` and is scheduled for removal. Please migrate to using `.playerFilter(...)` on the standard `Events.listen(...)` builder. + --- ## 3. Cancellable Events & State Handling diff --git a/docs/inventories.md b/docs/inventories.md index 8c4a4d7..17e4e01 100644 --- a/docs/inventories.md +++ b/docs/inventories.md @@ -128,55 +128,97 @@ To prevent standard menu bugs, OumLib implements two click protection safeguards ## 5. ItemBuilder Reference -To make inventory item creations clean, OumLib includes a chainable `ItemBuilder` helper: +To make inventory item and custom ItemStack creations clean and readable, OumLib features a chainable, component-aware `ItemBuilder` API. It supports Adventure components, MiniMessage templates, persistent data containers (PDC), and modern Paper 1.21+ data components. +### Building Items: ```java import dev.oum.oumlib.inventory.ItemBuilder; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; -// Create a new item from scratch -ItemStack item1 = ItemBuilder.of(Material.DIAMOND_SWORD) +// 1. Create a formatted item using MiniMessage and String lore: +ItemStack excalibur = ItemBuilder.of(Material.DIAMOND_SWORD) .name("Excalibur") - .lore("A legendary sword", "of kings") + .lore( + "A legendary sword", + "of ancient kings" + ) .enchant(Enchantment.SHARPNESS, 5) .glow() // Makes the item glow without showing enchantments flag .unbreakable(true) - .customModelData(101) // Resource pack CustomModelData .build(); -// Use Adventure Components directly: -ItemStack item2 = ItemBuilder.of(Material.DIAMOND_SWORD) - .name(Component.text("Custom Sword").color(NamedTextColor.GOLD)) +// 2. Use Adventure Components directly: +ItemStack item = ItemBuilder.of(Material.GOLDEN_APPLE) + .name(Component.text("Special Apple").color(NamedTextColor.GOLD)) .lore(List.of(Component.text("A custom lore line"))) .build(); -// Modify an existing item stack (Copy and Edit) -ItemStack copied = ItemBuilder.of(item1) - .type(Material.NETHERITE_SWORD) // Swaps material - .addLore("Modified by system") // Appends to existing lore +// 3. Quick-build formatted items in one statement: +ItemStack quickItem = ItemBuilder.quick( + Material.NETHERITE_INGOT, + "Netherite Alloy", + "High-grade metal used", + "for crafting gear." +); +``` + +### Modifying and Appending: +You can pass an existing `ItemStack` into the builder to copy it and make incremental edits: +```java +ItemStack copied = ItemBuilder.of(excalibur) + .type(Material.NETHERITE_SWORD) // Swaps material to netherite + .addLore("Modified by system") // Appends to existing lore lines + .amount(5) // Sets quantity + .build(); +``` + +### Modern Paper Data Component Features (1.21 & 1.21.4+): +```java +ItemStack modernItem = ItemBuilder.of(Material.SHIELD) + // Sets the client-side 3D model path (replaces legacy CustomModelData) + .itemModel(NamespacedKey.fromString("myplugin:custom_shield")) + + // Override item glint shine override + .glintOverride(true) + + // Set custom maximum stack size (e.g. stack shields/swords up to 16) + .maxStackSize(16) + + // Set custom max durability + .maxDamage(500) + + // Set fire/lava immunity (won't burn when dropped) + .fireResistant(true) + .build(); +``` -// Store custom data in the item's Persistent Data Container (PDC) +### Persistent Data Container (PDC) Integration: +Attach custom typed metadata directly to items without verbose serialization wrappers: +```java ItemStack itemWithPdc = ItemBuilder.of(Material.GOLD_INGOT) .name("Treasure Ingot") .pdc("key_string", "some-metadata") .pdc("key_int", 42) .pdc("key_double", 3.14) .pdc("key_boolean", true) - // You can even serialize and store sub-items or lists of items inside this item's PDC: + + // You can even store sub-items or lists of items inside this item's PDC: .pdc("key_sub_item", new ItemStack(Material.APPLE)) .pdc("key_item_array", new ItemStack[]{ new ItemStack(Material.COOKIE) }) .build(); - -// Other utility methods: -// .clearLore() - Removes all lore lines -// .amount(int) - Sets stack quantity -// .flag(ItemFlag...) - Adds specific item flags ``` +### General Utilities: +- `.clearLore()`: Removes all lore lines. +- `.amount(int)`: Sets stack quantity. +- `.flag(ItemFlag...)`: Adds specific item flags. + --- ## 6. AnvilMenu Reference @@ -237,44 +279,3 @@ PaginatedMenu menu = PaginatedMenu.builder() // Open the menu for a player menu.open(player); ``` - ---- - -## 8. ItemBuilder Reference - -`ItemBuilder` provides a fluent, modern builder API to create and format `ItemStack` display names and lores using Adventure components or MiniMessage templates: - -```java -import dev.oum.oumlib.inventory.ItemBuilder; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; -import net.kyori.adventure.text.Component; -import java.util.List; - -// 1. Build a formatted item using MiniMessage and String lore: -ItemStack sword = ItemBuilder.of(Material.DIAMOND_SWORD) - .name("Sword of Destiny") - .lore( - "An ancient blade forged in", - "the fires of Oum." - ) - .glow() - .build(); - -// 2. Build using adventure components directly: -Component name = Component.text("Special Item"); -List lore = List.of(Component.text("Lore line 1")); - -ItemStack custom = ItemBuilder.of(Material.GOLDEN_APPLE) - .name(name) - .lore(lore) - .build(); - -// 3. Quick-build formatted items in one statement: -ItemStack quickItem = ItemBuilder.quick( - Material.NETHERITE_INGOT, - "Netherite Alloy", - "High-grade metal used", - "for crafting gear." -); -``` diff --git a/docs/setup.md b/docs/setup.md index d371eeb..ab075aa 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,15 +1,67 @@ # Main Setup & Initialization -OumLib must be initialized during your plugin's enable phase and shut down during the disable phase. This configures the internal scheduler adapters, registers the event bus, and automatically hooks into server-wide systems. +OumLib can be integrated either as a shared library plugin (recommended) or shaded directly into your plugin JAR. Depending on the setup method you choose, initialization is either handled automatically by the server/proxy or manually by your plugin code. ## Integration Auto-Detection -When you call `OumLib.init()`, the library automatically scans the server's plugin manager for supported integrations. You do not need to write any integration code. It currently hooks into: +During initialization, the library automatically scans the server's plugin manager for supported integrations. You do not need to write any integration code. It currently hooks into: - **PlaceholderAPI (PAPI)**: Auto-registers registered OumLib placeholders into PAPI so other plugins can use them. - **MiniPlaceholders**: Bridges OumLib placeholders into MiniPlaceholders' global parsing context. --- -## Paper/Spigot Setup +## 1. Shared Plugin Mode (Recommended) + +By placing the built `oumlib-plugin` JAR in your server's `plugins` folder, the library operates as a central dependency. Downstream plugins can share a single classloader and reference the library without needing to shade or relocate it. + +### Maven Dependency +Declare `oumlib-core` with `provided` in your downstream plugin's `pom.xml`: + +```xml + + com.github.sun-mc-dev.oumlib + oumlib-core + VERSION + provided + +``` + +### Declaring Dependencies in Metadata + +#### Paper/Spigot (`plugin.yml` or `paper-plugin.yml`) +Add `OumLib` as a dependency so your plugin loads after the library: + +```yaml +# plugin.yml +depend: [OumLib] + +# OR paper-plugin.yml +dependencies: + - name: OumLib + required: true + bootstrap: false +``` + +#### Velocity (`@Plugin` Annotation) +Declare the dependency on `oumlib` inside your plugin definition: + +```java +@Plugin( + id = "myplugin", + name = "MyPlugin", + version = "1.0.0", + dependencies = { + @Dependency(id = "oumlib") + } +) +``` + +No initialization or shutdown calls (`OumLib.init(...)` / `OumLib.shutdown()`) are required in your plugin code; the central library plugin handles startup and cleanup automatically. + +--- + +## 2. Shaded Mode (Fallback) + +### Paper/Spigot Setup Initialize OumLib in your main class's `onEnable()` method, and call `shutdown()` in `onDisable()` to clean up background threads and scheduler tasks. @@ -25,8 +77,7 @@ public final class PaperExamplePlugin extends JavaPlugin { public void onEnable() { // Initialize OumLib for Paper. // OumLib.init returns an InitBuilder for method chaining configuration. - OumLib.init(this) - .build(); + OumLib.init(this); } @Override @@ -41,20 +92,18 @@ public final class PaperExamplePlugin extends JavaPlugin { ### The InitBuilder When calling `OumLib.init()`, it returns an `InitBuilder` which allows fluent configuration of library-wide features: - `.preset(Preset type, String prefix)`: Customizes formatting prefixes for global text presets. -- `.build()`: Completes initialization. Example: ```java OumLib.init(this) .preset(Preset.INFO, "[Info] ") .preset(Preset.SUCCESS, "[Success] ") - .preset(Preset.ERROR, "[Error] ") - .build(); + .preset(Preset.ERROR, "[Error] "); ``` --- -## Velocity Setup +### Velocity Setup Initialize OumLib in your proxy plugin constructor or proxy initialization subscriber. @@ -82,8 +131,7 @@ public final class VelocityExamplePlugin { @Subscribe public void onProxyInitialization(ProxyInitializeEvent event) { // Initialize OumLib for Velocity. - OumLib.init(server, this) - .build(); + OumLib.init(server, this); } @Subscribe @@ -96,6 +144,24 @@ public final class VelocityExamplePlugin { --- +## Platform-Independent Detection Utilities + +To facilitate building modules or shared libraries that operate seamlessly across both Paper (server-side) and Velocity (proxy-side), OumLib provides static checks to safely detect the host platform at runtime: + +```java +import dev.oum.oumlib.OumLib; + +if (OumLib.isPaper()) { + // Run Paper/Folia-specific code +} else if (OumLib.isVelocity()) { + // Run Velocity-specific code +} +``` + +These checks are completely safe to call on either platform. They avoid raising `IllegalStateException` or class loader errors, making it simple to write shared plugins or multi-platform systems. + +--- + ## Shading & Relocation Implications If you shade OumLib into multiple plugins running on the same server, you **must** relocate `dev.oum.oumlib`. If you do not relocate it: - The Java Virtual Machine will load only one instance of the `OumLib` class files (usually from the plugin that loaded first). diff --git a/docs/utilities.md b/docs/utilities.md index 0e209a2..3c194ab 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -164,9 +164,9 @@ Text.bossBarTemporary( ## 6. Proxy & Routing Utilities (Velocity-only) -OumLib includes built-in proxy utilities inside the `Proxy` class for handling server transfers, server fallback routing, player counts, and plugin messaging on Velocity. +OumLib includes built-in proxy utilities inside the `Proxy` class for handling server transfers, server groups, fallback routing, player counts, and cross-server plugin messaging on Velocity. -### Server Connection / Player Transfer +### Server Connection & Player Transfer Transfer players to registered backend servers: ```java import dev.oum.oumlib.util.Proxy; @@ -175,28 +175,81 @@ import com.velocitypowered.api.proxy.Player; Proxy.connect(player, "lobby"); // returns true if connection initiated ``` +### Server Groups & Balancer +You can register multiple backend servers under a named Server Group and connect players dynamically: +```java +import dev.oum.oumlib.util.Proxy; +import java.util.List; + +// Register server group names +Proxy.registerGroup("lobby", List.of("lobby-1", "lobby-2", "lobby-3")); + +// Connect a player to the server with the lowest player count in the group +Proxy.connectLeastPopulated(player, "lobby").thenAccept(success -> { + if (success) player.sendMessage("Connected to the best lobby!"); +}); + +// Connect a player to a random server in the group +Proxy.connectRandom(player, "lobby"); + +// Retrieve all RegisteredServer instances in a group +List servers = Proxy.getGroupServers("lobby"); +``` + ### Auto-Fallback Routing -When players are kicked or disconnected from a backend server (e.g., during a server crash or restart), automatically redirect them to fallback/lobby servers instead of kicking them from the proxy entirely: +Redirect players automatically when kicked from a backend server. You can specify server lists or group names: ```java import dev.oum.oumlib.util.Proxy; import java.util.List; -// Run this during Proxy initialization +// 1. Route to first available server in the list Proxy.registerFallbackRouter(this, List.of("lobby-1", "lobby-2", "hub")); + +// 2. Route to first available server in a registered group +Proxy.registerFallbackRouter(this, "lobby"); + +// 3. Route with a custom event filter (e.g. ignore disconnects/bans) +Proxy.registerFallbackRouter(this, "lobby", event -> { + return event.getServerKickReason().map(r -> !r.toString().contains("ban")).orElse(true); +}); ``` -### Server Player Counts -Retrieve player counts for a specific server or across a cluster of servers: +### Server Player Counts & Retrieval +Retrieve player counts or query connected players: ```java +// Count players on a single server int lobbyCount = Proxy.getPlayerCount("lobby-1"); + +// Count players across a group of servers int totalHubPlayers = Proxy.getPlayerCount(List.of("lobby-1", "lobby-2", "hub")); + +// Retrieve players on a server +Collection players = Proxy.getPlayersOn("lobby-1"); + +// Retrieve a specific player by name or UUID +Optional target = Proxy.getPlayer("Steve"); +Optional targetUuid = Proxy.getPlayer(uuid); ``` ### Cross-Server Plugin Messaging -Send plugin message payloads to the player's active backend server connection without writing verbose registration boilerplate: +Send plugin message payloads to the player's active backend server or broadcast messages across all servers: ```java -byte[] messagePayload = ...; -Proxy.sendPluginMessage(player, "myplugin:sync", messagePayload); +import dev.oum.oumlib.util.Proxy; + +// 1. Send raw bytes +byte[] data = ...; +Proxy.sendPluginMessage(player, "myplugin:sync", data); + +// 2. Send using fluent DataOutput stream builder +Proxy.sendPluginMessage(player, "myplugin:sync", out -> { + out.writeUTF("request_data"); + out.writeInt(12345); +}); + +// 3. Broadcast to all backend servers +Proxy.broadcastPluginMessage("myplugin:broadcast", out -> { + out.writeUTF("global_shutdown"); +}); ``` ### Dynamic Server Registration & Unregistration @@ -209,29 +262,26 @@ Proxy.registerServer("games-3", "192.168.1.15", 25568); Proxy.unregisterServer("games-3"); ``` -### Async Online Status Check (Ping) -Check if a backend server is online and accepting connections asynchronously: +### Promise-Based Server Querying (Ping & Status) +Check if backend servers are online or query full status asynchronously. These methods return OumLib `Promise` wrappers instead of raw `CompletableFuture`s: ```java +import dev.oum.oumlib.util.Proxy; + +// 1. Online check Proxy.isOnline("lobby-1").thenAccept(online -> { - if (online) { - player.sendMessage("Lobby-1 is online!"); - } + if (online) player.sendMessage("Lobby-1 is online!"); }); -``` - -### Targeted Server Broadcasts -Broadcast MiniMessage-formatted messages and titles directly to all players connected to a specific server: -```java -// Broadcast chat message -Proxy.broadcastTo("games-1", "A new match is starting in 10 seconds!"); -// Broadcast title -Proxy.sendTitleTo("games-1", "Match Started", "Good luck!"); -``` +// 2. Query detailed ping info (players, MOTD, version) +Proxy.ping("lobby-1").thenAccept(result -> { + if (result.online()) { + player.sendMessage("Lobby players: " + result.currentPlayers() + "/" + result.maxPlayers()); + player.sendMessage("MOTD: " + result.motd()); + player.sendMessage("Version: " + result.version()); + } +}); -### Smart Load Balancer -Find the best server (the one online with the lowest player count) from a list of options: -```java +// 3. Find the lowest populated online server from a custom list Proxy.getBestServer(List.of("lobby-1", "lobby-2", "lobby-3")) .thenAccept(optServer -> { optServer.ifPresent(server -> { @@ -391,3 +441,59 @@ Effects.sound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP) .play(player); ``` +--- + +## 12. Countdown Timer API + +OumLib features a builder-based, customizable `Countdown` timer utility. It runs on asynchronous scheduler tasks, making it safe for Folia servers, and provides fluent display configurations (titles, action bars, chat announcements), custom intervals, and sounds. + +### Simple Countdown: +```java +import dev.oum.oumlib.util.Countdown; +import java.time.Duration; + +Countdown.builder(player, 10) // 10 seconds countdown targeting player/audience + .displayMode(Countdown.Display.TITLE) // Displays countdown on screen + .onComplete(audience -> { + audience.sendMessage(Component.text("Go!")); + }) + .start(); +``` + +### Advanced Features & Customizable Formats: +```java +import dev.oum.oumlib.util.Countdown; +import dev.oum.oumlib.effect.Sounds; +import java.time.Duration; + +Countdown.builder(player, 30) + .displayMode(Countdown.Display.CHAT) + // Overloaded string template utilizing Format.java utilities: + // %time% -> raw seconds remaining (e.g. 5) + // %duration% -> formatted duration (e.g. 1m 30s) + // %digital% -> digital clock format (e.g. 01:30) + .format("Game starting in %duration%...") + + // Play a tick sound every second + .tickSound(Sounds.TICK) + + // Define custom display intervals (e.g., only show at 30s, 15s, 10s, and under 5s) + .intervals(30, 15, 10, 5, 4, 3, 2, 1) + + // Or filter display times dynamically via Predicate: + .displayFilter(seconds -> seconds % 10 == 0 || seconds <= 5) + + .onComplete(audience -> { + audience.sendMessage(MiniMessage.miniMessage().deserialize("Match Started!")); + }) + .start(); +``` + +### Chat Mode Smart Default Filter: +When using `Display.CHAT` without configuring custom intervals or filters, the system automatically uses a non-spammy default filter that announces the remaining time only at: +- Multiples of `10` seconds (e.g., `30s`, `20s`, `10s`) +- Every second under `5` seconds (`5s`, `4s`, `3s`, `2s`, `1s`) + +This keeps player chat clean and spam-free by default. +``` + diff --git a/example-plugin/pom.xml b/example-plugin/pom.xml index cc1dc0b..e76b51e 100644 --- a/example-plugin/pom.xml +++ b/example-plugin/pom.xml @@ -8,7 +8,7 @@ dev.oum oumlib - 1.0.6 + 1.0.7 ../pom.xml @@ -20,7 +20,7 @@ dev.oum oumlib-core - 1.0.6 + 1.0.7 compile diff --git a/oumlib-core/pom.xml b/oumlib-core/pom.xml index be44aea..2e5ba4e 100644 --- a/oumlib-core/pom.xml +++ b/oumlib-core/pom.xml @@ -8,7 +8,7 @@ dev.oum oumlib - 1.0.6 + 1.0.7 ../pom.xml @@ -48,6 +48,13 @@ 7.0.2 compile + + net.luckperms + api + 5.4 + provided + true + diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java b/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java index ae44a68..418ca24 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java @@ -125,20 +125,7 @@ private static void handleSpigotAutocompleteMessage(Player player, byte[] messag String queryType = dis.readUTF(); String remaining = dis.readUTF(); - List suggestions = new ArrayList<>(); - if (queryType.equalsIgnoreCase("worlds")) { - for (World w : Bukkit.getWorlds()) { - if (w.getName().toLowerCase(Locale.ROOT).startsWith(remaining.toLowerCase(Locale.ROOT))) { - suggestions.add(w.getName()); - } - } - } else if (queryType.equalsIgnoreCase("players")) { - for (Player p : Bukkit.getOnlinePlayers()) { - if (p.getName().toLowerCase(Locale.ROOT).startsWith(remaining.toLowerCase(Locale.ROOT))) { - suggestions.add(p.getName()); - } - } - } + List suggestions = getSuggestions(queryType, remaining); ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); @@ -152,6 +139,24 @@ private static void handleSpigotAutocompleteMessage(Player player, byte[] messag } } + private static @NonNull List getSuggestions(@NonNull String queryType, String remaining) { + List suggestions = new ArrayList<>(); + if (queryType.equalsIgnoreCase("worlds")) { + for (World w : Bukkit.getWorlds()) { + if (w.getName().toLowerCase(Locale.ROOT).startsWith(remaining.toLowerCase(Locale.ROOT))) { + suggestions.add(w.getName()); + } + } + } else if (queryType.equalsIgnoreCase("players")) { + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.getName().toLowerCase(Locale.ROOT).startsWith(remaining.toLowerCase(Locale.ROOT))) { + suggestions.add(p.getName()); + } + } + } + return suggestions; + } + private static void detectIntegrations(@NonNull Plugin p) { if (p.getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) { PapiHelper.register(p, placeholderRegistry); @@ -195,6 +200,14 @@ public static Object velocityPlugin() { return velocityPlugin; } + public static boolean isPaper() { + return plugin != null; + } + + public static boolean isVelocity() { + return proxyServer != null; + } + public static @NonNull File getDataFolder() { assertInit(); if (plugin != null) { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/PermissionBridge.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/PermissionBridge.java index 15e926c..f3973cb 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/PermissionBridge.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/PermissionBridge.java @@ -1,149 +1,171 @@ package dev.oum.oumlib.bridge.permission; +import dev.oum.oumlib.OumLib; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.InheritanceNode; +import org.bukkit.Bukkit; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; -import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.UUID; -import java.util.concurrent.CompletableFuture; + +interface PermissionHandler { + String getPrimaryGroup(UUID uuid); + + String getPrefix(UUID uuid); + + String getSuffix(UUID uuid); + + String getMetaValue(UUID uuid, String key); + + List getGroups(UUID uuid); + + void setGroups(UUID uuid, List groups, String primaryGroup); +} public final class PermissionBridge { - private static Object luckPerms; + private static PermissionHandler handler; + private static boolean initialized = false; + private static boolean hasProviderClass = false; static { try { - Class provider = Class.forName("net.luckperms.api.LuckPermsProvider"); - luckPerms = provider.getMethod("get").invoke(null); - } catch (Throwable ignored) { + Class.forName("net.luckperms.api.LuckPermsProvider"); + hasProviderClass = true; + } catch (ClassNotFoundException ignored) { } } private PermissionBridge() { } + private static synchronized @Nullable PermissionHandler getHandler() { + if (initialized) return handler; + if (!hasProviderClass) { + initialized = true; + return null; + } + try { + Class.forName("org.bukkit.Bukkit"); + if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) { + handler = new LuckPermsHandler(); + initialized = true; + return handler; + } + } catch (Throwable ignored) { + } + try { + Class.forName("com.velocitypowered.api.proxy.ProxyServer"); + if (OumLib.proxy().getPluginManager().isLoaded("luckperms")) { + handler = new LuckPermsHandler(); + initialized = true; + return handler; + } + } catch (Throwable ignored) { + } + initialized = true; + return handler; + } + public static boolean isAvailable() { - return luckPerms != null; + return getHandler() != null; } public static @Nullable String getPrimaryGroup(@NonNull UUID uuid) { - if (luckPerms == null) return null; - try { - Object userManager = luckPerms.getClass().getMethod("getUserManager").invoke(luckPerms); - Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, uuid); - if (user == null) return null; - return (String) user.getClass().getMethod("getPrimaryGroup").invoke(user); - } catch (Exception ignored) { - } - return null; + PermissionHandler h = getHandler(); + return h != null ? h.getPrimaryGroup(uuid) : null; } public static @Nullable String getPrefix(@NonNull UUID uuid) { - if (luckPerms == null) return null; - try { - Object userManager = luckPerms.getClass().getMethod("getUserManager").invoke(luckPerms); - Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, uuid); - if (user == null) return null; - Object cachedData = user.getClass().getMethod("getCachedData").invoke(user); - Object metaData = cachedData.getClass().getMethod("getMetaData").invoke(cachedData); - return (String) metaData.getClass().getMethod("getPrefix").invoke(metaData); - } catch (Exception ignored) { - } - return null; + PermissionHandler h = getHandler(); + return h != null ? h.getPrefix(uuid) : null; } public static @Nullable String getSuffix(@NonNull UUID uuid) { - if (luckPerms == null) return null; - try { - Object userManager = luckPerms.getClass().getMethod("getUserManager").invoke(luckPerms); - Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, uuid); - if (user == null) return null; - Object cachedData = user.getClass().getMethod("getCachedData").invoke(user); - Object metaData = cachedData.getClass().getMethod("getMetaData").invoke(cachedData); - return (String) metaData.getClass().getMethod("getSuffix").invoke(metaData); - } catch (Exception ignored) { - } - return null; + PermissionHandler h = getHandler(); + return h != null ? h.getSuffix(uuid) : null; } public static @Nullable String getMetaValue(@NonNull UUID uuid, @NonNull String key) { - if (luckPerms == null) return null; - try { - Object userManager = luckPerms.getClass().getMethod("getUserManager").invoke(luckPerms); - Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, uuid); - if (user == null) return null; - Object cachedData = user.getClass().getMethod("getCachedData").invoke(user); - Object metaData = cachedData.getClass().getMethod("getMetaData").invoke(cachedData); - return (String) metaData.getClass().getMethod("getMetaValue", String.class).invoke(metaData, key); - } catch (Exception ignored) { - } - return null; + PermissionHandler h = getHandler(); + return h != null ? h.getMetaValue(uuid, key) : null; } public static @Nullable List getGroups(@NonNull UUID uuid) { - if (luckPerms == null) return null; - try { - Object userManager = luckPerms.getClass().getMethod("getUserManager").invoke(luckPerms); - Object user = userManager.getClass().getMethod("getUser", UUID.class).invoke(userManager, uuid); - if (user == null) return null; - List groups = new ArrayList<>(); - Collection nodes = (Collection) user.getClass().getMethod("getNodes").invoke(user); - Class inheritanceNodeClass = Class.forName("net.luckperms.api.node.types.InheritanceNode"); - Method getGroupNameMethod = inheritanceNodeClass.getMethod("getGroupName"); - for (Object node : nodes) { - if (inheritanceNodeClass.isInstance(node)) { - groups.add((String) getGroupNameMethod.invoke(node)); - } - } - return groups; - } catch (Exception ignored) { - } - return null; + PermissionHandler h = getHandler(); + return h != null ? h.getGroups(uuid) : null; } public static void setGroups(@NonNull UUID uuid, @NonNull List groups, @Nullable String primaryGroup) { - if (luckPerms == null) return; - try { - Object userManager = luckPerms.getClass().getMethod("getUserManager").invoke(luckPerms); - CompletableFuture future = (CompletableFuture) userManager.getClass() - .getMethod("loadUser", UUID.class) - .invoke(userManager, uuid); - - future.thenAcceptAsync(user -> { - try { - Object data = user.getClass().getMethod("data").invoke(user); - - Class nodeTypeClass = Class.forName("net.luckperms.api.node.NodeType"); - Object inheritanceType = nodeTypeClass.getField("INHERITANCE").get(null); - - data.getClass().getMethod("clear", nodeTypeClass).invoke(data, inheritanceType); - - Class inheritanceNodeClass = Class.forName("net.luckperms.api.node.types.InheritanceNode"); - Method builderMethod = inheritanceNodeClass.getMethod("builder", String.class); - - Class nodeClass = Class.forName("net.luckperms.api.node.Node"); - Method addMethod = data.getClass().getMethod("add", nodeClass); - - for (String group : groups) { - Object builder = builderMethod.invoke(null, group); - Object node = builder.getClass().getMethod("build").invoke(builder); - addMethod.invoke(data, node); - } - - if (primaryGroup != null) { - user.getClass().getMethod("setPrimaryGroup", String.class).invoke(user, primaryGroup); - } - - Class userClass = Class.forName("net.luckperms.api.model.user.User"); - userManager.getClass().getMethod("saveUser", userClass).invoke(userManager, user); - } catch (Exception e) { - e.printStackTrace(); - } - }); - } catch (Exception ignored) { + PermissionHandler h = getHandler(); + if (h != null) { + h.setGroups(uuid, groups, primaryGroup); + } + } +} + +class LuckPermsHandler implements PermissionHandler { + private final LuckPerms api = LuckPermsProvider.get(); + + @Override + public String getPrimaryGroup(UUID uuid) { + User user = api.getUserManager().getUser(uuid); + return user != null ? user.getPrimaryGroup() : null; + } + + @Override + public String getPrefix(UUID uuid) { + User user = api.getUserManager().getUser(uuid); + if (user == null) return null; + return user.getCachedData().getMetaData().getPrefix(); + } + + @Override + public String getSuffix(UUID uuid) { + User user = api.getUserManager().getUser(uuid); + if (user == null) return null; + return user.getCachedData().getMetaData().getSuffix(); + } + + @Override + public String getMetaValue(UUID uuid, String key) { + User user = api.getUserManager().getUser(uuid); + if (user == null) return null; + return user.getCachedData().getMetaData().getMetaValue(key); + } + + @Override + public List getGroups(UUID uuid) { + User user = api.getUserManager().getUser(uuid); + if (user == null) return null; + List groups = new ArrayList<>(); + for (Node node : user.getNodes()) { + if (node instanceof InheritanceNode inheritanceNode) { + groups.add(inheritanceNode.getGroupName()); + } } + return groups; + } + + @Override + public void setGroups(UUID uuid, List groups, String primaryGroup) { + api.getUserManager().loadUser(uuid).thenAcceptAsync(user -> { + if (user == null) return; + user.data().clear(NodeType.INHERITANCE::matches); + for (String group : groups) { + user.data().add(InheritanceNode.builder(group).build()); + } + if (primaryGroup != null) { + user.setPrimaryGroup(primaryGroup); + } + api.getUserManager().saveUser(user); + }); } } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/Arguments.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/Arguments.java index c6cfb67..94e2d3e 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/Arguments.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/Arguments.java @@ -1,14 +1,11 @@ package dev.oum.oumlib.command; -import com.mojang.brigadier.arguments.BoolArgumentType; -import com.mojang.brigadier.arguments.DoubleArgumentType; -import com.mojang.brigadier.arguments.FloatArgumentType; -import com.mojang.brigadier.arguments.IntegerArgumentType; -import com.mojang.brigadier.arguments.LongArgumentType; -import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.arguments.*; import dev.oum.oumlib.OumLib; import dev.oum.oumlib.util.Format; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.entity.Player; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; @@ -67,7 +64,14 @@ private Arguments() { .invoke(null, name); } catch (Exception ignored) { } - + try { + Class.forName("org.bukkit.Bukkit"); + return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { + String nameStr = (String) raw; + return Bukkit.getPlayer(nameStr); + }); + } catch (Exception ignored) { + } return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { String nameStr = (String) raw; return OumLib.proxy().getPlayer(nameStr).orElse(null); @@ -107,6 +111,15 @@ private Arguments() { .invoke(null, name); } catch (Exception ignored) { } + try { + Class.forName("org.bukkit.Bukkit"); + return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { + String nameStr = (String) raw; + var player = Bukkit.getPlayer(nameStr); + return player != null ? List.of(player) : List.of(); + }); + } catch (Exception ignored) { + } return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { String nameStr = (String) raw; var player = OumLib.proxy().getPlayer(nameStr).orElse(null); @@ -193,11 +206,11 @@ private Arguments() { Class.forName("org.bukkit.Bukkit"); return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { String nameStr = (String) raw; - return org.bukkit.Bukkit.getOfflinePlayer(nameStr); + return Bukkit.getOfflinePlayer(nameStr); }).suggests(context -> { try { List suggestions = new ArrayList<>(); - for (org.bukkit.entity.Player p : org.bukkit.Bukkit.getOnlinePlayers()) { + for (Player p : Bukkit.getOnlinePlayers()) { suggestions.add(p.getName()); } return suggestions; diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/CommandBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/CommandBuilder.java index 0a411f3..b7a63fd 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/CommandBuilder.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/CommandBuilder.java @@ -109,7 +109,7 @@ private CommandBuilder(String label) { public void register() { try { CommandRegistrar registrar; - if (OumLib.plugin() != null) { + if (OumLib.isPaper()) { registrar = (CommandRegistrar) Class.forName("dev.oum.oumlib.command.platform.PaperCommandRegistrar") .getDeclaredConstructor().newInstance(); } else { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/CooldownMap.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/CooldownMap.java deleted file mode 100644 index 0049a18..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/CooldownMap.java +++ /dev/null @@ -1,37 +0,0 @@ -package dev.oum.oumlib.command; - -import java.time.Duration; -import java.time.Instant; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -@Deprecated(forRemoval = true, since = "1.0.1") -public final class CooldownMap { - - private final Map timestamps = new ConcurrentHashMap<>(); - private final Duration duration; - - public CooldownMap(Duration duration) { - this.duration = duration; - } - - public boolean isOnCooldown(UUID id) { - Instant last = timestamps.get(id); - return last != null && Instant.now().isBefore(last.plus(duration)); - } - - public long remainingSeconds(UUID id) { - Instant last = timestamps.get(id); - if (last == null) return 0; - return Math.max(0, Duration.between(Instant.now(), last.plus(duration)).getSeconds()); - } - - public void set(UUID id) { - timestamps.put(id, Instant.now()); - } - - public void clear(UUID id) { - timestamps.remove(id); - } -} \ No newline at end of file diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/PaperCommandHelper.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/PaperCommandHelper.java index ec47e7c..8149c3f 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/PaperCommandHelper.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/PaperCommandHelper.java @@ -6,18 +6,18 @@ import io.papermc.paper.command.brigadier.argument.ArgumentTypes; import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver; import io.papermc.paper.command.brigadier.argument.resolvers.FinePositionResolver; +import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver; import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver; import io.papermc.paper.math.BlockPosition; import io.papermc.paper.math.FinePosition; import org.bukkit.Location; import org.bukkit.NamespacedKey; import org.bukkit.World; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; -import org.bukkit.entity.Entity; -import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver; import java.util.List; public final class PaperCommandHelper { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/database/Database.java b/oumlib-core/src/main/java/dev/oum/oumlib/database/Database.java index 088955d..ac9cde9 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/database/Database.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/database/Database.java @@ -335,41 +335,6 @@ public void setSlowQueryThresholdMs(long ms) { }); } - @CheckReturnValue - @Deprecated(since = "1.0.5", forRemoval = false) - public @NonNull Promise transaction(@NonNull TransactionCallback callback) { - return Promise.supplyVirtual(() -> { - long start = System.currentTimeMillis(); - try (Connection conn = getConnection()) { - boolean wasAutoCommit = conn.getAutoCommit(); - try { - conn.setAutoCommit(false); - R result = callback.execute(conn); - conn.commit(); - long elapsed = System.currentTimeMillis() - start; - if (elapsed > slowQueryThresholdMs) { - OumLib.logger().warning("SLOW TRANSACTION (" + elapsed + "ms)"); - } - return result; - } catch (Throwable t) { - try { - conn.rollback(); - } catch (SQLException ex) { - t.addSuppressed(ex); - } - throw t; - } finally { - try { - conn.setAutoCommit(wasAutoCommit); - } catch (SQLException ignored) { - } - } - } catch (SQLException e) { - throw new RuntimeException("SQL transaction execution failed", e); - } - }); - } - @CheckReturnValue public @NonNull Promise transaction(@NonNull TransactionContextCallback callback) { return Promise.supplyVirtual(() -> { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/database/TransactionCallback.java b/oumlib-core/src/main/java/dev/oum/oumlib/database/TransactionCallback.java deleted file mode 100644 index ded1ca1..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/database/TransactionCallback.java +++ /dev/null @@ -1,11 +0,0 @@ -package dev.oum.oumlib.database; - -import org.jspecify.annotations.NonNull; - -import java.sql.Connection; -import java.sql.SQLException; - -@FunctionalInterface -public interface TransactionCallback { - R execute(@NonNull Connection connection) throws SQLException; -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/DisplayBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/DisplayBuilder.java new file mode 100644 index 0000000..065a5f3 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/DisplayBuilder.java @@ -0,0 +1,269 @@ +package dev.oum.oumlib.entity; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.*; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Transformation; +import org.jetbrains.annotations.Contract; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public abstract class DisplayBuilder> { + + protected final Location location; + protected final EntityType type; + protected Display.Billboard billboard; + protected Vector3f scale; + protected Vector3f translation; + protected Quaternionf leftRotation; + protected Quaternionf rightRotation; + protected Float shadowRadius; + protected Float shadowStrength; + protected Float viewRange; + protected Color glowColor; + protected Integer interpolationDuration; + protected Integer teleportDuration; + protected Boolean glowing; + + protected DisplayBuilder(Location location, EntityType type) { + this.location = location; + this.type = type; + } + + @Contract("_, _ -> new") + public static @NonNull TextDisplayBuilder text(@NonNull Location location, @NonNull Component text) { + return new TextDisplayBuilder(location, text); + } + + @Contract("_, _ -> new") + public static @NonNull TextDisplayBuilder text(@NonNull Location location, @NonNull String miniMessage) { + return new TextDisplayBuilder(location, MiniMessage.miniMessage().deserialize(miniMessage)); + } + + @Contract("_, _ -> new") + public static @NonNull BlockDisplayBuilder block(@NonNull Location location, @NonNull BlockData blockData) { + return new BlockDisplayBuilder(location, blockData); + } + + @Contract("_, _ -> new") + public static @NonNull ItemDisplayBuilder item(@NonNull Location location, @NonNull ItemStack itemStack) { + return new ItemDisplayBuilder(location, itemStack); + } + + @SuppressWarnings("unchecked") + protected B self() { + return (B) this; + } + + public B billboard(Display.@Nullable Billboard billboard) { + this.billboard = billboard; + return self(); + } + + public B scale(float x, float y, float z) { + this.scale = new Vector3f(x, y, z); + return self(); + } + + public B translation(float x, float y, float z) { + this.translation = new Vector3f(x, y, z); + return self(); + } + + public B leftRotation(float x, float y, float z, float w) { + this.leftRotation = new Quaternionf(x, y, z, w); + return self(); + } + + public B rightRotation(float x, float y, float z, float w) { + this.rightRotation = new Quaternionf(x, y, z, w); + return self(); + } + + public B shadowRadius(float shadowRadius) { + this.shadowRadius = shadowRadius; + return self(); + } + + public B shadowStrength(float shadowStrength) { + this.shadowStrength = shadowStrength; + return self(); + } + + public B viewRange(float viewRange) { + this.viewRange = viewRange; + return self(); + } + + public B glowColor(@Nullable Color glowColor) { + this.glowColor = glowColor; + return self(); + } + + public B glowing(boolean glowing) { + this.glowing = glowing; + return self(); + } + + public B interpolationDuration(int ticks) { + this.interpolationDuration = ticks; + return self(); + } + + public B teleportDuration(int ticks) { + this.teleportDuration = ticks; + return self(); + } + + protected void applyProperties(D display) { + if (billboard != null) { + display.setBillboard(billboard); + } + if (shadowRadius != null) { + display.setShadowRadius(shadowRadius); + } + if (shadowStrength != null) { + display.setShadowStrength(shadowStrength); + } + if (viewRange != null) { + display.setViewRange(viewRange); + } + if (glowColor != null) { + display.setGlowColorOverride(glowColor); + } + if (glowing != null) { + display.setGlowing(glowing); + } + if (interpolationDuration != null) { + display.setInterpolationDuration(interpolationDuration); + } + if (teleportDuration != null) { + display.setTeleportDuration(teleportDuration); + } + if (scale != null || translation != null || leftRotation != null || rightRotation != null) { + Vector3f t = translation != null ? translation : new Vector3f(); + Quaternionf lr = leftRotation != null ? leftRotation : new Quaternionf(); + Vector3f s = scale != null ? scale : new Vector3f(1, 1, 1); + Quaternionf rr = rightRotation != null ? rightRotation : new Quaternionf(); + display.setTransformation(new Transformation(t, lr, s, rr)); + } + } + + @SuppressWarnings("unchecked") + public D spawn() { + if (location.getWorld() == null) { + throw new IllegalStateException("World is null"); + } + D display = (D) location.getWorld().spawnEntity(location, type); + applyProperties(display); + return display; + } + + public static final class TextDisplayBuilder extends DisplayBuilder { + private final Component text; + private Color backgroundColor; + private Integer lineWidth; + private Byte opacity; + private TextDisplay.TextAlignment alignment; + private Boolean seeThrough; + private Boolean shadow; + + private TextDisplayBuilder(Location location, Component text) { + super(location, EntityType.TEXT_DISPLAY); + this.text = text; + } + + public TextDisplayBuilder backgroundColor(@Nullable Color color) { + this.backgroundColor = color; + return this; + } + + public TextDisplayBuilder lineWidth(int width) { + this.lineWidth = width; + return this; + } + + public TextDisplayBuilder opacity(byte opacity) { + this.opacity = opacity; + return this; + } + + public TextDisplayBuilder alignment(TextDisplay.@Nullable TextAlignment alignment) { + this.alignment = alignment; + return this; + } + + public TextDisplayBuilder seeThrough(boolean seeThrough) { + this.seeThrough = seeThrough; + return this; + } + + public TextDisplayBuilder shadow(boolean shadow) { + this.shadow = shadow; + return this; + } + + @Override + public @NonNull TextDisplay spawn() { + TextDisplay display = super.spawn(); + display.text(text); + if (backgroundColor != null) { + display.setBackgroundColor(backgroundColor); + } + if (lineWidth != null) { + display.setLineWidth(lineWidth); + } + if (opacity != null) { + display.setTextOpacity(opacity); + } + if (alignment != null) { + display.setAlignment(alignment); + } + if (seeThrough != null) { + display.setSeeThrough(seeThrough); + } + if (shadow != null) { + display.setShadowed(shadow); + } + return display; + } + } + + public static final class BlockDisplayBuilder extends DisplayBuilder { + private final BlockData blockData; + + private BlockDisplayBuilder(Location location, BlockData blockData) { + super(location, EntityType.BLOCK_DISPLAY); + this.blockData = blockData; + } + + @Override + public @NonNull BlockDisplay spawn() { + BlockDisplay display = super.spawn(); + display.setBlock(blockData); + return display; + } + } + + public static final class ItemDisplayBuilder extends DisplayBuilder { + private final ItemStack itemStack; + + private ItemDisplayBuilder(Location location, ItemStack itemStack) { + super(location, EntityType.ITEM_DISPLAY); + this.itemStack = itemStack; + } + + @Override + public @NonNull ItemDisplay spawn() { + ItemDisplay display = super.spawn(); + display.setItemStack(itemStack); + return display; + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/Entities.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/Entities.java new file mode 100644 index 0000000..4c192bb --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/Entities.java @@ -0,0 +1,198 @@ +package dev.oum.oumlib.entity; + +import dev.oum.oumlib.scheduler.Promise; +import org.bukkit.FluidCollisionMode; +import org.bukkit.Location; +import org.bukkit.Registry; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.potion.PotionEffect; +import org.bukkit.util.RayTraceResult; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +public final class Entities { + + private Entities() { + } + + public static @Nullable Block getTargetBlock(@NonNull Player player, int maxDistance) { + return getTargetBlock(player, maxDistance, FluidCollisionMode.NEVER); + } + + public static @Nullable Block getTargetBlock(@NonNull Player player, int maxDistance, + @NonNull FluidCollisionMode fluidMode) { + RayTraceResult result = player.getWorld().rayTraceBlocks( + player.getEyeLocation(), + player.getLocation().getDirection(), + maxDistance, + fluidMode + ); + return result != null ? result.getHitBlock() : null; + } + + public static @Nullable Entity getTargetEntity(@NonNull Player player, int maxDistance) { + return getTargetEntity(player, maxDistance, entity -> !entity.equals(player)); + } + + public static @Nullable Entity getTargetEntity(@NonNull Player player, int maxDistance, + @NonNull Predicate filter) { + RayTraceResult result = player.getWorld().rayTraceEntities( + player.getEyeLocation(), + player.getLocation().getDirection(), + maxDistance, + filter + ); + return result != null ? result.getHitEntity() : null; + } + + public static @Nullable RayTraceResult rayTrace(@NonNull Location origin, @NonNull Vector direction, + double maxDistance, @NonNull Predicate filter) { + if (origin.getWorld() == null) return null; + return origin.getWorld().rayTraceEntities(origin, direction, maxDistance, filter); + } + + public static @NonNull List nearbyPlayers(@NonNull Location location, double radius) { + return nearbyPlayers(location, radius, p -> true); + } + + public static @NonNull List nearbyPlayers(@NonNull Location location, double radius, + @NonNull Predicate filter) { + if (location.getWorld() == null) return List.of(); + return location.getWorld().getNearbyPlayers(location, radius).stream() + .filter(filter) + .toList(); + } + + public static @NonNull List nearbyEntities(@NonNull Location location, double radius, + @NonNull Class type) { + return nearbyEntities(location, radius, type, e -> true); + } + + @SuppressWarnings("unchecked") + public static @NonNull List nearbyEntities(@NonNull Location location, double radius, + @NonNull Class type, + @NonNull Predicate filter) { + if (location.getWorld() == null) return List.of(); + return location.getWorld().getNearbyEntities(location, radius, radius, radius).stream() + .filter(type::isInstance) + .map(e -> (T) e) + .filter(filter) + .toList(); + } + + public static @NonNull Optional closestPlayer(@NonNull Location location, double radius) { + return nearbyPlayers(location, radius).stream() + .min(Comparator.comparingDouble(p -> p.getLocation().distanceSquared(location))); + } + + public static @NonNull Optional closestEntity(@NonNull Location location, double radius, + @NonNull Class type) { + return nearbyEntities(location, radius, type).stream() + .min(Comparator.comparingDouble(e -> e.getLocation().distanceSquared(location))); + } + + public static boolean isWithinAABB(@NonNull Entity entity, @NonNull Location min, @NonNull Location max) { + return isWithinAABB(entity.getLocation(), min, max); + } + + public static boolean isWithinAABB(@NonNull Location loc, @NonNull Location min, @NonNull Location max) { + double minX = Math.min(min.getX(), max.getX()); + double minY = Math.min(min.getY(), max.getY()); + double minZ = Math.min(min.getZ(), max.getZ()); + double maxX = Math.max(min.getX(), max.getX()); + double maxY = Math.max(min.getY(), max.getY()); + double maxZ = Math.max(min.getZ(), max.getZ()); + + return loc.getX() >= minX && loc.getX() <= maxX + && loc.getY() >= minY && loc.getY() <= maxY + && loc.getZ() >= minZ && loc.getZ() <= maxZ; + } + + public static @NonNull Promise teleportAsync(@NonNull Entity entity, @NonNull Location location) { + return teleportAsync(entity, location, PlayerTeleportEvent.TeleportCause.PLUGIN); + } + + public static @NonNull Promise teleportAsync(@NonNull Entity entity, @NonNull Location location, + PlayerTeleportEvent.@NonNull TeleportCause cause) { + return Promise.fromCompletableFuture(entity.teleportAsync(location, cause)); + } + + public static void setGlowing(@NonNull Entity entity, boolean glowing) { + entity.setGlowing(glowing); + } + + public static void freeze(@NonNull Entity entity) { + entity.setFreezeTicks(entity.getMaxFreezeTicks()); + } + + public static void unfreeze(@NonNull Entity entity) { + entity.setFreezeTicks(0); + } + + public static void setInvulnerable(@NonNull Entity entity, boolean invulnerable) { + entity.setInvulnerable(invulnerable); + } + + public static void setInvisible(@NonNull LivingEntity entity, boolean invisible) { + entity.setInvisible(invisible); + } + + public static void removeAllEffects(@NonNull LivingEntity entity) { + Collection effects = entity.getActivePotionEffects(); + for (PotionEffect effect : effects) { + entity.removePotionEffect(effect.getType()); + } + } + + public static void setMaxHealth(@NonNull LivingEntity entity, double maxHealth) { + AttributeInstance attr = entity.getAttribute(Attribute.MAX_HEALTH); + if (attr != null) { + attr.setBaseValue(maxHealth); + } + } + + public static void heal(@NonNull LivingEntity entity) { + AttributeInstance attr = entity.getAttribute(Attribute.MAX_HEALTH); + if (attr != null) { + entity.setHealth(attr.getValue()); + } + } + + public static void heal(@NonNull LivingEntity entity, double amount) { + AttributeInstance attr = entity.getAttribute(Attribute.MAX_HEALTH); + double maxHealth = attr != null ? attr.getValue() : entity.getHealth(); + entity.setHealth(Math.min(entity.getHealth() + amount, maxHealth)); + } + + public static void setSpeed(@NonNull Player player, double speed) { + player.setWalkSpeed((float) Math.clamp(speed, -1.0, 1.0)); + } + + public static void resetSpeed(@NonNull Player player) { + player.setWalkSpeed(0.2f); + player.setFlySpeed(0.1f); + } + + public static void resetAttributes(@NonNull LivingEntity entity) { + for (Attribute attribute : Registry.ATTRIBUTE) { + AttributeInstance instance = entity.getAttribute(attribute); + if (instance != null) { + instance.setBaseValue(instance.getDefaultValue()); + instance.getModifiers().forEach(instance::removeModifier); + } + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/event/BukkitEvents.java b/oumlib-core/src/main/java/dev/oum/oumlib/event/BukkitEvents.java index 6107e9d..180637f 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/event/BukkitEvents.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/event/BukkitEvents.java @@ -7,6 +7,7 @@ import java.lang.reflect.Method; import java.util.UUID; +@Deprecated(since = "1.0.7", forRemoval = true) public final class BukkitEvents { private BukkitEvents() { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/event/EventBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/event/EventBuilder.java index 147ef1d..7b71dc9 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/event/EventBuilder.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/event/EventBuilder.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; public final class EventBuilder { @@ -47,6 +48,14 @@ public EventBuilder filter(Predicate predicate) { return this; } + public

EventBuilder playerFilter(Function extractor, Predicate

condition) { + filters.add(e -> { + P p = extractor.apply(e); + return p != null && condition.test(p); + }); + return this; + } + public EventBuilder maxFires(int count) { this.maxFires = count; return this; diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/event/platform/PaperEventBus.java b/oumlib-core/src/main/java/dev/oum/oumlib/event/platform/PaperEventBus.java index f22745e..9d710c0 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/event/platform/PaperEventBus.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/event/platform/PaperEventBus.java @@ -2,6 +2,7 @@ import dev.oum.oumlib.event.EventBuilder; import dev.oum.oumlib.event.ListenerHandle; +import dev.oum.oumlib.scheduler.Scheduler; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.Listener; @@ -11,7 +12,6 @@ import java.time.Instant; import java.util.concurrent.atomic.AtomicInteger; -import dev.oum.oumlib.scheduler.Scheduler; public final class PaperEventBus implements EventBusAdapter { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/AnvilMenu.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/AnvilMenu.java index 715071b..bf0a2ca 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/AnvilMenu.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/AnvilMenu.java @@ -16,6 +16,7 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.view.AnvilView; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.CheckReturnValue; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; @@ -29,6 +30,7 @@ import java.util.function.Consumer; @SuppressWarnings({"UnstableApiUsage", "unused"}) +@ApiStatus.Obsolete public final class AnvilMenu implements Menu { private static final MiniMessage MM = MiniMessage.miniMessage(); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ChestMenu.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ChestMenu.java index 2c7eacf..093a3a2 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ChestMenu.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ChestMenu.java @@ -2,6 +2,8 @@ import dev.oum.oumlib.event.Events; import dev.oum.oumlib.event.ListenerHandle; +import dev.oum.oumlib.scheduler.Scheduler; +import dev.oum.oumlib.scheduler.TaskHandle; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; @@ -17,6 +19,7 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import java.time.Duration; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; @@ -40,10 +43,12 @@ public final class ChestMenu implements Menu { private final Sound openSound; private final Sound closeSound; private final Sound clickSound; + private final Duration autoRefresh; private ListenerHandle clickHandle; private ListenerHandle dragHandle; private ListenerHandle closeHandle; + private TaskHandle refreshTask; private ChestMenu(@NonNull Builder builder) { this.title = builder.title; @@ -57,6 +62,7 @@ private ChestMenu(@NonNull Builder builder) { this.openSound = builder.openSound; this.closeSound = builder.closeSound; this.clickSound = builder.clickSound; + this.autoRefresh = builder.autoRefresh; } @Contract(" -> new") @@ -89,7 +95,7 @@ public void updateState(@NonNull Player player, @NonNull String key, @Nullable O } @Override - public void open(Player player) { + public void open(@NonNull Player player) { Map state = playerStates.computeIfAbsent(player.getUniqueId(), uuid -> { Map map = new HashMap<>(); stateProviders.forEach((key, provider) -> map.put(key, provider.apply(player))); @@ -110,6 +116,16 @@ public void open(Player player) { if (openSound != null) { player.playSound(openSound); } + if (autoRefresh != null && refreshTask == null) { + refreshTask = Scheduler.runRepeating(autoRefresh, autoRefresh, () -> { + for (UUID uuid : open.keySet()) { + Player p = Bukkit.getPlayer(uuid); + if (p != null && p.isOnline()) { + refresh(p); + } + } + }); + } } @Override @@ -122,6 +138,10 @@ public void close(@NonNull Player player) { } if (open.isEmpty()) { unregisterListeners(); + if (refreshTask != null) { + refreshTask.cancel(); + refreshTask = null; + } } } @@ -236,6 +256,10 @@ private synchronized void registerListeners() { playerStates.remove(player.getUniqueId()); if (open.isEmpty()) { unregisterListeners(); + if (refreshTask != null) { + refreshTask.cancel(); + refreshTask = null; + } } } }); @@ -282,6 +306,13 @@ public static final class Builder { private Sound openSound; private Sound closeSound; private Sound clickSound; + private Duration autoRefresh; + + @CheckReturnValue + public @NonNull Builder autoRefresh(@Nullable Duration duration) { + this.autoRefresh = duration; + return this; + } @CheckReturnValue public @NonNull Builder openSound(@Nullable Sound sound) { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemBuilder.java index ef64959..493a5ec 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemBuilder.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemBuilder.java @@ -4,6 +4,9 @@ import com.google.gson.Gson; import dev.oum.oumlib.OumLib; import dev.oum.oumlib.util.ItemSerializer; +import io.papermc.paper.datacomponent.DataComponentType; +import io.papermc.paper.datacomponent.DataComponentTypes; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; @@ -17,6 +20,7 @@ import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.persistence.PersistentDataType; import org.bukkit.profile.PlayerTextures; +import org.bukkit.tag.DamageTypeTags; import org.jetbrains.annotations.CheckReturnValue; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; @@ -25,10 +29,8 @@ import java.lang.reflect.Field; import java.net.URI; import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; +import java.nio.charset.StandardCharsets; +import java.util.*; import java.util.stream.Collectors; public final class ItemBuilder { @@ -130,8 +132,7 @@ private ItemBuilder(@NonNull ItemStack item) { @CheckReturnValue public @NonNull ItemBuilder glow() { - meta.addEnchant(Enchantment.UNBREAKING, 1, true); - meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + meta.setEnchantmentGlintOverride(true); return this; } @@ -154,6 +155,67 @@ private ItemBuilder(@NonNull ItemStack item) { return this; } + @CheckReturnValue + public @NonNull ItemBuilder itemModel(@NonNull Key key) { + meta.setItemModel(NamespacedKey.fromString(key.asString())); + return this; + } + + @CheckReturnValue + public @NonNull ItemBuilder itemModel(@NonNull NamespacedKey key) { + meta.setItemModel(key); + return this; + } + + @CheckReturnValue + public @NonNull ItemBuilder glintOverride(boolean glint) { + meta.setEnchantmentGlintOverride(glint); + return this; + } + + @CheckReturnValue + public @NonNull ItemBuilder maxStackSize(int maxStackSize) { + meta.setMaxStackSize(maxStackSize); + return this; + } + + @CheckReturnValue + public @NonNull ItemBuilder maxDamage(int maxDamage) { + stack.setData(DataComponentTypes.MAX_DAMAGE, maxDamage); + return this; + } + + @CheckReturnValue + public @NonNull ItemBuilder fireResistant(boolean resistant) { + if (resistant) { + meta.setDamageResistant(DamageTypeTags.IS_FIRE); + } else { + meta.setDamageResistant(null); + } + return this; + } + + @CheckReturnValue + public @NonNull ItemBuilder hideTooltip(boolean hide) { + meta.setHideTooltip(hide); + return this; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @CheckReturnValue + public @NonNull ItemBuilder data(DataComponentType.@NonNull Valued type, @NonNull Object value) { + stack.setItemMeta(meta); + stack.setData(type, value); + return this; + } + + @CheckReturnValue + public @NonNull ItemBuilder removeData(@NonNull DataComponentType type) { + stack.setItemMeta(meta); + stack.unsetData(type); + return this; + } + @CheckReturnValue public @NonNull ItemBuilder pdc(@NonNull String key, @NonNull String value) { NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); @@ -238,7 +300,28 @@ private ItemBuilder(@NonNull ItemStack item) { try { PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID(), null); PlayerTextures textures = profile.getTextures(); - URL url = URI.create("https://textures.minecraft.net/texture/" + textureValue).toURL(); + + String targetUrl = textureValue; + if (textureValue.startsWith("ey")) { + try { + String decodedStr = new String(Base64.getDecoder().decode(textureValue), StandardCharsets.UTF_8); + int index = decodedStr.indexOf("\"url\":\""); + if (index != -1) { + int start = index + 7; + int end = decodedStr.indexOf("\"", start); + if (end != -1) { + targetUrl = decodedStr.substring(start, end); + } + } + } catch (Throwable ignored) { + } + } + + if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) { + targetUrl = "https://textures.minecraft.net/texture/" + targetUrl; + } + + URL url = URI.create(targetUrl).toURL(); textures.setSkin(url); profile.setTextures(textures); skullMeta.setPlayerProfile(profile); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/PaginatedMenu.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/PaginatedMenu.java index 272fd3e..1c2e6e2 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/PaginatedMenu.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/PaginatedMenu.java @@ -13,6 +13,7 @@ import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.CheckReturnValue; import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; import org.jspecify.annotations.NonNull; import java.util.*; @@ -29,7 +30,7 @@ public final class PaginatedMenu implements Menu { private final int nextSlot; private final Function prevButton; private final Function nextButton; - private final List items; + private final Function> itemsSupplier; private final PaginatedClickHandler clickHandler; private final Map pages = new HashMap<>(); private final Map open = new HashMap<>(); @@ -45,7 +46,7 @@ private PaginatedMenu(@NonNull Builder builder) { this.nextSlot = builder.nextSlot; this.prevButton = builder.prevButton; this.nextButton = builder.nextButton; - this.items = List.copyOf(builder.items); + this.itemsSupplier = builder.itemsSupplier; this.clickHandler = builder.clickHandler; } @@ -56,7 +57,17 @@ private PaginatedMenu(@NonNull Builder builder) { } public int totalPages() { - return Math.max(1, (int) Math.ceil((double) items.size() / contentSlots.length)); + try { + return totalPages(null); + } catch (Exception e) { + return 1; + } + } + + public int totalPages(@Nullable Player player) { + List list = itemsSupplier.apply(player); + int size = list != null ? list.size() : 0; + return Math.max(1, (int) Math.ceil((double) size / contentSlots.length)); } @Override @@ -80,7 +91,7 @@ public void close(@NonNull Player player) { public void refresh(@NonNull Player player) { Inventory inv = open.get(player.getUniqueId()); if (inv == null) return; - populateItems(inv, pages.getOrDefault(player.getUniqueId(), 1)); + populateItems(player, inv, pages.getOrDefault(player.getUniqueId(), 1)); player.updateInventory(); } @@ -89,7 +100,7 @@ private void reopen(@NonNull Player player) { int page = pages.getOrDefault(player.getUniqueId(), 1); String resolvedTitle = title .replace("", String.valueOf(page)) - .replace("", String.valueOf(totalPages())); + .replace("", String.valueOf(totalPages(player))); var titleComponent = MM.deserialize(resolvedTitle); Inventory inv = open.get(player.getUniqueId()); @@ -104,27 +115,29 @@ private void reopen(@NonNull Player player) { } } catch (Exception ignored) { } - populateItems(inv, page); + populateItems(player, inv, page); player.updateInventory(); } else { inv = Bukkit.createInventory(null, rows * 9, titleComponent); - populateItems(inv, page); + populateItems(player, inv, page); open.put(player.getUniqueId(), inv); player.openInventory(inv); } } - private void populateItems(@NonNull Inventory inv, int page) { + private void populateItems(@NonNull Player player, @NonNull Inventory inv, int page) { inv.clear(); + List list = itemsSupplier.apply(player); + if (list == null) list = List.of(); int start = (page - 1) * contentSlots.length; for (int i = 0; i < contentSlots.length; i++) { int idx = start + i; - if (idx < items.size()) inv.setItem(contentSlots[i], items.get(idx)); + if (idx < list.size()) inv.setItem(contentSlots[i], list.get(idx)); } ItemStack prev = page > 1 ? prevButton.apply(page) : ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name("Previous").build(); - ItemStack next = page < totalPages() + ItemStack next = page < totalPages(player) ? nextButton.apply(page) : ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name("Next").build(); inv.setItem(prevSlot, prev); @@ -146,20 +159,22 @@ private synchronized void registerListeners() { pages.put(player.getUniqueId(), page - 1); reopen(player); return; - } else if (slot == nextSlot && page < totalPages()) { + } else if (slot == nextSlot && page < totalPages(player)) { pages.put(player.getUniqueId(), page + 1); reopen(player); return; } if (clickHandler != null) { + List list = itemsSupplier.apply(player); + if (list == null) list = List.of(); for (int i = 0; i < contentSlots.length; i++) { if (contentSlots[i] == slot) { int idx = (page - 1) * contentSlots.length + i; - if (idx < items.size()) { + if (idx < list.size()) { clickHandler.onClick( new ClickContext(player, ClickAction.from(event.getClick()), slot, this), - items.get(idx), + list.get(idx), idx ); } @@ -213,7 +228,7 @@ public interface PaginatedClickHandler { public static final class Builder { - private final List items = new ArrayList<>(); + private Function> itemsSupplier = p -> new ArrayList<>(); private String title = "Page /"; private int rows = 6; private int[] contentSlots = {10, 11, 12, 13, 14, 15, 16}; @@ -266,7 +281,13 @@ public static final class Builder { @CheckReturnValue public @NonNull Builder items(@NonNull List<@NonNull ItemStack> items) { - this.items.addAll(items); + this.itemsSupplier = player -> items; + return this; + } + + @CheckReturnValue + public @NonNull Builder items(@NonNull Function<@NonNull Player, @NonNull List<@NonNull ItemStack>> supplier) { + this.itemsSupplier = supplier; return this; } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Promise.java b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Promise.java index d923a1e..1844035 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Promise.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Promise.java @@ -17,6 +17,10 @@ private Promise(CompletableFuture future) { this.future = future; } + public static @NonNull Promise fromCompletableFuture(@NonNull CompletableFuture future) { + return new Promise<>(future); + } + @Contract("_ -> new") public static @NonNull Promise supplyAsync(@NonNull Supplier supplier) { CompletableFuture fut = new CompletableFuture<>(); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Scheduler.java b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Scheduler.java index 21da570..e3a269b 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Scheduler.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Scheduler.java @@ -42,18 +42,6 @@ private static SchedulerAdapter adapter() { return adapter().runLater(ticks, task); } - @Contract("_, _ -> new") - @Deprecated(since = "1.0.4", forRemoval = true) - public static @NonNull TaskHandle runDelayed(Duration delay, Runnable task) { - return runLater(delay, task); - } - - @Contract("_, _ -> new") - @Deprecated(since = "1.0.4", forRemoval = true) - public static @NonNull TaskHandle runDelayed(long ticks, Runnable task) { - return runLater(ticks, task); - } - @Contract("_, _, _ -> new") public static @NonNull TaskHandle runRepeating(Duration initialDelay, Duration interval, Runnable task) { return adapter().runRepeating(initialDelay, interval, task); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/BukkitText.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/BukkitText.java deleted file mode 100644 index d31cb16..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/BukkitText.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.oum.oumlib.text; - -import org.bukkit.Bukkit; - -@Deprecated(forRemoval = true, since = "1.0.4") -public final class BukkitText { - - private BukkitText() { - } - - public static void console(String message) { - Bukkit.getConsoleSender().sendMessage(Text.parse(message)); - } - - public static void broadcast(String message) { - Bukkit.getOnlinePlayers().forEach(p -> Text.send(p, message)); - } - - public static void broadcast(String message, String permission) { - Bukkit.getOnlinePlayers().stream() - .filter(p -> p.hasPermission(permission)) - .forEach(p -> Text.send(p, message)); - } -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java index 2fffa58..8a93226 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java @@ -5,7 +5,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; -import org.bukkit.entity.Player; + import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -92,8 +92,12 @@ private static void flatten(@NonNull String prefix, @NonNull Map public static @NonNull Component translateFor(@NonNull Object playerObj, @NonNull String key, TagResolver... resolvers) { String locale = defaultLang; try { - if (playerObj instanceof Player p) { - locale = p.locale().getLanguage(); + Class bukkitPlayerClass = Class.forName("org.bukkit.entity.Player"); + if (bukkitPlayerClass.isInstance(playerObj)) { + Locale loc = (Locale) playerObj.getClass().getMethod("locale").invoke(playerObj); + if (loc != null) { + locale = loc.getLanguage(); + } } else { Class velocityPlayerClass = Class.forName("com.velocitypowered.api.proxy.Player"); if (velocityPlayerClass.isInstance(playerObj)) { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/BossBars.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/BossBars.java deleted file mode 100644 index 467a356..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/BossBars.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.oum.oumlib.util; - -import dev.oum.oumlib.text.Text; -import net.kyori.adventure.audience.Audience; -import net.kyori.adventure.bossbar.BossBar; -import org.jspecify.annotations.NonNull; - -import java.time.Duration; - -@Deprecated(since = "1.0.1", forRemoval = true) -public final class BossBars { - - private BossBars() { - } - - @Deprecated(since = "1.0.1", forRemoval = true) - public static @NonNull BossBar show(@NonNull Audience audience, @NonNull String titleMiniMessage, - float progress, BossBar.@NonNull Color color, BossBar.@NonNull Overlay overlay) { - return Text.bossBar(audience, titleMiniMessage, progress, color, overlay); - } - - @Deprecated(since = "1.0.1", forRemoval = true) - public static void showTemporary(@NonNull Audience audience, @NonNull String titleMiniMessage, - float progress, BossBar.@NonNull Color color, BossBar.@NonNull Overlay overlay, - @NonNull Duration duration) { - Text.bossBarTemporary(audience, titleMiniMessage, progress, color, overlay, duration); - } -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Countdown.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/Countdown.java new file mode 100644 index 0000000..afeb9dd --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/util/Countdown.java @@ -0,0 +1,199 @@ +package dev.oum.oumlib.util; + +import dev.oum.oumlib.effect.Sounds; +import dev.oum.oumlib.scheduler.Scheduler; +import dev.oum.oumlib.scheduler.TaskHandle; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.title.Title; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public final class Countdown { + + private final Audience audience; + private final int totalSeconds; + private final Display displayMode; + private final Function formatFunction; + private final BiConsumer onTick; + private final Consumer onComplete; + private final Sounds.Effect tickSound; + private final Predicate displayFilter; + private int secondsRemaining; + private TaskHandle task; + @Contract(pure = true) + private Countdown(@NonNull Builder builder) { + this.audience = builder.audience; + this.totalSeconds = builder.totalSeconds; + this.displayMode = builder.displayMode; + this.formatFunction = builder.formatFunction; + this.onTick = builder.onTick; + this.onComplete = builder.onComplete; + this.tickSound = builder.tickSound; + this.displayFilter = builder.displayFilter; + this.secondsRemaining = totalSeconds; + } + + public static @NonNull Builder builder() { + return new Builder(); + } + + public @NonNull TaskHandle start() { + if (task != null) { + return task; + } + + task = Scheduler.runRepeating(Duration.ZERO, Duration.ofSeconds(1), () -> { + if (secondsRemaining <= 0) { + if (onComplete != null) { + onComplete.accept(audience); + } + cancel(); + return; + } + + if (displayFilter.test(secondsRemaining)) { + String formatted = formatFunction.apply(secondsRemaining); + Component message = MiniMessage.miniMessage().deserialize(formatted); + + if (displayMode == Display.TITLE) { + audience.showTitle(Title.title(message, Component.empty())); + } else if (displayMode == Display.ACTION_BAR) { + audience.sendActionBar(message); + } else if (displayMode == Display.CHAT) { + audience.sendMessage(message); + } + + if (tickSound != null && audience instanceof Player player) { + tickSound.play(player); + } + } + + if (onTick != null) { + onTick.accept(audience, secondsRemaining); + } + + secondsRemaining--; + }); + + return task; + } + + public void cancel() { + if (task != null) { + task.cancel(); + task = null; + } + } + + public enum Display { + TITLE, + ACTION_BAR, + CHAT + } + + public static final class Builder { + private Audience audience; + private int totalSeconds = 5; + private Display displayMode = Display.TITLE; + private Function formatFunction = s -> "" + s; + private BiConsumer onTick; + private Consumer onComplete; + private Sounds.Effect tickSound; + private Predicate displayFilter; + + private Builder() { + } + + public @NonNull Builder audience(@NonNull Audience audience) { + this.audience = audience; + return this; + } + + public @NonNull Builder duration(@NonNull Duration duration) { + this.totalSeconds = (int) duration.getSeconds(); + return this; + } + + public @NonNull Builder seconds(int seconds) { + this.totalSeconds = seconds; + return this; + } + + public @NonNull Builder display(@NonNull Display mode) { + this.displayMode = mode; + return this; + } + + public @NonNull Builder format(@NonNull Function format) { + this.formatFunction = format; + return this; + } + + public @NonNull Builder format(@NonNull String template) { + this.formatFunction = s -> template + .replace("%time%", String.valueOf(s)) + .replace("%duration%", Format.duration(Duration.ofSeconds(s))) + .replace("%digital%", Format.digitalTime(Duration.ofSeconds(s))); + return this; + } + + public @NonNull Builder onTick(@Nullable BiConsumer onTick) { + this.onTick = onTick; + return this; + } + + public @NonNull Builder onComplete(@Nullable Consumer onComplete) { + this.onComplete = onComplete; + return this; + } + + public @NonNull Builder sound(Sounds.@Nullable Effect sound) { + this.tickSound = sound; + return this; + } + + public @NonNull Builder displayFilter(@NonNull Predicate filter) { + this.displayFilter = filter; + return this; + } + + public @NonNull Builder intervals(int... seconds) { + Set set = new HashSet<>(); + for (int s : seconds) { + set.add(s); + } + this.displayFilter = set::contains; + return this; + } + + public @NonNull Countdown build() { + if (audience == null) { + throw new IllegalStateException("Audience must be set."); + } + if (displayFilter == null) { + if (displayMode == Display.CHAT) { + displayFilter = s -> s % 10 == 0 || s <= 5; + } else { + displayFilter = s -> true; + } + } + return new Countdown(this); + } + + public @NonNull TaskHandle start() { + return build().start(); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Format.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/Format.java index f787e81..9680596 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Format.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/util/Format.java @@ -12,6 +12,8 @@ public final class Format { private static final DecimalFormat NUMBER_FORMAT = new DecimalFormat("#,###.##"); private static final String[] SUFFIXES = {"", "k", "M", "B", "T"}; + private static final int[] ROMAN_VALUES = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; + private static final String[] ROMAN_SYMBOLS = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}; private Format() { } @@ -83,4 +85,37 @@ public static String compactNumber(double number) { } return Duration.ofSeconds(totalSeconds); } + + public static @NonNull String percentage(double value, double max) { + if (max <= 0) return "0.0%"; + double pct = (value / max) * 100.0; + return String.format(Locale.ROOT, "%.1f%%", pct); + } + + public static @NonNull String roman(int number) { + if (number <= 0) return ""; + StringBuilder sb = new StringBuilder(); + int remaining = number; + for (int i = 0; i < ROMAN_VALUES.length; i++) { + while (remaining >= ROMAN_VALUES[i]) { + sb.append(ROMAN_SYMBOLS[i]); + remaining -= ROMAN_VALUES[i]; + } + } + return sb.toString(); + } + + public static @NonNull String ordinal(int number) { + int mod100 = number % 100; + int mod10 = number % 10; + if (mod100 >= 11 && mod100 <= 13) { + return number + "th"; + } + return switch (mod10) { + case 1 -> number + "st"; + case 2 -> number + "nd"; + case 3 -> number + "rd"; + default -> number + "th"; + }; + } } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/ItemSerializer.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/ItemSerializer.java index 1ec4b08..6da9963 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/ItemSerializer.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/util/ItemSerializer.java @@ -95,6 +95,7 @@ private ItemSerializer() { } } + @SuppressWarnings("deprecation") private static byte[] serializeToBytes(@NonNull ItemStack item) throws Exception { if (paperBytesSupported) { return (byte[]) serializeAsBytesMethod.invoke(item); @@ -106,6 +107,7 @@ private static byte[] serializeToBytes(@NonNull ItemStack item) throws Exception } } + @SuppressWarnings("deprecation") private static @NonNull ItemStack deserializeFromBytes(byte @NonNull [] bytes) throws Exception { if (paperBytesSupported) { return (ItemStack) deserializeBytesMethod.invoke(null, (Object) bytes); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Locations.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/Locations.java index 57b89c7..e22ba69 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Locations.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/util/Locations.java @@ -8,6 +8,8 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import java.util.concurrent.ThreadLocalRandom; + public final class Locations { private Locations() { @@ -74,4 +76,53 @@ private Locations() { return null; } } + + public static double distance2D(@NonNull Location a, @NonNull Location b) { + double dx = a.getX() - b.getX(); + double dz = a.getZ() - b.getZ(); + return Math.sqrt(dx * dx + dz * dz); + } + + public static @NonNull Location midpoint(@NonNull Location a, @NonNull Location b) { + return new Location( + a.getWorld(), + (a.getX() + b.getX()) / 2.0, + (a.getY() + b.getY()) / 2.0, + (a.getZ() + b.getZ()) / 2.0, + (a.getYaw() + b.getYaw()) / 2.0f, + (a.getPitch() + b.getPitch()) / 2.0f + ); + } + + public static @NonNull Location centerBlock(@NonNull Location block) { + return new Location( + block.getWorld(), + block.getBlockX() + 0.5, + block.getBlockY() + 0.5, + block.getBlockZ() + 0.5, + block.getYaw(), + block.getPitch() + ); + } + + public static boolean isWithinAABB(@NonNull Location loc, @NonNull Location min, @NonNull Location max) { + double minX = Math.min(min.getX(), max.getX()); + double minY = Math.min(min.getY(), max.getY()); + double minZ = Math.min(min.getZ(), max.getZ()); + double maxX = Math.max(min.getX(), max.getX()); + double maxY = Math.max(min.getY(), max.getY()); + double maxZ = Math.max(min.getZ(), max.getZ()); + + return loc.getX() >= minX && loc.getX() <= maxX + && loc.getY() >= minY && loc.getY() <= maxY + && loc.getZ() >= minZ && loc.getZ() <= maxZ; + } + + public static @NonNull Location randomInRadius(@NonNull Location center, double radius) { + double angle = ThreadLocalRandom.current().nextDouble() * 2 * Math.PI; + double r = ThreadLocalRandom.current().nextDouble() * radius; + double x = center.getX() + r * Math.cos(angle); + double z = center.getZ() + r * Math.sin(angle); + return new Location(center.getWorld(), x, center.getY(), z, center.getYaw(), center.getPitch()); + } } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Pdc.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/Pdc.java index 853d0d2..c352aee 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Pdc.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/util/Pdc.java @@ -61,36 +61,6 @@ private static void triggerListeners(@NonNull Object target, @NonNull Namespaced return new PdcItem(item); } - @Deprecated(since = "1.0.4", forRemoval = true) - public static @Nullable String get(@NonNull ItemStack item, @NonNull String key) { - return of(item).get(key); - } - - @Deprecated(since = "1.0.4", forRemoval = true) - public static @Nullable Integer getInt(@NonNull ItemStack item, @NonNull String key) { - return of(item).getInt(key); - } - - @Deprecated(since = "1.0.4", forRemoval = true) - public static @Nullable Double getDouble(@NonNull ItemStack item, @NonNull String key) { - return of(item).getDouble(key); - } - - @Deprecated(since = "1.0.4", forRemoval = true) - public static @NonNull Boolean getBoolean(@NonNull ItemStack item, @NonNull String key) { - return of(item).getBoolean(key); - } - - @Deprecated(since = "1.0.4", forRemoval = true) - public static @Nullable Long getLong(@NonNull ItemStack item, @NonNull String key) { - return of(item).getLong(key); - } - - @Deprecated(since = "1.0.4", forRemoval = true) - public static @Nullable List getList(@NonNull ItemStack item, @NonNull String key) { - return of(item).getList(key); - } - public interface PdcChangeListener { void onChange(@NonNull Object target, @NonNull NamespacedKey key, @Nullable Object oldValue, @Nullable Object newValue); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/PlayerData.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/PlayerData.java deleted file mode 100644 index 56a6059..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/PlayerData.java +++ /dev/null @@ -1,103 +0,0 @@ -package dev.oum.oumlib.util; - -import org.bukkit.entity.Player; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import java.util.List; - -@Deprecated(since = "1.0.4", forRemoval = true) -public final class PlayerData { - - private final Player player; - private final Pdc.PdcHolder holder; - - private PlayerData(@NonNull Player player) { - this.player = player; - this.holder = Pdc.of(player); - } - - public static @NonNull PlayerData of(@NonNull Player player) { - return new PlayerData(player); - } - - public @NonNull Player player() { - return player; - } - - public void set(@NonNull String key, @Nullable String value) { - holder.set(key, value); - } - - public @Nullable String get(@NonNull String key) { - return holder.get(key); - } - - public @NonNull String getOrDefault(@NonNull String key, @NonNull String def) { - return holder.getOrDefault(key, def); - } - - public void setInt(@NonNull String key, int value) { - holder.setInt(key, value); - } - - public @Nullable Integer getInt(@NonNull String key) { - return holder.getInt(key); - } - - public int getIntOrDefault(@NonNull String key, int def) { - return holder.getIntOrDefault(key, def); - } - - public void setDouble(@NonNull String key, double value) { - holder.setDouble(key, value); - } - - public @Nullable Double getDouble(@NonNull String key) { - return holder.getDouble(key); - } - - public double getDoubleOrDefault(@NonNull String key, double def) { - return holder.getDoubleOrDefault(key, def); - } - - public void setBoolean(@NonNull String key, boolean value) { - holder.setBoolean(key, value); - } - - public boolean getBoolean(@NonNull String key) { - return holder.getBoolean(key); - } - - public boolean getBooleanOrDefault(@NonNull String key, boolean def) { - return holder.getBooleanOrDefault(key, def); - } - - public void setLong(@NonNull String key, long value) { - holder.setLong(key, value); - } - - public @Nullable Long getLong(@NonNull String key) { - return holder.getLong(key); - } - - public long getLongOrDefault(@NonNull String key, long def) { - return holder.getLongOrDefault(key, def); - } - - public void setList(@NonNull String key, @Nullable List value) { - holder.setList(key, value); - } - - public @Nullable List getList(@NonNull String key) { - return holder.getList(key); - } - - public void remove(@NonNull String key) { - holder.remove(key); - } - - public boolean has(@NonNull String key) { - return holder.has(key); - } -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Players.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/Players.java index 279d65f..2379b0f 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Players.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/util/Players.java @@ -7,6 +7,7 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +@Deprecated(since = "1.0.7", forRemoval = true) public final class Players { private Players() { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Proxy.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/Proxy.java index a2e7acc..a7456ee 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Proxy.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/util/Proxy.java @@ -1,5 +1,7 @@ package dev.oum.oumlib.util; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; import com.velocitypowered.api.event.player.KickedFromServerEvent; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; @@ -7,19 +9,25 @@ import com.velocitypowered.api.proxy.server.ServerInfo; import dev.oum.oumlib.OumLib; import dev.oum.oumlib.event.Events; +import dev.oum.oumlib.scheduler.Promise; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.title.Title; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.net.InetSocketAddress; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; +import java.util.function.Predicate; public final class Proxy { + private static final Map> SERVER_GROUPS = new ConcurrentHashMap<>(); + private Proxy() { } @@ -99,12 +107,14 @@ public static boolean unregisterServer(@NonNull String name) { /** * Pings a server asynchronously to determine if it is online. */ - public static @NonNull CompletableFuture isOnline(@NonNull String serverName) { - return OumLib.proxy().getServer(serverName) - .map(server -> server.ping() - .thenApply(ping -> true) - .exceptionally(err -> false)) - .orElseGet(() -> CompletableFuture.completedFuture(false)); + public static @NonNull Promise isOnline(@NonNull String serverName) { + return Promise.fromCompletableFuture( + OumLib.proxy().getServer(serverName) + .map(server -> server.ping() + .thenApply(ping -> true) + .exceptionally(err -> false)) + .orElseGet(() -> CompletableFuture.completedFuture(false)) + ); } /** @@ -136,7 +146,7 @@ public static void sendTitleTo(@NonNull String serverName, @NonNull String title /** * Finds the least populated online server from the provided list. */ - public static @NonNull CompletableFuture> getBestServer(@NonNull List serverNames) { + public static @NonNull Promise> getBestServer(@NonNull List serverNames) { var futures = serverNames.stream() .map(name -> OumLib.proxy().getServer(name)) .flatMap(Optional::stream) @@ -145,22 +155,153 @@ public static void sendTitleTo(@NonNull String serverName, @NonNull String title .exceptionally(err -> null)) .toList(); - return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) - .thenApply(v -> { - RegisteredServer best = null; - int minPlayers = Integer.MAX_VALUE; - - for (var future : futures) { - try { - var entry = future.getNow(null); - if (entry != null && entry.getValue() < minPlayers) { - minPlayers = entry.getValue(); - best = entry.getKey(); + return Promise.fromCompletableFuture( + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> { + RegisteredServer best = null; + int minPlayers = Integer.MAX_VALUE; + + for (var future : futures) { + try { + var entry = future.getNow(null); + if (entry != null && entry.getValue() < minPlayers) { + minPlayers = entry.getValue(); + best = entry.getKey(); + } + } catch (Exception ignored) { + } } - } catch (Exception ignored) { - } + return Optional.ofNullable(best); + }) + ); + } + + public static void registerGroup(@NonNull String groupName, @NonNull List serverNames) { + SERVER_GROUPS.put(groupName.toLowerCase(Locale.ROOT), List.copyOf(serverNames)); + } + + public static @NonNull List getGroupServers(@NonNull String groupName) { + List names = SERVER_GROUPS.get(groupName.toLowerCase(Locale.ROOT)); + if (names == null) return List.of(); + return names.stream() + .map(name -> OumLib.proxy().getServer(name)) + .flatMap(Optional::stream) + .toList(); + } + + public static @NonNull Promise connectLeastPopulated(@NonNull Player player, @NonNull String groupName) { + List names = SERVER_GROUPS.get(groupName.toLowerCase(Locale.ROOT)); + if (names == null || names.isEmpty()) + return Promise.fromCompletableFuture(CompletableFuture.completedFuture(false)); + return Promise.fromCompletableFuture( + getBestServer(names).toCompletableFuture().thenCompose(opt -> { + if (opt.isPresent()) { + player.createConnectionRequest(opt.get()).fireAndForget(); + return CompletableFuture.completedFuture(true); } - return Optional.ofNullable(best); - }); + return CompletableFuture.completedFuture(false); + }) + ); + } + + public static @NonNull Promise connectRandom(@NonNull Player player, @NonNull String groupName) { + List servers = getGroupServers(groupName); + if (servers.isEmpty()) return Promise.fromCompletableFuture(CompletableFuture.completedFuture(false)); + int idx = ThreadLocalRandom.current().nextInt(servers.size()); + player.createConnectionRequest(servers.get(idx)).fireAndForget(); + return Promise.fromCompletableFuture(CompletableFuture.completedFuture(true)); + } + + public static void registerFallbackRouter(@NonNull Object plugin, @NonNull String groupName) { + registerFallbackRouter(plugin, groupName, event -> true); + } + + public static void registerFallbackRouter(@NonNull Object plugin, @NonNull String groupName, @NonNull Predicate filter) { + Events.listen(KickedFromServerEvent.class, event -> { + if (!filter.test(event)) return; + String kickedFrom = event.getServer().getServerInfo().getName(); + List lobbies = SERVER_GROUPS.get(groupName.toLowerCase(Locale.ROOT)); + if (lobbies == null || lobbies.isEmpty()) return; + + List filteredLobbies = lobbies.stream() + .filter(name -> !name.equalsIgnoreCase(kickedFrom)) + .toList(); + + if (filteredLobbies.isEmpty()) return; + + for (String lobbyName : filteredLobbies) { + var optionalServer = OumLib.proxy().getServer(lobbyName); + if (optionalServer.isPresent()) { + event.setResult(KickedFromServerEvent.RedirectPlayer.create( + optionalServer.get(), + event.getServerKickReason().orElse(null) + )); + return; + } + } + }); + } + + public static boolean sendPluginMessage(@NonNull Player player, @NonNull String channelName, @NonNull Consumer writer) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + writer.accept(out); + return sendPluginMessage(player, channelName, out.toByteArray()); + } + + public static void broadcastPluginMessage(@NonNull String channelName, byte[] data) { + MinecraftChannelIdentifier identifier = MinecraftChannelIdentifier.from(channelName); + OumLib.proxy().getChannelRegistrar().register(identifier); + for (RegisteredServer server : OumLib.proxy().getAllServers()) { + server.sendPluginMessage(identifier, data); + } + } + + public static void broadcastPluginMessage(@NonNull String channelName, @NonNull Consumer writer) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + writer.accept(out); + broadcastPluginMessage(channelName, out.toByteArray()); + } + + public static @NonNull Promise ping(@NonNull String serverName) { + return Promise.fromCompletableFuture( + OumLib.proxy().getServer(serverName) + .map(server -> server.ping() + .thenApply(ping -> { + String motd = ping.getDescriptionComponent().toString(); + String ver = ping.getVersion() != null ? ping.getVersion().getName() : null; + int online = ping.getPlayers().isPresent() ? ping.getPlayers().get().getOnline() : 0; + int max = ping.getPlayers().isPresent() ? ping.getPlayers().get().getMax() : 0; + return new ServerPingResult(true, online, max, motd, ver); + }) + .exceptionally(err -> new ServerPingResult(false, 0, 0, null, null))) + .orElseGet(() -> CompletableFuture.completedFuture(new ServerPingResult(false, 0, 0, null, null))) + ); + } + + public static @NonNull Optional getPlayer(@NonNull String name) { + return OumLib.proxy().getPlayer(name); + } + + public static @NonNull Optional getPlayer(@NonNull UUID uuid) { + return OumLib.proxy().getPlayer(uuid); + } + + public static @NonNull Collection getPlayers() { + return OumLib.proxy().getAllPlayers(); + } + + public static @NonNull Collection getPlayersOn(@NonNull String serverName) { + return OumLib.proxy().getServer(serverName) + .map(RegisteredServer::getPlayersConnected) + .orElse(List.of()); + } + + public record ServerPingResult( + boolean online, + int currentPlayers, + int maxPlayers, + @Nullable String motd, + @Nullable String version + ) { } } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbedField.java b/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbedField.java index 183cc96..3860168 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbedField.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbedField.java @@ -1,3 +1,4 @@ package dev.oum.oumlib.web; -public record WebhookEmbedField(String name, String value, boolean inline) {} +public record WebhookEmbedField(String name, String value, boolean inline) { +} diff --git a/oumlib-plugin/pom.xml b/oumlib-plugin/pom.xml new file mode 100644 index 0000000..75744ec --- /dev/null +++ b/oumlib-plugin/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + + dev.oum + oumlib + 1.0.7 + ../pom.xml + + + oumlib-plugin + jar + + + + dev.oum + oumlib-core + 1.0.7 + compile + + + io.papermc.paper + paper-api + 1.21.11-R0.1-SNAPSHOT + provided + + + com.velocitypowered + velocity-api + 3.5.0-SNAPSHOT + provided + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.2 + + + package + + shade + + + false + false + + + + + + + diff --git a/oumlib-plugin/src/main/java/dev/oum/oumlib/plugin/PaperOumLibPlugin.java b/oumlib-plugin/src/main/java/dev/oum/oumlib/plugin/PaperOumLibPlugin.java new file mode 100644 index 0000000..9dc5b35 --- /dev/null +++ b/oumlib-plugin/src/main/java/dev/oum/oumlib/plugin/PaperOumLibPlugin.java @@ -0,0 +1,17 @@ +package dev.oum.oumlib.plugin; + +import dev.oum.oumlib.OumLib; +import org.bukkit.plugin.java.JavaPlugin; + +public final class PaperOumLibPlugin extends JavaPlugin { + + @Override + public void onEnable() { + OumLib.init(this); + } + + @Override + public void onDisable() { + OumLib.shutdown(); + } +} diff --git a/oumlib-plugin/src/main/java/dev/oum/oumlib/plugin/VelocityOumLibPlugin.java b/oumlib-plugin/src/main/java/dev/oum/oumlib/plugin/VelocityOumLibPlugin.java new file mode 100644 index 0000000..4b1699b --- /dev/null +++ b/oumlib-plugin/src/main/java/dev/oum/oumlib/plugin/VelocityOumLibPlugin.java @@ -0,0 +1,30 @@ +package dev.oum.oumlib.plugin; + +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.proxy.ProxyServer; +import dev.oum.oumlib.OumLib; + +@Plugin(id = "oumlib", name = "OumLib", version = "1.0.7", description = "Provided plugin library for OumLib.") +public final class VelocityOumLibPlugin { + + private final ProxyServer server; + + @Inject + public VelocityOumLibPlugin(ProxyServer server) { + this.server = server; + } + + @Subscribe + public void onProxyInitialization(ProxyInitializeEvent event) { + OumLib.init(server, this); + } + + @Subscribe + public void onProxyShutdown(ProxyShutdownEvent event) { + OumLib.shutdown(); + } +} \ No newline at end of file diff --git a/oumlib-plugin/src/main/resources/paper-plugin.yml b/oumlib-plugin/src/main/resources/paper-plugin.yml new file mode 100644 index 0000000..7abfd4f --- /dev/null +++ b/oumlib-plugin/src/main/resources/paper-plugin.yml @@ -0,0 +1,6 @@ +name: OumLib +version: 1.0.7 +main: dev.oum.oumlib.plugin.PaperOumLibPlugin +api-version: '1.21.4' +author: sun-mc-dev +description: Provided plugin library for OumLib. diff --git a/pom.xml b/pom.xml index 872f04b..03db624 100644 --- a/pom.xml +++ b/pom.xml @@ -7,11 +7,12 @@ dev.oum oumlib - 1.0.6 + 1.0.7 pom oumlib-core + oumlib-plugin example-plugin