diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 48b7c72..f952b03 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -43,6 +43,6 @@ jobs: - 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." + gh release create dev-build oumlib-core/target/oumlib-core-*.jar --title "Development Build" --notes "Automated pre-release build of OumLib." env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 2639685..9853625 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,4 @@ /example-plugin/example-plugin.iml /example-plugin/target /oumlib-core/oumlib-core.iml -/oumlib-core/target -/oumlib-plugin/target -/oumlib-plugin/oumlib-plugin.iml \ No newline at end of file +/oumlib-core/target \ No newline at end of file diff --git a/README.md b/README.md index d5835cc..d697f23 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,40 @@ -# oumlib +# OumLib -[![](https://jitpack.io/v/sun-mc-dev/oumlib.svg)](https://jitpack.io/#sun-mc-dev/oumlib) +[![](https://img.shields.io/github/v/release/sun-mc-dev/oumlib?color=orange&style=for-the-badge)](https://github.com/sun-mc-dev/oumlib/releases) +[![](https://img.shields.io/jitpack/v/github/sun-mc-dev/oumlib?color=yellow&style=for-the-badge)](https://jitpack.io/#sun-mc-dev/oumlib) +[![](https://img.shields.io/badge/Java-21+-orange?style=for-the-badge&logo=openjdk)](https://adoptium.net/) +[![](https://img.shields.io/badge/Folia-Compatible-gold?style=for-the-badge)](https://github.com/PaperMC/Folia) -A lightweight, multi-platform library for Paper (Minecraft servers) and Velocity (proxies) built for Java 21. It helps developers write clean, boilerplate-free code. +OumLib is a lightweight, utility-centric library designed for Minecraft servers (Paper/Spigot) and proxy networks (Velocity). Built around Java 21 virtual threads, it provides modern, compile-safe, and thread-safe abstractions to eliminate boilerplate code. -## Requirements & Compatibility +OumLib is designed to be shaded and relocated directly into your plugin JAR. -- **Java**: Java 21 or higher (utilizes Virtual Threads). -- **Paper**: Version 1.21.4 or newer. -- **Velocity**: Version 3.5.x or newer. -- **Folia**: Fully compatible. Tasks run on safe async scheduler structures, and it supports Folia's multi-threaded tick environment. +--- + +## Quick Navigation + +| Module | Description | Documentation | +|:---------------------|:-------------------------------------------------------------|:-------------------------------------------| +| **Setup & Platform** | Shaded setup lifecycle and platform detection utilities | **[Setup Guide](docs/setup.md)** | +| **Commands** | Fluent Brigadier command builders with cooldown support | **[Commands](docs/commands.md)** | +| **Configuration** | Automatic-reloading record configurations with comments | **[Configuration](docs/configuration.md)** | +| **Menus & GUIs** | Easy chest-layouts, button binding, paginated interfaces | **[Inventories](docs/inventories.md)** | +| **Scheduler** | Virtual-thread loops, TaskGroups, and Folia adaptors | **[Scheduler](docs/scheduler.md)** | +| **Events** | Chainable context-aware event registers with filters | **[Events](docs/events.md)** | +| **Math Utilities** | FastMath shortcuts, Vector3D, Volume3D, MathEval expressions | **[Math](docs/math.md)** | +| **Visual Effects** | Particle pathways: bezier curves, lines, helices | **[Visual Effects](docs/effects.md)** | +| **Display Entities** | Fluent transforms and DisplayBuilder controls | **[Display Entities](docs/entities.md)** | +| **Text & PAPI** | Kyori MiniMessage presets, placeholder hooks | **[Text & Placeholders](docs/text.md)** | +| **Database** | Non-blocking database connectors for SQLite and MySQL | **[Database](docs/database.md)** | +| **Plugin Bridges** | Auto-hooks for Economy, Permissions, Nexo and CustomItems | **[Bridges](docs/bridges.md)** | +| **General Utils** | PDC wrappers, duration parses, location serializing | **[Utilities](docs/utilities.md)** | +| **Web Hookers** | Asynchronous HTTP requests and Discord webhook builders | **[Web & Discord](docs/web.md)** | --- ## Installation -Add the JitPack repository and the library dependency to your `pom.xml`. +Declare the JitPack repository and OumLib dependency in your `pom.xml`. ### 1. Add Repository ```xml @@ -26,9 +45,6 @@ Add the JitPack repository and the library dependency to your `pom.xml`. ``` ### 2. Add Dependency -If you are shading the library into your plugin, declare the compile-scope dependency below. -Look for "VERSION" at our github releases page: [Releases](https://github.com/sun-mc-dev/oumlib/releases) - ```xml com.github.sun-mc-dev.oumlib @@ -38,12 +54,10 @@ Look for "VERSION" at our github releases page: [Releases](https://github.com/su ``` -### 3. Shade and Relocate the Dependency -To prevent classpath conflicts with other plugins running different versions of OumLib on the same server, you must **relocate** the library packages into your own plugin's package space. +### 3. Shading & Relocation +You must relocate OumLib inside your package space to prevent classpath conflicts with other plugins running different versions of OumLib on the same server. -Also, exclude dependency security signatures to avoid runtime validation errors. - -Add this `maven-shade-plugin` configuration to your plugin's `pom.xml`: +Add this configured `maven-shade-plugin` to your `pom.xml`: ```xml @@ -55,14 +69,12 @@ Add this `maven-shade-plugin` configuration to your plugin's `pom.xml`: false - dev.oum.oumlib your.plugin.package.libs.oumlib - *:* @@ -91,86 +103,98 @@ Add this `maven-shade-plugin` configuration to your plugin's `pom.xml`: ## Quick Start Example -Here is a short preview showing how clean it is to set up a configuration, a command, and a chest GUI on Paper: +Here is a real-world scenario showing how to load a player profile asynchronously from a SQLite database, register a command to open a GUI shop, and play custom leveling sound/particle effects upon purchase: ```java -// 1. Declare config as a Java Record -public record MyConfig( - @Comment("Default broadcast message") String broadcastMessage -) implements ConfigSection {} - -// 2. Load the configuration -ConfigManager config = ConfigManager.of(MyConfig.class, "config.yml", - () -> new MyConfig("Default broadcast message!") -).enableAutoReload(); - -// 3. Declare permission and register a command to open a GUI -Permission adminPermission = Permission.builder("myplugin.admin").build(); - -Commands.literal("admin") - .permission(adminPermission) - .subcommand(sub -> sub - .label("menu") - .executes(context -> { - if (!context.isPlayer()) return; - - // Build and open a GUI - ChestMenu.builder() - .title("Dashboard") - .rows(3) - .pattern( - "#########", - "# B #", - "#########" - ) - .bind('B', ItemBuilder.of(Material.BOOK).name("Broadcast").build()) - .onClick('B', click -> { - // Send message and close menu - Text.send(click.player(), config.get().broadcastMessage()); +import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.command.Commands; +import dev.oum.oumlib.config.ConfigManager; +import dev.oum.oumlib.config.ConfigSection; +import dev.oum.oumlib.database.Database; +import dev.oum.oumlib.effect.Effects; +import dev.oum.oumlib.inventory.ChestMenu; +import dev.oum.oumlib.inventory.ItemBuilder; +import dev.oum.oumlib.scheduler.Scheduler; +import dev.oum.oumlib.text.Text; +import dev.oum.oumlib.util.Permission; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import java.io.File; + +public record ShopConfig(String itemTitle, int itemPrice) implements ConfigSection {} + +public final class ProfileShopPlugin extends JavaPlugin { + private ConfigManager config; + private Database db; + + @Override + public void onEnable() { + OumLib.init(this); + + config = ConfigManager.of(ShopConfig.class, "shop.yml", + () -> new ShopConfig("Super Star", 100) + ).enableAutoReload(); + + db = Database.sqlite(new File(getDataFolder(), "profiles.db")); + db.executeUpdate("CREATE TABLE IF NOT EXISTS economy (uuid VARCHAR(36) PRIMARY KEY, balance INT)"); + + Commands.literal("shop") + .permission(Permission.builder("myplugin.shop").build()) + .executes(context -> { + if (!context.isPlayer()) { + return; + } + Player player = context.playerOrThrow(); + + db.executeQuery("SELECT balance FROM economy WHERE uuid = ?", player.getUniqueId().toString()) + .thenAcceptSync(rows -> { + int balance = rows.isEmpty() ? 500 : (int) rows.getFirst().get("balance"); + openShopMenu(player, balance); + }); + }).register(); + } + + private void openShopMenu(Player player, int balance) { + ChestMenu.builder() + .title("Server Shop | Balance: " + balance + "") + .rows(3) + .pattern( + "#########", + "# P #", + "#########" + ) + .bind('#', ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build()) + .bind('P', ItemBuilder.of(Material.NETHER_STAR).name(config.get().itemTitle()).lore("Price: " + config.get().itemPrice() + "").build()) + .onClick('P', click -> { + if (balance < config.get().itemPrice()) { + Text.send(click.player(), "Insufficient balance!"); click.player().closeInventory(); - }) - .build() - .open(context.playerOrThrow()); - }) - ).register(); + return; + } + + int newBalance = balance - config.get().itemPrice(); + db.executeUpdate("INSERT INTO economy (uuid, balance) VALUES (?, ?) ON DUPLICATE KEY UPDATE balance = ?", + click.player().getUniqueId().toString(), newBalance, newBalance); + + Text.send(click.player(), "Purchased successfully!"); + click.player().closeInventory(); + + Effects.sound(Sound.ENTITY_PLAYER_LEVELUP).volume(1.0F).pitch(1.2F).play(click.player()); + Effects.particle(Particle.HAPPY_VILLAGER).count(15).offset(0.5, 0.5, 0.5).spawn(click.player().getLocation()); + }) + .build() + .open(player); + } + + @Override + public void onDisable() { + if (db != null) { + db.close(); + } + OumLib.shutdown(); + } +} ``` - ---- - -## Local Compilation & Fallback - -If you want to build the library yourself, or if you prefer to install it locally instead of using JitPack: - -1. Clone this repository. -2. Open a terminal in the root directory. -3. Run the Maven installation command: - ```bash - mvn clean install - ``` -4. You can now reference the library in your local projects without declaring the JitPack repository: - ```xml - - dev.oum - oumlib-core - VERSION - compile - - ``` - ---- - -## Features & Documentation - -Detailed guides for each package: - -- **[Main Setup & Initialization](docs/setup.md)**: How to initialize OumLib on Paper and Velocity. -- **[Commands](docs/commands.md)**: A simple Brigadier-based command system with cooldowns. -- **[Configurations](docs/configuration.md)**: Auto-reloading record-based configurations. -- **[Database](docs/database.md)**: Non-blocking asynchronous SQLite and MySQL database helpers. -- **[Events](docs/events.md)**: Dynamic, fluent event listeners for Paper and Velocity. -- **[GUI & Chest Menus](docs/inventories.md)**: Easy chest menus with layouts and click actions. -- **[Scheduler](docs/scheduler.md)**: Platform-agnostic scheduler for delayed and repeating tasks. -- **[Text & Placeholders](docs/text.md)**: MiniMessage text presets and custom placeholders. -- **[Plugin Bridges](docs/bridges.md)**: Economy, Custom Items, Nexo, and LuckPerms Permission Bridges. -- **[General Utilities](docs/utilities.md)**: PDC wrappers, duration formatting, and location serialization. -- **[Web & Discord Webhooks](docs/web.md)**: Asynchronous Discord Webhook client and embed builder. diff --git a/docs/bridges.md b/docs/bridges.md index 9c2a609..f468165 100644 --- a/docs/bridges.md +++ b/docs/bridges.md @@ -1,42 +1,82 @@ -# Server and Integration Bridges +# Integration & Plugin Bridges -OumLib features high-performance, classloading-safe cross-plugin integration bridges, allowing your plugins to interact with multiple economies and custom item providers without compile-time dependencies. +OumLib features classloading-safe cross-plugin integration bridges, allowing your plugins to interact with multiple economies, custom item systems, and permission managers without compile-time dependencies. --- -## 1. Economy Bridge +## Real-world Example: VIP Rank Purchase -The `EconomyBridge` provides simultaneous support for multiple currency/economy plugins (e.g. Vault and PlayerPoints). It handles background registration and safe fallback detection automatically. - -### Checking or Modifying Player Balances - -You can interact with the player's balance using the default provider, or query a specific provider: +Here is a store manager that checks if a player has a primary LuckPerms group matching VIP, confirms their Vault economy points balance can cover the purchase, takes the coins, and adds the VIP group to the player: ```java import dev.oum.oumlib.bridge.economy.EconomyBridge; +import dev.oum.oumlib.bridge.permission.PermissionBridge; +import dev.oum.oumlib.text.Text; import org.bukkit.entity.Player; +import org.bukkit.Bukkit; + +public final class RankPurchaseManager { + public void purchaseVipRank(Player player) { + if (!PermissionBridge.isAvailable()) { + Text.send(player, "Permissions system is currently offline."); + return; + } + + String primaryGroup = PermissionBridge.getPrimaryGroup(player.getUniqueId()); + if (primaryGroup.equalsIgnoreCase("vip") || primaryGroup.equalsIgnoreCase("admin")) { + Text.send(player, "You already own the VIP rank!"); + return; + } + + double price = 5000.0; + double balance = EconomyBridge.balance(player); + + if (balance < price) { + Text.send(player, "You need " + (price - balance) + " more coins to purchase VIP!"); + return; + } + + boolean success = EconomyBridge.withdraw(player, price); + if (success) { + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "lp user " + player.getName() + " parent set vip"); + Text.send(player, "Congratulations! You are now a VIP rank member."); + } else { + Text.send(player, "Transaction declined by payment provider."); + } + } +} +``` -Player player = ...; +--- -// Query the player's balance using the default provider (e.g. Vault) -double vaultBalance = EconomyBridge.balance(player); +## Custom Item Bridging -// Query using a specific provider (e.g. PlayerPoints) -double pointsBalance = EconomyBridge.balance("playerpoints", player); +Resolve `ItemStack` instances from Minecraft, ItemsAdder, Oraxen, MMOItems, MythicMobs, and Nexo dynamically: -// Checking if a player has enough currency -if (EconomyBridge.has(player, 100.0)) { - // Withdraw currency - boolean success = EconomyBridge.withdraw(player, 100.0); -} +```java +import dev.oum.oumlib.bridge.item.ItemBridge; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import java.util.Optional; -// Depositing currency -EconomyBridge.deposit("playerpoints", player, 50.0); +public final class CustomItemLoader { + public void giveCustomItems(Player player) { + Optional nexoSword = ItemBridge.getItem("nexo:emerald_sword"); + Optional mythicKey = ItemBridge.getItem("mythicmobs:skeleton_key"); + Optional standardDiamond = ItemBridge.getItem("minecraft:diamond"); + + nexoSword.ifPresent(item -> player.getInventory().addItem(item)); + mythicKey.ifPresent(item -> player.getInventory().addItem(item)); + standardDiamond.ifPresent(item -> player.getInventory().addItem(item)); + } +} ``` -### Registering Custom Economy Providers +--- -You can register custom economy providers (e.g. for custom gems or tokens) by implementing `EconomyProvider`: +## Registering Custom Economy Providers + +Register custom economy tokens or custom coin providers to the global bridge: ```java import dev.oum.oumlib.bridge.economy.EconomyProvider; @@ -44,120 +84,44 @@ import dev.oum.oumlib.bridge.economy.EconomyBridge; import org.bukkit.OfflinePlayer; import org.jspecify.annotations.NonNull; -public class GemEconomyProvider implements EconomyProvider { +public class CustomTokenProvider implements EconomyProvider { @Override - public @NonNull String name() { return "gems"; } + public @NonNull String name() { + return "customtokens"; + } @Override public boolean has(@NonNull OfflinePlayer player, double amount) { - return getGems(player) >= amount; + return getTokens(player) >= amount; } @Override public boolean withdraw(@NonNull OfflinePlayer player, double amount) { - return takeGems(player, (int) amount); + return modifyTokens(player, -(int) amount); } @Override public boolean deposit(@NonNull OfflinePlayer player, double amount) { - return giveGems(player, (int) amount); + return modifyTokens(player, (int) amount); } @Override public double balance(@NonNull OfflinePlayer player) { - return getGems(player); + return getTokens(player); } -} - -// Register it during plugin initialization -EconomyBridge.registerProvider(new GemEconomyProvider()); -``` - ---- - -## 2. Item Bridge - -The `ItemBridge` allows you to resolve `ItemStack` objects from various plugins (Minecraft, ItemsAdder, Oraxen, MMOItems, and MythicMobs) using a single, unified string configuration format (namespace strings). - -### Resolving Custom Items - -To fetch an item stack from any source, use `ItemBridge.getItem(String identifier)`: - -```java -import dev.oum.oumlib.bridge.item.ItemBridge; -import org.bukkit.inventory.ItemStack; -import java.util.Optional; -// Resolve standard Minecraft items -Optional diamond = ItemBridge.getItem("minecraft:diamond"); // or just "diamond" - -// Resolve ItemsAdder items -Optional customBlock = ItemBridge.getItem("itemsadder:ruby_ore"); - -// Resolve Oraxen items -Optional customSword = ItemBridge.getItem("oraxen:mythic_sword"); - -// Resolve MMOItems (format: "mmoitems:TYPE:ID") -Optional excalibur = ItemBridge.getItem("mmoitems:SWORD:EXCALIBUR"); - -// Resolve MythicMobs custom items -Optional key = ItemBridge.getItem("mythicmobs:skeleton_key"); - -// Resolve Nexo custom items -Optional nexoSword = ItemBridge.getItem("nexo:emerald_sword"); -``` - -### Registering Custom Item Providers - -You can register custom item systems to the bridge by implementing `ItemProvider`: - -```java -import dev.oum.oumlib.bridge.item.ItemProvider; -import dev.oum.oumlib.bridge.item.ItemBridge; -import org.bukkit.inventory.ItemStack; -import org.jspecify.annotations.NonNull; -import java.util.Optional; - -public class CustomItemProvider implements ItemProvider { - @Override - public @NonNull String name() { return "myplugin"; } + private int getTokens(OfflinePlayer player) { + return 1000; + } - @Override - public @NonNull Optional getItem(@NonNull String id) { - ItemStack item = MyPluginAPI.getItem(id); - return Optional.ofNullable(item); + private boolean modifyTokens(OfflinePlayer player, int amount) { + return true; } } -// Register during initialization -ItemBridge.registerProvider(new CustomItemProvider()); -``` - ---- - -## 3. Permissions Bridge - -The `PermissionBridge` provides unified, cross-platform (Paper and Velocity) access to **LuckPerms** user metadata (prefixes, suffixes, groups, and custom meta). - -### Querying Player Groups and Meta - -Since the permissions bridge operates entirely using player `UUID`s, you can use the exact same methods on both platforms: - -```java -import dev.oum.oumlib.bridge.permission.PermissionBridge; -import java.util.UUID; - -UUID playerUuid = ...; - -if (PermissionBridge.isAvailable()) { - // Get primary group - String primaryGroup = PermissionBridge.getPrimaryGroup(playerUuid); // e.g. "admin" - - // Get prefix/suffix - String prefix = PermissionBridge.getPrefix(playerUuid); // e.g. "[Admin] " - String suffix = PermissionBridge.getSuffix(playerUuid); - - // Get custom metadata value - String coinsMultiplier = PermissionBridge.getMetaValue(playerUuid, "multiplier"); // e.g. "1.5" +public class TokenInitializer { + public void register() { + EconomyBridge.registerProvider(new CustomTokenProvider()); + } } ``` diff --git a/docs/commands.md b/docs/commands.md index c8786fe..3ccbefc 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,108 +1,79 @@ -# Command System +# Commands & Brigadier Wrapper -OumLib features a builder-based wrapper for Brigadier, providing modern command registration with platform-agnostic structures, typed arguments, dynamic completions, and integrated cooldowns. +OumLib features a builder-based wrapper for Brigadier, providing modern command registration with platform-agnostic structures, typed arguments, completions, and cooldowns. -## Registering a Command +--- + +## Real-world Example: Warp System -A command consists of literals (names), arguments (parameters), subcommands, execution blocks, and optional parameters like permissions and cooldowns. +Here is a warp command system supporting coordinates storage, permissions, a teleportation cooldown, and rich hover tooltips for tab completion suggestions: ```java import dev.oum.oumlib.command.Arguments; import dev.oum.oumlib.command.Commands; +import dev.oum.oumlib.command.Argument; +import dev.oum.oumlib.command.RichSuggestion; import dev.oum.oumlib.text.Text; import dev.oum.oumlib.util.Permission; +import org.bukkit.Location; +import org.bukkit.entity.Player; import java.time.Duration; +import java.util.HashMap; import java.util.List; - -public class MyCommands { - - public static void register() { - Permission gamemodePermission = Permission.builder("myplugin.gamemode") - .description("Allows player to change gamemodes") - .defaultValue(Permission.Default.OP) - .build(); - - // Define an argument with custom suggestions - var modeArg = Arguments.string("mode") - .suggests(context -> List.of("survival", "creative", "adventure", "spectator")); - - Commands.literal("gamemode") - .permission(gamemodePermission) - .cooldown( - Duration.ofSeconds(5), - "Please wait seconds before changing gamemodes again." - ) - .argument(modeArg) +import java.util.Map; + +public final class WarpCommandRegistry { + private final Map warps = new HashMap<>(); + + public void register() { + Permission warpPermission = Permission.builder("myplugin.warp.use").build(); + Permission adminPermission = Permission.builder("myplugin.warp.admin").build(); + + Argument warpArg = Arguments.string("warp") + .suggestsRich(context -> List.of( + RichSuggestion.of("spawn", "Teleport to the main server spawn"), + RichSuggestion.of("pvp", "Teleport to the PvP combat arena"), + RichSuggestion.of("shop", "Teleport to the server shop market") + )); + + Commands.literal("warp") + .permission(warpPermission) + .cooldown(Duration.ofSeconds(10), "Wait s before warping again.") + .argument(warpArg) .executes(context -> { if (!context.isPlayer()) { - Text.Preset.error(context.sender(), "Only players can use this command."); + Text.send(context.sender(), "Console cannot teleport!"); return; } - - String selectedMode = context.args().get(modeArg); - Text.Preset.success(context.sender(), "Changed gamemode to: " + selectedMode); - }) - .register(); - } -} -``` ---- - -## Subcommands + Player player = context.playerOrThrow(); + String warpName = context.args().get(warpArg); + Location loc = warps.get(warpName); -You can attach subcommands to your command builder using `.subcommand(Consumer)`. Subcommands support their own permissions, arguments, and execution logic. - -```java -Permission warpPermission = Permission.builder("myplugin.warp").build(); -Permission warpAdminPermission = Permission.builder("myplugin.warp.admin").build(); - -Commands.literal("warp") - .permission(warpPermission) - .subcommand(sub -> sub - .label("create") - .aliases("set", "add") // Registers aliases for subcommand - .permission(warpAdminPermission) - .argument(Arguments.string("name")) - .executes(context -> { - String name = context.args().getString("name"); - Text.Preset.success(context.sender(), "Warp created: " + name); - }) - ) - .subcommand(sub -> sub - .label("tp") - .argument(Arguments.string("name")) - .executes(context -> { - String name = context.args().getString("name"); - Text.Preset.info(context.sender(), "Teleporting to: " + name); - }) - ) - .register(); -``` - ---- - -## Command Permissions - -You can secure commands and subcommands using raw permission `String` nodes, or by using OumLib's cross-platform `Permission` utility: - -```java -import dev.oum.oumlib.command.Commands; -import dev.oum.oumlib.util.Permission; - -public class AdminCommand { - - private static final Permission ADMIN_PERM = Permission.builder("myplugin.admin") - .description("Allows admin command execution") - .defaultValue(Permission.Default.OP) - .build(); + if (loc == null) { + Text.send(player, "Warp '" + warpName + "' does not exist!"); + return; + } - public static void register() { - Commands.literal("admin") - .permission(ADMIN_PERM) // Natively supports OumLib's Permission objects - .executes(context -> { - Text.Preset.success(context.sender(), "Admin menu opened."); + player.teleport(loc); + Text.send(player, "Warped to " + warpName + "!"); }) + .subcommand(sub -> sub + .label("set") + .permission(adminPermission) + .argument(Arguments.string("name")) + .executes(context -> { + if (!context.isPlayer()) { + Text.send(context.sender(), "Only players can set warps."); + return; + } + + Player player = context.playerOrThrow(); + String warpName = context.args().getString("name"); + warps.put(warpName, player.getLocation()); + Text.send(player, "Warp '" + warpName + "' has been set to your location!"); + }) + ) .register(); } } @@ -110,107 +81,74 @@ public class AdminCommand { --- -## The CommandContext Structure +## Command Context API Reference The `CommandContext` object represents the execution environment: -- `context.sender()`: Returns the Kyori `Audience` representing the command executor. On Paper, this can be cast directly to a Bukkit `Player` or `ConsoleCommandSender`. -- `context.playerOrThrow()`: Returns the player object cast to the appropriate platform type, throwing an `IllegalStateException` if the sender is not a player. -- `context.isPlayer()`: Utility check returning `true` if the sender is a player. -- `context.isConsole()`: Utility check returning `true` if the sender is the console. -- `context.source()`: The underlying platform execution source. On Paper, this is Brigadier's `CommandSourceStack`. On Velocity, it is a `CommandSource`. -- `context.args()`: Accessor for parsed command arguments. You can fetch arguments by their `Argument` definition, or by their parameter name directly: +- `context.sender()`: Returns the Kyori `Audience` representing the command executor. +- `context.playerOrThrow()`: Returns the player object cast to the appropriate platform type. +- `context.isPlayer()`: Returns `true` if the sender is a player. +- `context.isConsole()`: Returns `true` if the sender is the console. +- `context.args()`: Accessor for parsed command arguments: - `args.get(Argument)`: Returns the type-safe parsed value. - `args.getString("name")`: Returns the parsed String, or `""` if not found. - `args.getInt("name")`: Returns the parsed integer, or `0` if not found. - `args.getDouble("name")`: Returns the parsed double, or `0.0` if not found. - `args.getBoolean("name")`: Returns the parsed boolean, or `false` if not found. - - `args.get("name", Class)`: Returns the parsed object of the specified class directly from Brigadier. - `context.reply(Component)`: Sends a pre-built Adventure `Component` to the sender. -- `context.reply(String, TagResolver...)`: Parses a MiniMessage template with optional resolvers and sends it. -- `context.sendTranslated(String, TagResolver...)`: Automatically detects the sender's language locale, resolves the translation key from `Localization` files, parses MiniMessage placeholders, and sends the localized message. -- `context.sendActionBar(String / Component, TagResolver...)`: Sends a MiniMessage-parsed or raw component action bar message to the sender. -- `context.sendTitle(String title, String subtitle, TagResolver...)`: Shows a title/subtitle parsed via MiniMessage. -- `context.sendTitle(String title, String subtitle, Duration fadeIn, Duration stay, Duration fadeOut, TagResolver...)`: Shows a title with specific fade-in, stay, and fade-out timings. -- `context.clearTitle()`: Clears any currently displayed titles. - ---- - -## Custom suggestions - -To register tab completions for arguments dynamically, use the `.suggests(...)` method. You can supply a static list or compute suggestions dynamically using the executor: - -```java -var warpArg = Arguments.string("warp") - .suggests(context -> { - // Return a list of strings to display in tab completion - return List.of("spawn", "shop", "pvp", "lounge"); - }); -``` - ---- - -## Rich Suggestions with Tooltips - -On modern Brigadier platforms, tab completion suggestions can show hoverable descriptive tooltips. OumLib supports this via `RichSuggestion` and `.suggestsRich(...)`: - -```java -import dev.oum.oumlib.command.RichSuggestion; -import dev.oum.oumlib.command.Arguments; - -var warpArg = Arguments.string("warp") - .suggestsRich(context -> List.of( - RichSuggestion.of("spawn", "Teleport to the main lobby spawn area"), - RichSuggestion.of("pvp", "Teleport to the PvP combat arena"), - RichSuggestion.of("shop", "Teleport to the server marketplace") - )); -``` +- `context.reply(String, TagResolver...)`: Parses a MiniMessage template and sends it. --- ## Cooldowns & Bypasses -When you configure a command cooldown: +Configure rate-limits per player UUID automatically: ```java .cooldown(Duration.ofSeconds(10), "Cooldown active: s") ``` -OumLib handles rate-limiting per player UUID automatically. -- **Bypass Permission**: Any player who possesses the bypass permission will not trigger the cooldown. The bypass permission is automatically calculated as: - `.bypass` (e.g. `myplugin.gamemode.bypass`) - If the command has no permission defined, it defaults to: - `.bypass` (e.g. `gamemode.bypass`) + +Any player who possesses the bypass permission will not trigger the cooldown. The bypass permission is automatically calculated as: +`.bypass` (e.g. `myplugin.warp.use.bypass`) +If the command has no permission defined, it defaults to: +`.bypass` (e.g. `warp.bypass`) --- ## Command Exception Handling -To prevent raw stack traces from leaking to players and to log command errors gracefully, OumLib features a centralized exception handling pipeline. - -### 1. Global Command Error Handler -Configure a fallback handler during OumLib initialization to log command execution failures globally (e.g. sending alerts to Discord or external log services): +Configure a fallback error handler globally during OumLib initialization or define builder-specific callbacks: +### Global Command Error Handler ```java -OumLib.init(this) - .commandErrorHandler((context, exception) -> { - // Send a custom error message to the player - context.sender().sendMessage(MiniMessage.miniMessage() - .deserialize("An unexpected error occurred: " + exception.getMessage() + "")); - - // Log to console/SLF4J - OumLib.logError("Unhandled error in /" + context.label(), exception); - }); +import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.text.Text; +import org.bukkit.plugin.java.JavaPlugin; + +public class CommandInitializer { + public void setup(JavaPlugin plugin) { + OumLib.init(plugin) + .commandErrorHandler((context, exception) -> { + Text.send(context.sender(), "An error occurred executing this command: " + exception.getMessage() + ""); + }); + } +} ``` -### 2. Builder-Specific Exception Handler -Define a custom handler for a single command to clean up local resources, reset user state, or display custom transaction error screens: - +### Builder-Specific Exception Handler ```java -Commands.literal("buy") - .onException((context, exception) -> { - context.sender().sendMessage("Transaction failed: Your balance was not charged."); - }) - .executes(context -> { - // Business logic that may throw payment exceptions - }); -``` +import dev.oum.oumlib.command.Commands; +import dev.oum.oumlib.text.Text; +public class TransactionCommand { + public void register() { + Commands.literal("pay") + .onException((context, exception) -> { + Text.send(context.sender(), "Payment failed: Transaction rolled back."); + }) + .executes(context -> { + throw new RuntimeException("Bank server timed out"); + }) + .register(); + } +} +``` diff --git a/docs/configuration.md b/docs/configuration.md index b1ed456..f1e2fea 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,103 +1,127 @@ # Configuration System -OumLib allows developers to define configuration files as Java `record` types. The library handles YAML parsing, key-merging on upgrades, custom/unknown user key preservation, and filesystem monitoring. +OumLib allows developers to define configuration files as Java `record` types. The library handles YAML parsing, key-merging on upgrades, custom key preservation, and filesystem monitoring. --- -## 1. Nested Configurations +## Real-world Example: Minigame Arena Config -You can organize configuration files by nesting records. Any record that implements `ConfigSection` can be used as a component of another configuration record: +Here is a configuration record for a minigame arena, utilizing comments, nested sections, and default value definitions: ```java import dev.oum.oumlib.config.Comment; import dev.oum.oumlib.config.ConfigSection; -public record DatabaseSettings( - String host, - int port, - String username, +public record MySQLCredentials( + @Comment("Hostname or IP of the MySQL database") String host, + @Comment("Database port") int port, + @Comment("Database credentials") String username, String password ) implements ConfigSection {} -public record PluginConfig( - @Comment("Database configuration settings.") - DatabaseSettings database, +public record LobbyLocation( + String world, + double x, + double y, + double z +) implements ConfigSection {} + +public record ArenaConfig( + @Comment("Database connection pool configuration") + MySQLCredentials database, + + @Comment("Arena lobby spawn location") + LobbyLocation lobby, + + @Comment("Maximum players allowed inside this arena") + int maxPlayers, - @Comment("Enable debug logs.") - boolean debug + @Comment("Whether debug messages are printed to the console") + boolean debugMode ) implements ConfigSection {} ``` -This will automatically generate a nested YAML format: +This generates the following structured YAML layout automatically: ```yaml -# Database configuration settings. +# Database connection pool configuration database: - host: "localhost" + # Hostname or IP of the MySQL database + host: "127.0.0.1" + # Database port port: 3306 + # Database credentials username: "root" password: "password" -# Enable debug logs. -debug: true -``` +# Arena lobby spawn location +lobby: + world: "world" + x: 0.0 + y: 64.0 + z: 0.0 ---- +# Maximum players allowed inside this arena +maxPlayers: 16 -## 2. Supported Data Types -When parsing configuration files, OumLib reads values and automatically converts them to their corresponding Java types: -- **Strings**: Surrounded by single/double quotes, or left raw. Quotes are stripped and internal escapes (e.g. `\"`) are resolved. -- **Numbers**: Values with decimal points are parsed as `Double`. Values without decimals are parsed as `Integer`. -- **Booleans**: Case-insensitive matches for `true` and `false`. -- **Nulls**: Case-insensitive matches for `null`. +# Whether debug messages are printed to the console +debugMode: true +``` --- -## 3. Preserving Custom/Unknown Keys -If a user adds custom keys to the YAML configuration file (e.g., custom metadata or variables for external integrations), OumLib detects that they do not belong to the Java record class. -- Rather than discarding them when the configuration is rewritten or saved, OumLib **tracks them internally**. -- When `save()` is executed, the library appends all unknown keys back into the file under an `# Additional keys` comment header, preserving user data. +## Loading and Auto-Reload Watcher ---- +Set up a configuration file mapping and enable background file watch services to automatically re-read values and fire updates: -## 4. How the Auto-Reload Watcher Works -When you call `.enableAutoReload()`: -1. OumLib registers a Java `WatchService` on the plugin's data folder directory path. -2. It starts a platform-safe **Virtual Thread** to monitor standard filesystem events in the background. -3. When the service catches an `ENTRY_MODIFY` event for the specific configuration file (e.g., `config.yml`), it triggers the load sequence. -4. **Key Mismatch Safety**: The loader parses the modified file, merges any missing default values, and determines if it needs to rewrite the file. If all keys are present, it loads the file silently into memory. It only writes to disk if keys are missing, preventing recursive write-watch loops. -5. If the reload succeeds, any registered `onReload(Consumer)` callbacks are invoked on the virtual thread. +```java +import dev.oum.oumlib.config.ConfigManager; +import org.bukkit.plugin.java.JavaPlugin; + +public final class ArenaManager { + private ConfigManager configManager; + + public void initialize(JavaPlugin plugin) { + MySQLCredentials defaultDb = new MySQLCredentials("127.0.0.1", 3306, "root", "password"); + LobbyLocation defaultLobby = new LobbyLocation("world", 0.0, 64.0, 0.0); + + configManager = ConfigManager.of(ArenaConfig.class, "arena.yml", + () -> new ArenaConfig(defaultDb, defaultLobby, 16, true) + ).enableAutoReload(); + + configManager.onReload(newConfig -> { + plugin.getLogger().info("Arena configurations re-read from disk successfully!"); + applyNewSettings(newConfig); + }); + } + + private void applyNewSettings(ArenaConfig config) { + System.out.println("Maximum players updated to: " + config.maxPlayers()); + } +} +``` --- -## 5. Configuration Schema Auto-Migration - -To handle config changes as your plugin evolves, OumLib provides a step-based migration system using `ConfigMigrationRegistry`. This allows you to rename keys, set default values, or restructure existing files on start-up. +## Configuration Schema Auto-Migration -### Defining and Registering Migrations: -Register sequential upgrade blocks targeting specific config version numbers: +To modify keys and values as your plugin version upgrades, register sequential version migrations: ```java import dev.oum.oumlib.config.ConfigManager; import dev.oum.oumlib.config.ConfigMigrationRegistry; -ConfigManager manager = ConfigManager.of( - PluginConfig.class, - "config.yml", - PluginConfig::new -); - -manager.migrate(new ConfigMigrationRegistry() - .add(2, map -> { - // Version 2 migration: Rename 'old-cooldown' to 'cooldown-seconds' - if (map.containsKey("old-cooldown")) { - map.put("cooldown-seconds", map.remove("old-cooldown")); - } - }) - .add(3, map -> { - // Version 3 migration: Set default value for a new configuration key - map.putIfAbsent("enable-mysql", false); - }) -); +public final class ArenaUpgrader { + public void setupMigrations(ConfigManager manager) { + manager.migrate(new ConfigMigrationRegistry() + .add(2, map -> { + if (map.containsKey("old-max-players")) { + map.put("maxPlayers", map.remove("old-max-players")); + } + }) + .add(3, map -> map.putIfAbsent("debugMode", false)) + ); + } +} ``` When OumLib loads the config: @@ -105,4 +129,3 @@ When OumLib loads the config: 2. It applies each registered migration step with a key higher than the file's current version sequentially. 3. It updates `config-version` to the highest migrated version. 4. It saves the modified YAML structure back to disk automatically. - diff --git a/docs/database.md b/docs/database.md index c23cb09..25ac627 100644 --- a/docs/database.md +++ b/docs/database.md @@ -1,223 +1,105 @@ # Database Tools -OumLib includes a high-performance SQL database wrapper powered by **HikariCP 7.0.2** supporting both **SQLite** and **MySQL**. It utilizes Java 25 virtual threads via `Promise` for non-blocking asynchronous operations, preventing server thread stalling. +OumLib includes a high-performance SQL database wrapper powered by HikariCP supporting both SQLite and MySQL. It utilizes virtual threads via `Promise` for non-blocking asynchronous operations. --- -## 1. Connecting to a Database +## Real-world Example: Player Profile Management -OumLib automatically establishes a Hikari connection pool with sensible defaults (like statement caching, performance optimizations, and correct write-concurrency limits). +Here is a profile system that loads players' statistics upon connection, updates their score values, and handles transaction-safe currency transfers between two players: -### SQLite Connection (Single-threaded file lock safety) -For SQLite, OumLib enforces a `maximumPoolSize` of `1` by default. This is the industry-standard best practice to prevent concurrent write locks on the SQLite file. ```java import dev.oum.oumlib.database.Database; +import dev.oum.oumlib.text.Text; +import org.bukkit.entity.Player; import java.io.File; +import java.util.UUID; -// Connects using sensible SQLite defaults -Database db = Database.sqlite(new File(getDataFolder(), "data.db")); -``` - -### SQLite Connection with Custom Hikari Settings -```java -Database db = Database.sqlite(new File(getDataFolder(), "data.db"), config -> { - config.setConnectionTimeout(10000); // 10 seconds -}); -``` - -### MySQL Connection -For MySQL, OumLib automatically activates optimized configuration flags (e.g., preparation statement caching, server prep statement usage, and batch rewrite support) to ensure lowest latency. -```java -Database db = Database.mysql("127.0.0.1", 3306, "my_database", "username", "password"); -``` - -### MySQL Connection with Custom Hikari Settings -You can customize Hikari parameters using the config customizer callback: -```java -Database db = Database.mysql("127.0.0.1", 3306, "my_database", "username", "password", config -> { - config.setMaximumPoolSize(20); - config.setMinimumIdle(5); - config.setPoolName("MyPlugin-Pool"); -}); -``` - ---- - -## 2. Executing Updates (DDL/DML) - -Use `.executeUpdate(String sql, Object... params)` to run `CREATE TABLE`, `INSERT`, `UPDATE`, or `DELETE` statements asynchronously on virtual threads: - -```java -// Create a table if it does not exist -db.executeUpdate("CREATE TABLE IF NOT EXISTS players (uuid VARCHAR(36) PRIMARY KEY, coins INT)") - .thenAcceptSync(result -> { - getLogger().info("Database initialization check complete!"); - }); - -// Insert or update player coins -String playerUuid = player.getUniqueId().toString(); -db.executeUpdate("INSERT INTO players (uuid, coins) VALUES (?, ?) ON DUPLICATE KEY UPDATE coins = ?", - playerUuid, 100, 100); -``` +public record UserProfile(String uuid, int coins, int level) {} ---- - -## 3. Querying Data - -Use `.executeQuery(String sql, Object... params)` to retrieve data. Results are returned as a list of key-value maps representing rows and columns: +public final class ProfileDatabaseManager { + private final Database db; -```java -db.executeQuery("SELECT coins FROM players WHERE uuid = ?", player.getUniqueId().toString()) - .thenAcceptSync(rows -> { - if (rows.isEmpty()) { - player.sendMessage("No profile found."); - return; - } - - // Retrieve values by column name - int coins = (int) rows.getFirst().get("coins"); - player.sendMessage("You have " + coins + " coins!"); - }); -``` + public ProfileDatabaseManager(File dataFolder) { + db = Database.sqlite(new File(dataFolder, "data.db")); + db.executeUpdate("CREATE TABLE IF NOT EXISTS profiles (uuid VARCHAR(36) PRIMARY KEY, coins INT, level INT)"); + } ---- + public void loadProfile(Player player) { + db.executeQuery("SELECT uuid, coins, level FROM profiles WHERE uuid = ?", UserProfile.class, player.getUniqueId().toString()) + .thenAcceptSync(profiles -> { + if (profiles.isEmpty()) { + createDefaultProfile(player); + return; + } + + UserProfile profile = profiles.getFirst(); + Text.send(player, "Profile loaded: Level " + profile.level() + " (" + profile.coins() + " coins)"); + }); + } -## 4. Batch Operations + private void createDefaultProfile(Player player) { + db.executeUpdate("INSERT INTO profiles (uuid, coins, level) VALUES (?, 100, 1)", player.getUniqueId().toString()) + .thenAcceptSync(v -> Text.send(player, "Default profile created!")); + } -To execute multiple updates in bulk efficiently (e.g., during automatic profile saving), use `.executeBatch(String sql, List parameterBatch)`: + public void transferCoins(Player sender, UUID targetUuid, int amount) { + db.transaction(ctx -> { + var senderRows = ctx.executeQuery("SELECT coins FROM profiles WHERE uuid = ?", sender.getUniqueId().toString()); + if (senderRows.isEmpty()) { + throw new IllegalStateException("Profile not found"); + } + + int senderCoins = (int) senderRows.getFirst().get("coins"); + if (senderCoins < amount) { + throw new IllegalStateException("Insufficient balance"); + } + + ctx.executeUpdate("UPDATE profiles SET coins = coins - ? WHERE uuid = ?", amount, sender.getUniqueId().toString()); + ctx.executeUpdate("UPDATE profiles SET coins = coins + ? WHERE uuid = ?", amount, targetUuid.toString()); + + return true; + }).whenCompleteSync( + success -> Text.send(sender, "Transferred " + amount + " coins successfully!"), + error -> Text.send(sender, "Transaction aborted: " + error.getMessage() + "") + ); + } -```java -List batchParams = new ArrayList<>(); -for (Player player : Bukkit.getOnlinePlayers()) { - batchParams.add(new Object[] { player.getUniqueId().toString(), 150, 150 }); + public void close() { + db.close(); + } } - -db.executeBatch("INSERT INTO players (uuid, coins) VALUES (?, ?) ON DUPLICATE KEY UPDATE coins = ?", batchParams) - .thenAcceptSync(rowsUpdated -> { - getLogger().info("Successfully saved " + rowsUpdated.length + " player records in batch."); - }); ``` --- -## 5. Transactions - -Use `.transaction(TransactionContextCallback)` (recommended) to execute multiple queries sequentially within a transaction context. This context exposes the same class-mapping and batch execution helpers as the main `Database` object, executing them synchronously on the active transaction connection. +## Connection Setup Reference -The wrapper automatically disables auto-commit, commits upon successful execution, and rolls back all operations if any error/exception is encountered. +Establish optimized database connection pools: -### High-level Transaction Context (Recommended): +### SQLite Connection ```java -db.transaction(ctx -> { - // Both statements execute sequentially on the same active transaction connection - ctx.executeUpdate("UPDATE players SET coins = coins - 10 WHERE uuid = ?", playerUuid); - ctx.executeUpdate("INSERT INTO transactions (uuid, amount) VALUES (?, -10)", playerUuid); - - // You can retrieve values or maps inside the transaction too - return ctx.executeQuery("SELECT coins FROM players WHERE uuid = ?", Integer.class, playerUuid).getFirst(); -}).whenCompleteSync( - coins -> getLogger().info("Transaction committed. New balance: " + coins), - error -> getLogger().severe("Transaction failed and rolled back: " + error.getMessage()) -); -``` +import dev.oum.oumlib.database.Database; +import java.io.File; -### Low-level Raw Connection Transaction (Optional): -For direct JDBC operations, pass a `TransactionCallback` to interact with the raw `Connection` object: -```java -db.transaction(conn -> { - try (var stmt1 = conn.prepareStatement("UPDATE players SET coins = coins - 10 WHERE uuid = ?"); - var stmt2 = conn.prepareStatement("INSERT INTO transactions (uuid, amount) VALUES (?, -10)")) { - - stmt1.setString(1, playerUuid); - stmt1.executeUpdate(); - - stmt2.setString(1, playerUuid); - stmt2.executeUpdate(); +public class SqliteConfig { + public Database init(File dataFolder) { + return Database.sqlite(new File(dataFolder, "data.db")); } - return null; -}); -``` - ---- - -## 6. Schema Script Execution - -Use `.executeScript(String sqlScript)` to execute a multi-statement SQL script (e.g. schema tables setup). It splits statements by semicolons and filters out line (`--`) and block (`/* */`) comments: - -```java -String initScript = - "CREATE TABLE IF NOT EXISTS players (uuid VARCHAR(36) PRIMARY KEY, coins INT);\n" + - "CREATE TABLE IF NOT EXISTS logs (id INTEGER PRIMARY KEY AUTOINCREMENT, msg TEXT);"; - -db.executeScript(initScript) - .thenAcceptSync(v -> getLogger().info("Schema successfully created.")); -``` - ---- - -## 7. Advanced Integration (Row Mapping & Resource Loading) - -### Automatic Class & Record Mapping -Instead of mapping database fields manually, you can pass a Java `Record` or custom class directly. OumLib will automatically inspect constructor parameters or class fields and map the query result rows directly: - -```java -public record PlayerCoins(String uuid, int coins) {} - -// Auto-maps fields in the result set to the record constructor -db.executeQuery("SELECT uuid, coins FROM players", PlayerCoins.class) - .thenAcceptSync(profiles -> { - for (PlayerCoins profile : profiles) { - getLogger().info(profile.uuid() + " has " + profile.coins() + " coins!"); - } - }); +} ``` -### Manual Mapping with `RowMapper` -If you need custom mapping behavior (such as custom data type conversions or composite objects), implement a custom `RowMapper`: - +### MySQL Connection ```java -import dev.oum.oumlib.database.RowMapper; - -public record PlayerCoins(String uuid, int coins) {} +import dev.oum.oumlib.database.Database; -// Executing and mapping the result set manually -db.executeQuery("SELECT uuid, coins FROM players", rs -> new PlayerCoins( - rs.getString("uuid"), - rs.getInt("coins") -)).thenAcceptSync(profiles -> { - for (PlayerCoins profile : profiles) { - getLogger().info(profile.uuid() + " has " + profile.coins() + " coins!"); +public class MysqlConfig { + public Database init() { + return Database.mysql("127.0.0.1", 3306, "my_database", "username", "password", config -> { + config.setMaximumPoolSize(10); + config.setMinimumIdle(2); + config.setPoolName("Plugin-Pool"); + }); } -}); -``` - -### Loading SQL Scripts from Plugin Resources -Instead of hardcoding script strings, you can execute SQL scripts directly from your JAR resource folder (e.g. `schema.sql`) using an `InputStream`: - -```java -import java.io.InputStream; - -InputStream schemaStream = getResource("schema.sql"); -if (schemaStream != null) { - db.executeScript(schemaStream) - .thenAcceptSync(v -> getLogger().info("Database tables initialized from schema.sql!")) - .whenCompleteSync(null, err -> getLogger().severe("Failed to initialize database: " + err.getMessage())); } ``` - ---- - -## 8. Custom ORMs & Raw DataSource Access - -If you are using external SQL libraries (like JOOQ, Requery, or MyBatis) or need raw access to the datasource, retrieve it directly: - -```java -import com.zaxxer.hikari.HikariDataSource; - -HikariDataSource ds = db.dataSource(); -``` - -To close the connection pool and release resources on plugin disable: -```java -db.close(); -``` diff --git a/docs/effects.md b/docs/effects.md new file mode 100644 index 0000000..9bc705d --- /dev/null +++ b/docs/effects.md @@ -0,0 +1,94 @@ +# Visual & Sound Effects + +OumLib provides dynamic, chainable builders for playing particles and sound effects under the `dev.oum.oumlib.effect` package. + +--- + +## Real-world Example: Level Up Helix + +Play a chime sound and render a golden spiral helix around a player whenever they level up: + +```java +import dev.oum.oumlib.effect.Effects; +import dev.oum.oumlib.effect.Particles; +import dev.oum.oumlib.effect.SoundBuilder; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +public final class LevelUpAnimation { + public void animate(Player player) { + Location center = player.getLocation(); + + Effects.sound(Sound.ENTITY_PLAYER_LEVELUP) + .volume(1.0F) + .pitch(1.2F) + .play(player); + + Particles.spawnHelix( + center, + 1.0, + 0.2, + 2.0, + 50, + Effects.particle(Particle.DUST).color(Color.YELLOW, 1.0F) + ); + } +} +``` + +--- + +## Real-world Example: Gun Bullet Tracer + +Draws a line of smoke particles representing a gun tracer from the player's eye location to their target hit point, playing a gunshot sound: + +```java +import dev.oum.oumlib.effect.Effects; +import dev.oum.oumlib.effect.Particles; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +public final class WeaponTracer { + public void fireTracer(Player player, Location targetLoc) { + Location origin = player.getEyeLocation(); + + Effects.sound(Sound.ENTITY_FIREWORK_ROCKET_BLAST) + .volume(0.8F) + .pitch(1.5F) + .play(origin); + + Particles.spawnLine( + origin, + targetLoc, + Effects.particle(Particle.CRIT).count(1), + 15 + ); + } +} +``` + +--- + +## Static Sound & Particle Spawners + +Trigger audio files or play standard dust options using fast static shortcuts: + +```java +import dev.oum.oumlib.effect.Particles; +import dev.oum.oumlib.effect.Sounds; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +public final class VisualShortcuts { + public void execute(Player player, Location location) { + Particles.spawnDust(location, Color.RED, 1.2F, 10); + Sounds.play(player, "block.note_block.pling", 1.0F, 1.2F); + } +} +``` diff --git a/docs/entities.md b/docs/entities.md new file mode 100644 index 0000000..f1d29a3 --- /dev/null +++ b/docs/entities.md @@ -0,0 +1,81 @@ +# Entity Utilities & Display Builders + +OumLib offers high-level entity utilities and a fluent display entity generator under the `dev.oum.oumlib.entity` package. + +--- + +## Real-world Example: Holographic Stat Billboard + +Spawn a billboard-aligned text hologram floating above an NPC's head displaying player rankings: + +```java +import dev.oum.oumlib.entity.DisplayBuilder; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.entity.Display; +import org.bukkit.entity.TextDisplay; + +public final class HologramManager { + public TextDisplay spawnStatsHologram(Location location) { + Location spawnLoc = location.clone().add(0, 2.5, 0); + + return DisplayBuilder.text(spawnLoc, "Server Leaderboards\n1. sun_mc - 1,200 points") + .billboard(Display.Billboard.CENTER) + .shadow(true) + .seeThrough(false) + .backgroundColor(Color.fromARGB(150, 0, 0, 0)) + .scale(1.2F, 1.2F, 1.2F) + .spawn(); + } +} +``` + +--- + +## Real-world Example: Spinning In-Game Shop Showcase + +Spawns a glowing, double-sized block display (e.g. Diamond Block) floating above a shop chest, rotated at a 45-degree angle: + +```java +import dev.oum.oumlib.entity.DisplayBuilder; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.BlockDisplay; + +public final class ItemShowcaseManager { + public BlockDisplay spawnChestShowcase(Location chestLoc) { + Location spawnLoc = chestLoc.clone().add(-0.25, 1.2, -0.25); + + return DisplayBuilder.block(spawnLoc, Material.DIAMOND_BLOCK.createBlockData()) + .scale(0.5F, 0.5F, 0.5F) + .leftRotation(0.0F, 0.382F, 0.0F, 0.924F) + .glowing(true) + .glowColor(Color.AQUA) + .spawn(); + } +} +``` + +--- + +## Entities Target Raytracing + +Perform entity scans or trace the exact block a player is aiming at: + +```java +import dev.oum.oumlib.entity.Entities; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import java.util.List; + +public final class WeaponScanner { + public void execute(Player player) { + Block targetBlock = Entities.getTargetBlock(player, 15); + Entity targetEntity = Entities.getTargetEntity(player, 25); + + List nearby = Entities.nearbyPlayers(player.getLocation(), 10.0); + } +} +``` diff --git a/docs/events.md b/docs/events.md index 5ab8a92..ee0b6f7 100644 --- a/docs/events.md +++ b/docs/events.md @@ -1,129 +1,112 @@ -# Event System +# Events & Listeners Bus -OumLib features a modern, fluent event bus wrapper. It provides cleaner listener registration, custom conditional filtering, execution boundaries, and platform-level lifecycle management. +OumLib features a modern, fluent event bus wrapper. It provides cleaner listener registration, conditional filtering, execution boundaries, and unregistration hooks. --- -## 1. Registration & Custom Filtering +## Real-world Example: Combat Tagging System -Use `.filter(...)` to add condition checks to your event listener. The callback handler will only execute if all filter checks return `true`. +Here is a combat tagging module that flags players in combat upon entity damage, blocks teleportation requests, intercepts quit actions, and automatically expires when players log off or combat times out: ```java import dev.oum.oumlib.event.Events; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.inventory.EquipmentSlot; - -Events.listen(PlayerInteractEvent.class) - // Filter out off-hand clicks - .filter(event -> event.getHand() == EquipmentSlot.HAND) - // Filter to only trigger if the player is holding a diamond sword - .filter(event -> event.getPlayer().getInventory().getItemInMainHand().getType().name().contains("DIAMOND_SWORD")) - .handler(event -> { - event.getPlayer().sendMessage("You swung a diamond sword!"); - }); -``` - -### 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.Events; -import org.bukkit.event.player.PlayerInteractEvent; +import dev.oum.oumlib.text.Text; import org.bukkit.entity.Player; - -Player targetPlayer = ...; - -Events.listen(PlayerInteractEvent.class) - .playerFilter(PlayerInteractEvent::getPlayer, player -> player.equals(targetPlayer)) - .handler(event -> { - 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 - }); +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; + +public final class CombatTagManager { + private final Set activeCombat = new HashSet<>(); + + public void initialize() { + Events.listen(EntityDamageByEntityEvent.class) + .filter(event -> event.getEntity() instanceof Player) + .filter(event -> event.getDamager() instanceof Player) + .handler(event -> { + Player victim = (Player) event.getEntity(); + Player attacker = (Player) event.getDamager(); + + tagPlayer(victim); + tagPlayer(attacker); + }); + + Events.listen(PlayerTeleportEvent.class) + .filter(event -> activeCombat.contains(event.getPlayer())) + .filter(event -> event.getCause() == PlayerTeleportEvent.TeleportCause.COMMAND) + .handler(event -> { + event.setCancelled(true); + Text.send(event.getPlayer(), "You cannot teleport while in combat!"); + }); + } + + private void tagPlayer(Player player) { + if (activeCombat.add(player)) { + Text.send(player, "You are now in combat! Do not log out."); + + Events.listen(PlayerQuitEvent.class) + .playerFilter(PlayerQuitEvent::getPlayer, p -> p.equals(player)) + .maxFires(1) + .expireAfter(Duration.ofSeconds(15)) + .handler(event -> { + System.out.println(player.getName() + " logged out while in combat!"); + activeCombat.remove(player); + }); + + Events.listen(PlayerQuitEvent.class) + .playerFilter(PlayerQuitEvent::getPlayer, p -> p.equals(player)) + .expireAfter(Duration.ofSeconds(15)) + .expireIf(event -> !activeCombat.contains(player)) + .handler(event -> activeCombat.remove(player)); + } + } +} ``` -> [!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 +## Cancellable Events & State Handling -You can configure how the event listener behaves regarding cancelled events: -- **`ignoreCancelled()`**: The listener will skip execution if another plugin has already cancelled the event. -- **`onlyIfCancelled()`**: The listener will *only* fire if the event has already been cancelled by another plugin. +Configure how the event listener behaves regarding cancelled events: +- **`ignoreCancelled()`**: Skip execution if another plugin has already cancelled the event. +- **`onlyIfCancelled()`**: Only fire if the event has already been cancelled. ```java +import dev.oum.oumlib.event.Events; import org.bukkit.event.block.BlockBreakEvent; -Events.listen(BlockBreakEvent.class) - .ignoreCancelled() // Skip if block break is protected/cancelled - .handler(event -> { - System.out.println("A block was successfully broken by " + event.getPlayer().getName()); - }); +public class BlockLogger { + public void register() { + Events.listen(BlockBreakEvent.class) + .ignoreCancelled() + .handler(event -> { + System.out.println("Block broken: " + event.getBlock().getType()); + }); + } +} ``` --- -## 4. Automatic Unregistration & Memory Protection +## Asynchronous Thread Listeners -To avoid memory leaks, you can configure listeners to unregister themselves automatically when certain thresholds or conditions are reached: -- **`maxFires(int count)`**: Automatically cleans up and unregisters the listener after it executes a set number of times. -- **`expireAfter(Duration duration)`**: Automatically unregisters the listener after a set period of time has elapsed since registration. -- **`expireIf(Predicate condition)`**: Evaluates a custom condition on each event execution, unregistering the listener immediately if the predicate returns `true`. +Offload heavy I/O calculations (like database calls) to async thread pools: ```java -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.player.AsyncPlayerChatEvent; -import java.time.Duration; - -// 1. Fire-once or timed expiration -Events.listen(PlayerQuitEvent.class) - .maxFires(1) // Runs once, then cleans up. Perfect for single-event wait loops. - .expireAfter(Duration.ofMinutes(10)) // Automatically unregisters after 10 minutes - .handler(event -> { - System.out.println("This message will print for at most one quit event within the next 10 minutes."); - }); - -// 2. Conditional expiration (e.g. stop intercepting chat once user types 'exit') -Events.listen(AsyncPlayerChatEvent.class) - .filter(event -> event.getPlayer().equals(targetPlayer)) - .expireIf(event -> event.getMessage().equalsIgnoreCase("exit")) - .handler(event -> { - targetPlayer.sendMessage("Intercepted message: " + event.getMessage()); - event.setCancelled(true); - }); -``` - ---- - -## 5. Thread Safety & Folia Compatibility - -- **Paper/Spigot**: Event handlers run synchronously on the server's main tick thread (the standard Bukkit behavior), unless the event itself is marked as asynchronous (e.g. `AsyncChatEvent`). -- **Velocity**: Event handlers are run asynchronously on Netty thread pools. Velocity events do not run on a single main thread. -- **Folia**: Folia runs events on the thread executing the region tick where the event occurred. Since region tick threads change dynamically, ensure any external data mutations triggered inside your handlers are thread-safe (e.g. using `ConcurrentHashMap` or thread-safe atomic variables). +import dev.oum.oumlib.event.Events; +import org.bukkit.event.player.PlayerInteractEvent; -### Asynchronous Listener Support -To execute listener code on a background thread pool (or virtual thread on Paper/Folia), chain `.async()` on the builder: -```java -Events.listen(PlayerInteractEvent.class) - .async() // Offloads execution to virtual/async threads - .handler(event -> { - // Safe to execute blocking database lookups or HTTP webhooks - // Note: Event modification/cancellation is not supported in async mode - }); +public class AsyncLogger { + public void register() { + Events.listen(PlayerInteractEvent.class) + .async() + .handler(event -> { + System.out.println("Processing heavy interaction logs on virtual threads."); + }); + } +} ``` +*Note: Event modification/cancellation is not supported in async mode.* diff --git a/docs/inventories.md b/docs/inventories.md index 17e4e01..fa6fc7a 100644 --- a/docs/inventories.md +++ b/docs/inventories.md @@ -1,281 +1,119 @@ # GUI & Chest Menus -OumLib includes a simple, lightweight inventory menu system for Paper/Bukkit. It uses layout patterns, item bindings, and event handlers to build custom menus without listener boilerplate. +OumLib includes a simple, lightweight inventory menu system for Paper/Bukkit. It uses layout patterns, item bindings, and click handlers to build custom menus. --- -## 1. Defining Layouts with Dynamic Bindings +## Real-world Example: Virtual Coin Shop -The `Layout` system maps text patterns to inventory slots. You can bind specific characters to static items or **dynamic item suppliers** (which execute every time the menu is opened or refreshed). +Here is a virtual store interface that reads a player's balance dynamically, checks if they can afford an item via `EconomyBridge`, deducts the balance, updates the menu state placeholders, and plays sound effects: ```java +import dev.oum.oumlib.bridge.economy.EconomyBridge; +import dev.oum.oumlib.effect.Effects; import dev.oum.oumlib.inventory.ChestMenu; -import dev.oum.oumlib.inventory.Layout; +import dev.oum.oumlib.inventory.ItemBuilder; +import dev.oum.oumlib.text.Text; import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import net.kyori.adventure.text.minimessage.MiniMessage; - -public class SettingsMenu { - - private static boolean settingEnabled = true; - public static ChestMenu build() { - return ChestMenu.builder() - .title("System Options") +public final class CoinShopMenu { + public static void open(Player player) { + ChestMenu.builder() + .title("Coin Shop | Coins: {coins_balance}") .rows(3) + .state("coins_balance", p -> (int) EconomyBridge.balance(p)) .pattern( "#########", - "# T #", + "# G S #", "#########" ) - // Bind a static border item - .bind('#', new ItemStack(Material.GRAY_STAINED_GLASS_PANE)) - // Bind a dynamic supplier for 'T' (Toggle) - .bind('T', () -> { - Material mat = settingEnabled ? Material.LIME_WOOL : Material.RED_WOOL; - ItemStack item = new ItemStack(mat); - ItemMeta meta = item.getItemMeta(); - if (meta != null) { - meta.displayName(MiniMessage.miniMessage().deserialize( - settingEnabled ? "Setting Enabled" : "Setting Disabled" - )); - item.setItemMeta(meta); + .bind('#', ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build()) + .bind('G', () -> ItemBuilder.of(Material.GOLD_INGOT).name("Gold Pack").lore("Price: 100 points").build()) + .bind('S', () -> ItemBuilder.of(Material.NETHER_STAR).name("Server Booster").lore("Price: 500 points").build()) + .onClick('G', click -> { + double balance = EconomyBridge.balance(click.player()); + if (balance < 100.0) { + Text.send(click.player(), "Insufficient points!"); + return; } - return item; + EconomyBridge.withdraw(click.player(), 100.0); + click.player().getInventory().addItem(new ItemStack(Material.GOLD_INGOT, 16)); + + int newBalance = (int) EconomyBridge.balance(click.player()); + click.menu().updateState(click.player(), "coins_balance", newBalance); + Effects.sound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP).volume(1.0F).pitch(1.0F).play(click.player()); }) - // Handle clicks on key 'T' - .onClick('T', context -> { - // Toggle state - settingEnabled = !settingEnabled; - context.player().sendMessage("Toggled setting!"); + .onClick('S', click -> { + double balance = EconomyBridge.balance(click.player()); + if (balance < 500.0) { + Text.send(click.player(), "Insufficient points!"); + return; + } + EconomyBridge.withdraw(click.player(), 500.0); - // Refresh the menu for the player to update the wool color - context.menu().refresh(context.player()); + int newBalance = (int) EconomyBridge.balance(click.player()); + click.menu().updateState(click.player(), "coins_balance", newBalance); + Effects.sound(Sound.UI_TOAST_CHALLENGE_COMPLETE).volume(1.0F).pitch(1.0F).play(click.player()); }) - .build(); + .build() + .open(player); } } ``` --- -## 2. Menu State Management - -`ChestMenu` features built-in reactive state tracking. You can register state providers, embed state placeholders in the title, and update state values dynamically. OumLib handles title rendering, player-specific state scopes, and automated layout refreshes. - -```java -import dev.oum.oumlib.inventory.ChestMenu; - -ChestMenu menu = ChestMenu.builder() - // Define a title containing a state placeholder - .title("Level: {player_level}") - .rows(3) - // Register initial state supplier - .state("player_level", player -> player.getLevel()) - .pattern( - "#########", - "# L #", - "#########" - ) - .bind('#', new ItemStack(Material.GRAY_STAINED_GLASS_PANE)) - // Bind 'L' to level up item - .bind('L', () -> new ItemStack(Material.EXPERIENCE_BOTTLE)) - .onClick('L', context -> { - // Update the state value dynamically - int currentLevel = (int) context.menu().getState(context.player(), "player_level"); - context.player().setLevel(currentLevel + 1); - - // This updates the tracked state and automatically refreshes - // the GUI layout and window title. - context.menu().updateState(context.player(), "player_level", currentLevel + 1); - }) - .build(); -``` - ---- - -## 3. The ClickContext Object - -When a slot is clicked, the register handler receives a `ClickContext` containing: -- **`context.player()`**: The player who clicked. -- **`context.slot()`**: The slot index clicked. -- **`context.action()`**: The click action type (translates Bukkit's click actions into basic types: `LEFT`, `RIGHT`, `SHIFT_LEFT`, `SHIFT_RIGHT`, `MIDDLE`, or `UNKNOWN`). - -```java -.onClick(13, context -> { - if (context.action() == ClickAction.RIGHT) { - context.player().sendMessage("You right-clicked!"); - } else { - context.player().sendMessage("You clicked!"); - } -}); -``` - ---- - -## 4. Inventory Click Protection Guards - -To prevent standard menu bugs, OumLib implements two click protection safeguards internally: -1. **Auto-Cancellation**: All clicks inside the menu container are cancelled (`event.setCancelled(true)`) to prevent players from taking items. -2. **Player Inventory Isolation**: The click listener checks: - ```java - if (event.getClickedInventory() == null || !event.getClickedInventory().equals(inv)) return; - ``` - This ensures that clicking items inside the player's own inventory hotbar does *not* trigger the GUI slot handlers, preventing unexpected actions or item glitches. - ---- +## Real-world Example: Server Selector -## 5. ItemBuilder Reference - -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; - -// 1. Create a formatted item using MiniMessage and String lore: -ItemStack excalibur = ItemBuilder.of(Material.DIAMOND_SWORD) - .name("Excalibur") - .lore( - "A legendary sword", - "of ancient kings" - ) - .enchant(Enchantment.SHARPNESS, 5) - .glow() // Makes the item glow without showing enchantments flag - .unbreakable(true) - .build(); - -// 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(); - -// 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(); -``` - -### 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 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(); -``` - -### General Utilities: -- `.clearLore()`: Removes all lore lines. -- `.amount(int)`: Sets stack quantity. -- `.flag(ItemFlag...)`: Adds specific item flags. - ---- - -## 6. AnvilMenu Reference - -`AnvilMenu` provides a simple way to prompt players for text input using Minecraft's anvil interface. - -```java -import dev.oum.oumlib.inventory.AnvilMenu; - -AnvilMenu menu = AnvilMenu.builder() - .title("Rename Item") - .placeholder("Enter new name...") - .onConfirm((player, text) -> { - player.sendMessage("You entered: " + text); - }) - .onClose(player -> { - player.sendMessage("You cancelled the input."); - }) - .build(); - -// Open the menu for a player -menu.open(player); -``` - ---- - -## 7. PaginatedMenu Reference - -`PaginatedMenu` simplifies displaying lists of items across multiple pages, handling page navigation arrows and content distribution automatically. +Here is a multi-lobby server selector utilizing the `PaginatedMenu` controller to automatically distribute servers across pages: ```java import dev.oum.oumlib.inventory.ItemBuilder; import dev.oum.oumlib.inventory.PaginatedMenu; +import dev.oum.oumlib.util.Proxy; import org.bukkit.Material; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import java.util.ArrayList; import java.util.List; -List items = new ArrayList<>(); -for (int i = 1; i <= 50; i++) { - items.add(ItemBuilder.of(Material.GOLD_INGOT) - .name("Reward #" + i + "") - .build()); +public final class LobbySelector { + public static void open(Player player) { + List servers = new ArrayList<>(); + List targetServers = List.of("lobby-1", "lobby-2", "lobby-3", "lobby-4"); + + for (String server : targetServers) { + int online = Proxy.getPlayerCount(server); + servers.add(ItemBuilder.of(Material.BEACON) + .name("" + server + "") + .lore("Online Players: " + online + "", "Click to connect!") + .build()); + } + + PaginatedMenu menu = PaginatedMenu.builder() + .title("Lobby List (/)") + .rows(4) + .contentSlots(10, 11, 12, 13, 14, 15, 16) + .items(servers) + .onClick((context, item, index) -> { + String targetServer = targetServers.get(index); + player.sendMessage("Connecting to " + targetServer + "..."); + player.closeInventory(); + }) + .build(); + + menu.open(player); + } } +``` + +--- -PaginatedMenu menu = PaginatedMenu.builder() - .title("Rewards Selection (/)") - .rows(6) - // The slots where content items are populated - .contentSlots(10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25) - .items(items) - // Optional click handler for when a content item is clicked - .onClick((context, item, index) -> { - context.player().sendMessage("You clicked index " + index); - }) - .build(); +## Click Protection Safeguards -// Open the menu for a player -menu.open(player); -``` +To prevent GUI exploits, OumLib implements two safeguards internally: +1. **Auto-Cancellation**: Clicks on items inside the menu container are cancelled (`event.setCancelled(true)`) to prevent players from taking layout items. +2. **Player Inventory Isolation**: Clicks within the player's own inventory hotbar do not trigger GUI slot click handlers, preventing item duplication. diff --git a/docs/math.md b/docs/math.md new file mode 100644 index 0000000..1b8dfaf --- /dev/null +++ b/docs/math.md @@ -0,0 +1,129 @@ +# Mathematical Utilities + +OumLib features a platform-independent mathematical package `dev.oum.oumlib.math` optimized for 3D physics, spatial partitioning, expressions evaluation, and noise generation. + +--- + +## Real-world Example: Regional Claim Protection Zone + +Here is a protection system that maps safe-zone regions (Sphere, Cylinder, or AABB boxes) and checks if a player is standing inside the safe zone: + +```java +import dev.oum.oumlib.math.Vector3D; +import dev.oum.oumlib.math.Volume3D; +import dev.oum.oumlib.math.Volume3D.AABB3D; +import dev.oum.oumlib.math.Volume3D.Cylinder3D; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import java.util.ArrayList; +import java.util.List; + +public final class ProtectionZoneManager { + private final List safeZones = new ArrayList<>(); + + public void createZones() { + safeZones.add(new AABB3D(new Vector3D(-100, 0, -100), new Vector3D(100, 256, 100))); + safeZones.add(new Cylinder3D(new Vector3D(200, 64, 200), 15.0, 10.0)); + } + + public boolean isSafe(Player player) { + Location loc = player.getLocation(); + Vector3D pt = new Vector3D(loc.getX(), loc.getY(), loc.getZ()); + + for (Volume3D zone : safeZones) { + if (zone.contains(pt)) { + return true; + } + } + return false; + } +} +``` + +--- + +## Real-world Example: Dynamic Skill Damage Evaluation + +Evaluate dynamic math formulas loaded from config files (e.g. `base_dmg * (1.5 ^ level)`), replacing variables with active player levels dynamically: + +```java +import dev.oum.oumlib.math.MathEval; +import dev.oum.oumlib.text.Text; +import org.bukkit.entity.Player; +import java.util.Map; + +public final class SkillDamageEvaluator { + public void castSkill(Player player, String formula) { + MathEval eval = new MathEval(formula); + double dmg = eval.evaluate(Map.of( + "level", (double) player.getLevel(), + "base_dmg", 10.0 + )); + + Text.send(player, "You dealt " + dmg + " damage!"); + } +} +``` + +--- + +## Easing Animations + +Standard mathematical easing functions for UI displays: + +```java +import dev.oum.oumlib.math.Easing; + +public final class AnimationPlotter { + public double getProgress(double time) { + return Easing.BOUNCE_OUT.apply(time); + } +} +``` + +--- + +## Vector3D Reference + +Immutable Vector operations: + +```java +import dev.oum.oumlib.math.Vector3D; + +public final class VectorMath { + public void calculate() { + Vector3D v1 = new Vector3D(1.0, 2.0, 3.0); + Vector3D v2 = new Vector3D(4.0, 5.0, 6.0); + + Vector3D added = v1.add(v2); + Vector3D dot = v1.multiply(v2.normalize()); + } +} +``` + +--- + +## Loot Table Chance Rolles + +Determine loot rewards using weights: + +```java +import dev.oum.oumlib.math.Chance; +import dev.oum.oumlib.math.WeightedSelector; +import java.util.Map; + +public final class LootRoller { + public String rollReward() { + if (Chance.percent(5.0)) { + return "special_crate"; + } + + WeightedSelector selector = WeightedSelector.of(Map.of( + "gold", 70.0, + "diamond", 25.0, + "netherite", 5.0 + )); + return selector.select(); + } +} +``` diff --git a/docs/scheduler.md b/docs/scheduler.md index 19af7ba..2ed2680 100644 --- a/docs/scheduler.md +++ b/docs/scheduler.md @@ -1,156 +1,144 @@ # Scheduler System -OumLib features a platform-agnostic scheduler. It utilizes Java **Virtual Threads** for asynchronous execution and bridges to platform-specific tick loops for synchronous tasks. +OumLib features a platform-agnostic scheduler. It utilizes Java virtual threads for asynchronous execution and bridges to platform-specific tick loops for synchronous tasks. --- -## 1. Virtual Threads for Async Tasks +## Real-world Example: Profile Auto-Save Manager -All asynchronous tasks registered via `Scheduler.runAsync()` or `Scheduler.runRepeating()` run on **Java Virtual Threads** (`Thread.ofVirtual()`). -- Virtual threads are lightweight, managed by the JVM, and do not block expensive platform platform-worker threads. -- They allow you to perform blocking network operations, database queries, or disk writes without causing server lag or TPS drops. +Here is a background manager that schedules a repeating database save task running on virtual threads, ensuring that blocking database calls never interrupt server tick performance, and gracefully cleans up resources when the plugin disables: ```java -import dev.oum.oumlib.scheduler.Scheduler; - -Scheduler.runAsync(() -> { - // Perform database operations here - // Running this in a blocking way is safe on virtual threads! - db.saveUserData(); -}); -``` - ---- - -## 2. Sync Tick Scheduling on Paper - -For operations that must interact with Bukkit/Paper API directly (like spawning entities or modifying blocks), you must run on the server's main thread: - -- **`Scheduler.run(Runnable)`**: Schedules a task to execute on the next tick of the server's main thread. -- **`Scheduler.runLater(Duration, Runnable)`**: Schedules a task to execute after a specified delay on the server's main thread. -- **`Scheduler.runLater(long ticks, Runnable)`**: Schedules a task to execute after a specified amount of server ticks. - -> [!WARNING] -> The method `Scheduler.runDelayed(...)` is deprecated in `v1.0.4` and scheduled for removal in `v1.0.9`. Developers must migrate to `Scheduler.runLater(...)`. - -```java -import dev.oum.oumlib.scheduler.Scheduler; +import dev.oum.oumlib.database.Database; +import dev.oum.oumlib.scheduler.TaskGroup; +import org.bukkit.entity.Player; import org.bukkit.Bukkit; import java.time.Duration; -import net.kyori.adventure.text.minimessage.MiniMessage; +import java.util.ArrayList; +import java.util.List; -// Run tasks later using durations -Scheduler.runLater(Duration.ofSeconds(2), () -> { - Bukkit.broadcast(MiniMessage.miniMessage().deserialize("2 seconds elapsed!")); -}); - -// Or using ticks (e.g. 20 ticks = 1 second) -Scheduler.runLater(20L, () -> { - Bukkit.broadcast(MiniMessage.miniMessage().deserialize("1 second elapsed!")); -}); -``` - ---- +public final class ProfileAutoSaver { + private final TaskGroup taskGroup = new TaskGroup(); + private final Database db; -## 3. Folia Compatibility + public ProfileAutoSaver(Database db) { + this.db = db; + } -On **Folia**, tasks scheduled asynchronously (`runAsync`) are fully supported and execute on virtual threads. -- Since Folia uses multi-threaded regional tick loops instead of a single main thread, OumLib executes synchronous tasks (`runSync`) on Folia's global thread pool. -- For tick-precise regional operations (like block changes at a specific location or modifications to entities), you should use OumLib's region-aware scheduling methods, which transparently fall back to standard synchronous main thread execution on non-Folia platforms: - - `Scheduler.runAt(Location, Runnable)`: Schedules a task on the region corresponding to the location. - - `Scheduler.runFor(Entity, Runnable)`: Schedules a task on the region corresponding to the entity. + public void startSaveCycle() { + taskGroup.runRepeating(Duration.ofSeconds(60), Duration.ofSeconds(60), () -> { + List batchParams = new ArrayList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + int score = player.getLevel(); + batchParams.add(new Object[] { player.getUniqueId().toString(), score, score }); + } + + if (!batchParams.isEmpty()) { + db.executeBatch("INSERT INTO profiles (uuid, level) VALUES (?, ?) ON DUPLICATE KEY UPDATE level = ?", batchParams); + } + }); + } -These methods are also supported inside `TaskGroup`s for automatic lifecycle cleanup. + public void stop() { + taskGroup.cancelAll(); + } +} +``` --- -## 4. TaskGroups and Lifecycle Management +## Real-world Example: Combat Tag Transition (TaskChains) -To avoid leaking background tasks when your plugin reloads or disables, use a `TaskGroup` to register and manage tasks. +Teleport a player back to spawn after checking their combat status on the database and showing a visual countdown on the screen: ```java -import dev.oum.oumlib.scheduler.TaskGroup; +import dev.oum.oumlib.database.Database; +import dev.oum.oumlib.scheduler.TaskChain; +import dev.oum.oumlib.text.Text; +import org.bukkit.Location; +import org.bukkit.entity.Player; import java.time.Duration; -public class Announcer { +public final class SpawnTeleportHandler { + private final Database db; + private final Location spawnLocation; - private final TaskGroup taskGroup = new TaskGroup(); - - public void startAnnouncements() { - // Automatically registers the repeating task to this group - taskGroup.runRepeating(Duration.ZERO, Duration.ofSeconds(30), () -> { - System.out.println("Announcing server rules..."); - }); + public SpawnTeleportHandler(Database db, Location spawnLocation) { + this.db = db; + this.spawnLocation = spawnLocation; } - public void stop() { - // Instantly cancels all active repeating or delayed tasks in this group - taskGroup.cancelAll(); + public void initiateTeleport(Player player) { + TaskChain.create(player.getUniqueId().toString()) + .async(uuid -> { + var rows = db.executeQuery("SELECT combat_tagged FROM player_states WHERE uuid = ?", uuid).join(); + boolean tagged = !rows.isEmpty() && (int) rows.getFirst().get("combat_tagged") == 1; + if (tagged) { + throw new IllegalStateException("You are in combat!"); + } + return uuid; + }) + .sync(uuid -> { + Text.send(player, "Teleporting in 3 seconds..."); + return uuid; + }) + .delay(Duration.ofSeconds(3)) + .sync(uuid -> { + player.teleport(spawnLocation); + Text.send(player, "Teleported to spawn!"); + }) + .onException((uuid, ex) -> { + Text.send(player, "Teleport failed: " + ex.getMessage() + ""); + }) + .execute(); } } ``` -When your plugin disables, calling `taskGroup.cancelAll()` ensures all repeating loops stop immediately. --- -## 5. Promises and Callback Chaining - -OumLib provides a `Promise` wrapper around Java's `CompletableFuture` that simplifies asynchronous operations with callbacks targeted automatically at the correct server tick thread. +## Folia Compatibility -This is extremely useful for retrieving data (e.g., database or web queries) asynchronously, and then updating the game state or sending messages on the synchronous main thread safely: +Folia schedules synchronous tasks on its global thread pool. For tick-precise regional operations, use location or entity-aware methods, which fallback to standard main thread execution on non-Folia platforms: ```java import dev.oum.oumlib.scheduler.Scheduler; -import dev.oum.oumlib.scheduler.Promise; - -// 1. Fetch values asynchronously -Scheduler.supplyAsync(() -> database.loadUserCoins(uuid)) - // 2. Perform callback on the sync main tick thread safely - .thenAcceptSync(coins -> { - player.sendMessage("Loaded " + coins + " coins from database!"); - }); -``` - -### Virtual Threads (Java 21+) - -For heavy blocking I/O operations (like HTTP requests, Web APIs, or database queries), you can leverage Java 21 Virtual Threads using the virtual scheduler methods to keep thread overhead at a minimum: +import org.bukkit.Location; +import org.bukkit.entity.Entity; -```java -// 1. Fetch values asynchronously on a JVM Virtual Thread -Scheduler.supplyVirtual(() -> database.loadUserCoins(uuid)) - // 2. Perform callback on the sync main tick thread safely - .thenAcceptSync(coins -> { - player.sendMessage("Loaded " + coins + " coins via Virtual Threads!"); - }); +public final class RegionalScheduler { + public void executeRegionalAction(Location location, Entity entity, Runnable action) { + Scheduler.runAt(location, action); + Scheduler.runFor(entity, action); + } +} ``` --- -## 6. TaskChains (Sequential execution control) +## Promises and Callback Chaining -`TaskChain` allows developers to build a sequence of tasks that execute step-by-step, shifting back and forth between synchronous and asynchronous threads with custom delays. +Execute database queries asynchronously on virtual threads and pass the results to synchronous player interactions safely: ```java -import dev.oum.oumlib.scheduler.TaskChain; -import java.time.Duration; +import dev.oum.oumlib.database.Database; +import dev.oum.oumlib.scheduler.Scheduler; +import org.bukkit.entity.Player; -TaskChain.create("Initial Value") - // Step 1: Run asynchronously (e.g., fetch from DB or Web API) - .async(value -> { - return value + " -> Async Step"; - }) - // Step 2: Delay for 2 seconds - .delay(Duration.ofSeconds(2)) - // Step 3: Run on the main sync server thread - .sync(value -> { - player.sendMessage("Current chain status: " + value); - return value + " -> Sync Step"; - }) - // Step 4: Delay for another 10 server ticks - .delay(10) - // Step 5: Finish asynchronously - .async(value -> { - System.out.println("Chain finished: " + value); - }) - .execute(); +public final class ProfileLoader { + private final Database db; + + public ProfileLoader(Database db) { + this.db = db; + } + + public void loadBalance(Player player) { + Scheduler.supplyVirtual(() -> { + var rows = db.executeQuery("SELECT coins FROM economy WHERE uuid = ?", player.getUniqueId().toString()).join(); + return rows.isEmpty() ? 0 : (int) rows.getFirst().get("coins"); + }).thenAcceptSync(coins -> { + player.sendMessage("Balance: " + coins + " coins."); + }); + } +} ``` diff --git a/docs/setup.md b/docs/setup.md index ab075aa..7a74230 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,142 +1,119 @@ -# Main Setup & Initialization +# Setup & Initialization -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 -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. +OumLib is designed to be shaded and relocated directly into your plugin JAR. --- -## 1. Shared Plugin Mode (Recommended) +## Auto-Detection of Integrations + +During initialization, OumLib automatically scans the server's plugin environment and hooks into the following platforms if detected: +- **PlaceholderAPI (PAPI)**: Auto-registers OumLib placeholders into PAPI. +- **MiniPlaceholders**: Bridges OumLib placeholders into the MiniPlaceholders parsing context. + +--- -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. +## 1. Maven Dependency Configuration -### Maven Dependency -Declare `oumlib-core` with `provided` in your downstream plugin's `pom.xml`: +Add the JitPack repository and declare `oumlib-core` with `compile` scope in your plugin's `pom.xml`: ```xml com.github.sun-mc-dev.oumlib oumlib-core VERSION - provided + compile ``` -### 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 -``` +You must relocate OumLib inside your package space to prevent conflicts with other plugins using different versions of the library: -#### 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") - } -) +```xml + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.2 + + false + + + dev.oum.oumlib + your.plugin.package.libs.oumlib + + + + + + package + + shade + + + + + + ``` -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 +## 2. Bootstrapping OumLib (Paper / Spigot) -Initialize OumLib in your main class's `onEnable()` method, and call `shutdown()` in `onDisable()` to clean up background threads and scheduler tasks. +Initialize OumLib in your main class's `onEnable()` method, and call `shutdown()` in `onDisable()` to clean up scheduled tasks and databases: ```java -package dev.oum.example; - import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.text.Preset; import org.bukkit.plugin.java.JavaPlugin; -public final class PaperExamplePlugin extends JavaPlugin { - +public final class MyPlugin extends JavaPlugin { @Override public void onEnable() { - // Initialize OumLib for Paper. - // OumLib.init returns an InitBuilder for method chaining configuration. - OumLib.init(this); + OumLib.init(this) + .preset(Preset.INFO, "[MyPlugin] ") + .preset(Preset.SUCCESS, "[MyPlugin - Success] ") + .preset(Preset.ERROR, "[MyPlugin - Error] "); } @Override public void onDisable() { - // Shutdown all active background tasks, repeating loops, - // and threads created by the OumLib scheduler. OumLib.shutdown(); } } ``` -### 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. - -Example: -```java -OumLib.init(this) - .preset(Preset.INFO, "[Info] ") - .preset(Preset.SUCCESS, "[Success] ") - .preset(Preset.ERROR, "[Error] "); -``` - --- -### Velocity Setup +## 3. Bootstrapping OumLib (Velocity Proxy) -Initialize OumLib in your proxy plugin constructor or proxy initialization subscriber. +Initialize OumLib inside the proxy plugin constructor or initialization event: ```java -package dev.oum.example; - -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; +import com.google.inject.Inject; -@Plugin(id = "example-proxy", name = "ExampleProxy", version = "1.0.0") -public final class VelocityExamplePlugin { - +@Plugin(id = "my-proxy", name = "MyProxy", version = "1.0.0") +public final class VelocityProxyPlugin { private final ProxyServer server; @Inject - public VelocityExamplePlugin(ProxyServer server) { + public VelocityProxyPlugin(ProxyServer server) { this.server = server; } @Subscribe public void onProxyInitialization(ProxyInitializeEvent event) { - // Initialize OumLib for Velocity. OumLib.init(server, this); } @Subscribe public void onProxyShutdown(ProxyShutdownEvent event) { - // Stop all active background scheduler tasks. OumLib.shutdown(); } } @@ -144,29 +121,22 @@ public final class VelocityExamplePlugin { --- -## Platform-Independent Detection Utilities +## Platform 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: +To build multi-platform modules running across both Paper and Velocity, check 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 +public class PlatformDetector { + public void logPlatformInfo() { + if (OumLib.isPaper()) { + System.out.println("Executing on Paper/Folia Server platform"); + } else if (OumLib.isVelocity()) { + System.out.println("Executing on Velocity Proxy platform"); + } + } } ``` -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). -- Callbacks, scheduler mappings, and internal variables of the other plugins will overwrite each other, causing errors or silent failures. -- Shading relocation keeps your plugin's dependency sandbox isolated. - -> [!NOTE] -> All compile-scope dependencies utilized internally by OumLib (such as HikariCP) are automatically relocated to `dev.oum.oumlib.internal.hikari` when the OumLib jar is built. This ensures that downstream plugin developers only need to relocate the main `dev.oum.oumlib` package prefix (e.g. to `your.plugin.oumlib`) and all nested internal dependencies will automatically follow without needing manual relocation rules. +These checks are safe to call on either platform and do not raise class loading errors. diff --git a/docs/text.md b/docs/text.md index b981580..33024a2 100644 --- a/docs/text.md +++ b/docs/text.md @@ -1,210 +1,121 @@ -# Text & Placeholders +# Text & Placeholders System -OumLib handles chat and message rendering using Kyori Adventure's **MiniMessage** format. It includes default message styling presets and a custom placeholder registration system that bridges dynamically into PlaceholderAPI (PAPI) and MiniPlaceholders. +OumLib handles chat formatting and text rendering using Kyori Adventure's MiniMessage format. It includes default message styling presets and a custom placeholder registration system that bridges dynamically into PlaceholderAPI (PAPI) and MiniPlaceholders. --- -## 1. MiniMessage & Formatting +## Real-world Example: Chat Trivia Challenge -All text functions in OumLib parse MiniMessage format by default. You can use standard MiniMessage tags like ``, ``, and ``: +Here is a chat trivia challenge manager. It broadcasts localized question messages and opens a secure chat-capture session for the player using `TextInput`, validating answers and playing audio cues: ```java +import dev.oum.oumlib.effect.Effects; +import dev.oum.oumlib.text.Localization; import dev.oum.oumlib.text.Text; +import dev.oum.oumlib.text.TextInput; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import java.time.Duration; -Text.send(player, "Welcome! Click [here]."); -``` - ---- - -## 2. Text Presets - -OumLib defines standard text presets to ensure clean, consistent messaging styles across your plugin: -- **`Text.Preset.info(player, message)`**: Displays a standard informational message (typically gray/blue). -- **`Text.Preset.success(player, message)`**: Displays a success message (typically green). -- **`Text.Preset.error(player, message)`**: Displays an error message (typically red). - -You can configure the prefix for these presets during OumLib initialization: -```java -OumLib.init(this) - .preset(Preset.INFO, "[Info] ") - .preset(Preset.SUCCESS, "[Success] ") - .preset(Preset.ERROR, "[Error] "); -``` - ---- - -## 3. Placeholder System - -You can register custom placeholders under your plugin's identifier. - -### Dynamic Placeholder Mappings -When you register a placeholder (e.g. `coins` under the namespace `myplugin`): -1. **OumLib Internal Resolver**: Resolves `` inside OumLib text calls. -2. **PlaceholderAPI Bridge**: Automatically registers `%myplugin_coins%` in PlaceholderAPI. -3. **MiniPlaceholders Bridge**: Automatically registers `` in MiniPlaceholders. - -```java -import dev.oum.oumlib.OumLib; - -// Register player-specific coins placeholder -OumLib.placeholders("myplugin") - .add("coins", player -> { - // Since OumLib is cross-platform, player is passed as Object. - // Cast it to org.bukkit.entity.Player (Paper) or com.velocitypowered.api.proxy.Player (Velocity) - if (player instanceof org.bukkit.entity.Player p) { - return String.valueOf(p.getLevel() * 10); - } - return "0"; - }); +public final class TriviaChallengeManager { + public void startTrivia(Player player) { + player.sendMessage(Localization.translateFor(player, "trivia.start")); + + TextInput.builder() + .prompt(Localization.translateFor(player, "trivia.question")) + .timeout(Duration.ofSeconds(15)) + .cancelWord("quit") + .onTimeout(p -> { + Text.send(p, "Time's up! You failed the challenge."); + Effects.sound(Sound.BLOCK_ANVIL_LAND).volume(1.0F).pitch(1.0F).play(p); + }) + .onCancel(p -> { + Text.send(p, "Trivia session closed."); + }) + .onInput((p, answer) -> { + if (answer.equalsIgnoreCase("Minecraft")) { + Text.send(p, "Correct answer!"); + Effects.sound(Sound.ENTITY_PLAYER_LEVELUP).volume(1.0F).pitch(1.0F).play(p); + + p.sendMessage(Localization.translateFor(p, "trivia.completed", + Placeholder.parsed("score", "100") + )); + return true; + } + + Text.send(p, "Incorrect! Try again (or type 'quit'):"); + return false; + }) + .start(player); + } +} ``` ---- - -## 4. Manual Placeholder Resolution - -If you need to parse placeholders inside a raw string manually (handling both MiniMessage format and legacy PlaceholderAPI formats), use `Placeholders.resolve()`: - -```java -import dev.oum.oumlib.text.Placeholders; - -String parsed = Placeholders.resolve( - player, - "You have %myplugin_coins% coins remaining." -); +YAML translation resource file (`lang/en.yml`): +```yaml +trivia: + start: "[Trivia] A new challenge has started!" + question: "[Trivia] What game is this plugin written for?" + completed: "Challenge completed! Score added: " ``` -OumLib will first evaluate standard placeholders internally, and if PlaceholderAPI is enabled on the server, it will pass the text to PAPI for a secondary evaluation, resolving all server-wide placeholders. --- -## 5. Global Broadcasts & Platform-Agnostic Audiences +## Text Presets -Instead of manually fetching players from Paper/Velocity APIs to send messages, OumLib provides platform-agnostic methods to access all connected players or the console. - -### Platform-Agnostic Audiences -- **`OumLib.players()`**: Returns a unified Kyori `Audience` containing all online players on the server (Paper) or proxy (Velocity). -- **`OumLib.console()`**: Returns the console command sender/source `Audience`. - -### Global Broadcast Methods -You can broadcast MiniMessage formatted text to all online players dynamically: +OumLib defines standard text presets for consistent messaging styles: ```java import dev.oum.oumlib.text.Text; +import org.bukkit.entity.Player; -// 1. Simple text broadcast -Text.broadcast("The server is reloading in 5 minutes!"); - -// 2. Broadcast with placeholders -Text.broadcast("Winner is !", "winner", playerName); - -// 3. Broadcast with a Record data injector -Text.broadcast("Stats update: Joins: | Sent: ", myStatsRecord); - -// 4. Broadcast to action bars -Text.broadcastActionBar("Warning: System overload!"); - -// 5. Broadcast global titles -Text.broadcastTitle("VICTORY!", "Blue team won the match"); -``` - -### Broadcast Presets -You can also trigger styled broadcasts matching your registered preset styles: - -```java -// Sends success message with success prefix to all players -Text.Preset.successBroadcast("A global event has begun!"); - -// Sends error message with error prefix to all players -Text.Preset.errorBroadcast("The database connection was lost!"); +public class FeedbackSender { + public void sendFeedback(Player player) { + Text.Preset.info(player, "Your profile is loading."); + Text.Preset.success(player, "Coins added to balance."); + Text.Preset.error(player, "Payment rejected!"); + } +} ``` --- -## 6. Localization & Multi-Language (I18n) - -OumLib features a built-in localization (I18n) system that automatically loads, parses, and translates keys into localized `Component`s using MiniMessage format. It supports player client locale translation out-of-the-box on both Paper and Velocity. - -### Initializing the System - -To initialize, specify the default language code (e.g. `en`). OumLib will look inside the `lang/` subfolder in your plugin's data folder (and automatically extract the default lang file if it doesn't exist): - -```java -import dev.oum.oumlib.text.Localization; - -// Load translation files (e.g. lang/en.yml, lang/es.yml) -Localization.load("en"); -``` - -### Structuring Lang Files - -Language files use standard nested YAML, which OumLib flattens to dot-notation paths internally: - -```yaml -# lang/en.yml -general: - welcome: "Welcome back, !" - no-permission: "You do not have permission to do this." -``` - -### Translating Messages +## Placeholder Registration -You can translate using the default language, or fetch translations dynamically targeting a player's client language: +Register custom placeholders under your plugin's identifier namespace. Registered placeholders are automatically bridged into PAPI and MiniPlaceholders: ```java -import dev.oum.oumlib.text.Localization; -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import dev.oum.oumlib.OumLib; import org.bukkit.entity.Player; -Player player = ...; - -// 1. Send localized message matching player's Minecraft client locale settings (e.g. Spanish) -player.sendMessage(Localization.translateFor(player, "general.welcome", - Placeholder.parsed("player", player.getName()) -)); - -// 2. Fallback to default server translation -Component msg = Localization.translate("general.no-permission"); +public class LevelPlaceholderRegistry { + public void register() { + OumLib.placeholders("myplugin") + .add("level", player -> { + if (player instanceof Player p) { + return String.valueOf(p.getLevel()); + } + return "0"; + }); + } +} ``` --- -## 7. Asynchronous Chat-based Text Input (TextInput) - -`TextInput` provides a clean, async, and non-blocking way to capture text input from players directly via the standard server chat, without opening graphical inventory UIs like anvils. +## Global Broadcasts -It automatically intercepts the player's next chat message, cancels the chat event to prevent others from seeing the input, and passes it to your callback. It includes full session timeout handling, quit-listener cleanup, and customized cancellation keywords. +Broadcast MiniMessage-formatted text, action bars, or titles to all connected players or console: ```java -import dev.oum.oumlib.text.TextInput; -import net.kyori.adventure.text.minimessage.MiniMessage; -import java.time.Duration; +import dev.oum.oumlib.text.Text; -TextInput.builder() - // Define the prompt message sent to the player when the session starts - .prompt(MiniMessage.miniMessage().deserialize("Please type your new home name (or type 'cancel' to exit):")) - // Set a session timeout duration (defaults to 10s) - .timeout(Duration.ofSeconds(30)) - // Custom cancellation word (defaults to "cancel", case-insensitive) - .cancelWord("cancel") - // Triggered if the session times out - .onTimeout(player -> { - player.sendMessage("Home creation timed out."); - }) - // Triggered if the player cancels explicitly - .onCancel(player -> { - player.sendMessage("Home creation cancelled."); - }) - // Core input callback. Return true to accept and end the session, - // or false to reject (keeping the input session open for another attempt). - .onInput((player, message) -> { - if (message.length() < 3) { - player.sendMessage("Name is too short! Try again:"); - return false; - } - - homes.createHome(player, message); - player.sendMessage("Home '" + message + "' successfully created!"); - return true; - }) - .start(player); +public class AlertBroadcaster { + public void announceMaintenance() { + Text.broadcast("[Alert] Maintenance starts in 10 minutes!"); + Text.broadcastActionBar("Warning: Save progress now!"); + Text.broadcastTitle("MAINTENANCE", "Please finish transactions"); + } +} ``` - diff --git a/docs/utilities.md b/docs/utilities.md index 3c194ab..40b9a28 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1,499 +1,112 @@ # General Utilities -OumLib includes several practical helper classes to handle metadata, formatting, and location serialization without writing verbose boilerplate. +OumLib contains helpers to manage item serialization, Persistent Data Containers, countdowns, cooldowns, and server proxy routing. --- -## 1. Persistent Data Container (PDC) Helpers +## Real-world Example: PDC Inventory Backpack -Attaching custom metadata to items, entities, chunks, block states, and more is a core requirement for plugins. OumLib provides a modern, unified, and type-safe PDC API that operates on both `ItemStack` and any `PersistentDataHolder`. - -### Accessing the PDC Wrapper -Use `Pdc.of(item)` for items, or `Pdc.of(holder)` for entities, chunks, block states, etc. +Here is a system that serializes player backpack inventories into base64 and stores them directly inside the player's Persistent Data Container (PDC) using OumLib's helper classes, persisting them across relogs: ```java import dev.oum.oumlib.util.Pdc; -import org.bukkit.inventory.ItemStack; +import dev.oum.oumlib.util.ItemSerializer; import org.bukkit.entity.Player; -import java.util.List; - -ItemStack sword = ...; - -// Write values fluently -Pdc.of(sword) - .set("item-id", "fire_sword") - .set("custom-damage", 15) - .set("multiplier", 1.5) - .set("unlocked", true) - .set("tags", List.of("legendary", "fire")); - -// Read values (returns null/defaultValue if the key doesn't exist) -String itemId = Pdc.of(sword).get("item-id"); -int customDamage = Pdc.of(sword).getOrDefault("custom-damage", 0); -double multiplier = Pdc.of(sword).getOrDefault("multiplier", 1.0); -boolean unlocked = Pdc.of(sword).getOrDefault("unlocked", false); -List tags = Pdc.of(sword).getList("tags"); -``` - -### Namespaced PDC Contexts -You can scope keys under a specific namespace (e.g. your plugin name) to avoid conflicts: -```java -// Writes to namespaced keys: "myplugin:cooldown" and "myplugin:uses" -Pdc.of(sword).namespaced("myplugin") - .set("cooldown", 10) - .set("uses", 5); - -int cooldown = Pdc.of(sword).namespaced("myplugin").getOrDefault("cooldown", 0); -``` - -### PDC Change Listeners -You can register global listeners to execute logic whenever specific metadata changes on a player, block, or item: -```java -import org.bukkit.NamespacedKey; - -NamespacedKey key = new NamespacedKey("myplugin", "coins"); +import org.bukkit.inventory.ItemStack; -Pdc.registerListener(key, (holder, k, oldValue, newValue) -> { - if (holder instanceof Player player) { - player.sendMessage("Your coins updated from " + oldValue + " to " + newValue + "!"); +public final class PlayerBackpackSaver { + public void saveBackpack(Player player, ItemStack[] contents) { + String base64 = ItemSerializer.serializeArray(contents); + Pdc.of(player) + .namespaced("myplugin") + .set("backpack_contents", base64); + + player.sendMessage("Backpack saved successfully!"); } -}); -``` - -> [!WARNING] -> Legacy static methods like `Pdc.get(item, key)` or `Pdc.getInt(...)` are deprecated and scheduled for removal in `v1.0.5`. Please migrate to the fluent `Pdc.of(...)` API. ---- - -## 2. Formatting (Time & Numbers) - -Use the `Format` class to display clean durations, digital timers, and compact numbers to players: - -```java -import dev.oum.oumlib.util.Format; -import java.time.Duration; - -// 1. Durations (e.g., for cooldowns or match timers) -Format.duration(Duration.ofSeconds(125)); // "2m 5s" -Format.duration(Duration.ofSeconds(45)); // "45s" - -// 2. Digital Clocks (e.g., "02:05", "01:10:05") -Format.digitalTime(Duration.ofSeconds(125)); // "02:05" -Format.digitalTime(Duration.ofSeconds(3665)); // "01:01:05" - -// 3. Number Formatting with commas -Format.number(1250000); // "1,250,000" - -// 4. Compact Numbers (e.g., ELO stats or money balances) -Format.compactNumber(1500); // "1.5k" -Format.compactNumber(2500000); // "2.5M" -``` - ---- - -## 3. Location Serializers - -Saving locations to configs or databases is simplified using the `Locations` utility to convert Bukkit `Location` objects to and from database-friendly strings. - -```java -import dev.oum.oumlib.util.Locations; -import org.bukkit.Location; - -Location loc = player.getLocation(); - -// 1. Full serialization (world,x,y,z,yaw,pitch) -String serialized = Locations.serialize(loc); -// Output example: "world,120.5,64.0,-250.3,90.0,0.0" - -// 2. Block-only serialization (world,x,y,z) -String blockSerialized = Locations.serializeBlock(loc); -// Output example: "world,120,64,-251" - -// 3. Folia-compatible Region serialization (world,x,y,z,yaw,pitch,chunkX,chunkZ) -String regionSerialized = Locations.serializeRegion(loc); -// Output example: "world,120.5,64.0,-250.3,90.0,0.0,7,-16" - -// 4. Deserialization (handles both standard and region serialization formats) -Location parsedLoc = Locations.deserialize(serialized); -``` - ---- + public ItemStack[] loadBackpack(Player player) { + String base64 = Pdc.of(player) + .namespaced("myplugin") + .get("backpack_contents"); -## 4. Player Targeting & Raytracing (Paper-only) + if (base64 == null || base64.isEmpty()) { + return new ItemStack[0]; + } -Determining what block or entity a player is looking at is crucial for spells, guns, and interactive builds. The `Players` utility exposes clean wrappers using Bukkit's raytracing system: - -```java -import dev.oum.oumlib.util.Players; -import org.bukkit.block.Block; -import org.bukkit.entity.Entity; - -// 1. Get exact targeted block within 30 blocks -Block block = Players.getTargetBlock(player, 30); - -// 2. Get targeted entity within 50 blocks (automatically ignores the self player) -Entity entity = Players.getTargetEntity(player, 50); -``` - ---- - -## 5. Temporary BossBars - -Kyori's native BossBar API is powerful but requires manual scheduler tracking to clean up. OumLib makes creating self-expiring bossbars easy on both Paper and Velocity: - -> [!NOTE] -> The old utility class `dev.oum.oumlib.util.BossBars` is deprecated since `v1.0.1` and marked for removal in `v1.0.9`. Please use `Text.bossBar` or `Text.bossBarTemporary` instead. - -```java -import dev.oum.oumlib.text.Text; -import net.kyori.adventure.bossbar.BossBar; -import java.time.Duration; - -// Show a temporary BossBar that automatically vanishes after 10 seconds -Text.bossBarTemporary( - player, - "Danger Zone", - 1.0f, // progress (0.0 to 1.0) - BossBar.Color.RED, - BossBar.Overlay.PROGRESS, - Duration.ofSeconds(10) -); + return ItemSerializer.deserializeArray(base64); + } +} ``` --- -## 6. Proxy & Routing Utilities (Velocity-only) - -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 -Transfer players to registered backend servers: -```java -import dev.oum.oumlib.util.Proxy; -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 -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; - -// 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 & 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 or broadcast messages across all servers: -```java -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); -}); +## Real-world Example: Match Lobby Countdown -// 3. Broadcast to all backend servers -Proxy.broadcastPluginMessage("myplugin:broadcast", out -> { - out.writeUTF("global_shutdown"); -}); -``` +Play tick sounds and announce remaining seconds on the screen during a game start countdown, executing startup actions once finished: -### Dynamic Server Registration & Unregistration -Register or unregister backend Minecraft servers dynamically at runtime without restarting the proxy: ```java -// Register a new game server instance -Proxy.registerServer("games-3", "192.168.1.15", 25568); - -// Unregister a server when it shuts down -Proxy.unregisterServer("games-3"); -``` - -### 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!"); -}); - -// 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()); +import dev.oum.oumlib.util.Countdown; +import dev.oum.oumlib.effect.Sounds; +import org.bukkit.entity.Player; +import org.bukkit.Bukkit; + +public final class LobbyCountdownManager { + public void startLobbyCountdown(Player player) { + Countdown.builder(player, 10) + .displayMode(Countdown.Display.TITLE) + .format("Match starting in %duration%") + .tickSound(Sounds.TICK) + .onComplete(audience -> { + if (audience instanceof Player p) { + p.sendMessage("Game started!"); + Sounds.play(p, "entity.generic.explode", 1.0f, 1.0f); + } + }) + .start(); } -}); - -// 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 -> { - player.sendMessage("Connecting to best server: " + server.getServerInfo().getName()); - player.createConnectionRequest(server).fireAndForget(); - }); - }); +} ``` --- -## 7. Standalone Cooldowns +## Standalone Cooldown System -A standalone cooldown system mapping `UUID`s to durations, completely independent of command contexts. +A standalone map-based cooldown timer: ```java import dev.oum.oumlib.util.Cooldown; import java.time.Duration; +import java.util.UUID; -// Create a cooldown that lasts for 5 seconds -Cooldown speedCooldown = Cooldown.of(Duration.ofSeconds(5)); +public final class SkillManager { + private final Cooldown fireboltCooldown = Cooldown.of(Duration.ofSeconds(8)); -if (speedCooldown.isOnCooldown(player.getUniqueId())) { - player.sendMessage("Remaining time: " + speedCooldown.remainingSeconds(player.getUniqueId()) + "s"); -} else { - // Activate speed boost... - speedCooldown.set(player.getUniqueId()); -} -``` + public boolean useFirebolt(UUID playerUuid) { + if (fireboltCooldown.isOnCooldown(playerUuid)) { + return false; + } ---- - -## 8. PlayerData Persistence (Paper-only) - -> [!WARNING] -> The `dev.oum.oumlib.util.PlayerData` helper wrapper is deprecated in `v1.0.3` and scheduled for removal in `v1.0.9`. Developers must migrate to the modern and unified `Pdc.of(player)` API. - -### Migration Example: -```java -import dev.oum.oumlib.util.Pdc; - -// Set player-bound persistent values using Pdc.of -Pdc.of(player) - .set("rank", "MVP") - .set("coins", 500) - .set("claimed-reward", true); - -// Get values -String rank = Pdc.of(player).getOrDefault("rank", "Default"); -int coins = Pdc.of(player).getOrDefault("coins", 0); -boolean claimed = Pdc.of(player).getOrDefault("claimed-reward", false); -``` - ---- - -## 9. Permission Builder - -A cross-platform permission checker and builder that automatically registers permissions on Paper/Bukkit with default permission states. - -```java -import dev.oum.oumlib.util.Permission; - -Permission adminPerm = Permission.builder("myplugin.admin") - .description("Allows admin commands access.") - .defaultValue(Permission.Default.OP) // Auto-registers with OP default on Paper - .build(); - -if (adminPerm.has(sender)) { - // Perform admin actions... + fireboltCooldown.set(playerUuid); + return true; + } } ``` --- -## 10. Base64 Item Serializer - -OumLib features `ItemSerializer` to serialize and deserialize single `ItemStack` instances or `ItemStack[]` arrays to safe Base64 strings. It automatically uses component-aware modern Paper API serialization if running on Paper 1.20.6+, and falls back to standard object streams on legacy platforms. - -### Serializing a single item: -```java -import dev.oum.oumlib.util.ItemSerializer; -import org.bukkit.inventory.ItemStack; - -ItemStack sword = ...; -String base64 = ItemSerializer.serialize(sword); +## Duration & Digital Clock Formats -// Deserializing back: -ItemStack restored = ItemSerializer.deserialize(base64); -``` +Format times into readable clocks or text spans: -### Serializing an array of items (e.g. inventories): ```java -ItemStack[] inventory = player.getInventory().getContents(); -String base64 = ItemSerializer.serializeArray(inventory); - -// Deserializing back: -ItemStack[] restoredInventory = ItemSerializer.deserializeArray(base64); -``` - -### Fluent Persistent Data Container (PDC) Integration: -You can read and write `ItemStack` and `ItemStack[]` values directly via `Pdc` helper objects without manual serialization: -```java -import dev.oum.oumlib.util.Pdc; - -// Store an item directly in the player's PDC -Pdc.of(player).setItem("saved-sword", sword); - -// Retrieve the item later -ItemStack restored = Pdc.of(player).getItem("saved-sword"); - -// Store or retrieve an entire item array (e.g. vaults/inventories) -Pdc.of(player).setItemArray("backpack", inventory); -ItemStack[] restoredBackpack = Pdc.of(player).getItemArray("backpack"); -``` - ---- - -## 11. Fluent Effects & Particle Player - -OumLib features a builder-based effects system (`Effects`) to spawn particles and play sounds with volume, pitch, velocity, colors, and specific target groups fluently. - -### Spawning Particles: -```java -import dev.oum.oumlib.effect.Effects; -import org.bukkit.Particle; -import org.bukkit.Color; - -// Spawn standard particle at a location -Effects.particle(Particle.HAPPY_VILLAGER) - .count(15) - .speed(0.1) - .offset(0.5, 0.5, 0.5) - .spawn(location); - -// Spawn a colorized dust particle -Effects.particle(Particle.DUST) - .color(Color.RED, 1.2f) - .spawn(location); - -// Spawn particles only visible to a specific player -Effects.particle(Particle.FLAME) - .spawn(player, location); -``` - -### Playing Sounds: -```java -import dev.oum.oumlib.effect.Effects; -import org.bukkit.Sound; -import org.bukkit.SoundCategory; - -// Play a sound with random pitch variance (e.g. 1.0 +/- 0.2) -Effects.sound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP) - .category(SoundCategory.PLAYERS) - .volume(0.8f) - .pitch(1.0f) - .pitchVariance(0.2f) - .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 dev.oum.oumlib.util.Format; 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. +public final class FormatPrinter { + public void printStats() { + String timeRemaining = Format.duration(Duration.ofSeconds(95)); + String clockFace = Format.digitalTime(Duration.ofSeconds(3665)); + String scoreCommas = Format.number(5000000); + } +} ``` - diff --git a/docs/web.md b/docs/web.md index 09ba631..429b94d 100644 --- a/docs/web.md +++ b/docs/web.md @@ -1,112 +1,56 @@ # Web & Discord Webhooks -OumLib includes a built-in, lightweight, asynchronous Discord Webhook client that relies on Java's standard `HttpClient` and a custom, zero-reflection JSON builder. This allows plugins to log matches, alerts, and announcements directly to Discord without causing server tick lag. +OumLib includes a built-in, lightweight, asynchronous Discord Webhook client that relies on Java's standard `HttpClient` and a custom, zero-reflection JSON builder. --- -## 1. Asynchronous Discord Webhook Client +## Real-world Example: Anti-Cheat Alert Dispatcher -The Webhook builder makes it simple to construct and dispatch standard Discord webhooks. Since the network requests are executed asynchronously on a background thread pool, they are safe to use on both Paper (including Folia) and Velocity. - -### Simple Text Message -To send a plain text message: - -```java -import dev.oum.oumlib.web.Webhook; - -Webhook.url("https://discord.com/api/webhooks/...") - .username("Server Logger") - .avatarUrl("https://my-icon-url.png") - .content("The server has successfully started up!") - .sendAsync() - .whenComplete((result, exception) -> { - if (exception != null) { - System.err.println("Failed to send webhook: " + exception.getMessage()); - } - }); -``` - ---- - -## 2. Rich Discord Embeds - -For structured logs, you can build rich Discord Embeds featuring custom titles, colors, fields, footers, authors, and images. - -### Example: Match Logger Embed +Here is an anti-cheat logging manager that queues and batches player alerts in the background. If 20 players flag checks at the same time, OumLib batches them into combined embeds sent every second, preventing Discord API rate limit blocks: ```java import dev.oum.oumlib.web.Webhook; import dev.oum.oumlib.web.WebhookEmbed; - -Webhook.url("https://discord.com/api/webhooks/...") - .username("CoinFlip Logger") - .avatarUrl("https://my-avatar-url.png") - .embed(embed -> embed - .title("CoinFlip Match Concluded") - .description("maceful won **$100.00** against sun_mc in a coinflip!") - .color(0xFFAA00) // Hex or decimal color code - .thumbnail("https://my-coin-image.png") - .author("CoinFlip Logs", null, null) - .field("Winner", "maceful", true) - .field("Loser", "sun_mc", true) - .field("Wager", "$50.00", true) - .footer("CoinFlip • Match Logs", null) - ) - .sendAsync() - .whenComplete(null, ex -> { - System.err.println("Failed to send log to Discord: " + ex.getMessage()); - }); -``` - -### Advanced: Custom WebhookEmbed Objects -If you prefer to separate the construction of the embed from the webhook dispatcher, you can build a reusable `WebhookEmbed` object: - -```java -import dev.oum.oumlib.web.WebhookEmbed; - -WebhookEmbed matchEmbed = WebhookEmbed.builder() - .title("Leaderboard Update") - .description("A new player has entered the top leaderboard!") - .color(0x55FF55) - .field("Player", "rompepitas", false) - .field("Wins", "150", true) - .field("Win Rate", "74.5%", true) - .footer("Leaderboards Tracker", null) - .build(); - -Webhook.url("https://discord.com/api/webhooks/...") - .username("Leaderboards") - .embed(matchEmbed) - .sendAsync(); +import org.bukkit.entity.Player; + +public final class AntiCheatDiscordLogger { + private final String webhookUrl; + + public AntiCheatDiscordLogger(String webhookUrl) { + this.webhookUrl = webhookUrl; + } + + public void logDetection(Player player, String hackType, double violationLevel) { + WebhookEmbed alertEmbed = WebhookEmbed.builder() + .title("Security Alert") + .description("Player **" + player.getName() + "** failed security verification checks.") + .color(0xFF3333) + .field("Violator", player.getName(), true) + .field("Detection", hackType, true) + .field("VL Score", String.valueOf(violationLevel), true) + .footer("AntiCheat Security Daemon", null) + .build(); + + Webhook.queueEmbed(webhookUrl, alertEmbed); + } +} ``` --- -## 3. Webhook Queuing & Rate Limiting +## Simple Discord Webhook Sending -To prevent rate limiting when dispatching multiple logs in rapid succession (e.g. chat logging, block tracking), OumLib features a built-in message and embed buffering system. Messages/embeds are queued, consolidated, and sent in batch batches of up to 10 embeds or merged multiline text blocks every second. +Send standard Discord webhooks asynchronously: -### Queuing Text Messages: ```java import dev.oum.oumlib.web.Webhook; -String webhookUrl = "https://discord.com/api/webhooks/..."; - -// These will be grouped together and sent as a single combined message -Webhook.queueMessage(webhookUrl, "[Log] Player sun_mc joined the game."); -Webhook.queueMessage(webhookUrl, "[Log] Player rompepitas joined the game."); -``` - -### Queuing Embeds: -```java -import dev.oum.oumlib.web.Webhook; -import dev.oum.oumlib.web.WebhookEmbed; - -WebhookEmbed alert = WebhookEmbed.builder() - .title("Anti-Cheat Alert") - .description("rompepitas flagged Killaura") - .build(); - -// Queues the embed to be batched and sent automatically in the background -Webhook.queueEmbed(webhookUrl, alert); +public final class StatusNotifier { + public void notifyStartup(String url) { + Webhook.url(url) + .username("Status Monitor") + .content("Plugin successfully initialized on server startup.") + .sendAsync(); + } +} ``` diff --git a/example-plugin/pom.xml b/example-plugin/pom.xml index e76b51e..1e99ae4 100644 --- a/example-plugin/pom.xml +++ b/example-plugin/pom.xml @@ -8,7 +8,7 @@ dev.oum oumlib - 1.0.7 + 1.0.8 ../pom.xml @@ -20,7 +20,7 @@ dev.oum oumlib-core - 1.0.7 + 1.0.8 compile diff --git a/oumlib-core/pom.xml b/oumlib-core/pom.xml index 2e5ba4e..799f92c 100644 --- a/oumlib-core/pom.xml +++ b/oumlib-core/pom.xml @@ -8,7 +8,7 @@ dev.oum oumlib - 1.0.7 + 1.0.8 ../pom.xml 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 418ca24..c0bc943 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java @@ -68,9 +68,8 @@ private OumLib() { BukkitSchedulerAdapter.initialize(p); Scheduler.initialize(BukkitSchedulerAdapter.get()); - p.getServer().getMessenger().registerIncomingPluginChannel(p, "oumlib:autocomplete", (channel, player, message) -> { - handleSpigotAutocompleteMessage(player, message); - }); + p.getServer().getMessenger().registerIncomingPluginChannel(p, "oumlib:autocomplete", + (channel, player, message) -> handleSpigotAutocompleteMessage(player, message)); p.getServer().getMessenger().registerOutgoingPluginChannel(p, "oumlib:autocomplete"); detectIntegrations(p); 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 f3973cb..05e1a0f 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,6 +1,7 @@ package dev.oum.oumlib.bridge.permission; import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.scheduler.Promise; import net.luckperms.api.LuckPerms; import net.luckperms.api.LuckPermsProvider; import net.luckperms.api.model.user.User; @@ -27,6 +28,18 @@ interface PermissionHandler { List getGroups(UUID uuid); void setGroups(UUID uuid, List groups, String primaryGroup); + + Promise getPrimaryGroupAsync(UUID uuid); + + Promise getPrefixAsync(UUID uuid); + + Promise getSuffixAsync(UUID uuid); + + Promise getMetaValueAsync(UUID uuid, String key); + + Promise> getGroupsAsync(UUID uuid); + + Promise setGroupsAsync(UUID uuid, List groups, String primaryGroup); } public final class PermissionBridge { @@ -109,11 +122,45 @@ public static void setGroups(@NonNull UUID uuid, @NonNull List groups, @ h.setGroups(uuid, groups, primaryGroup); } } + + public static @Nullable Promise getPrimaryGroupAsync(@NonNull UUID uuid) { + PermissionHandler h = getHandler(); + return h != null ? h.getPrimaryGroupAsync(uuid) : null; + } + + public static @Nullable Promise getPrefixAsync(@NonNull UUID uuid) { + PermissionHandler h = getHandler(); + return h != null ? h.getPrefixAsync(uuid) : null; + } + + public static @Nullable Promise getSuffixAsync(@NonNull UUID uuid) { + PermissionHandler h = getHandler(); + return h != null ? h.getSuffixAsync(uuid) : null; + } + + public static @Nullable Promise getMetaValueAsync(@NonNull UUID uuid, @NonNull String key) { + PermissionHandler h = getHandler(); + return h != null ? h.getMetaValueAsync(uuid, key) : null; + } + + public static @Nullable Promise> getGroupsAsync(@NonNull UUID uuid) { + PermissionHandler h = getHandler(); + return h != null ? h.getGroupsAsync(uuid) : null; + } + + public static @Nullable Promise setGroupsAsync(@NonNull UUID uuid, @NonNull List groups, @Nullable String primaryGroup) { + PermissionHandler h = getHandler(); + return h != null ? h.setGroupsAsync(uuid, groups, primaryGroup) : null; + } } class LuckPermsHandler implements PermissionHandler { private final LuckPerms api = LuckPermsProvider.get(); + private Promise loadUser(UUID uuid) { + return Promise.fromCompletableFuture(api.getUserManager().loadUser(uuid)); + } + @Override public String getPrimaryGroup(UUID uuid) { User user = api.getUserManager().getUser(uuid); @@ -168,4 +215,52 @@ public void setGroups(UUID uuid, List groups, String primaryGroup) { api.getUserManager().saveUser(user); }); } + + @Override + public Promise getPrimaryGroupAsync(UUID uuid) { + return loadUser(uuid).map(User::getPrimaryGroup); + } + + @Override + public Promise getPrefixAsync(UUID uuid) { + return loadUser(uuid).map(user -> user.getCachedData().getMetaData().getPrefix()); + } + + @Override + public Promise getSuffixAsync(UUID uuid) { + return loadUser(uuid).map(user -> user.getCachedData().getMetaData().getSuffix()); + } + + @Override + public Promise getMetaValueAsync(UUID uuid, String key) { + return loadUser(uuid).map(user -> user.getCachedData().getMetaData().getMetaValue(key)); + } + + @Override + public Promise> getGroupsAsync(UUID uuid) { + return loadUser(uuid).map(user -> { + List groups = new ArrayList<>(); + for (Node node : user.getNodes()) { + if (node instanceof InheritanceNode inheritanceNode) { + groups.add(inheritanceNode.getGroupName()); + } + } + return groups; + }); + } + + @Override + public Promise setGroupsAsync(UUID uuid, List groups, String primaryGroup) { + return loadUser(uuid).map(user -> { + 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); + return null; + }); + } } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/config/ConfigManager.java b/oumlib-core/src/main/java/dev/oum/oumlib/config/ConfigManager.java index 7695b4a..a5a83cd 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/config/ConfigManager.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/config/ConfigManager.java @@ -1,6 +1,8 @@ package dev.oum.oumlib.config; import dev.oum.oumlib.OumLib; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; @@ -47,6 +49,10 @@ private static Object convertValue(Object val, Class targetType) { if (val == null) return null; if (targetType.isInstance(val)) return val; + if (targetType == Component.class) { + return MiniMessage.miniMessage().deserialize(String.valueOf(val)); + } + if (targetType == boolean.class || targetType == Boolean.class) { if (val instanceof Boolean b) return b; if (val instanceof String s) return Boolean.parseBoolean(s); @@ -199,8 +205,12 @@ private static boolean isMissingKey(Map yamlMap, Class target } private static String toYamlValue(Object value) { - if (value instanceof String s) return "\"" + s.replace("\"", "\\\"") + "\""; if (value == null) return "null"; + if (value instanceof Component comp) { + String s = MiniMessage.miniMessage().serialize(comp); + return "\"" + s.replace("\"", "\\\"") + "\""; + } + if (value instanceof String s) return "\"" + s.replace("\"", "\\\"") + "\""; return String.valueOf(value); } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/effect/Particles.java b/oumlib-core/src/main/java/dev/oum/oumlib/effect/Particles.java index dcdd669..247d214 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/effect/Particles.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/effect/Particles.java @@ -1,11 +1,16 @@ package dev.oum.oumlib.effect; +import dev.oum.oumlib.math.Geometry3D; +import dev.oum.oumlib.math.Vector3D; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.Particle; +import org.bukkit.World; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import java.util.List; + public final class Particles { private Particles() { @@ -59,4 +64,43 @@ public static void spawnDustTransition(@NonNull Location loc, @NonNull Color fro public static void spawnDustTransition(@NonNull Location loc, @NonNull Color fromColor, @NonNull Color toColor, float size, int count, double offsetX, double offsetY, double offsetZ) { spawn(loc, Particle.DUST_COLOR_TRANSITION, count, offsetX, offsetY, offsetZ, 1, new Particle.DustTransition(fromColor, toColor, size)); } + + public static void spawnLine(@NonNull Location start, @NonNull Location end, + @NonNull ParticleBuilder builder, int segments) { + Vector3D startV = Vector3D.fromLocation(start); + Vector3D endV = Vector3D.fromLocation(end); + World world = start.getWorld(); + if (world == null) return; + for (int i = 0; i <= segments; i++) { + Vector3D pt = startV.lerp(endV, (double) i / segments); + builder.spawn(pt.toLocation(world)); + } + } + + public static void spawnBezier(@NonNull Location start, @NonNull Location control1, + @NonNull Location control2, @NonNull Location end, + @NonNull ParticleBuilder builder, int segments) { + Vector3D startV = Vector3D.fromLocation(start); + Vector3D c1 = Vector3D.fromLocation(control1); + Vector3D c2 = Vector3D.fromLocation(control2); + Vector3D endV = Vector3D.fromLocation(end); + List points = Geometry3D.bezier(startV, c1, c2, endV, segments); + World world = start.getWorld(); + if (world == null) return; + for (Vector3D pt : points) { + builder.spawn(pt.toLocation(world)); + } + } + + public static void spawnHelix(@NonNull Location center, double radius, + double pitch, double height, int steps, + @NonNull ParticleBuilder builder) { + Vector3D centerV = Vector3D.fromLocation(center); + List points = Geometry3D.helix(centerV, radius, pitch, height, steps); + World world = center.getWorld(); + if (world == null) return; + for (Vector3D pt : points) { + builder.spawn(pt.toLocation(world)); + } + } } \ No newline at end of file diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/effect/SoundBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/effect/SoundBuilder.java index 738f33c..4be4b47 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/effect/SoundBuilder.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/effect/SoundBuilder.java @@ -1,7 +1,10 @@ package dev.oum.oumlib.effect; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound.Source; import org.bukkit.Location; -import org.bukkit.Sound; +import org.bukkit.Registry; import org.bukkit.SoundCategory; import org.bukkit.entity.Player; import org.jspecify.annotations.NonNull; @@ -10,18 +13,35 @@ import java.util.concurrent.ThreadLocalRandom; public final class SoundBuilder { - private final Sound sound; - private SoundCategory category = SoundCategory.MASTER; + private final Key soundKey; + private Source source = Source.MASTER; private float volume = 1.0f; private float pitch = 1.0f; private float pitchRange = 0.0f; - public SoundBuilder(@NonNull Sound sound) { - this.sound = sound; + public SoundBuilder(@NonNull Key soundKey) { + this.soundKey = soundKey; + } + + public SoundBuilder(net.kyori.adventure.sound.Sound.@NonNull Type soundType) { + this.soundKey = soundType.key(); + } + + public SoundBuilder(org.bukkit.@NonNull Sound sound) { + this.soundKey = Registry.SOUNDS.getKey(sound); + } + + public @NonNull SoundBuilder source(@NonNull Source source) { + this.source = source; + return this; } public @NonNull SoundBuilder category(@NonNull SoundCategory category) { - this.category = category; + try { + this.source = Source.valueOf(category.name()); + } catch (IllegalArgumentException e) { + this.source = Source.MASTER; + } return this; } @@ -47,24 +67,58 @@ private float calculatePitch() { return (float) ThreadLocalRandom.current().nextDouble(min, max); } + public net.kyori.adventure.sound.@NonNull Sound build() { + return net.kyori.adventure.sound.Sound.sound(soundKey, source, volume, calculatePitch()); + } + + public void play(@NonNull Audience audience) { + audience.playSound(build()); + } + + public void play(@NonNull Audience audience, double x, double y, double z) { + audience.playSound(build(), x, y, z); + } + public void play(@NonNull Location location) { if (location.getWorld() != null) { - location.getWorld().playSound(location, sound, category, volume, calculatePitch()); + SoundCategory cat; + try { + cat = SoundCategory.valueOf(source.name()); + } catch (Exception e) { + cat = SoundCategory.MASTER; + } + location.getWorld().playSound(location, soundKey.asString(), cat, volume, calculatePitch()); } } public void play(@NonNull Player player) { - player.playSound(player.getLocation(), sound, category, volume, calculatePitch()); + play((Audience) player); } public void play(@NonNull Player player, @NonNull Location location) { - player.playSound(location, sound, category, volume, calculatePitch()); + if (player.getWorld().equals(location.getWorld())) { + SoundCategory cat; + try { + cat = SoundCategory.valueOf(source.name()); + } catch (Exception e) { + cat = SoundCategory.MASTER; + } + player.playSound(location, soundKey.asString(), cat, volume, calculatePitch()); + } } public void play(@NonNull Collection players, @NonNull Location location) { float finalPitch = calculatePitch(); + SoundCategory cat; + try { + cat = SoundCategory.valueOf(source.name()); + } catch (Exception e) { + cat = SoundCategory.MASTER; + } for (Player p : players) { - p.playSound(location, sound, category, volume, finalPitch); + if (p.getWorld().equals(location.getWorld())) { + p.playSound(location, soundKey.asString(), cat, volume, finalPitch); + } } } } 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 bf0a2ca..4ed8163 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 @@ -23,9 +23,9 @@ import org.jspecify.annotations.NonNull; import java.util.ArrayList; -import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -39,9 +39,9 @@ public final class AnvilMenu implements Menu { private final String placeholder; private final BiConsumer onConfirm; private final Consumer onClose; - private final Map open = new HashMap<>(); - private final Map originalLevels = new HashMap<>(); - private final Map originalExp = new HashMap<>(); + private final Map open = new ConcurrentHashMap<>(); + private final Map originalLevels = new ConcurrentHashMap<>(); + private final Map originalExp = new ConcurrentHashMap<>(); private ListenerHandle clickHandle; private ListenerHandle closeHandle; @@ -63,29 +63,33 @@ private AnvilMenu(@NonNull Builder builder) { @Override public void open(@NonNull Player player) { - Inventory inv = player.getServer().createInventory(player, InventoryType.ANVIL, MM.deserialize(title)); - inv.setItem(0, ItemBuilder.of(Material.PAPER).name(placeholder).build()); - - originalLevels.put(player.getUniqueId(), player.getLevel()); - originalExp.put(player.getUniqueId(), player.getExp()); - if (player.getLevel() < 1) { - player.setLevel(1); - player.setExp(0.0f); - } + Scheduler.runFor(player, () -> { + Inventory inv = player.getServer().createInventory(player, InventoryType.ANVIL, MM.deserialize(title)); + inv.setItem(0, ItemBuilder.of(Material.PAPER).name(placeholder).build()); + + originalLevels.put(player.getUniqueId(), player.getLevel()); + originalExp.put(player.getUniqueId(), player.getExp()); + if (player.getLevel() < 1) { + player.setLevel(1); + player.setExp(0.0f); + } - open.put(player.getUniqueId(), inv); - registerListeners(); - player.openInventory(inv); + open.put(player.getUniqueId(), inv); + registerListeners(); + player.openInventory(inv); + }); } @Override public void close(@NonNull Player player) { - open.remove(player.getUniqueId()); - restoreExperience(player); - player.closeInventory(); - if (open.isEmpty()) { - unregisterListeners(); - } + Scheduler.runFor(player, () -> { + open.remove(player.getUniqueId()); + restoreExperience(player); + player.closeInventory(); + if (open.isEmpty()) { + unregisterListeners(); + } + }); } private void restoreExperience(@NonNull Player player) { 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 093a3a2..70b9291 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 @@ -5,6 +5,7 @@ import dev.oum.oumlib.scheduler.Scheduler; import dev.oum.oumlib.scheduler.TaskHandle; import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -39,7 +40,7 @@ public final class ChestMenu implements Menu { private final Map> playerStates = new ConcurrentHashMap<>(); private final List unlockedSlots; private final Consumer onClose; - private final Map open = new HashMap<>(); + private final Map open = new ConcurrentHashMap<>(); private final Sound openSound; private final Sound closeSound; private final Sound clickSound; @@ -77,87 +78,102 @@ private ChestMenu(@NonNull Builder builder) { return state != null ? (T) state.get(key) : null; } - @SuppressWarnings("deprecation") public void updateState(@NonNull Player player, @NonNull String key, @Nullable Object value) { - Map state = playerStates.get(player.getUniqueId()); - if (state != null) { - state.put(key, value); - String resolvedTitle = title; - for (Map.Entry entry : state.entrySet()) { - resolvedTitle = resolvedTitle.replace("{" + entry.getKey() + "}", String.valueOf(entry.getValue())); - } - try { - player.getOpenInventory().setTitle(resolvedTitle); - } catch (Throwable ignored) { + Scheduler.runFor(player, () -> { + Map state = playerStates.get(player.getUniqueId()); + if (state != null) { + state.put(key, value); + String resolvedTitle = title; + for (Map.Entry entry : state.entrySet()) { + resolvedTitle = resolvedTitle.replace("{" + entry.getKey() + "}", String.valueOf(entry.getValue())); + } + try { + var view = player.getOpenInventory(); + try { + var method = view.getClass().getMethod("setTitle", Component.class); + method.invoke(view, MM.deserialize(resolvedTitle)); + } catch (NoSuchMethodException e) { + view.setTitle(resolvedTitle); + } + } catch (Throwable ignored) { + } + refresh(player); } - refresh(player); - } + }); } @Override 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))); - return map; - }); + Scheduler.runFor(player, () -> { + Map state = playerStates.computeIfAbsent(player.getUniqueId(), uuid -> { + Map map = new HashMap<>(); + stateProviders.forEach((key, provider) -> map.put(key, provider.apply(player))); + return map; + }); - String resolvedTitle = title; - for (Map.Entry entry : state.entrySet()) { - resolvedTitle = resolvedTitle.replace("{" + entry.getKey() + "}", String.valueOf(entry.getValue())); - } + String resolvedTitle = title; + for (Map.Entry entry : state.entrySet()) { + resolvedTitle = resolvedTitle.replace("{" + entry.getKey() + "}", String.valueOf(entry.getValue())); + } - Inventory inv = Bukkit.createInventory(null, rows * 9, MM.deserialize(resolvedTitle)); - if (layout != null) layout.apply(inv, player); - slotItems.forEach((slot, function) -> inv.setItem(slot, function.apply(player))); - open.put(player.getUniqueId(), inv); - registerListeners(); - player.openInventory(inv); - 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); + Inventory inv = Bukkit.createInventory(null, rows * 9, MM.deserialize(resolvedTitle)); + if (layout != null) layout.apply(inv, player); + slotItems.forEach((slot, function) -> inv.setItem(slot, function.apply(player))); + open.put(player.getUniqueId(), inv); + registerListeners(); + player.openInventory(inv); + 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 public void close(@NonNull Player player) { - open.remove(player.getUniqueId()); - playerStates.remove(player.getUniqueId()); - player.closeInventory(); - if (closeSound != null) { - player.playSound(closeSound); - } - if (open.isEmpty()) { - unregisterListeners(); - if (refreshTask != null) { - refreshTask.cancel(); - refreshTask = null; + Scheduler.runFor(player, () -> { + open.remove(player.getUniqueId()); + playerStates.remove(player.getUniqueId()); + player.closeInventory(); + if (closeSound != null) { + player.playSound(closeSound); } - } + if (open.isEmpty()) { + unregisterListeners(); + if (refreshTask != null) { + refreshTask.cancel(); + refreshTask = null; + } + } + }); } public void refresh(@NonNull Player player) { - Inventory inv = open.get(player.getUniqueId()); - if (inv == null) return; - if (layout != null) layout.apply(inv, player); - slotItems.forEach((slot, function) -> inv.setItem(slot, function.apply(player))); - player.updateInventory(); + Scheduler.runFor(player, () -> { + Inventory inv = open.get(player.getUniqueId()); + if (inv == null) return; + if (layout != null) layout.apply(inv, player); + slotItems.forEach((slot, function) -> inv.setItem(slot, function.apply(player))); + player.updateInventory(); + }); } public void setItem(@NonNull Player player, int slot, ItemStack item) { - Inventory inv = open.get(player.getUniqueId()); - if (inv == null) return; - inv.setItem(slot, item); - player.updateInventory(); + Scheduler.runFor(player, () -> { + Inventory inv = open.get(player.getUniqueId()); + if (inv == null) return; + inv.setItem(slot, item); + player.updateInventory(); + }); } private @Nullable ItemStack moveItemToUnlockedSlots(Inventory inv, @NonNull ItemStack toMove) { 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 493a5ec..4cb4d59 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 @@ -26,7 +26,6 @@ import org.jetbrains.annotations.Nullable; import org.jspecify.annotations.NonNull; -import java.lang.reflect.Field; import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -284,6 +283,17 @@ private ItemBuilder(@NonNull ItemStack item) { return this; } + @CheckReturnValue + public @NonNull ItemBuilder pdc(@NonNull String key, @Nullable Component value) { + NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); + if (value == null) { + meta.getPersistentDataContainer().remove(nsk); + } else { + meta.getPersistentDataContainer().set(nsk, PersistentDataType.STRING, MM.serialize(value)); + } + return this; + } + @CheckReturnValue @SuppressWarnings("unused") public @NonNull ItemBuilder skull(@NonNull OfflinePlayer player) { @@ -325,22 +335,7 @@ private ItemBuilder(@NonNull ItemStack item) { textures.setSkin(url); profile.setTextures(textures); skullMeta.setPlayerProfile(profile); - } catch (Throwable t) { - try { - Class gameProfileClass = Class.forName("com.mojang.authlib.GameProfile"); - Class propertyClass = Class.forName("com.mojang.authlib.properties.Property"); - Object profile = gameProfileClass.getConstructor(UUID.class, String.class) - .newInstance(UUID.randomUUID(), null); - Object property = propertyClass.getConstructor(String.class, String.class) - .newInstance("textures", textureValue); - Object properties = gameProfileClass.getMethod("getProperties").invoke(profile); - properties.getClass().getMethod("put", Object.class, Object.class).invoke(properties, "textures", property); - - Field profileField = skullMeta.getClass().getDeclaredField("profile"); - profileField.setAccessible(true); - profileField.set(skullMeta, profile); - } catch (Throwable ignored) { - } + } catch (Throwable ignored) { } } return this; 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 1c2e6e2..1f53887 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 @@ -2,6 +2,7 @@ import dev.oum.oumlib.event.Events; import dev.oum.oumlib.event.ListenerHandle; +import dev.oum.oumlib.scheduler.Scheduler; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; @@ -16,7 +17,11 @@ import org.jetbrains.annotations.Nullable; import org.jspecify.annotations.NonNull; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; public final class PaginatedMenu implements Menu { @@ -32,8 +37,8 @@ public final class PaginatedMenu implements Menu { private final Function nextButton; private final Function> itemsSupplier; private final PaginatedClickHandler clickHandler; - private final Map pages = new HashMap<>(); - private final Map open = new HashMap<>(); + private final Map pages = new ConcurrentHashMap<>(); + private final Map open = new ConcurrentHashMap<>(); private ListenerHandle clickHandle; private ListenerHandle closeHandle; @@ -72,57 +77,64 @@ public int totalPages(@Nullable Player player) { @Override public void open(@NonNull Player player) { - pages.putIfAbsent(player.getUniqueId(), 1); - registerListeners(); - reopen(player); + Scheduler.runFor(player, () -> { + pages.putIfAbsent(player.getUniqueId(), 1); + registerListeners(); + reopen(player); + }); } @Override public void close(@NonNull Player player) { - open.remove(player.getUniqueId()); - pages.remove(player.getUniqueId()); - player.closeInventory(); - if (open.isEmpty()) { - unregisterListeners(); - } + Scheduler.runFor(player, () -> { + open.remove(player.getUniqueId()); + pages.remove(player.getUniqueId()); + player.closeInventory(); + if (open.isEmpty()) { + unregisterListeners(); + } + }); } @SuppressWarnings("unused") public void refresh(@NonNull Player player) { - Inventory inv = open.get(player.getUniqueId()); - if (inv == null) return; - populateItems(player, inv, pages.getOrDefault(player.getUniqueId(), 1)); - player.updateInventory(); + Scheduler.runFor(player, () -> { + Inventory inv = open.get(player.getUniqueId()); + if (inv == null) return; + populateItems(player, inv, pages.getOrDefault(player.getUniqueId(), 1)); + player.updateInventory(); + }); } - @SuppressWarnings("deprecation") private void reopen(@NonNull Player player) { - int page = pages.getOrDefault(player.getUniqueId(), 1); - String resolvedTitle = title - .replace("", String.valueOf(page)) - .replace("", String.valueOf(totalPages(player))); - var titleComponent = MM.deserialize(resolvedTitle); - - Inventory inv = open.get(player.getUniqueId()); - if (inv != null) { - try { - var view = player.getOpenInventory(); + Scheduler.runFor(player, () -> { + int page = pages.getOrDefault(player.getUniqueId(), 1); + String resolvedTitle = title + .replace("", String.valueOf(page)) + .replace("", String.valueOf(totalPages(player))); + var titleComponent = MM.deserialize(resolvedTitle); + + Inventory inv = open.get(player.getUniqueId()); + if (inv != null) { try { - var method = view.getClass().getMethod("setTitle", Component.class); - method.invoke(view, titleComponent); - } catch (NoSuchMethodException e) { - view.setTitle(resolvedTitle); + var view = player.getOpenInventory(); + try { + var method = view.getClass().getMethod("setTitle", Component.class); + method.invoke(view, titleComponent); + } catch (NoSuchMethodException e) { + view.setTitle(resolvedTitle); + } + } catch (Exception ignored) { } - } catch (Exception ignored) { + populateItems(player, inv, page); + player.updateInventory(); + } else { + inv = Bukkit.createInventory(null, rows * 9, titleComponent); + populateItems(player, inv, page); + open.put(player.getUniqueId(), inv); + player.openInventory(inv); } - populateItems(player, inv, page); - player.updateInventory(); - } else { - inv = Bukkit.createInventory(null, rows * 9, titleComponent); - populateItems(player, inv, page); - open.put(player.getUniqueId(), inv); - player.openInventory(inv); - } + }); } private void populateItems(@NonNull Player player, @NonNull Inventory inv, int page) { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/Chance.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Chance.java new file mode 100644 index 0000000..c5882a3 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Chance.java @@ -0,0 +1,35 @@ +package dev.oum.oumlib.math; + +import org.jetbrains.annotations.Contract; + +import java.util.concurrent.ThreadLocalRandom; + +public final class Chance { + + private Chance() { + } + + @Contract + public static boolean test(double probability) { + if (probability <= 0.0) return false; + if (probability >= 1.0) return true; + return ThreadLocalRandom.current().nextDouble() < probability; + } + + @Contract + public static boolean testPercent(double percentage) { + return test(percentage / 100.0); + } + + @Contract + public static double randomIn(double min, double max) { + if (min >= max) return min; + return min + (max - min) * ThreadLocalRandom.current().nextDouble(); + } + + @Contract + public static int randomIn(int min, int max) { + if (min >= max) return min; + return ThreadLocalRandom.current().nextInt(min, max + 1); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/Easing.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Easing.java new file mode 100644 index 0000000..4a193bc --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Easing.java @@ -0,0 +1,125 @@ +package dev.oum.oumlib.math; + +import org.jetbrains.annotations.Contract; + +public enum Easing { + + LINEAR { + @Override + public double apply(double t) { + return t; + } + }, + SINE_IN { + @Override + public double apply(double t) { + return 1.0 - Math.cos(t * Math.PI * 0.5); + } + }, + SINE_OUT { + @Override + public double apply(double t) { + return Math.sin(t * Math.PI * 0.5); + } + }, + SINE_IN_OUT { + @Override + public double apply(double t) { + return -(Math.cos(Math.PI * t) - 1.0) * 0.5; + } + }, + QUAD_IN { + @Override + public double apply(double t) { + return t * t; + } + }, + QUAD_OUT { + @Override + public double apply(double t) { + return t * (2.0 - t); + } + }, + QUAD_IN_OUT { + @Override + public double apply(double t) { + return t < 0.5 ? 2.0 * t * t : -1.0 + (4.0 - 2.0 * t) * t; + } + }, + CUBIC_IN { + @Override + public double apply(double t) { + return t * t * t; + } + }, + CUBIC_OUT { + @Override + public double apply(double t) { + double f = t - 1.0; + return f * f * f + 1.0; + } + }, + CUBIC_IN_OUT { + @Override + public double apply(double t) { + return t < 0.5 ? 4.0 * t * t * t : (t - 1.0) * (2.0 * t - 2.0) * (2.0 * t - 2.0) + 1.0; + } + }, + EXP_IN { + @Override + public double apply(double t) { + return t == 0.0 ? 0.0 : Math.pow(2.0, 10.0 * (t - 1.0)); + } + }, + EXP_OUT { + @Override + public double apply(double t) { + return t == 1.0 ? 1.0 : 1.0 - Math.pow(2.0, -10.0 * t); + } + }, + EXP_IN_OUT { + @Override + public double apply(double t) { + if (t == 0.0) return 0.0; + if (t == 1.0) return 1.0; + if ((t *= 2.0) < 1.0) return 0.5 * Math.pow(2.0, 10.0 * (t - 1.0)); + return 0.5 * (2.0 - Math.pow(2.0, -10.0 * (t - 1.0))); + } + }, + BOUNCE_OUT { + @Override + public double apply(double t) { + double n1 = 7.5625; + double d1 = 2.75; + if (t < 1.0 / d1) { + return n1 * t * t; + } else if (t < 2.0 / d1) { + double nt = t - 1.5 / d1; + return n1 * nt * nt + 0.75; + } else if (t < 2.5 / d1) { + double nt = t - 2.25 / d1; + return n1 * nt * nt + 0.9375; + } else { + double nt = t - 2.625 / d1; + return n1 * nt * nt + 0.984375; + } + } + }, + BOUNCE_IN { + @Override + public double apply(double t) { + return 1.0 - BOUNCE_OUT.apply(1.0 - t); + } + }, + BOUNCE_IN_OUT { + @Override + public double apply(double t) { + return t < 0.5 + ? 0.5 * BOUNCE_IN.apply(t * 2.0) + : 0.5 * BOUNCE_OUT.apply(t * 2.0 - 1.0) + 0.5; + } + }; + + @Contract(pure = true) + public abstract double apply(double t); +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/FastMath.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/FastMath.java new file mode 100644 index 0000000..bf90a76 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/FastMath.java @@ -0,0 +1,59 @@ +package dev.oum.oumlib.math; + +import org.jetbrains.annotations.Contract; + +public final class FastMath { + + private FastMath() { + } + + @Contract(pure = true) + public static double safeDivide(double numerator, double denominator, double fallback) { + if (denominator == 0.0 || Double.isNaN(denominator) || Double.isInfinite(denominator)) { + return fallback; + } + double result = numerator / denominator; + if (Double.isNaN(result) || Double.isInfinite(result)) { + return fallback; + } + return result; + } + + @Contract(pure = true) + public static double clamp(double val, double min, double max) { + if (val < min) return min; + if (val > max) return max; + return val; + } + + @Contract(pure = true) + public static int clamp(int val, int min, int max) { + if (val < min) return min; + if (val > max) return max; + return val; + } + + @Contract(pure = true) + public static double map(double val, double fromMin, double fromMax, double toMin, double toMax) { + if (fromMax - fromMin == 0.0) return toMin; + return toMin + (val - fromMin) / (fromMax - fromMin) * (toMax - toMin); + } + + @Contract(pure = true) + public static double sin(double x) { + x = x % (2.0 * Math.PI); + if (x < -Math.PI) x += 2.0 * Math.PI; + else if (x > Math.PI) x -= 2.0 * Math.PI; + if (x < 0.0) { + double nx = -x; + return -16.0 * nx * (Math.PI - nx) / (5.0 * Math.PI * Math.PI - 4.0 * nx * (Math.PI - nx)); + } else { + return 16.0 * x * (Math.PI - x) / (5.0 * Math.PI * Math.PI - 4.0 * x * (Math.PI - x)); + } + } + + @Contract(pure = true) + public static double cos(double x) { + return sin(x + Math.PI * 0.5); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/FormatMath.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/FormatMath.java new file mode 100644 index 0000000..9b6e233 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/FormatMath.java @@ -0,0 +1,67 @@ +package dev.oum.oumlib.math; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.Locale; +import java.util.TreeMap; + +public final class FormatMath { + + private static final TreeMap ROMAN_NUMERALS = new TreeMap<>(); + private static final String[] SUFFIXES = {"", "k", "M", "B", "T", "Q"}; + + static { + ROMAN_NUMERALS.put(1000, "M"); + ROMAN_NUMERALS.put(900, "CM"); + ROMAN_NUMERALS.put(500, "D"); + ROMAN_NUMERALS.put(400, "CD"); + ROMAN_NUMERALS.put(100, "C"); + ROMAN_NUMERALS.put(90, "XC"); + ROMAN_NUMERALS.put(50, "L"); + ROMAN_NUMERALS.put(40, "XL"); + ROMAN_NUMERALS.put(10, "X"); + ROMAN_NUMERALS.put(9, "IX"); + ROMAN_NUMERALS.put(5, "V"); + ROMAN_NUMERALS.put(4, "IV"); + ROMAN_NUMERALS.put(1, "I"); + } + + private FormatMath() { + } + + @Contract(pure = true) + public static @NonNull String compact(double number) { + if (Double.isNaN(number) || Double.isInfinite(number)) return "0"; + boolean negative = number < 0; + double absValue = Math.abs(number); + if (absValue < 1000.0) { + return (negative ? "-" : "") + formatDecimal(absValue); + } + int index = 0; + while (absValue >= 1000.0 && index < SUFFIXES.length - 1) { + absValue /= 1000.0; + index++; + } + return (negative ? "-" : "") + formatDecimal(absValue) + SUFFIXES[index]; + } + + private static String formatDecimal(double val) { + if (val == (long) val) { + return String.format("%d", (long) val); + } + return String.format(Locale.US, "%.2f", val) + .replaceAll("0+$", "") + .replaceAll("\\.$", ""); + } + + @Contract(pure = true) + public static @NonNull String toRoman(int number) { + if (number <= 0 || number > 3999) return String.valueOf(number); + int l = ROMAN_NUMERALS.floorKey(number); + if (number == l) { + return ROMAN_NUMERALS.get(number); + } + return ROMAN_NUMERALS.get(l) + toRoman(number - l); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/Geometry3D.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Geometry3D.java new file mode 100644 index 0000000..76135c4 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Geometry3D.java @@ -0,0 +1,74 @@ +package dev.oum.oumlib.math; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; + +public final class Geometry3D { + + private Geometry3D() { + } + + @Contract(value = "_, _, _, _, _ -> new", pure = true) + public static @NonNull List bezier(@NonNull Vector3D start, @NonNull Vector3D control1, + @NonNull Vector3D control2, @NonNull Vector3D end, int segments) { + List points = new ArrayList<>(); + if (segments <= 0) { + points.add(start); + points.add(end); + return points; + } + for (int i = 0; i <= segments; i++) { + double t = (double) i / segments; + double u = 1.0 - t; + double tt = t * t; + double uu = u * u; + double uuu = uu * u; + double ttt = tt * t; + double x = uuu * start.x() + 3.0 * uu * t * control1.x() + 3.0 * u * tt * control2.x() + ttt * end.x(); + double y = uuu * start.y() + 3.0 * uu * t * control1.y() + 3.0 * u * tt * control2.y() + ttt * end.y(); + double z = uuu * start.z() + 3.0 * uu * t * control1.z() + 3.0 * u * tt * control2.z() + ttt * end.z(); + points.add(new Vector3D(x, y, z)); + } + return points; + } + + @Contract(value = "_, _, _, _, _ -> new", pure = true) + public static @NonNull List helix(@NonNull Vector3D center, double radius, + double pitch, double height, int steps) { + List points = new ArrayList<>(); + if (steps <= 0) return points; + for (int i = 0; i < steps; i++) { + double fraction = (double) i / steps; + double angle = fraction * height / pitch * 2.0 * Math.PI; + double yOffset = fraction * height; + double x = center.x() + radius * Math.cos(angle); + double z = center.z() + radius * Math.sin(angle); + double y = center.y() + yOffset; + points.add(new Vector3D(x, y, z)); + } + return points; + } + + @Contract(pure = true) + public static boolean intersects(@NonNull Vector3D rayOrigin, @NonNull Vector3D rayDirection, + @NonNull Vector3D minBound, @NonNull Vector3D maxBound) { + Vector3D dirFraction = new Vector3D( + rayDirection.x() == 0.0 ? Double.MAX_VALUE : 1.0 / rayDirection.x(), + rayDirection.y() == 0.0 ? Double.MAX_VALUE : 1.0 / rayDirection.y(), + rayDirection.z() == 0.0 ? Double.MAX_VALUE : 1.0 / rayDirection.z() + ); + double t1 = (minBound.x() - rayOrigin.x()) * dirFraction.x(); + double t2 = (maxBound.x() - rayOrigin.x()) * dirFraction.x(); + double t3 = (minBound.y() - rayOrigin.y()) * dirFraction.y(); + double t4 = (maxBound.y() - rayOrigin.y()) * dirFraction.y(); + double t5 = (minBound.z() - rayOrigin.z()) * dirFraction.z(); + double t6 = (maxBound.z() - rayOrigin.z()) * dirFraction.z(); + double tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6)); + double tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6)); + if (tmax < 0.0) return false; + return tmin <= tmax; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/MathEval.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/MathEval.java new file mode 100644 index 0000000..6102449 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/MathEval.java @@ -0,0 +1,132 @@ +package dev.oum.oumlib.math; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.Map; + +public final class MathEval { + + private MathEval() { + } + + @Contract(pure = true) + public static double eval(@NonNull String expr) { + return eval(expr, Map.of()); + } + + @Contract(pure = true) + public static double eval(@NonNull String expr, @NonNull Map variables) { + return new Parser(expr, variables).parse(); + } + + private static class Parser { + private final String str; + private final Map vars; + private int pos = -1; + private int ch; + + Parser(String str, Map vars) { + this.str = str; + this.vars = vars; + } + + void nextChar() { + ch = (++pos < str.length()) ? str.charAt(pos) : -1; + } + + boolean eat(int charToEat) { + while (ch == ' ') nextChar(); + if (ch == charToEat) { + nextChar(); + return true; + } + return false; + } + + double parse() { + nextChar(); + double x = parseExpression(); + if (pos < str.length()) throw new IllegalArgumentException("Unexpected character: " + (char) ch); + return x; + } + + double parseExpression() { + double x = parseTerm(); + for (; ; ) { + if (eat('+')) x += parseTerm(); + else if (eat('-')) x -= parseTerm(); + else return x; + } + } + + double parseTerm() { + double x = parseFactor(); + for (; ; ) { + if (eat('*')) x *= parseFactor(); + else if (eat('/')) { + double divisor = parseFactor(); + x = divisor == 0.0 ? 0.0 : x / divisor; + } else if (eat('%')) { + double divisor = parseFactor(); + x = divisor == 0.0 ? 0.0 : x % divisor; + } else return x; + } + } + + double parseFactor() { + if (eat('+')) return parseFactor(); + if (eat('-')) return -parseFactor(); + + double x; + int startPos = this.pos; + if (eat('(')) { + x = parseExpression(); + eat(')'); + } else if ((ch >= '0' && ch <= '9') || ch == '.') { + while ((ch >= '0' && ch <= '9') || ch == '.') nextChar(); + x = Double.parseDouble(str.substring(startPos, this.pos)); + } else if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch == '_') { + while (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9' || ch == '_') + nextChar(); + String name = str.substring(startPos, this.pos); + if (eat('(')) { + double arg = parseExpression(); + if (name.equals("sin")) x = Math.sin(arg); + else if (name.equals("cos")) x = Math.cos(arg); + else if (name.equals("tan")) x = Math.tan(arg); + else if (name.equals("sqrt")) x = Math.sqrt(arg); + else if (name.equals("abs")) x = Math.abs(arg); + else if (name.equals("round")) x = Math.round(arg); + else if (name.equals("floor")) x = Math.floor(arg); + else if (name.equals("ceil")) x = Math.ceil(arg); + else if (name.equals("min") || name.equals("max") || name.equals("clamp")) { + if (eat(',')) { + double arg2 = parseExpression(); + if (name.equals("min")) x = Math.min(arg, arg2); + else if (name.equals("max")) x = Math.max(arg, arg2); + else { + eat(','); + double arg3 = parseExpression(); + x = Math.max(arg2, Math.min(arg3, arg)); + } + } else { + throw new IllegalArgumentException("Function " + name + " expects multiple arguments"); + } + } else { + throw new IllegalArgumentException("Unknown function: " + name); + } + eat(')'); + } else { + x = vars.getOrDefault(name, 0.0); + } + } else { + throw new IllegalArgumentException("Unexpected character: " + (char) ch); + } + + if (eat('^')) x = Math.pow(x, parseFactor()); + + return x; + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/Matrix3.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Matrix3.java new file mode 100644 index 0000000..cb393cf --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Matrix3.java @@ -0,0 +1,75 @@ +package dev.oum.oumlib.math; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +public record Matrix3( + double m00, double m01, double m02, + double m10, double m11, double m12, + double m20, double m21, double m22 +) { + + public static final Matrix3 IDENTITY = new Matrix3( + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0 + ); + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Matrix3 rotationX(double angle) { + double c = Math.cos(angle); + double s = Math.sin(angle); + return new Matrix3( + 1.0, 0.0, 0.0, + 0.0, c, -s, + 0.0, s, c + ); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Matrix3 rotationY(double angle) { + double c = Math.cos(angle); + double s = Math.sin(angle); + return new Matrix3( + c, 0.0, s, + 0.0, 1.0, 0.0, + -s, 0.0, c + ); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Matrix3 rotationZ(double angle) { + double c = Math.cos(angle); + double s = Math.sin(angle); + return new Matrix3( + c, -s, 0.0, + s, c, 0.0, + 0.0, 0.0, 1.0 + ); + } + + @Contract(value = "_, _ -> new", pure = true) + public static @NonNull Matrix3 rotationAroundAxis(@NonNull Vector3D axis, double angle) { + double c = Math.cos(angle); + double s = Math.sin(angle); + double t = 1.0 - c; + Vector3D norm = axis.normalize(); + double x = norm.x(); + double y = norm.y(); + double z = norm.z(); + return new Matrix3( + t * x * x + c, t * x * y - s * z, t * x * z + s * y, + t * x * y + s * z, t * y * y + c, t * y * z - s * x, + t * x * z - s * y, t * y * z + s * x, t * z * z + c + ); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector3D transform(@NonNull Vector3D vec) { + return new Vector3D( + m00 * vec.x() + m01 * vec.y() + m02 * vec.z(), + m10 * vec.x() + m11 * vec.y() + m12 * vec.z(), + m20 * vec.x() + m21 * vec.y() + m22 * vec.z() + ); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/Noise.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Noise.java new file mode 100644 index 0000000..a5657fe --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Noise.java @@ -0,0 +1,213 @@ +package dev.oum.oumlib.math; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +public final class Noise { + + private static final int[] p = { + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, + 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, + 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, + 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, + 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, + 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, + 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, + 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, + 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, + 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, + 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, + 218, 246, 147, 87, 1, 139, 0, 49, 128, 64, 121, 67, 77, 124, 1, 234, + 142, 73, 144, 176, 244, 66, 228, 181, 156, 106, 81, 132, 28, 39, 63, 152, + 32, 114, 181, 248, 124, 110, 75, 122, 137, 253, 144, 157, 126, 84, 204, 176, + 145, 214, 140, 226, 250, 16, 123, 176, 174, 145, 134, 148, 254, 108, 7, 84 + }; + + private static final int[] perm = new int[512]; + private static final int[] permMod12 = new int[512]; + private static final double F2 = 0.5 * (Math.sqrt(3.0) - 1.0); + private static final double G2 = (3.0 - Math.sqrt(3.0)) / 6.0; + private static final double F3 = 1.0 / 3.0; + private static final double G3 = 1.0 / 6.0; + private static final int[][] grad3 = { + {1, 1, 0}, {-1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, + {1, 0, 1}, {-1, 0, 1}, {1, 0, -1}, {-1, 0, -1}, + {0, 1, 1}, {0, -1, 1}, {0, 1, -1}, {0, -1, -1} + }; + + static { + for (int i = 0; i < 512; i++) { + perm[i] = p[i & 255]; + permMod12[i] = perm[i] % 12; + } + } + + private Noise() { + } + + @Contract(pure = true) + private static double dot(int @NonNull [] g, double x, double y) { + return g[0] * x + g[1] * y; + } + + @Contract(pure = true) + private static double dot(int @NonNull [] g, double x, double y, double z) { + return g[0] * x + g[1] * y + g[2] * z; + } + + @Contract(pure = true) + public static double simplex2D(double xin, double yin) { + double n0, n1, n2; + double s = (xin + yin) * F2; + int i = (int) Math.floor(xin + s); + int j = (int) Math.floor(yin + s); + double t = (i + j) * G2; + double X0 = i - t; + double Y0 = j - t; + double x0 = xin - X0; + double y0 = yin - Y0; + int i1, j1; + if (x0 > y0) { + i1 = 1; + j1 = 0; + } else { + i1 = 0; + j1 = 1; + } + double x1 = x0 - i1 + G2; + double y1 = y0 - j1 + G2; + double x2 = x0 - 1.0 + 2.0 * G2; + double y2 = y0 - 1.0 + 2.0 * G2; + int ii = i & 255; + int jj = j & 255; + int gi0 = permMod12[ii + perm[jj]]; + int gi1 = permMod12[ii + i1 + perm[jj + j1]]; + int gi2 = permMod12[ii + 1 + perm[jj + 1]]; + double t0 = 0.5 - x0 * x0 - y0 * y0; + if (t0 < 0) n0 = 0.0; + else { + t0 *= t0; + n0 = t0 * t0 * dot(grad3[gi0], x0, y0); + } + double t1 = 0.5 - x1 * x1 - y1 * y1; + if (t1 < 0) n1 = 0.0; + else { + t1 *= t1; + n1 = t1 * t1 * dot(grad3[gi1], x1, y1); + } + double t2 = 0.5 - x2 * x2 - y2 * y2; + if (t2 < 0) n2 = 0.0; + else { + t2 *= t2; + n2 = t2 * t2 * dot(grad3[gi2], x2, y2); + } + return 70.0 * (n0 + n1 + n2); + } + + @Contract(pure = true) + public static double simplex3D(double xin, double yin, double zin) { + double n0, n1, n2, n3; + double s = (xin + yin + zin) * F3; + int i = (int) Math.floor(xin + s); + int j = (int) Math.floor(yin + s); + int k = (int) Math.floor(zin + s); + double t = (i + j + k) * G3; + double X0 = i - t; + double Y0 = j - t; + double Z0 = k - t; + double x0 = xin - X0; + double y0 = yin - Y0; + double z0 = zin - Z0; + int i1, j1, k1; + int i2, j2, k2; + if (x0 >= y0) { + if (y0 >= z0) { + i1 = 1; + j1 = 0; + k1 = 0; + i2 = 1; + j2 = 1; + k2 = 0; + } else if (x0 >= z0) { + i1 = 1; + j1 = 0; + k1 = 0; + i2 = 1; + j2 = 0; + k2 = 1; + } else { + i1 = 0; + j1 = 0; + k1 = 1; + i2 = 1; + j2 = 0; + k2 = 1; + } + } else { + if (y0 < z0) { + i1 = 0; + j1 = 0; + k1 = 1; + i2 = 0; + j2 = 1; + k2 = 1; + } else if (x0 < z0) { + i1 = 0; + j1 = 1; + k1 = 0; + i2 = 0; + j2 = 1; + k2 = 1; + } else { + i1 = 0; + j1 = 1; + k1 = 0; + i2 = 1; + j2 = 1; + k2 = 0; + } + } + double x1 = x0 - i1 + G3; + double y1 = y0 - j1 + G3; + double z1 = z0 - k1 + G3; + double x2 = x0 - i2 + 2.0 * G3; + double y2 = y0 - j2 + 2.0 * G3; + double z2 = z0 - k2 + 2.0 * G3; + double x3 = x0 - 1.0 + 3.0 * G3; + double y3 = y0 - 1.0 + 3.0 * G3; + double z3 = z0 - 1.0 + 3.0 * G3; + int ii = i & 255; + int jj = j & 255; + int kk = k & 255; + int gi0 = permMod12[ii + perm[jj + perm[kk]]]; + int gi1 = permMod12[ii + i1 + perm[jj + j1 + perm[kk + k1]]]; + int gi2 = permMod12[ii + i2 + perm[jj + j2 + perm[kk + k2]]]; + int gi3 = permMod12[ii + 1 + perm[jj + 1 + perm[kk + 1]]]; + double t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0; + if (t0 < 0) n0 = 0.0; + else { + t0 *= t0; + n0 = t0 * t0 * dot(grad3[gi0], x0, y0, z0); + } + double t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1; + if (t1 < 0) n1 = 0.0; + else { + t1 *= t1; + n1 = t1 * t1 * dot(grad3[gi1], x1, y1, z1); + } + double t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2; + if (t2 < 0) n2 = 0.0; + else { + t2 *= t2; + n2 = t2 * t2 * dot(grad3[gi2], x2, y2, z2); + } + double t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3; + if (t3 < 0) n3 = 0.0; + else { + t3 *= t3; + n3 = t3 * t3 * dot(grad3[gi3], x3, y3, z3); + } + return 32.0 * (n0 + n1 + n2 + n3); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/Quaternion.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Quaternion.java new file mode 100644 index 0000000..b475355 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Quaternion.java @@ -0,0 +1,158 @@ +package dev.oum.oumlib.math; + +import org.bukkit.util.EulerAngle; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public record Quaternion(double x, double y, double z, double w) { + + public static final Quaternion IDENTITY = new Quaternion(0.0, 0.0, 0.0, 1.0); + + @Contract(value = "_, _ -> new", pure = true) + public static @NonNull Quaternion fromAxisAngle(@NonNull Vector3D axis, double angle) { + double halfAngle = angle * 0.5; + double sin = Math.sin(halfAngle); + Vector3D norm = axis.normalize(); + return new Quaternion( + norm.x() * sin, + norm.y() * sin, + norm.z() * sin, + Math.cos(halfAngle) + ); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Quaternion fromEulerAngle(@Nullable EulerAngle angle) { + if (angle == null) return IDENTITY; + double cx = Math.cos(angle.getX() * 0.5); + double sx = Math.sin(angle.getX() * 0.5); + double cy = Math.cos(angle.getY() * 0.5); + double sy = Math.sin(angle.getY() * 0.5); + double cz = Math.cos(angle.getZ() * 0.5); + double sz = Math.sin(angle.getZ() * 0.5); + + return new Quaternion( + sx * cy * cz - cx * sy * sz, + cx * sy * cz + sx * cy * sz, + cx * cy * sz - sx * sy * cz, + cx * cy * cz + sx * sy * sz + ); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Quaternion read(@NonNull DataInputStream dis) throws IOException { + return new Quaternion(dis.readDouble(), dis.readDouble(), dis.readDouble(), dis.readDouble()); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Quaternion multiply(@NonNull Quaternion other) { + return new Quaternion( + w * other.x + x * other.w + y * other.z - z * other.y, + w * other.y - x * other.z + y * other.w + z * other.x, + w * other.z + x * other.y - y * other.x + z * other.w, + w * other.w - x * other.x - y * other.y - z * other.z + ); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector3D multiply(@NonNull Vector3D vec) { + double num1 = x * 2.0; + double num2 = y * 2.0; + double num3 = z * 2.0; + double num4 = x * num1; + double num5 = y * num2; + double num6 = z * num3; + double num7 = x * num2; + double num8 = x * num3; + double num9 = y * num3; + double num10 = w * num1; + double num11 = w * num2; + double num12 = w * num3; + return new Vector3D( + (1.0 - (num5 + num6)) * vec.x() + (num7 - num12) * vec.y() + (num8 + num11) * vec.z(), + (num7 + num12) * vec.x() + (1.0 - (num4 + num6)) * vec.y() + (num9 - num10) * vec.z(), + (num8 - num11) * vec.x() + (num9 + num10) * vec.y() + (1.0 - (num4 + num5)) * vec.z() + ); + } + + @Contract(value = " -> new", pure = true) + public @NonNull Quaternion conjugate() { + return new Quaternion(-x, -y, -z, w); + } + + @Contract(pure = true) + public double dot(@NonNull Quaternion other) { + return x * other.x + y * other.y + z * other.z + w * other.w; + } + + @Contract(value = " -> new", pure = true) + public @NonNull Quaternion normalize() { + double len = Math.sqrt(x * x + y * y + z * z + w * w); + if (len == 0.0) return IDENTITY; + return new Quaternion(x / len, y / len, z / len, w / len); + } + + @Contract(value = "_, _ -> new", pure = true) + public @NonNull Quaternion slerp(@NonNull Quaternion other, double t) { + double cosHalfTheta = dot(other); + Quaternion correctedOther = other; + if (cosHalfTheta < 0.0) { + correctedOther = new Quaternion(-other.x, -other.y, -other.z, -other.w); + cosHalfTheta = -cosHalfTheta; + } + if (Math.abs(cosHalfTheta) >= 1.0) { + return this; + } + double halfTheta = Math.acos(cosHalfTheta); + double sinHalfTheta = Math.sqrt(1.0 - cosHalfTheta * cosHalfTheta); + if (Math.abs(sinHalfTheta) < 0.001) { + return new Quaternion( + x * (1.0 - t) + correctedOther.x * t, + y * (1.0 - t) + correctedOther.y * t, + z * (1.0 - t) + correctedOther.z * t, + w * (1.0 - t) + correctedOther.w * t + ).normalize(); + } + double ratioA = Math.sin((1.0 - t) * halfTheta) / sinHalfTheta; + double ratioB = Math.sin(t * halfTheta) / sinHalfTheta; + return new Quaternion( + x * ratioA + correctedOther.x * ratioB, + y * ratioA + correctedOther.y * ratioB, + z * ratioA + correctedOther.z * ratioB, + w * ratioA + correctedOther.w * ratioB + ); + } + + @Contract(value = " -> new", pure = true) + public @NonNull EulerAngle toEulerAngle() { + double sinr_cosp = 2.0 * (w * x + y * z); + double cosr_cosp = 1.0 - 2.0 * (x * x + y * y); + double roll = Math.atan2(sinr_cosp, cosr_cosp); + + double sinp = 2.0 * (w * y - z * x); + double pitch; + if (Math.abs(sinp) >= 1.0) { + pitch = Math.copySign(Math.PI / 2.0, sinp); + } else { + pitch = Math.asin(sinp); + } + + double siny_cosp = 2.0 * (w * z + x * y); + double cosy_cosp = 1.0 - 2.0 * (y * y + z * z); + double yaw = Math.atan2(siny_cosp, cosy_cosp); + + return new EulerAngle(roll, pitch, yaw); + } + + public void write(@NonNull DataOutputStream dos) throws IOException { + dos.writeDouble(x); + dos.writeDouble(y); + dos.writeDouble(z); + dos.writeDouble(w); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/Vector3D.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Vector3D.java new file mode 100644 index 0000000..1074f33 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Vector3D.java @@ -0,0 +1,185 @@ +package dev.oum.oumlib.math; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.util.BlockVector; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public record Vector3D(double x, double y, double z) { + + public static final Vector3D ZERO = new Vector3D(0.0, 0.0, 0.0); + public static final Vector3D ONE = new Vector3D(1.0, 1.0, 1.0); + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector3D fromBukkit(@Nullable Vector vector) { + if (vector == null) return ZERO; + return new Vector3D(vector.getX(), vector.getY(), vector.getZ()); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector3D fromLocation(@Nullable Location loc) { + if (loc == null) return ZERO; + return new Vector3D(loc.getX(), loc.getY(), loc.getZ()); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector3D fromBlock(@Nullable Block block) { + if (block == null) return ZERO; + return new Vector3D(block.getX(), block.getY(), block.getZ()); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector3D fromEntity(@Nullable Entity entity) { + if (entity == null) return ZERO; + return fromLocation(entity.getLocation()); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector3D fromBlockVector(@Nullable BlockVector bv) { + if (bv == null) return ZERO; + return new Vector3D(bv.getX(), bv.getY(), bv.getZ()); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector3D read(@NonNull DataInputStream dis) throws IOException { + return new Vector3D(dis.readDouble(), dis.readDouble(), dis.readDouble()); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector3D add(@NonNull Vector3D other) { + return new Vector3D(x + other.x, y + other.y, z + other.z); + } + + @Contract(value = "_, _, _ -> new", pure = true) + public @NonNull Vector3D add(double dx, double dy, double dz) { + return new Vector3D(x + dx, y + dy, z + dz); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector3D subtract(@NonNull Vector3D other) { + return new Vector3D(x - other.x, y - other.y, z - other.z); + } + + @Contract(value = "_, _, _ -> new", pure = true) + public @NonNull Vector3D subtract(double dx, double dy, double dz) { + return new Vector3D(x - dx, y - dy, z - dz); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector3D multiply(double factor) { + return new Vector3D(x * factor, y * factor, z * factor); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector3D multiply(@NonNull Vector3D other) { + return new Vector3D(x * other.x, y * other.y, z * other.z); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector3D divide(double divisor) { + if (divisor == 0.0) return ZERO; + return new Vector3D(x / divisor, y / divisor, z / divisor); + } + + @Contract(pure = true) + public double length() { + return Math.sqrt(x * x + y * y + z * z); + } + + @Contract(pure = true) + public double lengthSquared() { + return x * x + y * y + z * z; + } + + @Contract(value = " -> new", pure = true) + public @NonNull Vector3D normalize() { + double len = length(); + if (len == 0.0) return ZERO; + return new Vector3D(x / len, y / len, z / len); + } + + @Contract(pure = true) + public double dot(@NonNull Vector3D other) { + return x * other.x + y * other.y + z * other.z; + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector3D cross(@NonNull Vector3D other) { + return new Vector3D( + y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x + ); + } + + @Contract(pure = true) + public double distance(@NonNull Vector3D other) { + double dx = x - other.x; + double dy = y - other.y; + double dz = z - other.z; + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + @Contract(pure = true) + public double distanceSquared(@NonNull Vector3D other) { + double dx = x - other.x; + double dy = y - other.y; + double dz = z - other.z; + return dx * dx + dy * dy + dz * dz; + } + + @Contract(value = "_, _ -> new", pure = true) + public @NonNull Vector3D lerp(@NonNull Vector3D other, double t) { + double newX = x + (other.x - x) * t; + double newY = y + (other.y - y) * t; + double newZ = z + (other.z - z) * t; + return new Vector3D(newX, newY, newZ); + } + + @Contract(value = " -> new", pure = true) + public @NonNull Vector toBukkit() { + return new Vector(x, y, z); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Location toLocation(@Nullable World world) { + return new Location(world, x, y, z); + } + + @Contract(value = "_, _, _ -> new", pure = true) + public @NonNull Location toLocation(@Nullable World world, float yaw, float pitch) { + return new Location(world, x, y, z, yaw, pitch); + } + + public void applyVelocity(@Nullable Entity entity) { + if (entity != null) { + entity.setVelocity(toBukkit()); + } + } + + public void teleport(@Nullable Entity entity) { + if (entity != null) { + entity.teleport(toLocation(entity.getWorld())); + } + } + + @Contract(value = " -> new", pure = true) + public @NonNull BlockVector toBlockVector() { + return new BlockVector(x, y, z); + } + + public void write(@NonNull DataOutputStream dos) throws IOException { + dos.writeDouble(x); + dos.writeDouble(y); + dos.writeDouble(z); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/Volume3D.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Volume3D.java new file mode 100644 index 0000000..1ee8dad --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Volume3D.java @@ -0,0 +1,122 @@ +package dev.oum.oumlib.math; + +import org.bukkit.util.BoundingBox; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +public interface Volume3D { + + @Contract(pure = true) + boolean contains(@NonNull Vector3D point); + + @Contract(pure = true) + boolean intersects(@NonNull Volume3D other); + + record AABB3D(@NonNull Vector3D min, @NonNull Vector3D max) implements Volume3D { + @Contract(value = "_ -> new", pure = true) + public static @NonNull AABB3D fromBoundingBox(BoundingBox box) { + if (box == null) { + return new AABB3D(Vector3D.ZERO, Vector3D.ZERO); + } + return new AABB3D( + new Vector3D(box.getMinX(), box.getMinY(), box.getMinZ()), + new Vector3D(box.getMaxX(), box.getMaxY(), box.getMaxZ()) + ); + } + + @Override + @Contract(pure = true) + public boolean contains(@NonNull Vector3D p) { + return p.x() >= min.x() && p.x() <= max.x() && + p.y() >= min.y() && p.y() <= max.y() && + p.z() >= min.z() && p.z() <= max.z(); + } + + @Override + @Contract(pure = true) + public boolean intersects(@NonNull Volume3D other) { + if (other instanceof AABB3D(Vector3D min1, Vector3D max1)) { + return min.x() <= max1.x() && max.x() >= min1.x() && + min.y() <= max1.y() && max.y() >= min1.y() && + min.z() <= max1.z() && max.z() >= min1.z(); + } + if (other instanceof Sphere3D(Vector3D center, double radius)) { + double x = Math.max(min.x(), Math.min(center.x(), max.x())); + double y = Math.max(min.y(), Math.min(center.y(), max.y())); + double z = Math.max(min.z(), Math.min(center.z(), max.z())); + return center.distanceSquared(new Vector3D(x, y, z)) <= radius * radius; + } + return other.intersects(this); + } + + @Contract(value = " -> new", pure = true) + public @NonNull BoundingBox toBoundingBox() { + return new BoundingBox(min.x(), min.y(), min.z(), max.x(), max.y(), max.z()); + } + } + + record Sphere3D(@NonNull Vector3D center, double radius) implements Volume3D { + @Override + @Contract(pure = true) + public boolean contains(@NonNull Vector3D p) { + return center.distanceSquared(p) <= radius * radius; + } + + @Override + @Contract(pure = true) + public boolean intersects(@NonNull Volume3D other) { + if (other instanceof Sphere3D(Vector3D center1, double radius2)) { + double rSum = radius + radius2; + return center.distanceSquared(center1) <= rSum * rSum; + } + if (other instanceof AABB3D box) { + return box.intersects(this); + } + if (other instanceof Cylinder3D(Vector3D base, double radius1, double height)) { + double rSum = radius + radius1; + double dx = center.x() - base.x(); + double dz = center.z() - base.z(); + if (dx * dx + dz * dz > rSum * rSum) return false; + double clampedY = Math.max(base.y(), Math.min(center.y(), base.y() + height)); + double dy = center.y() - clampedY; + return dy * dy + dx * dx + dz * dz <= rSum * rSum; + } + return other.intersects(this); + } + } + + record Cylinder3D(@NonNull Vector3D base, double radius, double height) implements Volume3D { + @Override + @Contract(pure = true) + public boolean contains(@NonNull Vector3D p) { + if (p.y() < base.y() || p.y() > base.y() + height) return false; + double dx = p.x() - base.x(); + double dz = p.z() - base.z(); + return dx * dx + dz * dz <= radius * radius; + } + + @Override + @Contract(pure = true) + public boolean intersects(@NonNull Volume3D other) { + if (other instanceof Cylinder3D(Vector3D base1, double radius1, double height1)) { + double rSum = radius + radius1; + double dx = base.x() - base1.x(); + double dz = base.z() - base1.z(); + if (dx * dx + dz * dz > rSum * rSum) return false; + return base.y() <= base1.y() + height1 && base.y() + height >= base1.y(); + } + if (other instanceof Sphere3D sphere) { + return sphere.intersects(this); + } + if (other instanceof AABB3D(Vector3D min, Vector3D max)) { + double x = Math.max(min.x(), Math.min(base.x(), max.x())); + double z = Math.max(min.z(), Math.min(base.z(), max.z())); + double dx = base.x() - x; + double dz = base.z() - z; + if (dx * dx + dz * dz > radius * radius) return false; + return base.y() <= max.y() && base.y() + height >= min.y(); + } + return other.intersects(this); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/WeightedSelector.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/WeightedSelector.java new file mode 100644 index 0000000..01deb32 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/WeightedSelector.java @@ -0,0 +1,79 @@ +package dev.oum.oumlib.math; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public final class WeightedSelector { + + private final List> entries; + private final double totalWeight; + + private WeightedSelector(List> entries, double totalWeight) { + this.entries = List.copyOf(entries); + this.totalWeight = totalWeight; + } + + @Contract(value = " -> new", pure = true) + public static @NonNull Builder builder() { + return new Builder<>(); + } + + public @Nullable T select() { + if (entries.isEmpty()) return null; + if (totalWeight <= 0.0) return entries.get(0).value; + double target = ThreadLocalRandom.current().nextDouble() * totalWeight; + int low = 0; + int high = entries.size() - 1; + while (low < high) { + int mid = (low + high) >>> 1; + if (entries.get(mid).cumulativeWeight < target) { + low = mid + 1; + } else { + high = mid; + } + } + return entries.get(low).value; + } + + public @NonNull List values() { + List list = new ArrayList<>(); + for (Entry entry : entries) { + list.add(entry.value); + } + return list; + } + + private record Entry(T value, double cumulativeWeight) { + } + + public static final class Builder { + private final List> items = new ArrayList<>(); + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull Builder add(T value, double weight) { + if (weight > 0.0) { + items.add(new RawEntry<>(value, weight)); + } + return this; + } + + @Contract(value = " -> new", pure = true) + public @NonNull WeightedSelector build() { + List> compiled = new ArrayList<>(); + double cumulative = 0.0; + for (RawEntry item : items) { + cumulative += item.weight; + compiled.add(new Entry<>(item.value, cumulative)); + } + return new WeightedSelector<>(compiled, cumulative); + } + + private record RawEntry(T value, double weight) { + } + } +} 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 1844035..b0d1215 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 @@ -141,6 +141,56 @@ private Promise(CompletableFuture future) { return this; } + public @NonNull Promise mapSyncFor(@NonNull Object entity, @NonNull Function mapper) { + CompletableFuture next = new CompletableFuture<>(); + future.whenComplete((val, err) -> { + if (err != null) { + next.completeExceptionally(err); + } else { + Scheduler.runFor(entity, () -> { + try { + next.complete(mapper.apply(val)); + } catch (Throwable t) { + next.completeExceptionally(t); + } + }); + } + }); + return new Promise<>(next); + } + + public @NonNull Promise thenAcceptSyncFor(@NonNull Object entity, @NonNull Consumer action) { + CompletableFuture next = new CompletableFuture<>(); + future.whenComplete((val, err) -> { + if (err != null) { + next.completeExceptionally(err); + } else { + Scheduler.runFor(entity, () -> { + try { + action.accept(val); + next.complete(null); + } catch (Throwable t) { + next.completeExceptionally(t); + } + }); + } + }); + return new Promise<>(next); + } + + public @NonNull Promise whenCompleteSyncFor(@NonNull Object entity, @Nullable Consumer success, @Nullable Consumer failure) { + future.whenComplete((val, err) -> { + Scheduler.runFor(entity, () -> { + if (err != null) { + if (failure != null) failure.accept(err); + } else { + if (success != null) success.accept(val); + } + }); + }); + return this; + } + public @NonNull CompletableFuture toCompletableFuture() { return future; } 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 e3a269b..a5ba797 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 @@ -81,11 +81,51 @@ public static void runVirtual(Runnable task) { return adapter().runAt(location, task); } + @Contract("_, _, _ -> new") + public static @NonNull TaskHandle runLaterAt(Object location, Duration delay, Runnable task) { + return adapter().runLaterAt(location, delay, task); + } + + @Contract("_, _, _ -> new") + public static @NonNull TaskHandle runLaterAt(Object location, long ticks, Runnable task) { + return adapter().runLaterAt(location, ticks, task); + } + + @Contract("_, _, _, _ -> new") + public static @NonNull TaskHandle runRepeatingAt(Object location, Duration initialDelay, Duration period, Runnable task) { + return adapter().runRepeatingAt(location, initialDelay, period, task); + } + + @Contract("_, _, _, _ -> new") + public static @NonNull TaskHandle runRepeatingAt(Object location, long initialTicks, long periodTicks, Runnable task) { + return adapter().runRepeatingAt(location, initialTicks, periodTicks, task); + } + @Contract("_, _ -> new") public static @NonNull TaskHandle runFor(Object entity, Runnable task) { return adapter().runFor(entity, task); } + @Contract("_, _, _, _ -> new") + public static @NonNull TaskHandle runLaterFor(Object entity, Duration delay, Runnable task, Runnable retired) { + return adapter().runLaterFor(entity, delay, task, retired); + } + + @Contract("_, _, _, _ -> new") + public static @NonNull TaskHandle runLaterFor(Object entity, long ticks, Runnable task, Runnable retired) { + return adapter().runLaterFor(entity, ticks, task, retired); + } + + @Contract("_, _, _, _, _ -> new") + public static @NonNull TaskHandle runRepeatingFor(Object entity, Duration initialDelay, Duration period, Runnable task, Runnable retired) { + return adapter().runRepeatingFor(entity, initialDelay, period, task, retired); + } + + @Contract("_, _, _, _, _ -> new") + public static @NonNull TaskHandle runRepeatingFor(Object entity, long initialTicks, long periodTicks, Runnable task, Runnable retired) { + return adapter().runRepeatingFor(entity, initialTicks, periodTicks, task, retired); + } + @CheckReturnValue public static @NonNull TaskChain chain() { return TaskChain.create(); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/BukkitSchedulerAdapter.java b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/BukkitSchedulerAdapter.java index 6c16a66..d514d99 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/BukkitSchedulerAdapter.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/BukkitSchedulerAdapter.java @@ -19,7 +19,7 @@ public final class BukkitSchedulerAdapter implements SchedulerAdapter { static { boolean folia = false; try { - Class.forName("io.papermc.paper.threadedregions.RegionScheduler"); + Class.forName("io.papermc.paper.threadedregions.scheduler.RegionScheduler"); folia = true; } catch (ClassNotFoundException ignored) { } @@ -111,19 +111,117 @@ public static BukkitSchedulerAdapter get() { return run(task); } + @Contract("_, _, _ -> new") + @Override + public @NonNull TaskHandle runLaterAt(Object location, Duration delay, Runnable task) { + return runLaterAt(location, toTicks(delay), task); + } + + @Contract("_, _, _ -> new") + @Override + public @NonNull TaskHandle runLaterAt(Object location, long ticks, Runnable task) { + if (location instanceof Location loc) { + if (FOLIA) { + var scheduled = Bukkit.getRegionScheduler().runDelayed(plugin, loc, t -> task.run(), ticks); + return new TaskHandle(scheduled::cancel, scheduled::isCancelled); + } + } + return runLater(ticks, task); + } + + @Contract("_, _, _, _ -> new") + @Override + public @NonNull TaskHandle runRepeatingAt(Object location, Duration initialDelay, Duration period, Runnable task) { + return runRepeatingAt(location, toTicks(initialDelay), toTicks(period), task); + } + + @Contract("_, _, _, _ -> new") + @Override + public @NonNull TaskHandle runRepeatingAt(Object location, long initialTicks, long periodTicks, Runnable task) { + if (location instanceof Location loc) { + if (FOLIA) { + var scheduled = Bukkit.getRegionScheduler().runAtFixedRate(plugin, loc, t -> task.run(), initialTicks, periodTicks); + return new TaskHandle(scheduled::cancel, scheduled::isCancelled); + } + } + return runRepeating(initialTicks, periodTicks, task); + } + @Contract("_, _ -> new") @Override public @NonNull TaskHandle runFor(Object entity, Runnable task) { + return runLaterFor(entity, 0L, task, null); + } + + @Contract("_, _, _, _ -> new") + @Override + public @NonNull TaskHandle runLaterFor(Object entity, Duration delay, Runnable task, Runnable retired) { + return runLaterFor(entity, toTicks(delay), task, retired); + } + + @Contract("_, _, _, _ -> new") + @Override + public @NonNull TaskHandle runLaterFor(Object entity, long ticks, Runnable task, Runnable retired) { if (entity instanceof Entity ent) { if (FOLIA) { - var scheduled = ent.getScheduler().run(plugin, t -> task.run(), null); - assert scheduled != null; - return new TaskHandle(scheduled::cancel, scheduled::isCancelled); + var scheduled = ent.getScheduler().runDelayed(plugin, t -> task.run(), retired, ticks); + if (scheduled != null) { + return new TaskHandle(scheduled::cancel, scheduled::isCancelled); + } } + if (ticks <= 0) { + return run(() -> { + if (ent.isValid()) { + task.run(); + } else if (retired != null) { + retired.run(); + } + }); + } + return runLater(ticks, () -> { + if (ent.isValid()) { + task.run(); + } else if (retired != null) { + retired.run(); + } + }); + } + if (retired != null) { + retired.run(); } return run(task); } + @Contract("_, _, _, _, _ -> new") + @Override + public @NonNull TaskHandle runRepeatingFor(Object entity, Duration initialDelay, Duration period, Runnable task, Runnable retired) { + return runRepeatingFor(entity, toTicks(initialDelay), toTicks(period), task, retired); + } + + @Contract("_, _, _, _, _ -> new") + @Override + public @NonNull TaskHandle runRepeatingFor(Object entity, long initialTicks, long periodTicks, Runnable task, Runnable retired) { + if (entity instanceof Entity ent) { + if (FOLIA) { + var scheduled = ent.getScheduler().runAtFixedRate(plugin, t -> task.run(), retired, initialTicks, periodTicks); + if (scheduled != null) { + return new TaskHandle(scheduled::cancel, scheduled::isCancelled); + } + } + return runRepeating(initialTicks, periodTicks, () -> { + if (ent.isValid()) { + task.run(); + } else if (retired != null) { + retired.run(); + } + }); + } + if (retired != null) { + retired.run(); + } + return runRepeating(initialTicks, periodTicks, task); + } + private long toTicks(@NonNull Duration duration) { return Math.max(1L, duration.toMillis() / 50L); } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/SchedulerAdapter.java b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/SchedulerAdapter.java index 7df05e4..3fb1a28 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/SchedulerAdapter.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/SchedulerAdapter.java @@ -29,6 +29,30 @@ public interface SchedulerAdapter { @Contract("_, _ -> new") @NonNull TaskHandle runAt(Object location, Runnable task); + @Contract("_, _, _ -> new") + @NonNull TaskHandle runLaterAt(Object location, Duration delay, Runnable task); + + @Contract("_, _, _ -> new") + @NonNull TaskHandle runLaterAt(Object location, long ticks, Runnable task); + + @Contract("_, _, _, _ -> new") + @NonNull TaskHandle runRepeatingAt(Object location, Duration initialDelay, Duration period, Runnable task); + + @Contract("_, _, _, _ -> new") + @NonNull TaskHandle runRepeatingAt(Object location, long initialTicks, long periodTicks, Runnable task); + @Contract("_, _ -> new") @NonNull TaskHandle runFor(Object entity, Runnable task); + + @Contract("_, _, _, _ -> new") + @NonNull TaskHandle runLaterFor(Object entity, Duration delay, Runnable task, Runnable retired); + + @Contract("_, _, _, _ -> new") + @NonNull TaskHandle runLaterFor(Object entity, long ticks, Runnable task, Runnable retired); + + @Contract("_, _, _, _, _ -> new") + @NonNull TaskHandle runRepeatingFor(Object entity, Duration initialDelay, Duration period, Runnable task, Runnable retired); + + @Contract("_, _, _, _, _ -> new") + @NonNull TaskHandle runRepeatingFor(Object entity, long initialTicks, long periodTicks, Runnable task, Runnable retired); } \ No newline at end of file diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/VelocitySchedulerAdapter.java b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/VelocitySchedulerAdapter.java index 9c5c1f5..2d82a43 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/VelocitySchedulerAdapter.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/platform/VelocitySchedulerAdapter.java @@ -80,9 +80,57 @@ public static VelocitySchedulerAdapter get() { throw new UnsupportedOperationException("Folia regional scheduling is not supported on Velocity."); } + @Contract("_, _, _ -> fail") + @Override + public @NonNull TaskHandle runLaterAt(Object location, Duration delay, Runnable task) { + throw new UnsupportedOperationException("Folia regional scheduling is not supported on Velocity."); + } + + @Contract("_, _, _ -> fail") + @Override + public @NonNull TaskHandle runLaterAt(Object location, long ticks, Runnable task) { + throw new UnsupportedOperationException("Folia regional scheduling is not supported on Velocity."); + } + + @Contract("_, _, _, _ -> fail") + @Override + public @NonNull TaskHandle runRepeatingAt(Object location, Duration initialDelay, Duration period, Runnable task) { + throw new UnsupportedOperationException("Folia regional scheduling is not supported on Velocity."); + } + + @Contract("_, _, _, _ -> fail") + @Override + public @NonNull TaskHandle runRepeatingAt(Object location, long initialTicks, long periodTicks, Runnable task) { + throw new UnsupportedOperationException("Folia regional scheduling is not supported on Velocity."); + } + @Contract("_, _ -> fail") @Override public @NonNull TaskHandle runFor(Object entity, Runnable task) { throw new UnsupportedOperationException("Entity scheduling is not supported on Velocity."); } + + @Contract("_, _, _, _ -> fail") + @Override + public @NonNull TaskHandle runLaterFor(Object entity, Duration delay, Runnable task, Runnable retired) { + throw new UnsupportedOperationException("Entity scheduling is not supported on Velocity."); + } + + @Contract("_, _, _, _ -> fail") + @Override + public @NonNull TaskHandle runLaterFor(Object entity, long ticks, Runnable task, Runnable retired) { + throw new UnsupportedOperationException("Entity scheduling is not supported on Velocity."); + } + + @Contract("_, _, _, _, _ -> fail") + @Override + public @NonNull TaskHandle runRepeatingFor(Object entity, Duration initialDelay, Duration period, Runnable task, Runnable retired) { + throw new UnsupportedOperationException("Entity scheduling is not supported on Velocity."); + } + + @Contract("_, _, _, _, _ -> fail") + @Override + public @NonNull TaskHandle runRepeatingFor(Object entity, long initialTicks, long periodTicks, Runnable task, Runnable retired) { + throw new UnsupportedOperationException("Entity scheduling is not supported on Velocity."); + } } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/Text.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/Text.java index a03f144..d5329f4 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/Text.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/Text.java @@ -9,6 +9,8 @@ import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.title.Title; import org.jetbrains.annotations.CheckReturnValue; import org.jetbrains.annotations.Contract; @@ -16,8 +18,11 @@ import java.lang.reflect.RecordComponent; import java.time.Duration; +import java.util.ArrayList; import java.util.List; +import static dev.oum.oumlib.text.Preset.*; + public final class Text { private static final MiniMessage MM = MiniMessage.miniMessage(); @@ -26,33 +31,36 @@ private Text() { } public static void send(@NonNull Audience audience, String message, Object... pairs) { - audience.sendMessage(parse(resolve(message, audience, pairs))); + audience.sendMessage(parse(resolve(message, audience), createResolvers(pairs))); } public static void send(@NonNull Audience audience, String message) { audience.sendMessage(parse(resolve(message, audience))); } - public static void send(Audience audience, String message, Record data) { - String injected = injectRecord(message, data); - String resolved = resolve(injected, audience); - audience.sendMessage(parse(resolved)); + public static void send(@NonNull Audience audience, String message, Record data) { + audience.sendMessage(parse(resolve(message, audience), createResolvers(data))); } public static void sendLines(Audience audience, @NonNull List lines, Object... pairs) { - lines.forEach(line -> send(audience, line, pairs)); + TagResolver[] resolvers = createResolvers(pairs); + lines.forEach(line -> audience.sendMessage(parse(resolve(line, audience), resolvers))); } public static @NonNull Component parse(String message) { return MM.deserialize(message); } + public static @NonNull Component parse(String message, TagResolver... resolvers) { + return MM.deserialize(message, resolvers); + } + public static @NonNull String strip(String message) { return MM.stripTags(message); } public static void actionBar(@NonNull Audience audience, String message, Object... pairs) { - audience.sendActionBar(parse(resolve(message, audience, pairs))); + audience.sendActionBar(parse(resolve(message, audience), createResolvers(pairs))); } public static void title(@NonNull Audience audience, String title, String subtitle, @@ -65,17 +73,15 @@ public static void title(@NonNull Audience audience, String title, String subtit } public static void broadcast(String message, Object... pairs) { - OumLib.players().sendMessage(parse(resolve(message, null, pairs))); + OumLib.players().sendMessage(parse(resolve(message, null), createResolvers(pairs))); } public static void broadcast(String message, Record data) { - String injected = injectRecord(message, data); - String resolved = resolve(injected, null); - OumLib.players().sendMessage(parse(resolved)); + OumLib.players().sendMessage(parse(resolve(message, null), createResolvers(data))); } public static void broadcastActionBar(String message, Object... pairs) { - OumLib.players().sendActionBar(parse(resolve(message, null, pairs))); + OumLib.players().sendActionBar(parse(resolve(message, null), createResolvers(pairs))); } public static void broadcastTitle(String title, String subtitle, Duration fadeIn, Duration stay, Duration fadeOut) { @@ -98,30 +104,42 @@ public static Component clickable(String text, ClickEvent clickEvent, String hov return c; } - private static @NonNull String resolve(String input, Object player, Object @NonNull ... pairs) { - String result = input; + private static @NonNull TagResolver @NonNull [] createResolvers(Object @NonNull ... pairs) { + if (pairs.length == 0) return new TagResolver[0]; + List resolvers = new ArrayList<>(); for (int i = 0; i + 1 < pairs.length; i += 2) { - result = result.replace("<" + pairs[i] + ">", MM.escapeTags(String.valueOf(pairs[i + 1]))); + String key = String.valueOf(pairs[i]); + Object val = pairs[i + 1]; + if (val instanceof Component comp) { + resolvers.add(Placeholder.component(key, comp)); + } else { + resolvers.add(Placeholder.parsed(key, String.valueOf(val))); + } } - return PlaceholderResolver.resolveInternal(result, player); - } - - private static @NonNull String resolve(String input, Object player) { - return PlaceholderResolver.resolveInternal(input, player); + return resolvers.toArray(new TagResolver[0]); } - private static String injectRecord(String input, Record data) { - if (data == null) return input; - String result = input; + @Contract("null -> new") + private static @NonNull TagResolver @NonNull [] createResolvers(Record data) { + if (data == null) return new TagResolver[0]; + List resolvers = new ArrayList<>(); try { for (RecordComponent comp : data.getClass().getRecordComponents()) { Object val = comp.getAccessor().invoke(data); - result = result.replace("<" + comp.getName() + ">", - val != null ? MM.escapeTags(String.valueOf(val)) : ""); + String key = comp.getName(); + if (val instanceof Component compVal) { + resolvers.add(Placeholder.component(key, compVal)); + } else { + resolvers.add(Placeholder.parsed(key, val != null ? String.valueOf(val) : "")); + } } } catch (Exception ignored) { } - return result; + return resolvers.toArray(new TagResolver[0]); + } + + private static @NonNull String resolve(String input, Object player) { + return PlaceholderResolver.resolveInternal(input, player); } public static @NonNull BossBar bossBar(@NonNull Audience audience, @NonNull String titleMiniMessage, @@ -136,7 +154,20 @@ public static void bossBarTemporary(@NonNull Audience audience, @NonNull String float progress, BossBar.@NonNull Color color, BossBar.@NonNull Overlay overlay, @NonNull Duration duration) { BossBar bar = bossBar(audience, titleMiniMessage, progress, color, overlay); - Scheduler.runLater(duration, () -> audience.hideBossBar(bar)); + if (OumLib.isPaper() && isBukkitPlayer(audience)) { + Scheduler.runLaterFor(audience, duration, () -> audience.hideBossBar(bar), () -> { + }); + } else { + Scheduler.runLater(duration, () -> audience.hideBossBar(bar)); + } + } + + private static boolean isBukkitPlayer(Object audience) { + try { + return Class.forName("org.bukkit.entity.Player").isInstance(audience); + } catch (ClassNotFoundException e) { + return false; + } } public static void ascii(boolean colorized, String @NonNull ... lines) { @@ -159,35 +190,35 @@ private Preset() { } public static void success(Audience audience, String message, Object... pairs) { - send(audience, OumLib.presets().prefix(dev.oum.oumlib.text.Preset.SUCCESS) + message, pairs); + send(audience, OumLib.presets().prefix(SUCCESS) + message, pairs); } public static void error(Audience audience, String message, Object... pairs) { - send(audience, OumLib.presets().prefix(dev.oum.oumlib.text.Preset.ERROR) + message, pairs); + send(audience, OumLib.presets().prefix(ERROR) + message, pairs); } public static void info(Audience audience, String message, Object... pairs) { - send(audience, OumLib.presets().prefix(dev.oum.oumlib.text.Preset.INFO) + message, pairs); + send(audience, OumLib.presets().prefix(INFO) + message, pairs); } public static void warning(Audience audience, String message, Object... pairs) { - send(audience, OumLib.presets().prefix(dev.oum.oumlib.text.Preset.WARNING) + message, pairs); + send(audience, OumLib.presets().prefix(WARNING) + message, pairs); } public static void successBroadcast(String message, Object... pairs) { - broadcast(OumLib.presets().prefix(dev.oum.oumlib.text.Preset.SUCCESS) + message, pairs); + broadcast(OumLib.presets().prefix(SUCCESS) + message, pairs); } public static void errorBroadcast(String message, Object... pairs) { - broadcast(OumLib.presets().prefix(dev.oum.oumlib.text.Preset.ERROR) + message, pairs); + broadcast(OumLib.presets().prefix(ERROR) + message, pairs); } public static void infoBroadcast(String message, Object... pairs) { - broadcast(OumLib.presets().prefix(dev.oum.oumlib.text.Preset.INFO) + message, pairs); + broadcast(OumLib.presets().prefix(INFO) + message, pairs); } public static void warningBroadcast(String message, Object... pairs) { - broadcast(OumLib.presets().prefix(dev.oum.oumlib.text.Preset.WARNING) + message, pairs); + broadcast(OumLib.presets().prefix(WARNING) + message, pairs); } } } \ No newline at end of file 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 index afeb9dd..ff87360 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Countdown.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/util/Countdown.java @@ -32,6 +32,7 @@ public final class Countdown { private final Predicate displayFilter; private int secondsRemaining; private TaskHandle task; + @Contract(pure = true) private Countdown(@NonNull Builder builder) { this.audience = builder.audience; @@ -54,7 +55,7 @@ private Countdown(@NonNull Builder builder) { return task; } - task = Scheduler.runRepeating(Duration.ZERO, Duration.ofSeconds(1), () -> { + Runnable run = () -> { if (secondsRemaining <= 0) { if (onComplete != null) { onComplete.accept(audience); @@ -85,7 +86,14 @@ private Countdown(@NonNull Builder builder) { } secondsRemaining--; - }); + }; + + if (audience instanceof Player player) { + task = Scheduler.runRepeatingFor(player, Duration.ZERO, Duration.ofSeconds(1), run, () -> { + }); + } else { + task = Scheduler.runRepeating(Duration.ZERO, Duration.ofSeconds(1), run); + } return task; } @@ -169,7 +177,7 @@ private Builder() { return this; } - public @NonNull Builder intervals(int... seconds) { + public @NonNull Builder intervals(int @NonNull ... seconds) { Set set = new HashSet<>(); for (int s : seconds) { set.add(s); 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 9680596..97238fa 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 @@ -1,5 +1,6 @@ package dev.oum.oumlib.util; +import dev.oum.oumlib.math.FormatMath; import org.jspecify.annotations.NonNull; import java.text.DecimalFormat; @@ -11,9 +12,6 @@ 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() { } @@ -54,9 +52,7 @@ public static String number(double number) { } public static String compactNumber(double number) { - if (number < 1000) return String.valueOf((int) number); - int exp = (int) (Math.log(number) / Math.log(1000)); - return String.format("%.1f%s", number / Math.pow(1000, exp), SUFFIXES[exp]); + return FormatMath.compact(number); } public static @NonNull Duration parseDuration(@NonNull String input) { @@ -94,15 +90,7 @@ public static String compactNumber(double number) { 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(); + return FormatMath.toRoman(number); } public static @NonNull String ordinal(int number) { 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 6da9963..1262611 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 @@ -1,8 +1,6 @@ package dev.oum.oumlib.util; import org.bukkit.inventory.ItemStack; -import org.bukkit.util.io.BukkitObjectInputStream; -import org.bukkit.util.io.BukkitObjectOutputStream; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -10,31 +8,17 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; -import java.lang.reflect.Method; import java.util.Base64; public final class ItemSerializer { - private static Method serializeAsBytesMethod; - private static Method deserializeBytesMethod; - private static boolean paperBytesSupported = false; - - static { - try { - serializeAsBytesMethod = ItemStack.class.getMethod("serializeAsBytes"); - deserializeBytesMethod = ItemStack.class.getMethod("deserializeBytes", byte[].class); - paperBytesSupported = true; - } catch (NoSuchMethodException ignored) { - } - } - private ItemSerializer() { } public static @NonNull String serialize(@Nullable ItemStack item) { if (item == null || item.isEmpty()) return ""; try { - byte[] bytes = serializeToBytes(item); + byte[] bytes = item.serializeAsBytes(); return Base64.getEncoder().encodeToString(bytes); } catch (Exception e) { throw new RuntimeException("Failed to serialize ItemStack", e); @@ -45,7 +29,7 @@ private ItemSerializer() { if (base64.isEmpty()) return null; try { byte[] bytes = Base64.getDecoder().decode(base64); - return deserializeFromBytes(bytes); + return ItemStack.deserializeBytes(bytes); } catch (Exception e) { throw new RuntimeException("Failed to deserialize ItemStack", e); } @@ -59,7 +43,7 @@ private ItemSerializer() { if (item == null || item.isEmpty()) { dos.writeInt(-1); } else { - byte[] bytes = serializeToBytes(item); + byte[] bytes = item.serializeAsBytes(); dos.writeInt(bytes.length); dos.write(bytes); } @@ -85,7 +69,7 @@ private ItemSerializer() { } else { byte[] itemBytes = new byte[size]; dis.readFully(itemBytes); - items[i] = deserializeFromBytes(itemBytes); + items[i] = ItemStack.deserializeBytes(itemBytes); } } return items; @@ -94,27 +78,4 @@ private ItemSerializer() { throw new RuntimeException("Failed to deserialize ItemStack array", e); } } - - @SuppressWarnings("deprecation") - private static byte[] serializeToBytes(@NonNull ItemStack item) throws Exception { - if (paperBytesSupported) { - return (byte[]) serializeAsBytesMethod.invoke(item); - } - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - BukkitObjectOutputStream boos = new BukkitObjectOutputStream(baos)) { - boos.writeObject(item); - return baos.toByteArray(); - } - } - - @SuppressWarnings("deprecation") - private static @NonNull ItemStack deserializeFromBytes(byte @NonNull [] bytes) throws Exception { - if (paperBytesSupported) { - return (ItemStack) deserializeBytesMethod.invoke(null, (Object) bytes); - } - try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - BukkitObjectInputStream bois = new BukkitObjectInputStream(bais)) { - return (ItemStack) bois.readObject(); - } - } } 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 e22ba69..6932084 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 @@ -1,5 +1,9 @@ package dev.oum.oumlib.util; +import dev.oum.oumlib.math.Chance; +import dev.oum.oumlib.math.FastMath; +import dev.oum.oumlib.math.Vector3D; +import dev.oum.oumlib.math.Volume3D; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; @@ -8,8 +12,6 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; -import java.util.concurrent.ThreadLocalRandom; - public final class Locations { private Locations() { @@ -84,14 +86,10 @@ public static double distance2D(@NonNull Location a, @NonNull Location b) { } 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 - ); + Vector3D v1 = Vector3D.fromLocation(a); + Vector3D v2 = Vector3D.fromLocation(b); + Vector3D mid = v1.lerp(v2, 0.5); + return mid.toLocation(a.getWorld(), (a.getYaw() + b.getYaw()) / 2.0f, (a.getPitch() + b.getPitch()) / 2.0f); } public static @NonNull Location centerBlock(@NonNull Location block) { @@ -106,23 +104,18 @@ public static double distance2D(@NonNull Location a, @NonNull Location b) { } 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; + Volume3D.AABB3D aabb = new Volume3D.AABB3D( + new Vector3D(Math.min(min.getX(), max.getX()), Math.min(min.getY(), max.getY()), Math.min(min.getZ(), max.getZ())), + new Vector3D(Math.max(min.getX(), max.getX()), Math.max(min.getY(), max.getY()), Math.max(min.getZ(), max.getZ())) + ); + return aabb.contains(Vector3D.fromLocation(loc)); } 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); + double angle = Chance.randomIn(0.0, 2.0 * Math.PI); + double r = Chance.randomIn(0.0, radius); + double x = center.getX() + r * FastMath.cos(angle); + double z = center.getZ() + r * FastMath.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/PaperPermissionHelper.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/PaperPermissionHelper.java index 8e46419..70c6d44 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/PaperPermissionHelper.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/util/PaperPermissionHelper.java @@ -1,6 +1,8 @@ package dev.oum.oumlib.util; +import dev.oum.oumlib.util.Permission.Default; import org.bukkit.Bukkit; +import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.PluginManager; import org.jspecify.annotations.NonNull; @@ -10,7 +12,7 @@ public final class PaperPermissionHelper { private PaperPermissionHelper() { } - public static void register(String name, String description, Permission.@NonNull Default defaultValue) { + public static void register(String name, String description, @NonNull Default defaultValue) { PermissionDefault defaultVal = switch (defaultValue) { case TRUE -> PermissionDefault.TRUE; case FALSE -> PermissionDefault.FALSE; @@ -18,7 +20,7 @@ public static void register(String name, String description, Permission.@NonNull case NOT_OP -> PermissionDefault.NOT_OP; }; - org.bukkit.permissions.Permission bukkitPerm = new org.bukkit.permissions.Permission( + Permission bukkitPerm = new Permission( name, description, defaultVal 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 c352aee..9d0054d 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 @@ -2,6 +2,8 @@ import com.google.gson.Gson; import dev.oum.oumlib.OumLib; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; @@ -240,6 +242,30 @@ public long getLongOrDefault(@NonNull NamespacedKey key, long def) { return val != null ? val : def; } + public @NonNull PdcHolder setComponent(@NonNull String key, @Nullable Component value) { + return setComponent(nsk(key), value); + } + + public @NonNull PdcHolder setComponent(@NonNull NamespacedKey key, @Nullable Component value) { + Component oldValue = getComponent(key); + if (value == null) { + pdc.remove(key); + } else { + pdc.set(key, PersistentDataType.STRING, MiniMessage.miniMessage().serialize(value)); + } + triggerListeners(holder, key, oldValue, value); + return this; + } + + public @Nullable Component getComponent(@NonNull String key) { + return getComponent(nsk(key)); + } + + public @Nullable Component getComponent(@NonNull NamespacedKey key) { + String val = pdc.get(key, PersistentDataType.STRING); + return val != null ? MiniMessage.miniMessage().deserialize(val) : null; + } + public @NonNull PdcHolder setList(@NonNull String key, @Nullable List value) { return setList(nsk(key), value); } @@ -589,6 +615,35 @@ public long getLongOrDefault(@NonNull NamespacedKey key, long def) { return Arrays.asList(GSON.fromJson(raw, String[].class)); } + public @NonNull PdcItem setComponent(@NonNull String key, @Nullable Component value) { + return setComponent(nsk(key), value); + } + + public @NonNull PdcItem setComponent(@NonNull NamespacedKey key, @Nullable Component value) { + Component oldValue = getComponent(key); + updateMeta(meta -> { + if (value == null) { + meta.getPersistentDataContainer().remove(key); + } else { + meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, MiniMessage.miniMessage().serialize(value)); + } + }); + triggerListeners(item, key, oldValue, value); + return this; + } + + public @Nullable Component getComponent(@NonNull String key) { + return getComponent(nsk(key)); + } + + public @Nullable Component getComponent(@NonNull NamespacedKey key) { + if (!item.hasItemMeta()) return null; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + String val = meta.getPersistentDataContainer().get(key, PersistentDataType.STRING); + return val != null ? MiniMessage.miniMessage().deserialize(val) : null; + } + public @NonNull PdcItem setObject(@NonNull String key, @Nullable T value) { return setObject(nsk(key), value); } diff --git a/oumlib-plugin/pom.xml b/oumlib-plugin/pom.xml deleted file mode 100644 index 75744ec..0000000 --- a/oumlib-plugin/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - 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 deleted file mode 100644 index 9dc5b35..0000000 --- a/oumlib-plugin/src/main/java/dev/oum/oumlib/plugin/PaperOumLibPlugin.java +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 4b1699b..0000000 --- a/oumlib-plugin/src/main/java/dev/oum/oumlib/plugin/VelocityOumLibPlugin.java +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 7abfd4f..0000000 --- a/oumlib-plugin/src/main/resources/paper-plugin.yml +++ /dev/null @@ -1,6 +0,0 @@ -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 03db624..75d4429 100644 --- a/pom.xml +++ b/pom.xml @@ -7,12 +7,11 @@ dev.oum oumlib - 1.0.7 + 1.0.8 pom oumlib-core - oumlib-plugin example-plugin