diff --git a/.gitignore b/.gitignore index 9853625..07e780e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ /.idea /target -/example-plugin/example-plugin.iml -/example-plugin/target /oumlib-core/oumlib-core.iml /oumlib-core/target \ No newline at end of file diff --git a/README.md b/README.md index 47dd99b..5b858be 100644 --- a/README.md +++ b/README.md @@ -4,39 +4,44 @@ [![](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) +[![CodeFactor](https://www.codefactor.io/repository/github/sun-mc-dev/oumlib/badge)](https://www.codefactor.io/repository/github/sun-mc-dev/oumlib) -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, type-safe, and thread-aware abstractions to eliminate boilerplate code. +A utility library for Paper and Velocity plugins. Shade it into your jar, call `OumLib.init(this)`, and you get commands, menus, configs, scheduling, cooldowns, events, regions, holograms, recipes, database access, and more — all with a fluent API and zero external dependencies at runtime. -OumLib is designed to be shaded and relocated directly into your plugin JAR. +Built on Java 21 virtual threads. Works on Paper, Folia, and Velocity. --- -## 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** | Asynchronous 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)** | +## Docs + +| Module | What it does | Link | +|:---------------------|:---------------------------------------------------------------------|:------------------------------------------| +| **Setup** | Init lifecycle, shading, platform detection | [setup.md](docs/setup.md) | +| **Commands** | Brigadier command builder with typed args, cooldowns, subcommands | [commands.md](docs/commands.md) | +| **Configuration** | Record-based YAML configs with auto-reload | [configuration.md](docs/configuration.md) | +| **Menus & Items** | Chest GUIs, paginated menus, ItemBuilder, DataComponents | [inventories.md](docs/inventories.md) | +| **Recipes** | Shaped, shapeless, cooking, smithing, stonecutting DSL | [recipes.md](docs/recipes.md) | +| **Scheduler** | Sync/async/virtual tasks, Promise, TaskChain, Countdown, Folia-aware | [scheduler.md](docs/scheduler.md) | +| **Events** | Functional event listeners with filters, expiry, one-shot | [events.md](docs/events.md) | +| **Cooldowns** | CooldownManager, RateLimiter, persistent stores | [cooldowns.md](docs/cooldowns.md) | +| **Text** | MiniMessage helpers, placeholders, localization, text input | [text.md](docs/text.md) | +| **PDC** | Type-safe persistent data, DataKey, PdcModel records, PdcTree | [pdc.md](docs/pdc.md) | +| **Metadata** | In-memory volatile data with TTL auto-cleanup | [metadata.md](docs/metadata.md) | +| **Holograms** | Packet-based virtual displays with click handling | [holograms.md](docs/holograms.md) | +| **Display Entities** | DisplayBuilder for text/item/block displays | [entities.md](docs/entities.md) | +| **Regions** | Cuboid, cylinder, sphere, polygon regions with enter/leave tracking | [regions.md](docs/regions.md) | +| **Math** | Vector2D/3D, Volume3D, Noise, Easing, FastMath, MathEval | [math.md](docs/math.md) | +| **Effects** | Particle effects — lines, circles, helices, bezier curves | [effects.md](docs/effects.md) | +| **Database** | Async SQLite/MySQL via HikariCP | [database.md](docs/database.md) | +| **Bridges** | Vault, Nexo, ItemsAdder, MMOItems hooks | [bridges.md](docs/bridges.md) | +| **Utilities** | Duration parsing, location serialization, formatting | [utilities.md](docs/utilities.md) | --- ## Installation -Declare the JitPack repository and OumLib dependency in your `pom.xml`. +### Maven -### 1. Add Repository ```xml jitpack.io @@ -44,7 +49,6 @@ Declare the JitPack repository and OumLib dependency in your `pom.xml`. ``` -### 2. Add Dependency ```xml com.github.sun-mc-dev.oumlib @@ -54,11 +58,23 @@ Declare the JitPack repository and OumLib dependency in your `pom.xml`. ``` -### 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. +### Gradle (Kotlin DSL) -Add this configured `maven-shade-plugin` to your `pom.xml`: +```kotlin +repositories { + maven("https://jitpack.io") +} + +dependencies { + implementation("com.github.sun-mc-dev.oumlib:oumlib-core:VERSION") +} +``` + +### Shading + +You **must** shade and relocate OumLib into your plugin jar. This prevents version conflicts when multiple plugins use different OumLib versions on the same server. +**Maven:** ```xml @@ -99,67 +115,56 @@ Add this configured `maven-shade-plugin` to your `pom.xml`: ``` ---- +**Gradle (Shadow):** +```kotlin +plugins { + id("com.gradleup.shadow") version "9.0.0-beta12" +} -## Quick Start Example +tasks.shadowJar { + relocate("dev.oum.oumlib", "your.plugin.package.libs.oumlib") +} +``` -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 -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; +## Quick Start -public record ShopConfig(String itemTitle, int itemPrice) implements ConfigSection {} +A small plugin that loads config, registers a command, opens a shop GUI, and handles purchases: -public final class ProfileShopPlugin extends JavaPlugin { +```java +public final class ShopPlugin extends JavaPlugin { private ConfigManager config; private Database db; @Override public void onEnable() { OumLib.init(this); - - config = ConfigManager.of(ShopConfig.class, "shop.yml", + + 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 = Database.sqlite(new File(getDataFolder(), "data.db")); + db.executeUpdate("CREATE TABLE IF NOT EXISTS economy (uuid TEXT PRIMARY KEY, balance INT)"); - db.executeQuery("SELECT balance FROM economy WHERE uuid = ?", player.getUniqueId().toString()) + CommandBuilder.create("shop") + .description("Opens the shop") + .executes(ctx -> { + Player player = ctx.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); + openShop(player, balance); }); - }).register(); + }) + .register(); } - private void openShopMenu(Player player, int balance) { + private void openShop(Player player, int balance) { ChestMenu.builder() - .title("Server Shop | Balance: " + balance + "") + .title("Shop | " + balance + " coins") .rows(3) .pattern( "#########", @@ -167,23 +172,23 @@ public final class ProfileShopPlugin extends JavaPlugin { "#########" ) .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()) + .bind('P', ItemBuilder.of(Material.NETHER_STAR) + .name(config.get().itemTitle()) + .lore("Price: " + config.get().itemPrice() + "") + .build()) .onClick('P', click -> { + Player p = click.player(); if (balance < config.get().itemPrice()) { - Text.send(click.player(), "Insufficient balance!"); - click.player().closeInventory(); + Text.send(p, "Not enough coins!"); + p.closeInventory(); return; } - - int newBalance = balance - config.get().itemPrice(); - db.executeUpdate("INSERT INTO economy (uuid, balance) VALUES (?, ?) ON CONFLICT(uuid) DO UPDATE SET 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()); + int newBal = balance - config.get().itemPrice(); + db.executeUpdate( + "INSERT INTO economy VALUES (?,?) ON CONFLICT(uuid) DO UPDATE SET balance=?", + p.getUniqueId().toString(), newBal, newBal); + Text.send(p, "Purchased!"); + p.closeInventory(); }) .build() .open(player); @@ -191,10 +196,10 @@ public final class ProfileShopPlugin extends JavaPlugin { @Override public void onDisable() { - if (db != null) { - db.close(); - } + if (db != null) db.close(); OumLib.shutdown(); } } + +public record ShopConfig(String itemTitle, int itemPrice) implements ConfigSection {} ``` diff --git a/docs/bridges.md b/docs/bridges.md index f468165..16a3f70 100644 --- a/docs/bridges.md +++ b/docs/bridges.md @@ -1,127 +1,109 @@ -# Integration & Plugin Bridges +# Bridges -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. +`dev.oum.oumlib.bridge` · Paper --- -## Real-world Example: VIP Rank Purchase +## What Are Bridges? -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: +Bridges are wrappers around third-party plugins. They let you interact with Vault, ItemsAdder, Nexo, MMOItems, etc. through a unified API. If the plugin isn't installed, the bridge just returns defaults — no crashes. + +--- + +## Economy + +Works with Vault and PlayerPoints. ```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."); - } - } -} +EconomyBridge eco = EconomyBridge.detect(); + +double balance = eco.getBalance(player); +boolean success = eco.withdraw(player, 100.0); +eco.deposit(player, 50.0); +boolean hasEnough = eco.has(player, 200.0); ``` +If neither Vault nor PlayerPoints is installed, all operations return safe defaults (balance = 0, withdraw = false, etc.). + --- -## Custom Item Bridging +## Items + +Get items from custom item plugins using a single API: + +```java +ItemStack item = ItemBridge.getItem("nexo:ruby_sword"); +ItemStack item = ItemBridge.getItem("itemsadder:custom_gem"); +ItemStack item = ItemBridge.getItem("mmoitems:SWORD:FIRE_BLADE"); +ItemStack item = ItemBridge.getItem("oraxen:amethyst_pickaxe"); +ItemStack item = ItemBridge.getItem("mythicmobs:SkeletonKingSword"); +ItemStack item = ItemBridge.getItem("headdb:12345"); +ItemStack item = ItemBridge.getItem("minecraft:diamond_sword"); +``` + +The prefix before the `:` tells OumLib which provider to use. Supported providers: + +| Prefix | Plugin | +|:--------------|:--------------------------------------| +| `nexo:` | Nexo | +| `itemsadder:` | ItemsAdder | +| `mmoitems:` | MMOItems (format: `mmoitems:TYPE:ID`) | +| `oraxen:` | Oraxen | +| `mythicmobs:` | MythicMobs | +| `headdb:` | HeadDatabase | +| `minecraft:` | Vanilla Minecraft | + +If the plugin isn't installed, `getItem()` returns `null`. + +--- + +## Permissions + +Type-safe permission builder for Paper: + +```java +Permission perm = Permission.builder("myplugin.admin") + .description("Admin access") + .defaultValue(PermissionDefault.OP) + .child("myplugin.admin.ban", true) + .child("myplugin.admin.kick", true) + .build(); +``` + +Use with commands: + +```java +CommandBuilder.create("ban") + .permission(perm) + .executes(ctx -> { /* ... */ }) + .register(); +``` + +### Permission Bridge -Resolve `ItemStack` instances from Minecraft, ItemsAdder, Oraxen, MMOItems, MythicMobs, and Nexo dynamically: +Check and modify permissions at runtime: ```java -import dev.oum.oumlib.bridge.item.ItemBridge; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import java.util.Optional; - -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)); - } -} +PermissionBridge.has(player, "myplugin.vip"); +PermissionBridge.addPermission(player, "myplugin.fly"); +PermissionBridge.removePermission(player, "myplugin.fly"); + +// Group operations (requires LuckPerms or similar) +PermissionBridge.getGroup(player); +PermissionBridge.setGroup(player, "vip"); +PermissionBridge.addGroup(player, "donor"); +PermissionBridge.removeGroup(player, "donor"); ``` --- -## Registering Custom Economy Providers +## Statistics Bridge -Register custom economy tokens or custom coin providers to the global bridge: +Read Minecraft statistics: ```java -import dev.oum.oumlib.bridge.economy.EconomyProvider; -import dev.oum.oumlib.bridge.economy.EconomyBridge; -import org.bukkit.OfflinePlayer; -import org.jspecify.annotations.NonNull; - -public class CustomTokenProvider implements EconomyProvider { - @Override - public @NonNull String name() { - return "customtokens"; - } - - @Override - public boolean has(@NonNull OfflinePlayer player, double amount) { - return getTokens(player) >= amount; - } - - @Override - public boolean withdraw(@NonNull OfflinePlayer player, double amount) { - return modifyTokens(player, -(int) amount); - } - - @Override - public boolean deposit(@NonNull OfflinePlayer player, double amount) { - return modifyTokens(player, (int) amount); - } - - @Override - public double balance(@NonNull OfflinePlayer player) { - return getTokens(player); - } - - private int getTokens(OfflinePlayer player) { - return 1000; - } - - private boolean modifyTokens(OfflinePlayer player, int amount) { - return true; - } -} - -public class TokenInitializer { - public void register() { - EconomyBridge.registerProvider(new CustomTokenProvider()); - } -} +int blocksMined = StatisticsBridge.get(player, Statistic.MINE_BLOCK, Material.DIAMOND_ORE); +int kills = StatisticsBridge.get(player, Statistic.KILL_ENTITY, EntityType.ZOMBIE); +int deaths = StatisticsBridge.get(player, Statistic.DEATHS); +int playTime = StatisticsBridge.get(player, Statistic.PLAY_ONE_MINUTE); // in ticks ``` diff --git a/docs/commands.md b/docs/commands.md index 3ccbefc..aec9a9e 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,154 +1,201 @@ -# Commands & Brigadier Wrapper +# Commands -OumLib features a builder-based wrapper for Brigadier, providing modern command registration with platform-agnostic structures, typed arguments, completions, and cooldowns. +`dev.oum.oumlib.command` · Paper / Velocity --- -## Real-world Example: Warp System +## Basic Command -Here is a warp command system supporting coordinates storage, permissions, a teleportation cooldown, and rich hover tooltips for tab completion suggestions: +```java +CommandBuilder.create("hello") + .description("Says hello") + .executes(ctx -> { + ctx.reply("Hello, world!"); + }) + .register(); +``` + +That's it. Works on both Paper and Velocity. On Paper it registers through Brigadier. On Velocity it uses the Velocity command API. You don't have to care which. + +--- + +## Arguments + +Add typed arguments with the `Arguments` factory: + +```java +CommandBuilder.create("give-coins") + .argument(Arguments.player("target")) + .argument(Arguments.integer("amount", 1, 10000)) + .executes(ctx -> { + Player target = ctx.args().get("target"); + int amount = ctx.args().get("amount"); + ctx.reply("Gave " + amount + " coins to " + target.getName()); + }) + .register(); +``` + +### Available Argument Types + +| Method | Type | Notes | +|:--------------------------------------------|:----------------|:-----------------------------| +| `Arguments.word("name")` | `String` | Single word | +| `Arguments.string("name")` | `String` | Greedy (rest of input) | +| `Arguments.integer("name")` | `Integer` | Optional min/max | +| `Arguments.decimal("name")` | `Double` | Optional min/max | +| `Arguments.bool("name")` | `Boolean` | | +| `Arguments.floatArg("name")` | `Float` | Optional min/max | +| `Arguments.longArg("name")` | `Long` | Optional min/max | +| `Arguments.player("name")` | `Player` | Tab-completes online players | +| `Arguments.players("name")` | `List` | Multiple players | +| `Arguments.offlinePlayer("name")` | `OfflinePlayer` | | +| `Arguments.world("name")` | `World` | Paper only | +| `Arguments.material("name")` | `Material` | | +| `Arguments.enumValue("name", MyEnum.class)` | `Enum` | Any enum class | +| `Arguments.duration("name")` | `Duration` | Parses `1h30m`, `5s`, etc. | +| `Arguments.entity("name")` | `Entity` | Paper only | +| `Arguments.entities("name")` | `List` | Paper only | +| `Arguments.finePosition("name")` | `Location` | Paper only, exact coords | +| `Arguments.blockPosition("name")` | `BlockPosition` | Paper only | +| `Arguments.key("name")` | `NamespacedKey` | Paper only | + +### Custom 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; -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.send(context.sender(), "Console cannot teleport!"); - return; - } - - Player player = context.playerOrThrow(); - String warpName = context.args().get(warpArg); - Location loc = warps.get(warpName); - - if (loc == null) { - Text.send(player, "Warp '" + warpName + "' does not exist!"); - return; - } - - 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(); - } -} +Arguments.word("kit") + .suggests(ctx -> List.of("starter", "warrior", "mage")) ``` --- -## Command Context API Reference +## Subcommands + +```java +CommandBuilder.create("arena") + .subcommand(sub -> sub + .literal("join") + .argument(Arguments.word("name")) + .executes(ctx -> { + String name = ctx.args().get("name"); + ctx.reply("Joining arena: " + name); + }) + ) + .subcommand(sub -> sub + .literal("leave") + .executes(ctx -> ctx.reply("Left the arena")) + ) + .register(); +``` + +--- -The `CommandContext` object represents the execution environment: +## Permissions -- `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. -- `context.reply(Component)`: Sends a pre-built Adventure `Component` to the sender. -- `context.reply(String, TagResolver...)`: Parses a MiniMessage template and sends it. +```java +// Using a Permission object (from bridge module) +CommandBuilder.create("admin") + .permission(Permission.builder("myplugin.admin").build()) + .executes(ctx -> { /* ... */ }) + .register(); +``` --- -## Cooldowns & Bypasses +## Cooldowns + +Built-in cooldown support — no extra wiring needed: -Configure rate-limits per player UUID automatically: ```java -.cooldown(Duration.ofSeconds(10), "Cooldown active: s") +CommandBuilder.create("daily") + .cooldown(Duration.ofHours(24)) + .cooldownMessage("Come back in !") + .executes(ctx -> { + ctx.reply("Here's your daily reward!"); + }) + .register(); ``` -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`) +The `` placeholder gets replaced with a formatted countdown like `23h 59m`. + +You can share a cooldown across commands: + +```java +CooldownManager sharedCooldown = CooldownManager.create(); + +CommandBuilder.create("cmd1") + .cooldown(Duration.ofSeconds(30), sharedCooldown) + // ... + +CommandBuilder.create("cmd2") + .cooldown(Duration.ofSeconds(30), sharedCooldown) + // ... +``` + +To let certain players bypass the cooldown: + +```java +CommandBuilder.create("heal") + .cooldown(Duration.ofMinutes(5)) + .cooldownBypass(ctx -> ctx.sender().hasPermission("myplugin.heal.bypass")) + .executes(ctx -> { /* ... */ }) + .register(); +``` --- -## Command Exception Handling +## Aliases -Configure a fallback error handler globally during OumLib initialization or define builder-specific callbacks: +```java +CommandBuilder.create("teleport") + .aliases("tp", "goto") + .executes(ctx -> { /* ... */ }) + .register(); +``` + +--- + +## Error Handling + +Per-command exception handler: -### Global Command Error Handler ```java -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() + ""); - }); - } -} +CommandBuilder.create("risky") + .onException((ctx, ex) -> { + ctx.reply("That command failed. Check console."); + OumLib.logError("Command /risky failed", ex); + }) + .executes(ctx -> { + // if this throws, the handler above catches it + }) + .register(); ``` -### Builder-Specific Exception Handler +Or set a global handler during init: + ```java -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(); - } -} +OumLib.init(this) + .commandErrorHandler((ctx, ex) -> { + ctx.reply("An error occurred."); + }); ``` + +--- + +## CommandContext + +The `ctx` object passed to your executor has these helpers: + +| Method | What it does | +|:---------------------------------|:----------------------------------------| +| `ctx.sender()` | The `Audience` who ran the command | +| `ctx.isPlayer()` | Whether the sender is a player | +| `ctx.isConsole()` | Whether the sender is the console | +| `ctx.playerOrThrow()` | Returns the player or throws | +| `ctx.args()` | The `ArgumentMap` with parsed arguments | +| `ctx.label()` | The command label used | +| `ctx.reply(miniMessage)` | Sends a MiniMessage string | +| `ctx.reply(component)` | Sends a Component | +| `ctx.sendActionBar(msg)` | Action bar message | +| `ctx.sendTitle(title, subtitle)` | Title screen | +| `ctx.sendTranslated(key)` | Sends a localized message | +| `ctx.clearTitle()` | Clears the title | diff --git a/docs/configuration.md b/docs/configuration.md index f1e2fea..17fc176 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,131 +1,149 @@ -# Configuration System +# Configuration -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. +`dev.oum.oumlib.config` · Paper / Velocity --- -## Real-world Example: Minigame Arena Config +## How It Works -Here is a configuration record for a minigame arena, utilizing comments, nested sections, and default value definitions: +Configs are Java records that implement `ConfigSection`. You define defaults, OumLib writes the YAML file, and gives you a type-safe object to read from. ```java -import dev.oum.oumlib.config.Comment; -import dev.oum.oumlib.config.ConfigSection; - -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 +public record Settings( + String prefix, + int maxPlayers, + boolean debug, + double spawnRadius ) implements ConfigSection {} +``` -public record LobbyLocation( - String world, - double x, - double y, - double z -) implements ConfigSection {} +```java +ConfigManager config = ConfigManager.of( + Settings.class, + "settings.yml", + () -> new Settings("[Server]", 50, false, 10.0) +); +``` -public record ArenaConfig( - @Comment("Database connection pool configuration") - MySQLCredentials database, +This creates `settings.yml` in your plugin's data folder (if it doesn't exist) with the defaults. On load, it reads the file and maps values back into the record. - @Comment("Arena lobby spawn location") - LobbyLocation lobby, +### Reading Values - @Comment("Maximum players allowed inside this arena") - int maxPlayers, +```java +Settings s = config.get(); +String prefix = s.prefix(); +int max = s.maxPlayers(); +``` + +--- + +## Auto-Reload + +Watches the file for changes and reloads automatically: + +```java +ConfigManager config = ConfigManager.of( + Settings.class, "settings.yml", () -> new Settings(/* defaults */) +).enableAutoReload(); +``` + +You can also add a callback when the config reloads: + +```java +config.onReload(newSettings -> { + OumLib.logInfo("Config reloaded! Debug is now: " + newSettings.debug()); +}); +``` + +### Manual Reload + +```java +config.reload(); +``` - @Comment("Whether debug messages are printed to the console") - boolean debugMode +### Save + +Write the current values back to disk: + +```java +config.save(); +``` + +--- + +## Nested Records + +Records inside records work fine: + +```java +public record DatabaseConfig(String host, int port, String database) implements ConfigSection {} + +public record MainConfig( + String serverName, + DatabaseConfig database ) implements ConfigSection {} ``` -This generates the following structured YAML layout automatically: +This produces: + ```yaml -# Database connection pool configuration +server-name: "My Server" database: - # Hostname or IP of the MySQL database - host: "127.0.0.1" - # Database port + host: "localhost" port: 3306 - # Database credentials - username: "root" - password: "password" - -# Arena lobby spawn location -lobby: - world: "world" - x: 0.0 - y: 64.0 - z: 0.0 - -# Maximum players allowed inside this arena -maxPlayers: 16 - -# Whether debug messages are printed to the console -debugMode: true + database: "mydb" ``` --- -## Loading and Auto-Reload Watcher +## Comments -Set up a configuration file mapping and enable background file watch services to automatically re-read values and fire updates: +Use the `@Comment` annotation to add comments above fields in the YAML output: ```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()); - } -} +public record Settings( + @Comment("The prefix shown before messages") + String prefix, + + @Comment("Max players allowed in the arena") + int maxPlayers +) implements ConfigSection {} ``` +Produces: + +```yaml +# The prefix shown before messages +prefix: "[Server]" + +# Max players allowed in the arena +max-players: 50 +``` + +--- + +## Supported Types + +- Primitives: `int`, `double`, `float`, `long`, `short`, `boolean` +- `String` +- `Component` (stored as MiniMessage strings) +- `List`, `List`, `List`, etc. +- `Map` +- Nested `Record` types that implement `ConfigSection` +- Enums + --- -## Configuration Schema Auto-Migration +## Migrations -To modify keys and values as your plugin version upgrades, register sequential version migrations: +For when you rename or restructure config fields between versions: ```java -import dev.oum.oumlib.config.ConfigManager; -import dev.oum.oumlib.config.ConfigMigrationRegistry; - -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)) - ); - } -} +ConfigManager config = ConfigManager.of(Settings.class, "settings.yml", () -> defaults) + .migrations(registry -> { + registry.rename("old-field-name", "new-field-name"); + registry.remove("deprecated-field"); + }); ``` -When OumLib loads the config: -1. It reads the current `config-version` inside the YAML file (defaults to `1` if not found). -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. +> **Note:** Field names in YAML use kebab-case (`max-players`), not camelCase (`maxPlayers`). OumLib handles the conversion automatically. diff --git a/docs/cooldowns.md b/docs/cooldowns.md new file mode 100644 index 0000000..57a751b --- /dev/null +++ b/docs/cooldowns.md @@ -0,0 +1,142 @@ +# Cooldowns + +`dev.oum.oumlib.cooldown` · Paper / Velocity + +--- + +## CooldownManager + +A thread-safe, generic cooldown tracker. Key it on whatever you want — UUIDs, strings, integers, etc. + +```java +CooldownManager cooldowns = CooldownManager.create(); + +// Apply a 30-second cooldown +cooldowns.apply(player.getUniqueId(), Duration.ofSeconds(30)); + +// Check +if (cooldowns.isOnCooldown(player.getUniqueId())) { + String remaining = cooldowns.formatRemaining(player.getUniqueId()); + player.sendMessage("Wait " + remaining); + return; +} +``` + +### Test and Apply (Atomic) + +Checks and applies in one call. Returns `true` if the action was allowed, `false` if on cooldown: + +```java +if (!cooldowns.testAndApply(player.getUniqueId(), Duration.ofSeconds(10))) { + player.sendMessage("Too fast!"); + return; +} +// action goes here +``` + +### Bypass Predicate + +Skip cooldown for certain keys: + +```java +CooldownManager cooldowns = CooldownManager.create() + .bypassPredicate(uuid -> { + Player p = Bukkit.getPlayer(uuid); + return p != null && p.hasPermission("myplugin.cooldown.bypass"); + }); +``` + +### Expiry Listeners + +Run code when a cooldown expires: + +```java +cooldowns.onExpire((uuid, cooldown) -> { + Player p = Bukkit.getPlayer(uuid); + if (p != null) { + Text.send(p, "Your ability is ready again!"); + } +}); +``` + +### Custom Formatter + +```java +cooldowns.defaultFormatter(CooldownFormatter.COMPACT); // "1h 30m 5s" +``` + +### Extend / Reduce + +```java +cooldowns.extend(uuid, Duration.ofSeconds(10)); // add 10s +cooldowns.reduce(uuid, Duration.ofSeconds(5)); // remove 5s +``` + +### Persistent Store + +Save cooldowns across restarts: + +```java +CooldownManager cooldowns = CooldownManager.create() + .store(new MyCooldownStore()); + +// load on startup +cooldowns.loadAllFromStore().join(); +``` + +Implement `CooldownStore` with `save()`, `remove()`, `loadAll()`, and `clear()`. + +### Other Methods + +| Method | What it does | +|:-------------------------|:-----------------------------------| +| `isOnCooldown(key)` | Check if on cooldown | +| `test(key)` | Same as `isOnCooldown` | +| `remainingMillis(key)` | Remaining time in ms | +| `remainingDuration(key)` | Remaining time as `Duration` | +| `formatRemaining(key)` | Formatted string like `2m 30s` | +| `get(key)` | Returns `Optional>` | +| `reset(key)` | Remove the cooldown | +| `cleanUp()` | Remove all expired entries | +| `clear()` | Remove everything | +| `asMap()` | Unmodifiable view of all cooldowns | + +--- + +## RateLimiter + +Token-bucket rate limiter for things like chat spam or action throttling: + +```java +RateLimiter limiter = RateLimiter.create() + .maxTokens(5) + .refillRate(1, Duration.ofSeconds(2)) // 1 token every 2 seconds + .build(); + +if (!limiter.tryConsume(player.getUniqueId())) { + player.sendMessage("Slow down!"); + return; +} +``` + +The bucket starts full. Each action consumes a token. Tokens refill at the rate you set. + +--- + +## Cooldown Record + +The `Cooldown` record holds the data for a single cooldown: + +```java +Cooldown cd = cooldowns.get(uuid).orElse(null); +if (cd != null) { + cd.key(); // the key + cd.startTime(); // when it was applied + cd.expireTime(); // when it expires + cd.remainingMillis(); // ms left + cd.remainingDuration();// Duration left + cd.isExpired(); // true if expired + cd.isActive(); // true if still going + cd.metadata(); // optional attached data +} +``` diff --git a/docs/database.md b/docs/database.md index 25ac627..9b55e80 100644 --- a/docs/database.md +++ b/docs/database.md @@ -1,105 +1,115 @@ -# Database Tools +# Database -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. +`dev.oum.oumlib.database` · Paper / Velocity --- -## Real-world Example: Player Profile Management +## Creating a Database -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 ```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; +Database db = Database.sqlite(new File(getDataFolder(), "data.db")); +``` -public record UserProfile(String uuid, int coins, int level) {} +### MySQL -public final class ProfileDatabaseManager { - private final Database db; +```java +Database db = Database.mysql("localhost", 3306, "mydb", "user", "password"); +``` - 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)"); - } +Both use HikariCP connection pooling under the hood. - 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)"); - }); - } +--- - 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!")); - } +## Queries - 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() + "") - ); - } +All queries are async by default and return a `CompletableFuture`. - public void close() { - db.close(); - } -} +### Execute Update (INSERT, UPDATE, DELETE, CREATE) + +```java +db.executeUpdate("CREATE TABLE IF NOT EXISTS players (uuid TEXT PRIMARY KEY, name TEXT, coins INT)"); + +db.executeUpdate("INSERT INTO players (uuid, name, coins) VALUES (?, ?, ?)", + player.getUniqueId().toString(), player.getName(), 500); + +db.executeUpdate("UPDATE players SET coins = coins + ? WHERE uuid = ?", + 100, player.getUniqueId().toString()); ``` +### Execute Query (SELECT) + +```java +db.executeQuery("SELECT * FROM players WHERE uuid = ?", uuid.toString()) + .thenAcceptSync(rows -> { + if (!rows.isEmpty()) { + Map row = rows.getFirst(); + int coins = (int) row.get("coins"); + String name = (String) row.get("name"); + player.sendMessage("You have " + coins + " coins"); + } + }); +``` + +Each row is a `Map`. Column names are the keys. + --- -## Connection Setup Reference +## Sync Callback -Establish optimized database connection pools: +`.thenAcceptSync()` runs the callback on the main thread — safe for Bukkit API calls: -### SQLite Connection ```java -import dev.oum.oumlib.database.Database; -import java.io.File; +db.executeQuery("SELECT coins FROM players WHERE uuid = ?", uuid.toString()) + .thenAcceptSync(rows -> { + // this runs on the main thread + player.teleport(spawn); + }); +``` + +--- -public class SqliteConfig { - public Database init(File dataFolder) { - return Database.sqlite(new File(dataFolder, "data.db")); +## Transactions + +```java +db.transaction(connection -> { + try (var stmt = connection.prepareStatement("UPDATE players SET coins = coins - ? WHERE uuid = ?")) { + stmt.setInt(1, 100); + stmt.setString(2, buyerUuid); + stmt.executeUpdate(); } -} + try (var stmt = connection.prepareStatement("UPDATE players SET coins = coins + ? WHERE uuid = ?")) { + stmt.setInt(1, 100); + stmt.setString(2, sellerUuid); + stmt.executeUpdate(); + } +}); ``` -### MySQL Connection +If anything throws, the transaction rolls back. + +--- + +## Batch Operations + ```java -import dev.oum.oumlib.database.Database; - -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"); - }); +db.executeBatch("INSERT INTO players (uuid, name, coins) VALUES (?, ?, ?)", batch -> { + for (Player p : Bukkit.getOnlinePlayers()) { + batch.add(p.getUniqueId().toString(), p.getName(), 0); } +}); +``` + +--- + +## Close + +Always close the database when your plugin disables: + +```java +@Override +public void onDisable() { + if (db != null) db.close(); } ``` diff --git a/docs/effects.md b/docs/effects.md index 9bc705d..5997d0c 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -1,94 +1,90 @@ -# Visual & Sound Effects +# Effects -OumLib provides dynamic, chainable builders for playing particles and sound effects under the `dev.oum.oumlib.effect` package. +`dev.oum.oumlib.effect` · Paper --- -## Real-world Example: Level Up Helix +## Particles -Play a chime sound and render a golden spiral helix around a player whenever they level up: +Play particles with a fluent API: ```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) - ); - } -} +Effects.particle(Particle.FLAME) + .count(20) + .offset(0.5, 0.5, 0.5) + .speed(0.1) + .spawn(location); +``` + +Spawn at a player's location: + +```java +Effects.particle(Particle.HEART) + .count(5) + .offset(0.3, 0.5, 0.3) + .spawn(player.getLocation()); +``` + +--- + +## Sounds + +```java +Effects.sound(Sound.ENTITY_PLAYER_LEVELUP) + .volume(1.0f) + .pitch(1.2f) + .play(player); +``` + +Play at a location (everyone nearby hears it): + +```java +Effects.sound(Sound.BLOCK_NOTE_BLOCK_PLING) + .volume(0.8f) + .pitch(2.0f) + .play(location); ``` --- -## Real-world Example: Gun Bullet Tracer +## Particle Shapes -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: +### Line + +Draw a line of particles between two points: + +```java +Effects.line(start, end, Particle.FLAME, 20); // 20 points along the line +``` + +### Circle + +```java +Effects.circle(center, radius, Particle.ENCHANT, 30); // 30 points around the circle +``` + +### Helix ```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 - ); - } -} +Effects.helix(center, radius, height, Particle.FLAME, rotations, points); +``` + +### Bezier Curve + +```java +Effects.bezier(start, control, end, Particle.FLAME, 30); ``` --- -## Static Sound & Particle Spawners +## Combining -Trigger audio files or play standard dust options using fast static shortcuts: +Spawn multiple effects at once: ```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); - } -} +Location loc = player.getLocation(); + +Effects.sound(Sound.ENTITY_PLAYER_LEVELUP).volume(1f).pitch(1.2f).play(player); +Effects.particle(Particle.HAPPY_VILLAGER).count(15).offset(0.5, 0.5, 0.5).spawn(loc); +Effects.particle(Particle.FIREWORK).count(5).offset(0.2, 0.2, 0.2).speed(0.05).spawn(loc); ``` diff --git a/docs/entities.md b/docs/entities.md index f1d29a3..c65a24a 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -1,81 +1,127 @@ -# Entity Utilities & Display Builders +# Display Entities -OumLib offers high-level entity utilities and a fluent display entity generator under the `dev.oum.oumlib.entity` package. +`dev.oum.oumlib.entity.display` · Paper --- -## Real-world Example: Holographic Stat Billboard +## DisplayBuilder -Spawn a billboard-aligned text hologram floating above an NPC's head displaying player rankings: +Build text, item, and block display entities with a fluent API. These are real server-side entities (1.19.4+), unlike holograms which are packet-based. + +### Text Display + +```java +DisplayBuilder.text() + .location(location) + .text("Hello World!") + .backgroundColor(Color.fromARGB(128, 0, 0, 0)) // semi-transparent black + .billboard(Display.Billboard.CENTER) + .scale(1.5f, 1.5f, 1.5f) + .spawn(); +``` + +### Item Display + +```java +DisplayBuilder.item() + .location(location) + .item(new ItemStack(Material.DIAMOND_SWORD)) + .transform(ItemDisplay.ItemDisplayTransform.FIXED) + .billboard(Display.Billboard.VERTICAL) + .scale(2f, 2f, 2f) + .spawn(); +``` + +### Block Display + +```java +DisplayBuilder.block() + .location(location) + .block(Material.DIAMOND_BLOCK.createBlockData()) + .scale(0.5f, 0.5f, 0.5f) + .spawn(); +``` + +--- + +## Common Options + +All display types share these: + +| Method | What it does | +|:--------------------------------|:--------------------------------------------| +| `.location(loc)` | Where to spawn | +| `.scale(x, y, z)` | Size multiplier | +| `.translation(x, y, z)` | Offset from origin | +| `.billboard(type)` | `FIXED`, `CENTER`, `VERTICAL`, `HORIZONTAL` | +| `.glowing(bool)` | Glowing outline | +| `.glowColor(color)` | Glow color | +| `.shadow(radius, strength)` | Shadow settings | +| `.viewRange(float)` | How far away players can see it | +| `.interpolationDuration(ticks)` | Smooth animation duration | +| `.spawn()` | Spawns the entity and returns it | + +--- + +## Virtual Displays + +Packet-based displays that only specific players can see. No real entity on the server. + +### Virtual Text Display + +```java +VirtualTextDisplay display = VirtualTextDisplay.create(location) + .text("Only you can see this!") + .billboard(Display.Billboard.CENTER) + .show(player); +``` + +### Virtual Item Display + +```java +VirtualItemDisplay display = VirtualItemDisplay.create(location) + .item(new ItemStack(Material.GOLDEN_APPLE)) + .show(player); +``` + +### Virtual Block Display + +```java +VirtualBlockDisplay display = VirtualBlockDisplay.create(location) + .block(Material.EMERALD_BLOCK.createBlockData()) + .show(player); +``` + +### Update / Move / Destroy ```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(); - } -} +display.teleport(newLocation); +display.destroy(player); +display.destroyAll(); ``` --- -## Real-world Example: Spinning In-Game Shop Showcase +## Display Animations -Spawns a glowing, double-sized block display (e.g. Diamond Block) floating above a shop chest, rotated at a 45-degree angle: +Animate display entities with interpolation: ```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(); - } -} +DisplayAnimation.animate(display) + .scale(2f, 2f, 2f) + .translation(0f, 2f, 0f) + .duration(20) // ticks + .play(); ``` --- -## Entities Target Raytracing +## Entities Helper -Perform entity scans or trace the exact block a player is aiming at: +The `Entities` utility class has helpers for working with entities: ```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); - } -} +Entities.nearbyPlayers(location, 10); // players within 10 blocks +Entities.nearbyEntities(location, 5, type); // entities of a type +Entities.closestPlayer(location, 50); // nearest player ``` diff --git a/docs/events.md b/docs/events.md index ee0b6f7..bc47cbf 100644 --- a/docs/events.md +++ b/docs/events.md @@ -1,112 +1,169 @@ -# Events & Listeners Bus +# Events -OumLib features a modern, fluent event bus wrapper. It provides cleaner listener registration, conditional filtering, execution boundaries, and unregistration hooks. +`dev.oum.oumlib.event` · Paper / Velocity --- -## Real-world Example: Combat Tagging System +## Listening to Events -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 +Events.listen(PlayerJoinEvent.class, event -> { + event.getPlayer().sendMessage("Welcome!"); +}); +``` + +That's it. No `@EventHandler`, no `implements Listener`, no registration boilerplate. + +--- + +## Builder Style + +For more control, use the builder: ```java -import dev.oum.oumlib.event.Events; -import dev.oum.oumlib.text.Text; -import org.bukkit.entity.Player; -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)); - } - } -} +Events.listen(PlayerMoveEvent.class) + .ignoreCancelled() + .filter(e -> e.hasChangedBlock()) + .handler(event -> { + // only fires when the player actually moves to a new block + }); ``` --- -## Cancellable Events & State Handling +## Filters + +Chain multiple filters: -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 +Events.listen(EntityDamageByEntityEvent.class) + .filter(e -> e.getDamager() instanceof Player) + .filter(e -> e.getDamage() > 5.0) + .handler(event -> { + Player attacker = (Player) event.getDamager(); + attacker.sendMessage("Big hit!"); + }); +``` + +### Player Filter Shortcut ```java -import dev.oum.oumlib.event.Events; -import org.bukkit.event.block.BlockBreakEvent; - -public class BlockLogger { - public void register() { - Events.listen(BlockBreakEvent.class) - .ignoreCancelled() - .handler(event -> { - System.out.println("Block broken: " + event.getBlock().getType()); - }); - } -} +Events.listen(PlayerInteractEvent.class) + .playerFilter(PlayerInteractEvent::getPlayer, p -> p.hasPermission("myplugin.use")) + .handler(event -> { + // only fires for players with the permission + }); +``` + +--- + +## One-Shot Listeners + +Fire once and automatically unregister: + +```java +Events.listenOnce(PlayerJoinEvent.class, event -> { + Bukkit.broadcastMessage("First player joined!"); +}); +``` + +Or with the builder: + +```java +Events.listen(PlayerDeathEvent.class) + .maxFires(1) + .handler(event -> { + // fires once, then unregisters + }); +``` + +--- + +## Expiry + +Auto-unregister after a duration: + +```java +Events.listen(PlayerMoveEvent.class) + .expireAfter(Duration.ofMinutes(5)) + .handler(event -> { + // only active for 5 minutes + }); +``` + +Or expire on a condition: + +```java +Events.listen(PlayerMoveEvent.class) + .expireIf(event -> someGameState.isOver()) + .handler(event -> { + // unregisters when the game ends + }); +``` + +--- + +## Priority + +```java +Events.listen(PlayerJoinEvent.class) + .priority(EventPriority.HIGH) + .handler(event -> { /* ... */ }); +``` + +Available: `LOWEST`, `LOW`, `NORMAL`, `HIGH`, `HIGHEST`, `MONITOR`. + +--- + +## Cancelled Events + +```java +// Skip cancelled events (default Bukkit behavior) +Events.listen(PlayerInteractEvent.class) + .ignoreCancelled() + .handler(event -> { /* ... */ }); + +// Only run if the event WAS cancelled +Events.listen(PlayerInteractEvent.class) + .onlyIfCancelled() + .handler(event -> { /* ... */ }); +``` + +--- + +## Async + +Run the handler off the main thread: + +```java +Events.listen(AsyncChatEvent.class) + .async() + .handler(event -> { + // runs async + }); +``` + +--- + +## Unregistering + +The `handler()` call returns a `ListenerHandle`: + +```java +ListenerHandle handle = Events.listen(PlayerMoveEvent.class, event -> { /* ... */ }); + +// later +handle.unregister(); ``` --- -## Asynchronous Thread Listeners +## Velocity Events -Offload heavy I/O calculations (like database calls) to async thread pools: +Works the same way on Velocity: ```java -import dev.oum.oumlib.event.Events; -import org.bukkit.event.player.PlayerInteractEvent; - -public class AsyncLogger { - public void register() { - Events.listen(PlayerInteractEvent.class) - .async() - .handler(event -> { - System.out.println("Processing heavy interaction logs on virtual threads."); - }); - } -} +Events.listen(PostLoginEvent.class, event -> { + event.getPlayer().sendMessage(Component.text("Welcome to the network!")); +}); ``` -*Note: Event modification/cancellation is not supported in async mode.* diff --git a/docs/holograms.md b/docs/holograms.md new file mode 100644 index 0000000..9904151 --- /dev/null +++ b/docs/holograms.md @@ -0,0 +1,105 @@ +# Holograms + +`dev.oum.oumlib.entity.hologram` · Paper + +--- + +## Creating a Hologram + +Holograms are packet-based — they don't exist as real entities on the server. Only the player(s) you show them to can see them. + +```java +Hologram hologram = Hologram.builder() + .location(spawnLocation) + .line("Welcome to the Server!") + .line("Online: " + Bukkit.getOnlinePlayers().size()) + .build(); +``` + +### Show / Hide + +```java +hologram.show(player); // show to one player +hologram.showAll(); // show to all online players +hologram.hide(player); // hide from one player +hologram.hideAll(); // hide from everyone +``` + +--- + +## Updating Lines + +```java +hologram.setLine(0, "Updated Title!"); +hologram.setLine(1, "Players: " + count); +``` + +Lines are 0-indexed from the top. + +### Add / Remove Lines + +```java +hologram.addLine("New line at the bottom"); +hologram.removeLine(2); +``` + +--- + +## Moving + +```java +hologram.teleport(newLocation); +``` + +--- + +## Click Handling + +Handle when players click (interact with) the hologram: + +```java +Hologram hologram = Hologram.builder() + .location(location) + .line("[Click Me]") + .onClick(player -> player.sendMessage("You clicked the hologram!")) + .build(); +``` + +--- + +## Auto-Registration + +Register with the hologram registry for automatic visibility management: + +```java +OumLib.holograms().register("spawn-hologram", hologram); +``` + +Registered holograms are automatically shown to players when they join and hidden when they quit. They're also cleaned up on `OumLib.shutdown()`. + +### Unregister + +```java +OumLib.holograms().unregister("spawn-hologram"); +``` + +--- + +## Updating on a Timer + +A common pattern — update hologram text every few seconds: + +```java +Hologram holo = Hologram.builder() + .location(location) + .line("Server Stats") + .line("Loading...") + .build(); + +OumLib.holograms().register("stats", holo); + +Scheduler.runRepeating(Duration.ZERO, Duration.ofSeconds(5), () -> { + holo.setLine(1, "Online: " + Bukkit.getOnlinePlayers().size()); + holo.setLine(0, "TPS: " + String.format("%.1f", Bukkit.getTPS()[0])); +}); +``` diff --git a/docs/inventories.md b/docs/inventories.md index fa6fc7a..f6b4415 100644 --- a/docs/inventories.md +++ b/docs/inventories.md @@ -1,119 +1,199 @@ -# GUI & Chest Menus +# Inventories -OumLib includes a simple, lightweight inventory menu system for Paper/Bukkit. It uses layout patterns, item bindings, and click handlers to build custom menus. +`dev.oum.oumlib.inventory` · Paper --- -## Real-world Example: Virtual Coin Shop +## ChestMenu -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: +Build a chest GUI with a pattern layout: ```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.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; - -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( - "#########", - "# G S #", - "#########" - ) - .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; - } - 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()); - }) - .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); - - 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() - .open(player); - } -} +ChestMenu.builder() + .title("Warps") + .rows(3) + .pattern( + "#########", + "# A B C #", + "#########" + ) + .bind('#', ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build()) + .bind('A', ItemBuilder.of(Material.GRASS_BLOCK).name("Spawn").build()) + .bind('B', ItemBuilder.of(Material.NETHERRACK).name("Nether").build()) + .bind('C', ItemBuilder.of(Material.END_STONE).name("End").build()) + .onClick('A', click -> click.player().performCommand("warp spawn")) + .onClick('B', click -> click.player().performCommand("warp nether")) + .onClick('C', click -> click.player().performCommand("warp end")) + .build() + .open(player); +``` + +### Click Handlers + +Each bound character can have a click handler. The `ClickContext` gives you: + +```java +.onClick('X', click -> { + Player p = click.player(); // the player who clicked + ClickAction action = click.action(); // LEFT, RIGHT, SHIFT_LEFT, etc. + ItemStack item = click.item(); // the clicked item +}) +``` + +### Close Handler + +```java +.onClose(player -> { + player.sendMessage("Menu closed!"); +}) +``` + +### Prevent Taking Items + +By default, players can't take items from the menu. The whole inventory is locked. + +--- + +## PaginatedMenu + +For when you have a list of items and want pages: + +```java +List items = getShopItems(); // your list + +PaginatedMenu.builder() + .title("Shop - Page /") + .rows(6) + .items(items) + .contentSlots(Layout.rectangle(1, 1, 4, 7)) // rows 1-4, columns 1-7 + .previousButton(ItemBuilder.of(Material.ARROW).name("Previous Page").build(), 45) + .nextButton(ItemBuilder.of(Material.ARROW).name("Next Page").build(), 53) + .border(ItemBuilder.of(Material.BLACK_STAINED_GLASS_PANE).name(" ").build()) + .onItemClick((click, item) -> { + click.player().sendMessage("You clicked: " + item.getType()); + }) + .build() + .open(player); +``` + +The `` and `` placeholders in the title get replaced automatically. + +--- + +## ItemBuilder + +Fluent builder for creating items: + +```java +ItemStack sword = ItemBuilder.of(Material.DIAMOND_SWORD) + .name("Frost Blade") + .lore( + "A blade forged in ice.", + "", + "+15 Attack Damage" + ) + .enchant(Enchantment.SHARPNESS, 5) + .unbreakable(true) + .modelData(1001) + .amount(1) + .glow(true) + .build(); +``` + +### ItemBuilder Methods + +| Method | What it does | +|:---------------------------|:-------------------------------------------| +| `.name(miniMessage)` | Display name | +| `.lore(lines...)` | Lore lines (MiniMessage) | +| `.enchant(enchant, level)` | Add enchantment | +| `.unbreakable(bool)` | Set unbreakable | +| `.modelData(int)` | Custom model data | +| `.amount(int)` | Stack size | +| `.glow(bool)` | Enchantment glint without visible enchants | +| `.flags(flags...)` | Item flags | +| `.rarity(ItemRarity)` | Item rarity | +| `.maxStackSize(int)` | Override max stack size | +| `.skull(player)` | Player head | +| `.skullTexture(base64)` | Custom skull texture | +| `.skullUrl(url)` | Skull from URL | +| `.pdc(key, value)` | Store persistent data | +| `.pdc(key, component)` | Store a Component in PDC | +| `.meta(consumer)` | Modify raw ItemMeta | +| `.build()` | Creates the ItemStack | + +### Skull Heads + +```java +// Player head +ItemBuilder.of(Material.PLAYER_HEAD).skull(player).build(); + +// Custom texture via base64 +ItemBuilder.of(Material.PLAYER_HEAD) + .skullTexture("eyJ0ZXh0dXJlcyI6ey...") + .build(); + +// Custom texture via URL +ItemBuilder.of(Material.PLAYER_HEAD) + .skullUrl("https://textures.minecraft.net/texture/abc123") + .build(); +``` + +--- + +## DataComponents + +Paper 1.20.6+ data component access: + +```java +DataComponents.maxStackSize(item, 99); +DataComponents.rarity(item, ItemRarity.EPIC); +DataComponents.enchantGlint(item, true); +DataComponents.fireResistant(item, true); +DataComponents.hideTooltip(item, true); +DataComponents.unbreakable(item, true); +``` + +--- + +## ItemSerializer + +Convert items to/from Base64 for storage: + +```java +String encoded = ItemSerializer.toBase64(itemStack); +ItemStack decoded = ItemSerializer.fromBase64(encoded); +``` + +Also works with arrays: + +```java +String encoded = ItemSerializer.arrayToBase64(itemArray); +ItemStack[] decoded = ItemSerializer.arrayFromBase64(encoded); ``` --- -## Real-world Example: Server Selector +## PotionSerializer -Here is a multi-lobby server selector utilizing the `PaginatedMenu` controller to automatically distribute servers across pages: +Serialize/deserialize potion effects: ```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; - -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); - } -} +String json = PotionSerializer.serialize(potionEffect); +PotionEffect effect = PotionSerializer.deserialize(json); + +// Lists +String json = PotionSerializer.serializeList(effects); +List effects = PotionSerializer.deserializeList(json); ``` --- -## Click Protection Safeguards +## Layout -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. +Helper for generating slot lists: + +```java +List slots = Layout.rectangle(startRow, startCol, endRow, endCol); +List border = Layout.border(rows); +``` diff --git a/docs/math.md b/docs/math.md index 1b8dfaf..435205d 100644 --- a/docs/math.md +++ b/docs/math.md @@ -1,129 +1,217 @@ -# Mathematical Utilities +# Math -OumLib features a platform-independent mathematical package `dev.oum.oumlib.math` optimized for 3D physics, spatial partitioning, expressions evaluation, and noise generation. +`dev.oum.oumlib.math` · Paper / Velocity --- -## 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; - } -} +## Vector2D + +Immutable 2D vector (x, z) — useful for flat-plane calculations like polygon regions, map coordinates, or distance checks ignoring Y. + +```java +Vector2D a = Vector2D.of(10, 20); +Vector2D b = Vector2D.of(30, 40); + +Vector2D sum = a.add(b); // (40, 60) +Vector2D diff = a.subtract(b); // (-20, -20) +Vector2D scaled = a.multiply(2); // (20, 40) +double dist = a.distance(b); // distance between two points +double dot = a.dot(b); +Vector2D norm = a.normalize(); +Vector2D mid = a.lerp(b, 0.5); // midpoint +``` + +Convert to/from Bukkit types: + +```java +Vector2D from = Vector2D.fromLocation(location); +Location loc = from.toLocation(world, 64.0); // add Y ``` --- -## Real-world Example: Dynamic Skill Damage Evaluation +## Vector3D -Evaluate dynamic math formulas loaded from config files (e.g. `base_dmg * (1.5 ^ level)`), replacing variables with active player levels dynamically: +Immutable 3D vector. Same idea as Bukkit's `Vector` but immutable and with more math. ```java -import dev.oum.oumlib.math.MathEval; -import dev.oum.oumlib.text.Text; -import org.bukkit.entity.Player; -import java.util.Map; +Vector3D a = Vector3D.fromLocation(location); +Vector3D b = Vector3D.fromEntity(entity); + +Vector3D sum = a.add(b); +Vector3D cross = a.cross(b); +double dot = a.dot(b); +double dist = a.distance(b); +Vector3D norm = a.normalize(); +Vector3D lerped = a.lerp(b, 0.5); +Vector3D rotated = a.rotateY(Math.toRadians(45)); +``` -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 - )); +Convert back: - Text.send(player, "You dealt " + dmg + " damage!"); - } -} +```java +Vector bukkit = v.toBukkitVector(); +Location loc = v.toLocation(world); +``` + +Binary I/O: + +```java +v.write(dataOutputStream); +Vector3D v = Vector3D.read(dataInputStream); ``` --- -## Easing Animations +## Volume3D -Standard mathematical easing functions for UI displays: +Axis-aligned bounding box defined by two corners: ```java -import dev.oum.oumlib.math.Easing; +Volume3D box = Volume3D.of(min, max); -public final class AnimationPlotter { - public double getProgress(double time) { - return Easing.BOUNCE_OUT.apply(time); - } -} +boolean inside = box.contains(point); +boolean overlaps = box.intersects(otherBox); +Volume3D expanded = box.expand(2.0); +Vector3D center = box.center(); ``` --- -## Vector3D Reference +## FastMath -Immutable Vector operations: +Common math operations without the overhead of `Math.`: ```java -import dev.oum.oumlib.math.Vector3D; +FastMath.clamp(value, min, max); +FastMath.lerp(a, b, t); +FastMath.floor(double); +FastMath.ceil(double); +FastMath.round(double, decimals); +FastMath.sq(x); // x * x +FastMath.distanceSquared(x1, y1, z1, x2, y2, z2); +``` -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()); - } -} +## Noise + +Perlin and simplex noise for terrain generation, particle effects, or anything that needs smooth randomness: + +```java +double val = Noise.perlin2D(x, z, seed, frequency); +double val = Noise.simplex2D(x, z, seed); +double val = Noise.perlin3D(x, y, z, seed, frequency); + +// Octave noise for more detail +double val = Noise.fractal2D(x, z, seed, octaves, frequency, lacunarity, persistence); +``` + +--- + +## Easing + +Easing functions for animations: + +```java +double t = Easing.easeInOutCubic(progress); // 0.0 to 1.0 +double t = Easing.easeOutBounce(progress); +double t = Easing.easeInElastic(progress); +``` + +Available: `linear`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeInBack`, `easeOutBack`, `easeInOutBack`, `easeInElastic`, `easeOutElastic`, `easeInBounce`, `easeOutBounce`, `easeInOutBounce`. + +--- + +## MathEval + +Evaluate math expressions from strings: + +```java +double result = MathEval.evaluate("2 + 3 * 4"); // 14.0 +double result = MathEval.evaluate("sin(pi / 2)"); // 1.0 +double result = MathEval.evaluate("sqrt(144)"); // 12.0 +``` + +Supports: `+`, `-`, `*`, `/`, `^`, `%`, parentheses, `sin`, `cos`, `tan`, `sqrt`, `abs`, `floor`, `ceil`, `round`, `min`, `max`, `pi`, `e`. + +--- + +## Geometry3D + +3D geometry helpers: + +```java +Geometry3D.rotateAroundY(point, origin, angle); +Geometry3D.rotateAroundX(point, origin, angle); +Geometry3D.closestPointOnLine(lineStart, lineEnd, point); +Geometry3D.distanceToLine(lineStart, lineEnd, point); ``` --- -## Loot Table Chance Rolles +## Locations -Determine loot rewards using weights: +Location serialization and utilities: ```java -import dev.oum.oumlib.math.Chance; -import dev.oum.oumlib.math.WeightedSelector; -import java.util.Map; +String serialized = Locations.serialize(location); // "world,10.5,64.0,20.3,90.0,0.0" +Location loc = Locations.deserialize(serialized); -public final class LootRoller { - public String rollReward() { - if (Chance.percent(5.0)) { - return "special_crate"; - } +Locations.center(location); // center of the block +Locations.blockLocation(loc); // snap to block coords +``` - WeightedSelector selector = WeightedSelector.of(Map.of( - "gold", 70.0, - "diamond", 25.0, - "netherite", 5.0 - )); - return selector.select(); - } +--- + +## Chance + +Random chance checks: + +```java +if (Chance.percent(25)) { + // 25% chance to run } + +if (Chance.oneIn(10)) { + // 1 in 10 chance +} +``` + +--- + +## Matrix3 + +3x3 rotation matrix: + +```java +Matrix3 rot = Matrix3.rotationY(Math.toRadians(45)); +Vector3D rotated = rot.multiply(vector); +``` + +--- + +## Quaternion + +Quaternion rotations: + +```java +Quaternion q = Quaternion.fromAxisAngle(0, 1, 0, Math.toRadians(90)); +Vector3D rotated = q.rotate(vector); +Quaternion combined = q1.multiply(q2); +``` + +--- + +## WeightedSelector + +Pick random items with weights: + +```java +WeightedSelector selector = WeightedSelector.create() + .add("common", 60) + .add("rare", 30) + .add("legendary", 10); + +String picked = selector.select(); // weighted random pick ``` diff --git a/docs/metadata.md b/docs/metadata.md new file mode 100644 index 0000000..d9555d2 --- /dev/null +++ b/docs/metadata.md @@ -0,0 +1,63 @@ +# Metadata + +`dev.oum.oumlib.pdc.metadata` · Paper + +--- + +## VolatileData + +In-memory key-value store attached to players, entities, or any UUID. Unlike PDC, this data is **not saved to disk** — it's gone when the server stops or the player quits. + +Good for temporary state: combat tags, cooldown flags, GUI state, session data. + +```java +VolatileData.set(player, "in-combat", true); +VolatileData.set(player, "last-hit-time", System.currentTimeMillis()); + +boolean inCombat = VolatileData.get(player, "in-combat", false); +``` + +--- + +## Auto-Cleanup + +Volatile data for a player is automatically removed when they quit. No manual cleanup needed. + +--- + +## TTL (Time-To-Live) + +Set data that expires after a duration: + +```java +VolatileData.set(player, "speed-boost", true, Duration.ofSeconds(30)); + +// 30 seconds later, "speed-boost" is automatically removed +``` + +--- + +## Remove / Check + +```java +VolatileData.remove(player, "in-combat"); +boolean has = VolatileData.has(player, "in-combat"); +``` + +--- + +## Clear + +```java +VolatileData.clear(player); // remove all data for this player +VolatileData.clearAll(); // remove everything (called on shutdown) +``` + +--- + +## Use Cases + +- **Combat tagging:** Set a `"combat"` flag when a player attacks. Remove it after 15 seconds. If they try to log out while tagged, cancel it. +- **GUI state:** Store what page a player is on in a paginated menu. +- **Temporary buffs:** Mark a player as having a speed boost for 30 seconds. +- **Cooldown flags:** Quick boolean checks without the full `CooldownManager` overhead. diff --git a/docs/pdc.md b/docs/pdc.md new file mode 100644 index 0000000..09f64f8 --- /dev/null +++ b/docs/pdc.md @@ -0,0 +1,192 @@ +# PDC (Persistent Data Container) + +`dev.oum.oumlib.pdc` · Paper + +--- + +## PDC Helper + +Read and write persistent data on any block, entity, or item without dealing with `NamespacedKey` and `PersistentDataType` every time. + +```java +PDC.set(player, "coins", 500); +int coins = PDC.get(player, "coins", PersistentDataType.INTEGER, 0); + +PDC.set(player, "vip", true); +boolean vip = PDC.get(player, "vip", PersistentDataType.BOOLEAN, false); + +PDC.remove(player, "coins"); +PDC.has(player, "coins"); +``` + +--- + +## DataKey + +Type-safe keys so you never mix up types: + +```java +DataKey COINS = DataKey.ofInt("coins"); +DataKey RANK = DataKey.ofString("rank"); +DataKey VIP = DataKey.ofBoolean("vip"); +DataKey MULTIPLIER = DataKey.ofDouble("multiplier"); +``` + +Then use them: + +```java +PDC.set(player, COINS, 500); +int coins = PDC.get(player, COINS, 0); + +PDC.set(player, RANK, "warrior"); +String rank = PDC.get(player, RANK, "default"); +``` + +### Available DataKey Types + +| Factory | Stored Type | Java Type | +|:-----------------------------|:--------------|:----------| +| `DataKey.ofInt("key")` | INTEGER | `int` | +| `DataKey.ofString("key")` | STRING | `String` | +| `DataKey.ofBoolean("key")` | BOOLEAN | `boolean` | +| `DataKey.ofDouble("key")` | DOUBLE | `double` | +| `DataKey.ofFloat("key")` | FLOAT | `float` | +| `DataKey.ofLong("key")` | LONG | `long` | +| `DataKey.ofByte("key")` | BYTE | `byte` | +| `DataKey.ofByteArray("key")` | BYTE_ARRAY | `byte[]` | +| `DataKey.ofIntArray("key")` | INTEGER_ARRAY | `int[]` | +| `DataKey.ofLongArray("key")` | LONG_ARRAY | `long[]` | + +--- + +## PdcModel + +Map a Java record to PDC storage. Define a record, and OumLib handles serialization/deserialization: + +```java +public record PlayerStats(int kills, int deaths, double kdr) {} + +PdcModel model = PdcModel.of(PlayerStats.class, "stats"); + +// Save +model.set(player, new PlayerStats(10, 3, 3.33)); + +// Load +PlayerStats stats = model.get(player); +if (stats != null) { + int kills = stats.kills(); +} + +// Remove +model.remove(player); +``` + +Works with nested records too. Stored as JSON under the hood. + +--- + +## PdcHolder + +Wrap any PDC holder for a fluent API: + +```java +PdcHolder holder = PdcHolder.of(player); + +holder.setInt("level", 5); +holder.setString("class", "mage"); +holder.setBoolean("active", true); +holder.setDouble("speed", 1.5); + +int level = holder.getInt("level", 0); +String cls = holder.getString("class", "none"); + +// Components +holder.setComponent("display-name", Text.parse("Steve")); +Component name = holder.getComponent("display-name"); + +// Lists +holder.setList("friends", List.of("Alex", "Steve")); +List friends = holder.getList("friends"); +``` + +### Change Listeners + +Get notified when PDC values change: + +```java +PDC.addListener((holder, key, oldValue, newValue) -> { + OumLib.logInfo("PDC changed: " + key + " = " + newValue); +}); +``` + +--- + +## PdcItem + +Same as PdcHolder but for items: + +```java +PdcItem item = PdcItem.of(itemStack); + +item.setInt("durability", 100); +int dur = item.getInt("durability", 0); + +// Don't forget — item PDC returns a new ItemStack +ItemStack updated = item.toItemStack(); +``` + +--- + +## PdcProperty + +Observable, type-safe PDC property: + +```java +PdcProperty level = PdcProperty.ofInt("level", 1); + +level.set(player, 5); +int lvl = level.get(player); + +level.onChange((p, oldVal, newVal) -> { + Text.send(p, "Level up! " + oldVal + " → " + newVal); +}); +``` + +--- + +## PdcFlags + +Bitfield flags stored as a single integer in PDC: + +```java +PdcFlags flags = PdcFlags.of("player-flags"); + +int FLY = 0; +int VANISH = 1; +int GOD = 2; + +flags.set(player, FLY, true); +flags.set(player, VANISH, true); + +boolean canFly = flags.get(player, FLY); // true +boolean isGod = flags.get(player, GOD); // false +``` + +Stores multiple boolean flags in a single integer. Efficient for lots of on/off toggles. + +--- + +## PdcTree + +Nested PDC data, like a mini filesystem: + +```java +PdcTree tree = PdcTree.of(player, "quests"); + +tree.set("main.tutorial.completed", true); +tree.set("main.tutorial.step", 3); +tree.set("side.mining.progress", 75); + +boolean done = tree.getBoolean("main.tutorial.completed", false); +int step = tree.getInt("main.tutorial.step", 0); +``` diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 0000000..787dab7 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,116 @@ +# Recipes + +`dev.oum.oumlib.inventory.recipe` · Paper + +--- + +## RecipeDSL + +Register custom recipes with a fluent API. All recipes go through `OumLib.recipes()` and get cleaned up on shutdown. + +### Shaped + +```java +RecipeDSL.shaped("diamond_helmet_custom", Material.DIAMOND_HELMET) + .pattern( + "DDD", + "D D", + " " + ) + .ingredient('D', Material.DIAMOND) + .register(); +``` + +With a custom result item: + +```java +ItemStack result = ItemBuilder.of(Material.DIAMOND_SWORD) + .name("Ice Blade") + .enchant(Enchantment.SHARPNESS, 3) + .build(); + +RecipeDSL.shaped("ice_blade", result) + .pattern( + " D ", + " D ", + " S " + ) + .ingredient('D', Material.DIAMOND) + .ingredient('S', Material.STICK) + .register(); +``` + +### Shapeless + +```java +RecipeDSL.shapeless("golden_apple_easy", Material.GOLDEN_APPLE) + .ingredient(Material.APPLE) + .ingredient(Material.GOLD_INGOT, 4) + .register(); +``` + +### Smelting + +```java +RecipeDSL.smelting("custom_iron", Material.IRON_INGOT) + .input(Material.RAW_IRON) + .experience(1.0f) + .cookingTime(100) // ticks + .register(); +``` + +### Blasting + +```java +RecipeDSL.blasting("fast_iron", Material.IRON_INGOT) + .input(Material.RAW_IRON) + .cookingTime(50) + .register(); +``` + +### Smoking + +```java +RecipeDSL.smoking("cooked_beef_fast", Material.COOKED_BEEF) + .input(Material.BEEF) + .register(); +``` + +### Campfire + +```java +RecipeDSL.campfire("campfire_cod", Material.COOKED_COD) + .input(Material.COD) + .cookingTime(200) + .register(); +``` + +### Smithing + +```java +RecipeDSL.smithing("netherite_sword_custom", customSword) + .template(Material.NETHERITE_UPGRADE_SMITHING_TEMPLATE) + .base(Material.DIAMOND_SWORD) + .addition(Material.NETHERITE_INGOT) + .register(); +``` + +### Stonecutting + +```java +RecipeDSL.stonecutting("stone_bricks_cut", Material.STONE_BRICKS) + .input(Material.STONE) + .register(); +``` + +--- + +## Unregister + +All recipes registered through `RecipeDSL` are tracked. They get unregistered when you call `OumLib.shutdown()`. + +You can also unregister manually: + +```java +OumLib.recipes().unregisterAll(); +``` diff --git a/docs/regions.md b/docs/regions.md new file mode 100644 index 0000000..b386649 --- /dev/null +++ b/docs/regions.md @@ -0,0 +1,119 @@ +# Regions + +`dev.oum.oumlib.math.region` · Paper + +--- + +## Region Types + +Regions define 3D areas in a world. You can check if a location is inside, get all blocks, and track enter/leave events. + +### Cuboid + +```java +CuboidRegion region = CuboidRegion.of("world", pos1, pos2); +// pos1 and pos2 are Location or Vector3D corners +``` + +### Sphere + +```java +SphereRegion region = SphereRegion.of("world", center, radius); +``` + +### Cylinder + +```java +CylinderRegion region = CylinderRegion.of("world", center, radius, minY, maxY); +``` + +### Polygon + +```java +List points = List.of( + Vector2D.of(0, 0), + Vector2D.of(10, 0), + Vector2D.of(10, 10), + Vector2D.of(0, 10) +); +PolygonRegion region = PolygonRegion.of("world", points, 60, 120); +// minY=60, maxY=120 +``` + +### Compound + +Combine multiple regions: + +```java +CompoundRegion region = CompoundRegion.of(region1, region2, region3); +``` + +--- + +## Checking Locations + +```java +boolean inside = region.contains(location); +boolean inside = region.contains(x, y, z); +``` + +--- + +## Getting Blocks / Chunks + +```java +List blocks = region.getBlocks(); +Set chunks = region.getChunks(); +``` + +--- + +## Region Bounds + +```java +Location min = region.getMin(); +Location max = region.getMax(); +Location center = region.getCenter(); +double volume = region.getVolume(); +``` + +--- + +## Serialization + +Regions can be serialized to and from maps for storage: + +```java +Map data = region.serialize(); + +// later +CuboidRegion region = CuboidRegion.deserialize(data); +``` + +--- + +## Region Tracker + +Track players entering and leaving regions: + +```java +OumLib.regions().register("spawn-area", region); + +OumLib.regions().onEnter("spawn-area", player -> { + Text.send(player, "Welcome to spawn!"); +}); + +OumLib.regions().onLeave("spawn-area", player -> { + Text.send(player, "Leaving spawn area"); +}); +``` + +The tracker checks player positions every tick and fires events when they cross region boundaries. + +### Unregister + +```java +OumLib.regions().unregister("spawn-area"); +``` + +All regions are cleaned up on `OumLib.shutdown()`. diff --git a/docs/scheduler.md b/docs/scheduler.md index 2ed2680..cbb03f9 100644 --- a/docs/scheduler.md +++ b/docs/scheduler.md @@ -1,144 +1,180 @@ -# Scheduler System +# Scheduler -OumLib features a platform-agnostic scheduler. It utilizes Java virtual threads for asynchronous execution and bridges to platform-specific tick loops for synchronous tasks. +`dev.oum.oumlib.scheduler` · Paper / Velocity / Folia --- -## Real-world Example: Profile Auto-Save Manager +## Basics -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: +OumLib picks the right scheduler for your platform. On Paper it uses Bukkit's scheduler, on Folia it uses the region scheduler, on Velocity it uses Velocity's scheduler. You just call `Scheduler` and it works. + +### Run on Main Thread + +```java +Scheduler.run(() -> { + player.teleport(spawn); +}); +``` + +### Run Later + +```java +Scheduler.runLater(Duration.ofSeconds(5), () -> { + player.sendMessage("5 seconds passed!"); +}); + +// or in ticks +Scheduler.runLater(100L, () -> { + // 100 ticks = 5 seconds +}); +``` + +### Run Repeating + +```java +Scheduler.runRepeating(Duration.ZERO, Duration.ofSeconds(1), () -> { + // runs every second, starting now +}); + +// in ticks +Scheduler.runRepeating(0L, 20L, () -> { + // every 20 ticks +}); +``` + +### Run Async + +```java +Scheduler.runAsync(() -> { + // off the main thread + String data = fetchFromAPI(); + Scheduler.run(() -> { + // back on main thread + player.sendMessage(data); + }); +}); +``` + +### Virtual Threads + +Java 21 virtual threads for lightweight async work: ```java -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 java.util.ArrayList; -import java.util.List; - -public final class ProfileAutoSaver { - private final TaskGroup taskGroup = new TaskGroup(); - private final Database db; - - public ProfileAutoSaver(Database db) { - this.db = db; - } - - 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); - } - }); - } - - public void stop() { - taskGroup.cancelAll(); - } -} +Scheduler.runVirtual(() -> { + // runs on a virtual thread + // great for I/O: database, HTTP, file reads +}); ``` --- -## Real-world Example: Combat Tag Transition (TaskChains) +## TaskHandle -Teleport a player back to spawn after checking their combat status on the database and showing a visual countdown on the screen: +All `run*` methods return a `TaskHandle` you can cancel: ```java -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 final class SpawnTeleportHandler { - private final Database db; - private final Location spawnLocation; - - public SpawnTeleportHandler(Database db, Location spawnLocation) { - this.db = db; - this.spawnLocation = spawnLocation; - } - - 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(); - } -} +TaskHandle task = Scheduler.runRepeating(Duration.ZERO, Duration.ofSeconds(1), () -> { + // ... +}); + +// later +task.cancel(); ``` --- -## Folia Compatibility +## Promise + +Async result with sync callback: + +```java +Scheduler.supplyAsync(() -> { + return database.loadProfile(uuid); +}).thenAcceptSync(profile -> { + // runs on main thread with the result + player.sendMessage("Welcome, " + profile.name()); +}).exceptionally(ex -> { + player.sendMessage("Failed to load profile"); + return null; +}); +``` -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: +### Virtual Thread Promise ```java -import dev.oum.oumlib.scheduler.Scheduler; -import org.bukkit.Location; -import org.bukkit.entity.Entity; - -public final class RegionalScheduler { - public void executeRegionalAction(Location location, Entity entity, Runnable action) { - Scheduler.runAt(location, action); - Scheduler.runFor(entity, action); - } -} +Scheduler.supplyVirtual(() -> { + return httpClient.get("https://api.example.com/data"); +}).thenAcceptSync(data -> { + player.sendMessage("Got: " + data); +}); ``` --- -## Promises and Callback Chaining +## TaskChain -Execute database queries asynchronously on virtual threads and pass the results to synchronous player interactions safely: +Chain multiple sync/async steps: ```java -import dev.oum.oumlib.database.Database; -import dev.oum.oumlib.scheduler.Scheduler; -import org.bukkit.entity.Player; - -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."); - }); - } -} +Scheduler.chain() + .async(() -> database.load(uuid)) // load async + .syncWith((data) -> { // process on main + player.sendMessage("Loaded: " + data); + return data; + }) + .async(data -> database.save(data)) // save async + .execute(); ``` + +--- + +## TaskGroup + +Manage a group of tasks: + +```java +TaskGroup group = Scheduler.newGroup(); + +group.add(Scheduler.runRepeating(Duration.ZERO, Duration.ofSeconds(1), () -> { /* task 1 */ })); +group.add(Scheduler.runRepeating(Duration.ZERO, Duration.ofSeconds(2), () -> { /* task 2 */ })); + +// cancel all at once +group.cancelAll(); +``` + +--- + +## Countdown + +A countdown timer that ticks every second: + +```java +Countdown.create(30) // 30 seconds + .onTick(remaining -> { + Text.actionBar(player, "Starting in " + remaining + "s"); + }) + .onComplete(() -> { + Text.send(player, "Go!"); + }) + .start(); +``` + +--- + +## Folia Support + +On Folia, location-bound and entity-bound tasks run on the correct region thread: + +```java +// Run on the region that owns this location +Scheduler.runAt(location, () -> { + // safe for this region +}); + +// Run on the entity's owning thread +Scheduler.runFor(entity, () -> { + entity.setHealth(20); +}); +``` + +On non-Folia servers, these just run on the main thread. diff --git a/docs/setup.md b/docs/setup.md index 7a74230..3dbdb1a 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,79 +1,26 @@ -# Setup & Initialization +# Setup -OumLib is designed to be shaded and relocated directly into your plugin JAR. +`dev.oum.oumlib` · Paper / Velocity / Folia --- -## Auto-Detection of Integrations +## Paper -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. - ---- - -## 1. Maven Dependency Configuration - -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 - compile - -``` - -You must relocate OumLib inside your package space to prevent conflicts with other plugins using different versions of the library: - -```xml - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.2 - - false - - - dev.oum.oumlib - your.plugin.package.libs.oumlib - - - - - - package - - shade - - - - - - -``` - ---- - -## 2. Bootstrapping OumLib (Paper / Spigot) - -Initialize OumLib in your main class's `onEnable()` method, and call `shutdown()` in `onDisable()` to clean up scheduled tasks and databases: +Call `OumLib.init(this)` in your `onEnable` and `OumLib.shutdown()` in `onDisable`. ```java -import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.text.Preset; -import org.bukkit.plugin.java.JavaPlugin; - public final class MyPlugin extends JavaPlugin { + @Override public void onEnable() { OumLib.init(this) - .preset(Preset.INFO, "[MyPlugin] ") - .preset(Preset.SUCCESS, "[MyPlugin - Success] ") - .preset(Preset.ERROR, "[MyPlugin - Error] "); + .preset(Preset.SUCCESS, "") + .preset(Preset.ERROR, "") + .preset(Preset.INFO, "") + .commandErrorHandler((ctx, ex) -> { + ctx.reply("Something went wrong."); + OumLib.logError("Command error", ex); + }); } @Override @@ -83,60 +30,82 @@ public final class MyPlugin extends JavaPlugin { } ``` +`init()` sets up: +- The scheduler adapter (Bukkit or Folia, detected automatically) +- The event bus +- Hologram registry and region tracker +- Recipe registry +- Volatile metadata store +- PlaceholderAPI and MiniPlaceholders hooks (if those plugins are present) + +`shutdown()` cleans up everything — cancels tasks, removes holograms, stops config watchers. + --- -## 3. Bootstrapping OumLib (Velocity Proxy) +## Velocity -Initialize OumLib inside the proxy plugin constructor or initialization event: +Pass the `ProxyServer` and your plugin instance: ```java -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 = "my-proxy", name = "MyProxy", version = "1.0.0") -public final class VelocityProxyPlugin { - private final ProxyServer server; +@Plugin(id = "my-proxy-plugin") +public final class MyProxyPlugin { @Inject - public VelocityProxyPlugin(ProxyServer server) { - this.server = server; - } - - @Subscribe - public void onProxyInitialization(ProxyInitializeEvent event) { + public MyProxyPlugin(ProxyServer server) { OumLib.init(server, this); } - - @Subscribe - public void onProxyShutdown(ProxyShutdownEvent event) { - OumLib.shutdown(); - } } ``` +Same shutdown call: `OumLib.shutdown()`. + --- -## Platform Detection Utilities +## Platform Detection -To build multi-platform modules running across both Paper and Velocity, check the host platform at runtime: +```java +OumLib.isPaper(); // true on Paper/Folia +OumLib.isVelocity(); // true on Velocity +``` + +Use `OumLib.plugin()` to get the Bukkit `Plugin` instance, or `OumLib.proxy()` to get the `ProxyServer`. + +--- + +## InitBuilder Options + +The `init()` call returns an `InitBuilder` you can chain: + +| Method | What it does | +|:-------------------------------------|:-----------------------------------------------| +| `.preset(Preset.SUCCESS, "")` | Registers a text preset for `Text.send()` | +| `.commandErrorHandler(handler)` | Sets the global error handler for all commands | + +--- + +## Logging + +OumLib provides a few logging shortcuts that work on both Paper and Velocity: ```java -import dev.oum.oumlib.OumLib; - -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"); - } - } -} +OumLib.logInfo("Server started"); +OumLib.logWarning("Low memory"); +OumLib.logError("Database failed", exception); +OumLib.logDebug("Loading player data"); // only prints when debug mode is on +``` + +Toggle debug mode: +```java +OumLib.setDebug(true); +``` + +--- + +## Audience Helpers + +```java +OumLib.players(); // all online players as an Audience +OumLib.console(); // the console as an Audience ``` -These checks are safe to call on either platform and do not raise class loading errors. +Works on both Paper and Velocity. diff --git a/docs/text.md b/docs/text.md index 33024a2..fc86546 100644 --- a/docs/text.md +++ b/docs/text.md @@ -1,121 +1,245 @@ -# Text & Placeholders System +# Text -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. +`dev.oum.oumlib.text` · Paper / Velocity --- -## Real-world Example: Chat Trivia Challenge - -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; - -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); - } -} -``` - -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: " +## Sending Messages + +All text uses MiniMessage formatting. No `ChatColor`, no legacy codes. + +```java +Text.send(player, "You earned coins!", "amount", 50); +``` + +The key-value pairs at the end become MiniMessage placeholders. You can pass as many as you want: + +```java +Text.send(player, " killed !", "player", killer.getName(), "target", victim.getName()); +``` + +### Send with a Record + +Instead of key-value pairs, pass a record and its fields become placeholders automatically: + +```java +record KillData(String killer, String victim, int reward) {} + +Text.send(player, " killed for coins!", new KillData("Steve", "Alex", 100)); +``` + +### Send Multiple Lines + +```java +Text.sendLines(player, List.of( + "==============", + "Welcome back!", + "==============" +), "player", player.getName()); ``` --- -## Text Presets +## Parsing + +Convert a MiniMessage string to a Component: + +```java +Component comp = Text.parse("Hello!"); +Component comp = Text.parse("'s stats", Placeholder.parsed("player", name)); +``` -OumLib defines standard text presets for consistent messaging styles: +Reverse — Component back to MiniMessage string: ```java -import dev.oum.oumlib.text.Text; -import org.bukkit.entity.Player; +String mm = Text.serialize(component); +``` + +Strip all tags: -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!"); - } -} +```java +String plain = Text.strip("Hello world"); // "Hello world" ``` --- -## Placeholder Registration +## Action Bar -Register custom placeholders under your plugin's identifier namespace. Registered placeholders are automatically bridged into PAPI and MiniPlaceholders: +```java +Text.actionBar(player, "+5 XP", "xp", 5); +``` + +--- + +## Titles + +```java +Text.title(player, "Level Up!", "You are now level 10"); + +// with custom timing +Text.title(player, "GAME OVER", "Better luck next time", + Duration.ofMillis(300), Duration.ofSeconds(3), Duration.ofMillis(500)); +``` + +--- + +## Boss Bar ```java -import dev.oum.oumlib.OumLib; -import org.bukkit.entity.Player; +BossBar bar = Text.bossBar(player, "Boss Health", 0.75f, + BossBar.Color.RED, BossBar.Overlay.PROGRESS); +``` -public class LevelPlaceholderRegistry { - public void register() { - OumLib.placeholders("myplugin") - .add("level", player -> { - if (player instanceof Player p) { - return String.valueOf(p.getLevel()); - } - return "0"; - }); - } -} +Temporary boss bar that hides itself: + +```java +Text.bossBarTemporary(player, "Quest Complete!", 1.0f, + BossBar.Color.GREEN, BossBar.Overlay.PROGRESS, + Duration.ofSeconds(5)); ``` --- -## Global Broadcasts +## Broadcasting -Broadcast MiniMessage-formatted text, action bars, or titles to all connected players or console: +Send to all players: ```java -import dev.oum.oumlib.text.Text; +Text.broadcast("Server restarting in 5 minutes!"); +Text.broadcastActionBar("Double XP active!"); +Text.broadcastTitle("Event Started!", "Good luck!"); +``` + +--- + +## Text Builder + +Build clickable/hoverable components: + +```java +Component msg = Text.builder("Click here") + .click(ClickEvent.runCommand("/help")) + .hover("Click for help") + .build(); +player.sendMessage(msg); +``` + +Shortcut: + +```java +Component link = Text.clickable("[Click]", + ClickEvent.openUrl("https://example.com"), + "Opens a link"); +``` + +--- + +## Presets + +Register message prefixes during init: -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"); - } -} +```java +OumLib.init(this) + .preset(Preset.SUCCESS, "") + .preset(Preset.ERROR, "") + .preset(Preset.INFO, ""); +``` + +Then use them: + +```java +Text.Preset.success(player, "Item purchased!"); // "✔ Item purchased!" in green +Text.Preset.error(player, "Not enough coins!"); // "✖ Not enough coins!" in red +Text.Preset.info(player, "Your balance: 500"); // "ℹ Your balance: 500" in gray + +// broadcast versions +Text.Preset.successBroadcast("Server saved!"); +``` + +--- + +## Placeholders + +Register custom placeholders that resolve in any `Text.send()` call: + +```java +OumLib.placeholders("myplugin") + .register("level", player -> String.valueOf(getLevel(player))) + .register("coins", player -> String.valueOf(getCoins(player))); +``` + +Then use `%myplugin_level%` or `%myplugin_coins%` in any message. Works with PlaceholderAPI and MiniPlaceholders if those plugins are installed. + +--- + +## Localization + +Send messages based on the player's client language: + +```java +Localization.translateFor(player, "welcome-message"); +``` + +Register translations: + +```java +Localization.register("en", "welcome-message", "Welcome!"); +Localization.register("ko", "welcome-message", "환영합니다!"); +``` + +In commands: + +```java +ctx.sendTranslated("welcome-message"); +``` + +--- + +## TextInput + +Capture chat input from a player: + +```java +TextInput.request(player, "Type the item name:", input -> { + player.sendMessage("You typed: " + input); +}); +``` + +With a timeout: + +```java +TextInput.request(player, "Enter amount:", Duration.ofSeconds(30), input -> { + int amount = Integer.parseInt(input); + // ... +}, () -> { + player.sendMessage("Timed out!"); +}); +``` + +--- + +## Console ASCII Art + +Print startup banners to console: + +```java +Text.ascii(true, + " ___ _ _ __ __ ", + " / _ \\| | | | \\/ |", + "| | | | | | | |\\/| |", + "| |_| | |_| | | | |", + " \\___/ \\___/|_| |_|" +); +``` + +--- + +## Format Utilities + +Duration parsing and formatting live in `Format`: + +```java +Duration d = Format.parseDuration("1h30m"); // 1 hour 30 minutes +String s = Format.formatDuration(d); // "1h 30m" +String compact = Format.formatDurationCompact(d); // "01:30:00" ``` diff --git a/docs/utilities.md b/docs/utilities.md index 40b9a28..374eb98 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -1,112 +1,128 @@ -# General Utilities +# Utilities -OumLib contains helpers to manage item serialization, Persistent Data Containers, countdowns, cooldowns, and server proxy routing. +`dev.oum.oumlib.text`, `dev.oum.oumlib.math`, `dev.oum.oumlib.inventory` --- -## Real-world Example: PDC Inventory Backpack +## Duration Parsing -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: +Parse human-readable duration strings: ```java -import dev.oum.oumlib.util.Pdc; -import dev.oum.oumlib.util.ItemSerializer; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; - -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!"); - } - - public ItemStack[] loadBackpack(Player player) { - String base64 = Pdc.of(player) - .namespaced("myplugin") - .get("backpack_contents"); - - if (base64 == null || base64.isEmpty()) { - return new ItemStack[0]; - } - - return ItemSerializer.deserializeArray(base64); - } -} +Duration d = Format.parseDuration("1h30m"); // 1 hour 30 minutes +Duration d = Format.parseDuration("5s"); // 5 seconds +Duration d = Format.parseDuration("2d12h"); // 2 days 12 hours +Duration d = Format.parseDuration("500ms"); // 500 milliseconds +``` + +Format a Duration back to a string: + +```java +String s = Format.formatDuration(duration); // "1h 30m" +String s = Format.formatDurationCompact(duration); // "01:30:00" ``` --- -## Real-world Example: Match Lobby Countdown +## Number Formatting + +```java +Format.compact(1500); // "1.5K" +Format.compact(2300000); // "2.3M" +Format.percent(0.756); // "75.6%" +Format.ordinal(1); // "1st" +Format.ordinal(22); // "22nd" +``` + +--- + +## Location Serialization + +Serialize locations to strings for config/database storage: -Play tick sounds and announce remaining seconds on the screen during a game start countdown, executing startup actions once finished: +```java +String s = Locations.serialize(location); // "world,10.5,64.0,20.3,90.0,0.0" +Location loc = Locations.deserialize(s); +``` + +Center a location on a block: ```java -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(); - } -} +Location centered = Locations.center(location); // x.5, y, z.5 +Location block = Locations.blockLocation(location); // integer coords ``` --- -## Standalone Cooldown System +## Item Serialization -A standalone map-based cooldown timer: +Convert items to/from Base64: ```java -import dev.oum.oumlib.util.Cooldown; -import java.time.Duration; -import java.util.UUID; - -public final class SkillManager { - private final Cooldown fireboltCooldown = Cooldown.of(Duration.ofSeconds(8)); - - public boolean useFirebolt(UUID playerUuid) { - if (fireboltCooldown.isOnCooldown(playerUuid)) { - return false; - } - - fireboltCooldown.set(playerUuid); - return true; - } -} +String encoded = ItemSerializer.toBase64(itemStack); +ItemStack item = ItemSerializer.fromBase64(encoded); + +// arrays +String encoded = ItemSerializer.arrayToBase64(items); +ItemStack[] items = ItemSerializer.arrayFromBase64(encoded); ``` --- -## Duration & Digital Clock Formats +## Potion Serialization -Format times into readable clocks or text spans: +```java +String json = PotionSerializer.serialize(potionEffect); +PotionEffect effect = PotionSerializer.deserialize(json); + +String json = PotionSerializer.serializeList(effects); +List list = PotionSerializer.deserializeList(json); +``` + +--- + +## Random / Chance ```java -import dev.oum.oumlib.util.Format; -import java.time.Duration; - -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); - } -} +Chance.percent(25); // true 25% of the time +Chance.oneIn(10); // true 1/10 of the time +``` + +--- + +## Weighted Random Selection + +```java +WeightedSelector loot = WeightedSelector.create() + .add("common_sword", 50) + .add("rare_bow", 30) + .add("legendary_staff", 5); + +String drop = loot.select(); +``` + +--- + +## Math Expression Evaluator + +Evaluate math strings at runtime — useful for config-driven formulas: + +```java +double result = MathEval.evaluate("2 + 3 * 4"); // 14.0 +double result = MathEval.evaluate("100 * (1 + 0.05)^10"); // compound interest +double result = MathEval.evaluate("sqrt(144)"); // 12.0 +``` + +--- + +## Pagination + +Paginate a list for chat output: + +```java +Pagination pages = Pagination.of(allItems, 10); // 10 per page + +List page1 = pages.getPage(1); +int totalPages = pages.totalPages(); +boolean hasNext = pages.hasNext(1); ``` diff --git a/docs/web.md b/docs/web.md deleted file mode 100644 index 429b94d..0000000 --- a/docs/web.md +++ /dev/null @@ -1,56 +0,0 @@ -# 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. - ---- - -## Real-world Example: Anti-Cheat Alert Dispatcher - -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; -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); - } -} -``` - ---- - -## Simple Discord Webhook Sending - -Send standard Discord webhooks asynchronously: - -```java -import dev.oum.oumlib.web.Webhook; - -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 deleted file mode 100644 index 1e99ae4..0000000 --- a/example-plugin/pom.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - 4.0.0 - - - dev.oum - oumlib - 1.0.8 - ../pom.xml - - - example-plugin - jar - - - - - dev.oum - oumlib-core - 1.0.8 - compile - - - - - io.papermc.paper - paper-api - 1.21.11-R0.1-SNAPSHOT - provided - - - com.velocitypowered - velocity-api - 3.5.0-SNAPSHOT - provided - - - - - clean package - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.2 - - - package - - shade - - - false - - - - - - - diff --git a/example-plugin/src/main/java/dev/oum/example/ExampleAnnouncer.java b/example-plugin/src/main/java/dev/oum/example/ExampleAnnouncer.java deleted file mode 100644 index 0d239e9..0000000 --- a/example-plugin/src/main/java/dev/oum/example/ExampleAnnouncer.java +++ /dev/null @@ -1,223 +0,0 @@ -package dev.oum.example; - -import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.command.Arguments; -import dev.oum.oumlib.command.Commands; -import dev.oum.oumlib.config.ConfigManager; -import dev.oum.oumlib.scheduler.Scheduler; -import dev.oum.oumlib.scheduler.TaskGroup; -import dev.oum.oumlib.text.Text; -import net.kyori.adventure.text.minimessage.MiniMessage; - -import java.time.Duration; -import java.util.concurrent.atomic.AtomicInteger; - -public final class ExampleAnnouncer { - - private static final boolean IS_PAPER = checkIsPaper(); - private static final long startTime = System.currentTimeMillis(); - private static final AtomicInteger joinCount = new AtomicInteger(0); - private static final AtomicInteger broadcastCount = new AtomicInteger(0); - - private static ConfigManager configManager; - private static TaskGroup announcerGroup; - - private static boolean checkIsPaper() { - try { - Class.forName("org.bukkit.Bukkit"); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } - - public static void initialize() { - configManager = ConfigManager.of(PluginConfig.class, "config.yml", () -> new PluginConfig( - "[OumLib] ", - "[+] %player% joined!", - true, - "Reminder: You can reload this config in real-time!" - )); - - configManager.enableAutoReload(); - configManager.onReload(newConfig -> { - OumLib.logError("Announcer config was reloaded from disk!"); - rescheduleTasks(); - }); - - announcerGroup = Scheduler.newGroup(); - rescheduleTasks(); - - OumLib.globalRegistry() - .forNamespace("oumlib") - .add("uptime", player -> String.valueOf((System.currentTimeMillis() - startTime) / 1000)) - .add("broadcasts", player -> String.valueOf(broadcastCount.get())) - .add("joins", player -> String.valueOf(joinCount.get())) - .register(); - - registerCommands(); - } - - public static boolean isAutoBroadcastEnabled() { - return configManager.get().autoBroadcastEnabled(); - } - - public static int getJoinCount() { - return joinCount.get(); - } - - public static int getBroadcastCount() { - return broadcastCount.get(); - } - - public static void toggleAutoBroadcast() { - PluginConfig old = configManager.get(); - PluginConfig updated = new PluginConfig( - old.chatPrefix(), - old.joinMessageFormat(), - !old.autoBroadcastEnabled(), - old.broadcastTemplate() - ); - configManager.update(updated); - } - - public static void triggerManualBroadcast() { - PluginConfig config = configManager.get(); - broadcast(config.chatPrefix() + config.broadcastTemplate()); - broadcastCount.incrementAndGet(); - } - - private static void rescheduleTasks() { - announcerGroup.cancelAll(); - - PluginConfig config = configManager.get(); - if (!config.autoBroadcastEnabled()) return; - - announcerGroup.runRepeating(Duration.ofSeconds(30), Duration.ofSeconds(30), () -> { - broadcast(config.chatPrefix() + config.broadcastTemplate()); - broadcastCount.incrementAndGet(); - }); - - announcerGroup.runRepeating(Duration.ofSeconds(5), Duration.ofSeconds(5), () -> { - String barMsg = "Uptime: seconds"; - if (IS_PAPER) { - PaperAnnouncerHelper.sendActionbarToAll(barMsg); - } else { - VelocityAnnouncerHelper.sendActionbarToAll(barMsg); - } - }); - } - - private static void registerCommands() { - var broadcastArg = Arguments.string("message"); - var onceArg = Arguments.string("message"); - - Commands.create("oum") - .permission("oumlib.admin") - .executes(context -> { - Text.Preset.info(context.sender(), "OumLib Administration Dashboard"); - - Text.builder(" - [Reload Config]") - .clickRunCommand("/oum reload") - .hoverText("Click to reload config.yml") - .send(context.sender()); - - Text.builder(" - [Show Stats]") - .clickRunCommand("/oum stats") - .hoverText("Click to print current performance stats") - .send(context.sender()); - - if (IS_PAPER) { - Text.builder(" - [Open Menu]") - .clickRunCommand("/oum menu") - .hoverText("Click to open the chest GUI dashboard") - .send(context.sender()); - } - }) - .subcommand(sub -> sub - .label("reload") - .permission("oumlib.admin") - .executes(context -> { - configManager.reload(); - Text.Preset.success(context.sender(), "Configuration reloaded successfully!"); - }) - ) - .subcommand(sub -> sub - .label("stats") - .permission("oumlib.admin") - .executes(context -> { - AnnouncerStats stats = new AnnouncerStats( - joinCount.get(), - broadcastCount.get(), - (System.currentTimeMillis() - startTime) / 1000 - ); - Text.Preset.info(context.sender(), "Current statistics for OumLib Announcer:"); - Text.send(context.sender(), " Joins: | Broadcasts: | Uptime: s", stats); - }) - ) - .subcommand(sub -> sub - .label("broadcast") - .permission("oumlib.admin") - .argument(broadcastArg) - .executes(context -> { - String message = context.args().get(broadcastArg); - broadcast(configManager.get().chatPrefix() + message); - broadcastCount.incrementAndGet(); - }) - ) - .subcommand(sub -> sub - .label("once") - .permission("oumlib.admin") - .argument(onceArg) - .executes(context -> { - String messageToDeliver = context.args().get(onceArg); - Text.Preset.info(context.sender(), "Registered join listener. The next player to join will receive this message."); - - if (IS_PAPER) { - PaperAnnouncerHelper.registerOnceListener(messageToDeliver); - } else { - VelocityAnnouncerHelper.registerOnceListener(messageToDeliver); - } - }) - ) - .subcommand(sub -> sub - .label("menu") - .permission("oumlib.admin") - .executes(context -> { - if (context.isPlayer()) { - if (IS_PAPER) { - PaperAnnouncerHelper.openMenu(context.sender()); - } else { - Text.Preset.error(context.sender(), "Menus are only supported on Paper servers!"); - } - } else { - Text.Preset.error(context.sender(), "Only players can open menus!"); - } - }) - ) - .register(); - } - - public static void handlePlayerJoin(String playerName) { - joinCount.incrementAndGet(); - PluginConfig config = configManager.get(); - String joinMsg = config.joinMessageFormat().replace("%player%", playerName); - broadcast(config.chatPrefix() + joinMsg); - } - - private static void broadcast(String miniMessageText) { - var component = MiniMessage.miniMessage().deserialize(miniMessageText); - if (IS_PAPER) { - PaperAnnouncerHelper.broadcast(component); - } else { - VelocityAnnouncerHelper.broadcast(component); - } - } - - public record AnnouncerStats( - int joinCount, - int broadcastCount, - long uptimeSeconds - ) { - } -} diff --git a/example-plugin/src/main/java/dev/oum/example/PaperAnnouncerHelper.java b/example-plugin/src/main/java/dev/oum/example/PaperAnnouncerHelper.java deleted file mode 100644 index fa2b356..0000000 --- a/example-plugin/src/main/java/dev/oum/example/PaperAnnouncerHelper.java +++ /dev/null @@ -1,80 +0,0 @@ -package dev.oum.example; - -import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.event.Events; -import dev.oum.oumlib.inventory.ChestMenu; -import dev.oum.oumlib.inventory.ItemBuilder; -import dev.oum.oumlib.text.Text; -import net.kyori.adventure.text.Component; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.event.player.PlayerJoinEvent; - -public final class PaperAnnouncerHelper { - - private static ChestMenu adminMenu; - - private PaperAnnouncerHelper() { - } - - public static void sendActionbarToAll(String actionbarMessage) { - OumLib.plugin().getServer().getOnlinePlayers().forEach(player -> - Text.actionBar(player, actionbarMessage) - ); - } - - public static void registerOnceListener(String message) { - Events.listenOnce(PlayerJoinEvent.class, event -> - Text.Preset.info(event.getPlayer(), "Special Notice: " + message) - ); - } - - public static void broadcast(Component component) { - OumLib.plugin().getServer().sendMessage(component); - } - - public static void openMenu(Object playerObject) { - if (!(playerObject instanceof Player player)) return; - if (adminMenu == null) { - adminMenu = ChestMenu.builder() - .title("OumLib Dashboard") - .rows(3) - .pattern( - "#########", - "# C B S #", - "#########" - ) - .bind('#', ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build()) - .bind('C', () -> ItemBuilder.of(Material.BOOK) - .name("Auto-Broadcast Status") - .lore( - "Auto-Broadcast: " + (ExampleAnnouncer.isAutoBroadcastEnabled() ? "Enabled" : "Disabled"), - "Click to toggle" - ) - .build()) - .bind('B', ItemBuilder.of(Material.PAPER) - .name("Trigger Manual Broadcast") - .lore("Click to send broadcast") - .build()) - .bind('S', ItemBuilder.of(Material.COMPASS) - .name("Server Statistics") - .lore( - "Joins Handled: " + ExampleAnnouncer.getJoinCount() + "", - "Broadcasts Sent: " + ExampleAnnouncer.getBroadcastCount() + "", - "Click to close" - ) - .build()) - .onClick('C', context -> { - ExampleAnnouncer.toggleAutoBroadcast(); - adminMenu.refresh(context.player()); - }) - .onClick('B', context -> { - ExampleAnnouncer.triggerManualBroadcast(); - adminMenu.refresh(context.player()); - }) - .onClick('S', context -> context.player().closeInventory()) - .build(); - } - adminMenu.open(player); - } -} diff --git a/example-plugin/src/main/java/dev/oum/example/PaperExamplePlugin.java b/example-plugin/src/main/java/dev/oum/example/PaperExamplePlugin.java deleted file mode 100644 index 773bfb8..0000000 --- a/example-plugin/src/main/java/dev/oum/example/PaperExamplePlugin.java +++ /dev/null @@ -1,25 +0,0 @@ -package dev.oum.example; - -import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.event.Events; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.plugin.java.JavaPlugin; - -public final class PaperExamplePlugin extends JavaPlugin { - - @Override - public void onEnable() { - OumLib.init(this); - ExampleAnnouncer.initialize(); - - Events.listen(PlayerJoinEvent.class, event -> { - event.joinMessage(null); - ExampleAnnouncer.handlePlayerJoin(event.getPlayer().getName()); - }); - } - - @Override - public void onDisable() { - OumLib.shutdown(); - } -} diff --git a/example-plugin/src/main/java/dev/oum/example/PluginConfig.java b/example-plugin/src/main/java/dev/oum/example/PluginConfig.java deleted file mode 100644 index cdbdaa3..0000000 --- a/example-plugin/src/main/java/dev/oum/example/PluginConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.oum.example; - -import dev.oum.oumlib.config.Comment; -import dev.oum.oumlib.config.ConfigSection; - -public record PluginConfig( - @Comment("Prefix for all plugin chat messages") - String chatPrefix, - - @Comment("Join message format. Supports %player%") - String joinMessageFormat, - - @Comment("Enable periodic broadcasts") - boolean autoBroadcastEnabled, - - @Comment("Message template for periodic broadcasts") - String broadcastTemplate -) implements ConfigSection { -} diff --git a/example-plugin/src/main/java/dev/oum/example/VelocityAnnouncerHelper.java b/example-plugin/src/main/java/dev/oum/example/VelocityAnnouncerHelper.java deleted file mode 100644 index a8e1488..0000000 --- a/example-plugin/src/main/java/dev/oum/example/VelocityAnnouncerHelper.java +++ /dev/null @@ -1,41 +0,0 @@ -package dev.oum.example; - -import com.velocitypowered.api.event.connection.PostLoginEvent; -import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.event.Events; -import dev.oum.oumlib.text.Text; -import net.kyori.adventure.text.Component; - -public final class VelocityAnnouncerHelper { - - private VelocityAnnouncerHelper() { - } - - /** - * Sends an actionbar message to all online proxy players. - * Note: Adventure's {@code actionBar} on Velocity players is a no-op in most proxy setups. - * As a fallback, this method can display the message as a subtitle (title fallback) or chat message. - */ - public static void sendActionbarToAll(String actionbarMessage) { - OumLib.proxy().getAllPlayers().forEach(player -> { - // Direct action bar call (might be no-op depending on setup) - Text.actionBar(player, actionbarMessage); - - // Fallback Option A: Send as a subtitle with an empty main title to simulate actionbar - // Text.title(player, "", actionbarMessage, java.time.Duration.ofMillis(100), java.time.Duration.ofSeconds(2), java.time.Duration.ofMillis(100)); - - // Fallback Option B: Send as a standard chat message - // Text.send(player, actionbarMessage); - }); - } - - public static void registerOnceListener(String message) { - Events.listenOnce(PostLoginEvent.class, event -> - Text.Preset.info(event.getPlayer(), "Special Notice: " + message) - ); - } - - public static void broadcast(Component component) { - OumLib.proxy().sendMessage(component); - } -} diff --git a/example-plugin/src/main/java/dev/oum/example/VelocityExamplePlugin.java b/example-plugin/src/main/java/dev/oum/example/VelocityExamplePlugin.java deleted file mode 100644 index 7192e95..0000000 --- a/example-plugin/src/main/java/dev/oum/example/VelocityExamplePlugin.java +++ /dev/null @@ -1,37 +0,0 @@ -package dev.oum.example; - -import com.google.inject.Inject; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.connection.PostLoginEvent; -import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; -import com.velocitypowered.api.plugin.Plugin; -import com.velocitypowered.api.proxy.ProxyServer; -import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.event.Events; - -@Plugin( - id = "example-oum-plugin", - name = "Example Oum Plugin", - version = "1.0.0", - description = "An example plugin using oumlib", - authors = {"sun-dev"} -) -public final class VelocityExamplePlugin { - - private final ProxyServer server; - - @Inject - public VelocityExamplePlugin(ProxyServer server) { - this.server = server; - } - - @Subscribe - public void onProxyInitialization(ProxyInitializeEvent event) { - OumLib.init(server, this); - ExampleAnnouncer.initialize(); - - Events.listen(PostLoginEvent.class, e -> { - ExampleAnnouncer.handlePlayerJoin(e.getPlayer().getUsername()); - }); - } -} diff --git a/example-plugin/src/main/resources/paper-plugin.yml b/example-plugin/src/main/resources/paper-plugin.yml deleted file mode 100644 index b2a4ba2..0000000 --- a/example-plugin/src/main/resources/paper-plugin.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: ExampleOumPlugin -version: 1.0.0 -main: dev.oum.example.PaperExamplePlugin -api-version: '1.20' -folia-supported: true -description: An example plugin demonstrating the use of oumlib. -authors: - - sun-dev - -dependencies: - server: - PlaceholderAPI: - load: BEFORE - required: false - join-classpath: true - MiniPlaceholders: - load: BEFORE - required: false - join-classpath: true \ No newline at end of file diff --git a/oumlib-core/pom.xml b/oumlib-core/pom.xml index 799f92c..ffb8d19 100644 --- a/oumlib-core/pom.xml +++ b/oumlib-core/pom.xml @@ -8,7 +8,7 @@ dev.oum oumlib - 1.0.8 + 1.0.9 ../pom.xml @@ -55,6 +55,13 @@ provided true + + com.github.retrooper + packetevents-spigot + 2.12.2 + provided + true + diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java b/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java index 7f64341..02e5ca2 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/OumLib.java @@ -7,15 +7,20 @@ import dev.oum.oumlib.command.Argument; import dev.oum.oumlib.command.CommandContext; import dev.oum.oumlib.config.ConfigWatcher; +import dev.oum.oumlib.entity.hologram.HologramRegistry; import dev.oum.oumlib.event.EventBus; import dev.oum.oumlib.event.platform.PaperEventBus; import dev.oum.oumlib.event.platform.VelocityEventBus; import dev.oum.oumlib.inventory.MenuRegistry; +import dev.oum.oumlib.inventory.recipe.RecipeRegistry; +import dev.oum.oumlib.math.region.RegionTracker; +import dev.oum.oumlib.pdc.metadata.VolatileData; import dev.oum.oumlib.scheduler.Scheduler; import dev.oum.oumlib.scheduler.platform.BukkitSchedulerAdapter; import dev.oum.oumlib.scheduler.platform.VelocitySchedulerAdapter; import dev.oum.oumlib.text.Preset; import dev.oum.oumlib.text.PresetRegistry; +import dev.oum.oumlib.text.Text; import dev.oum.oumlib.text.placeholder.PlaceholderRegistry; import dev.oum.oumlib.text.placeholder.bridge.MiniPlaceholdersHelper; import dev.oum.oumlib.text.placeholder.bridge.PapiHelper; @@ -45,6 +50,9 @@ public final class OumLib { private static PresetRegistry presetRegistry; private static PlaceholderRegistry placeholderRegistry; + private static HologramRegistry hologramRegistry; + private static RegionTracker regionTracker; + private static RecipeRegistry recipeRegistry; private static boolean initialized; private static boolean debugMode = false; private static BiConsumer commandErrorHandler = (context, ex) -> { @@ -60,6 +68,7 @@ private OumLib() { public static @NonNull InitBuilder init(Plugin p) { if (initialized) throw new IllegalStateException("OumLib already initialized."); plugin = p; + initialized = true; presetRegistry = new PresetRegistry(); placeholderRegistry = new PlaceholderRegistry(); @@ -74,7 +83,13 @@ private OumLib() { p.getServer().getMessenger().registerOutgoingPluginChannel(p, "oumlib:autocomplete"); detectIntegrations(p); - initialized = true; + if (isPacketEventsPresent()) { + tryInitHolograms(p); + } + regionTracker = new RegionTracker(p); + regionTracker.start(); + recipeRegistry = new RecipeRegistry(); + VolatileData.initialize(); return new InitBuilder(); } @@ -83,6 +98,7 @@ private OumLib() { if (initialized) throw new IllegalStateException("OumLib already initialized."); proxyServer = server; velocityPlugin = pluginInstance; + initialized = true; presetRegistry = new PresetRegistry(); placeholderRegistry = new PlaceholderRegistry(); @@ -114,7 +130,6 @@ public void onPluginMessage(PluginMessageEvent event) { }); detectVelocityIntegrations(server); - initialized = true; return new InitBuilder(); } @@ -176,6 +191,19 @@ public static void shutdown() { Scheduler.shutdownAll(); MenuRegistry.shutdown(); ConfigWatcher.shutdown(); + if (hologramRegistry != null) { + hologramRegistry.stop(); + hologramRegistry = null; + } + if (regionTracker != null) { + regionTracker.stop(); + regionTracker = null; + } + if (recipeRegistry != null) { + recipeRegistry.unregisterAll(); + recipeRegistry = null; + } + VolatileData.clearAll(); plugin = null; proxyServer = null; velocityPlugin = null; @@ -215,6 +243,45 @@ public static boolean isVelocity() { return proxyServer != null; } + private static boolean isPacketEventsPresent() { + try { + Class.forName("com.github.retrooper.packetevents.PacketEvents"); + return true; + } catch (Throwable t) { + return false; + } + } + + private static void tryInitHolograms(Plugin p) { + try { + hologramRegistry = new HologramRegistry(p); + hologramRegistry.start(); + } catch (Throwable t) { + logWarning("PacketEvents detected but failed to initialize HologramRegistry: " + t.getMessage()); + } + } + + public static HologramRegistry holograms() { + assertInit(); + if (hologramRegistry == null) + throw new IllegalStateException("Hologram registry requires PacketEvents to be installed on Paper."); + return hologramRegistry; + } + + public static RegionTracker regions() { + assertInit(); + if (regionTracker == null) + throw new IllegalStateException("Region tracker is only available on Paper platform."); + return regionTracker; + } + + public static RecipeRegistry recipes() { + assertInit(); + if (recipeRegistry == null) + throw new IllegalStateException("Recipe registry is only available on Paper platform."); + return recipeRegistry; + } + public static @NonNull File getDataFolder() { assertInit(); if (plugin != null) { @@ -270,7 +337,12 @@ public static void logError(String message, Throwable t) { public static void logDebug(String message) { if (!debugMode) return; if (plugin != null) { - plugin.getSLF4JLogger().debug(message); + plugin.getSLF4JLogger().info("[DEBUG] " + message); + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.isOp() || p.hasPermission("oumlib.debug") || p.hasPermission(plugin.getName().toLowerCase(Locale.ROOT) + ".admin.debug") || p.hasPermission(plugin.getName().toLowerCase(Locale.ROOT) + ".admin")) { + p.sendMessage(Text.parse("[DEBUG] " + message + "")); + } + } } else { Logger.getLogger("OumLib").log(Level.INFO, "[DEBUG] " + message); } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/combat/CombatBridge.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/combat/CombatBridge.java new file mode 100644 index 0000000..0ef5937 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/combat/CombatBridge.java @@ -0,0 +1,162 @@ +package dev.oum.oumlib.bridge.combat; + +import dev.oum.oumlib.pdc.metadata.VolatileData; +import dev.oum.oumlib.scheduler.Scheduler; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; + +import java.lang.reflect.Method; +import java.time.Duration; + +public final class CombatBridge { + + private static boolean combatLogXChecked = false; + private static boolean combatLogXAvailable = false; + private static boolean pvpManagerChecked = false; + private static boolean pvpManagerAvailable = false; + private static boolean deluxeCombatChecked = false; + private static boolean deluxeCombatAvailable = false; + + private CombatBridge() { + } + + public static boolean isAvailable() { + return hasCombatLogX() || hasPvPManager() || hasDeluxeCombat(); + } + + public static boolean isInCombat(@NonNull Player player) { + if (hasCombatLogX()) { + try { + if (checkCombatLogX(player)) return true; + } catch (Throwable ignored) { + } + } + + if (hasPvPManager()) { + try { + if (checkPvPManager(player)) return true; + } catch (Throwable ignored) { + } + } + + if (hasDeluxeCombat()) { + try { + if (checkDeluxeCombat(player)) return true; + } catch (Throwable ignored) { + } + } + + return VolatileData.getOrDefault(player, "bounty_combat", false); + } + + public static void tag(@NonNull Player player, @NonNull Duration duration) { + VolatileData.set(player, "bounty_combat", true); + Scheduler.runLater(duration.toMillis() / 50L, () -> { + if (player.isOnline()) { + VolatileData.set(player, "bounty_combat", false); + } + }); + + if (hasCombatLogX()) { + try { + tagCombatLogX(player, duration); + } catch (Throwable ignored) { + } + } + + if (hasDeluxeCombat()) { + try { + tagDeluxeCombat(player, (int) duration.toSeconds()); + } catch (Throwable ignored) { + } + } + } + + public static void untag(@NonNull Player player) { + VolatileData.set(player, "bounty_combat", false); + } + + public static boolean hasCombatLogX() { + if (!combatLogXChecked) { + combatLogXChecked = true; + combatLogXAvailable = Bukkit.getPluginManager().getPlugin("CombatLogX") != null; + } + return combatLogXAvailable; + } + + public static boolean hasPvPManager() { + if (!pvpManagerChecked) { + pvpManagerChecked = true; + pvpManagerAvailable = Bukkit.getPluginManager().getPlugin("PvPManager") != null; + } + return pvpManagerAvailable; + } + + public static boolean hasDeluxeCombat() { + if (!deluxeCombatChecked) { + deluxeCombatChecked = true; + deluxeCombatAvailable = Bukkit.getPluginManager().getPlugin("DeluxeCombat") != null; + } + return deluxeCombatAvailable; + } + + private static boolean checkCombatLogX(@NonNull Player player) throws Exception { + Class clxClass = Class.forName("com.sirblobman.combatlogx.api.ICombatLogX"); + Object plugin = Bukkit.getPluginManager().getPlugin("CombatLogX"); + if (plugin == null) return false; + Object manager = clxClass.getMethod("getCombatManager").invoke(plugin); + Method isInCombat = manager.getClass().getMethod("isInCombat", Player.class); + return (boolean) isInCombat.invoke(manager, player); + } + + private static void tagCombatLogX(@NonNull Player player, @NonNull Duration duration) throws Exception { + Class clxClass = Class.forName("com.sirblobman.combatlogx.api.ICombatLogX"); + Object plugin = Bukkit.getPluginManager().getPlugin("CombatLogX"); + if (plugin == null) return; + Object manager = clxClass.getMethod("getCombatManager").invoke(plugin); + Method tagMethod = manager.getClass().getMethod("tag", Player.class, Player.class, int.class); + tagMethod.invoke(manager, player, null, (int) duration.toSeconds()); + } + + private static boolean checkPvPManager(@NonNull Player player) throws Exception { + Class pmClass; + try { + pmClass = Class.forName("me.NoChance.PvPManager.PvPManager"); + } catch (ClassNotFoundException e) { + pmClass = Class.forName("me.noonspill.pvpmanager.PvPManager"); + } + Method getInstance = pmClass.getMethod("getInstance"); + Object instance = getInstance.invoke(null); + Object playerHandler = instance.getClass().getMethod("getPlayerHandler").invoke(instance); + Object pvPlayer = playerHandler.getClass().getMethod("get", Player.class).invoke(playerHandler, player); + if (pvPlayer == null) return false; + Method isInCombat = pvPlayer.getClass().getMethod("isInCombat"); + return (boolean) isInCombat.invoke(pvPlayer); + } + + private static boolean checkDeluxeCombat(@NonNull Player player) throws Exception { + try { + Class dcApiClass = Class.forName("nl.marido.deluxecombat.api.DeluxeCombatAPI"); + Object apiInstance = dcApiClass.getDeclaredConstructor().newInstance(); + Method inCombat = dcApiClass.getMethod("isInCombat", Player.class); + return (boolean) inCombat.invoke(apiInstance, player); + } catch (Throwable t) { + Class dcClass = Class.forName("nl.marido.deluxecombat.DeluxeCombat"); + Object instance = dcClass.getMethod("getInstance").invoke(null); + Object combatHandler = instance.getClass().getMethod("getCombatHandler").invoke(instance); + Method isTagged = combatHandler.getClass().getMethod("isTagged", Player.class); + return (boolean) isTagged.invoke(combatHandler, player); + } + } + + private static void tagDeluxeCombat(@NonNull Player player, int seconds) throws Exception { + try { + Class dcApiClass = Class.forName("nl.marido.deluxecombat.api.DeluxeCombatAPI"); + Object apiInstance = dcApiClass.getDeclaredConstructor().newInstance(); + Method tagMethod = dcApiClass.getMethod("tag", Player.class, int.class); + tagMethod.invoke(apiInstance, player, seconds); + } catch (Throwable ignored) { + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/EconomyBridge.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/EconomyBridge.java index 3c63189..094dfb2 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/EconomyBridge.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/EconomyBridge.java @@ -44,11 +44,24 @@ public static void registerProvider(@NonNull EconomyProvider provider) { } public static @NonNull Optional getDefaultProvider() { - return Optional.ofNullable(providers.get(defaultProviderName)); + EconomyProvider def = providers.get(defaultProviderName); + if (def != null && def.isAvailable()) { + return Optional.of(def); + } + for (EconomyProvider p : providers.values()) { + if (p.isAvailable()) { + return Optional.of(p); + } + } + return Optional.ofNullable(def); + } + + public static boolean isAvailable() { + return getDefaultProvider().map(EconomyProvider::isAvailable).orElse(false); } - public static void setDefaultProvider(@NonNull String name) { - defaultProviderName = name.toLowerCase(); + public static boolean isAvailable(@NonNull String providerName) { + return getProvider(providerName).map(EconomyProvider::isAvailable).orElse(false); } public static double balance(@NonNull OfflinePlayer player) { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/EconomyProvider.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/EconomyProvider.java index 618004d..72ffd0d 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/EconomyProvider.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/EconomyProvider.java @@ -7,6 +7,10 @@ public interface EconomyProvider { @NonNull String name(); + default boolean isAvailable() { + return true; + } + boolean has(@NonNull OfflinePlayer player, double amount); boolean withdraw(@NonNull OfflinePlayer player, double amount); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/PlayerPointsProvider.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/PlayerPointsProvider.java index a9107a8..65194a0 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/PlayerPointsProvider.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/PlayerPointsProvider.java @@ -3,16 +3,22 @@ import org.bukkit.OfflinePlayer; import org.jspecify.annotations.NonNull; +import java.lang.reflect.Method; import java.util.UUID; public final class PlayerPointsProvider implements EconomyProvider { - private final Object api; + public PlayerPointsProvider() { + } - public PlayerPointsProvider() throws Exception { - Class ppClass = Class.forName("org.black_ghost.playerpoints.PlayerPoints"); - Object ppInstance = ppClass.getMethod("getInstance").invoke(null); - this.api = ppClass.getMethod("getAPI").invoke(ppInstance); + private Object getApi() { + try { + Class ppClass = Class.forName("org.black_ghost.playerpoints.PlayerPoints"); + Object ppInstance = ppClass.getMethod("getInstance").invoke(null); + return ppClass.getMethod("getAPI").invoke(ppInstance); + } catch (Throwable ignored) { + return null; + } } @Override @@ -20,6 +26,11 @@ public PlayerPointsProvider() throws Exception { return "playerpoints"; } + @Override + public boolean isAvailable() { + return getApi() != null; + } + @Override public boolean has(@NonNull OfflinePlayer player, double amount) { return balance(player) >= amount; @@ -27,9 +38,12 @@ public boolean has(@NonNull OfflinePlayer player, double amount) { @Override public boolean withdraw(@NonNull OfflinePlayer player, double amount) { + Object api = getApi(); + if (api == null) return false; try { int points = (int) Math.round(amount); - return (boolean) api.getClass().getMethod("take", UUID.class, int.class).invoke(api, player.getUniqueId(), points); + Method method = api.getClass().getMethod("take", UUID.class, int.class); + return (boolean) method.invoke(api, player.getUniqueId(), points); } catch (Exception e) { return false; } @@ -37,9 +51,12 @@ public boolean withdraw(@NonNull OfflinePlayer player, double amount) { @Override public boolean deposit(@NonNull OfflinePlayer player, double amount) { + Object api = getApi(); + if (api == null) return false; try { int points = (int) Math.round(amount); - return (boolean) api.getClass().getMethod("give", UUID.class, int.class).invoke(api, player.getUniqueId(), points); + Method method = api.getClass().getMethod("give", UUID.class, int.class); + return (boolean) method.invoke(api, player.getUniqueId(), points); } catch (Exception e) { return false; } @@ -47,8 +64,11 @@ public boolean deposit(@NonNull OfflinePlayer player, double amount) { @Override public double balance(@NonNull OfflinePlayer player) { + Object api = getApi(); + if (api == null) return 0.0; try { - return (int) api.getClass().getMethod("look", UUID.class).invoke(api, player.getUniqueId()); + Method method = api.getClass().getMethod("look", UUID.class); + return (int) method.invoke(api, player.getUniqueId()); } catch (Exception e) { return 0.0; } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/VaultProvider.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/VaultProvider.java index 048ad95..3585911 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/VaultProvider.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/economy/VaultProvider.java @@ -4,19 +4,26 @@ import org.bukkit.OfflinePlayer; import org.jspecify.annotations.NonNull; +import java.lang.reflect.Method; + public final class VaultProvider implements EconomyProvider { - private final Object economy; + public VaultProvider() { + } - public VaultProvider() throws Exception { - Class rspClass = Class.forName("org.bukkit.plugin.RegisteredServiceProvider"); - Class econClass = Class.forName("net.milkbowl.vault.economy.Economy"); - Object servicesManager = Bukkit.getServer().getClass().getMethod("getServicesManager").invoke(Bukkit.getServer()); - Object rsp = servicesManager.getClass().getMethod("getRegistration", Class.class).invoke(servicesManager, econClass); - if (rsp == null) { - throw new IllegalStateException("Vault economy provider registration not found"); + private Object getEconomy() { + try { + Class rspClass = Class.forName("org.bukkit.plugin.RegisteredServiceProvider"); + Class econClass = Class.forName("net.milkbowl.vault.economy.Economy"); + Object servicesManager = Bukkit.getServer().getClass().getMethod("getServicesManager").invoke(Bukkit.getServer()); + Object rsp = servicesManager.getClass().getMethod("getRegistration", Class.class).invoke(servicesManager, econClass); + if (rsp == null) { + return null; + } + return rspClass.getMethod("getProvider").invoke(rsp); + } catch (Throwable ignored) { + return null; } - this.economy = rspClass.getMethod("getProvider").invoke(rsp); } @Override @@ -24,10 +31,18 @@ public VaultProvider() throws Exception { return "vault"; } + @Override + public boolean isAvailable() { + return getEconomy() != null; + } + @Override public boolean has(@NonNull OfflinePlayer player, double amount) { + Object economy = getEconomy(); + if (economy == null) return false; try { - return (boolean) economy.getClass().getMethod("has", OfflinePlayer.class, double.class).invoke(economy, player, amount); + Method method = economy.getClass().getMethod("has", OfflinePlayer.class, double.class); + return (boolean) method.invoke(economy, player, amount); } catch (Exception e) { return false; } @@ -35,11 +50,13 @@ public boolean has(@NonNull OfflinePlayer player, double amount) { @Override public boolean withdraw(@NonNull OfflinePlayer player, double amount) { + Object economy = getEconomy(); + if (economy == null) return false; try { - Object response = economy.getClass().getMethod("withdrawPlayer", OfflinePlayer.class, double.class).invoke(economy, player, amount); + Method method = economy.getClass().getMethod("withdrawPlayer", OfflinePlayer.class, double.class); + Object response = method.invoke(economy, player, amount); Class responseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); Object type = responseClass.getField("type").get(response); - // EconomyResponse.ResponseType.SUCCESS return type != null && "SUCCESS".equals(type.toString()); } catch (Exception e) { return false; @@ -48,8 +65,11 @@ public boolean withdraw(@NonNull OfflinePlayer player, double amount) { @Override public boolean deposit(@NonNull OfflinePlayer player, double amount) { + Object economy = getEconomy(); + if (economy == null) return false; try { - Object response = economy.getClass().getMethod("depositPlayer", OfflinePlayer.class, double.class).invoke(economy, player, amount); + Method method = economy.getClass().getMethod("depositPlayer", OfflinePlayer.class, double.class); + Object response = method.invoke(economy, player, amount); Class responseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); Object type = responseClass.getField("type").get(response); return type != null && "SUCCESS".equals(type.toString()); @@ -60,8 +80,11 @@ public boolean deposit(@NonNull OfflinePlayer player, double amount) { @Override public double balance(@NonNull OfflinePlayer player) { + Object economy = getEconomy(); + if (economy == null) return 0.0; try { - return (double) economy.getClass().getMethod("getBalance", OfflinePlayer.class).invoke(economy, player); + Method method = economy.getClass().getMethod("getBalance", OfflinePlayer.class); + return (double) method.invoke(economy, player); } catch (Exception e) { return 0.0; } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/ItemBridge.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/ItemBridge.java index 633b9a2..d407354 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/ItemBridge.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/ItemBridge.java @@ -43,6 +43,11 @@ public final class ItemBridge { registerProvider(new HeadDatabaseProvider()); } catch (Throwable ignored) { } + + try { + registerProvider(new TextureHeadProvider()); + } catch (Throwable ignored) { + } } private ItemBridge() { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/MMOItemsProvider.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/MMOItemsProvider.java index 410eac7..ed001e6 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/MMOItemsProvider.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/MMOItemsProvider.java @@ -22,7 +22,6 @@ public MMOItemsProvider() throws ClassNotFoundException { Class mmoItemsClass = Class.forName("net.Indyuce.mmoitems.MMOItems"); Object plugin = mmoItemsClass.getField("plugin").get(null); - // Format can be "TYPE:ID" (e.g. "SWORD:EXCALIBUR") String[] parts = id.split(":", 2); if (parts.length == 2) { Class typeClass = Class.forName("net.Indyuce.mmoitems.api.Type"); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/TextureHeadProvider.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/TextureHeadProvider.java new file mode 100644 index 0000000..cbb86e2 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/item/TextureHeadProvider.java @@ -0,0 +1,31 @@ +package dev.oum.oumlib.bridge.item; + +import dev.oum.oumlib.inventory.ItemBuilder; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NonNull; + +import java.util.Optional; + +public final class TextureHeadProvider implements ItemProvider { + + @Override + public @NonNull String name() { + return "head"; + } + + @Override + public @NonNull Optional getItem(@NonNull String id) { + if (id.isEmpty()) { + return Optional.empty(); + } + try { + ItemStack skull = ItemBuilder.of(Material.PLAYER_HEAD) + .skull(id) + .build(); + return Optional.of(skull); + } catch (Throwable ignored) { + return Optional.empty(); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/PaperPermissionHelper.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/PaperPermissionHelper.java similarity index 90% rename from oumlib-core/src/main/java/dev/oum/oumlib/util/PaperPermissionHelper.java rename to oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/PaperPermissionHelper.java index 70c6d44..617862e 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/PaperPermissionHelper.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/PaperPermissionHelper.java @@ -1,6 +1,6 @@ -package dev.oum.oumlib.util; +package dev.oum.oumlib.bridge.permission; -import dev.oum.oumlib.util.Permission.Default; +import dev.oum.oumlib.bridge.permission.Permission.Default; import org.bukkit.Bukkit; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Permission.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/Permission.java similarity index 92% rename from oumlib-core/src/main/java/dev/oum/oumlib/util/Permission.java rename to oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/Permission.java index 78bfe27..4671f0d 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Permission.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/permission/Permission.java @@ -1,4 +1,4 @@ -package dev.oum.oumlib.util; +package dev.oum.oumlib.bridge.permission; import dev.oum.oumlib.OumLib; import net.kyori.adventure.audience.Audience; @@ -23,6 +23,10 @@ private Permission(@NonNull Builder builder) { return new Builder(name); } + public static @NonNull Permission of(@NonNull String name) { + return builder(name).build(); + } + public @NonNull String name() { return name; } @@ -60,7 +64,7 @@ public boolean has(@NonNull Audience audience) { private void registerOnPaper() { try { Class.forName("org.bukkit.permissions.Permission"); - Class.forName("dev.oum.oumlib.util.PaperPermissionHelper") + Class.forName("dev.oum.oumlib.bridge.permission.PaperPermissionHelper") .getMethod("register", String.class, String.class, Default.class) .invoke(null, name, description, defaultValue); } catch (ClassNotFoundException ignored) { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/bridge/region/RegionBridge.java b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/region/RegionBridge.java new file mode 100644 index 0000000..828fb81 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/bridge/region/RegionBridge.java @@ -0,0 +1,162 @@ +package dev.oum.oumlib.bridge.region; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; + +public final class RegionBridge { + + private static boolean worldGuardChecked = false; + private static boolean worldGuardAvailable = false; + private static boolean townyChecked = false; + private static boolean townyAvailable = false; + private static boolean griefPreventionChecked = false; + private static boolean griefPreventionAvailable = false; + + private RegionBridge() { + } + + public static boolean isAvailable() { + return hasWorldGuard() || hasTowny() || hasGriefPrevention(); + } + + public static boolean isPvPAllowed(@NonNull Location location) { + return isPvPAllowed(location, null); + } + + public static boolean isPvPAllowed(@NonNull Location location, @Nullable Player player) { + if (hasWorldGuard()) { + try { + if (!checkWorldGuardPvP(location, player)) { + return false; + } + } catch (Throwable ignored) { + } + } + + if (hasTowny()) { + try { + if (!checkTownyPvP(location)) { + return false; + } + } catch (Throwable ignored) { + } + } + + if (hasGriefPrevention()) { + try { + if (!checkGriefPreventionPvP(location)) { + return false; + } + } catch (Throwable ignored) { + } + } + + return true; + } + + public static boolean isSafeZone(@NonNull Location location) { + return !isPvPAllowed(location, null); + } + + public static boolean hasWorldGuard() { + if (!worldGuardChecked) { + worldGuardChecked = true; + worldGuardAvailable = Bukkit.getPluginManager().getPlugin("WorldGuard") != null; + } + return worldGuardAvailable; + } + + public static boolean hasTowny() { + if (!townyChecked) { + townyChecked = true; + townyAvailable = Bukkit.getPluginManager().getPlugin("Towny") != null; + } + return townyAvailable; + } + + public static boolean hasGriefPrevention() { + if (!griefPreventionChecked) { + griefPreventionChecked = true; + griefPreventionAvailable = Bukkit.getPluginManager().getPlugin("GriefPrevention") != null; + } + return griefPreventionAvailable; + } + + private static boolean checkWorldGuardPvP(@NonNull Location location, @Nullable Player player) throws Exception { + Class wgClass = Class.forName("com.sk89q.worldguard.WorldGuard"); + Object wgInstance = wgClass.getMethod("getInstance").invoke(null); + Object platform = wgClass.getMethod("getPlatform").invoke(wgInstance); + Object container = platform.getClass().getMethod("getRegionContainer").invoke(platform); + Object query = container.getClass().getMethod("createQuery").invoke(container); + + Class adapterClass = Class.forName("com.sk89q.worldedit.bukkit.BukkitAdapter"); + Method adaptLoc = adapterClass.getMethod("adapt", Location.class); + Object adaptedLoc = adaptLoc.invoke(null, location); + + Class flagsClass = Class.forName("com.sk89q.worldguard.protection.flags.Flags"); + Object pvpFlag = flagsClass.getField("PVP").get(null); + + Object subject = null; + if (player != null) { + try { + Class wgPluginClass = Class.forName("com.sk89q.worldguard.bukkit.WorldGuardPlugin"); + Object wgPluginInstance = wgPluginClass.getMethod("inst").invoke(null); + subject = wgPluginClass.getMethod("wrapPlayer", Player.class).invoke(wgPluginInstance, player); + } catch (Throwable ignored) { + Method adaptPlayer = adapterClass.getMethod("adapt", Player.class); + subject = adaptPlayer.invoke(null, player); + } + } + + Method testState = query.getClass().getMethod("testState", + Class.forName("com.sk89q.worldedit.util.Location"), + Class.forName("com.sk89q.worldguard.protection.association.RegionAssociable"), + Class.forName("com.sk89q.worldguard.protection.flags.StateFlag[]")); + + Class stateFlagClass = Class.forName("com.sk89q.worldguard.protection.flags.StateFlag"); + Object flagArray = Array.newInstance(stateFlagClass, 1); + Array.set(flagArray, 0, pvpFlag); + + Object result = testState.invoke(query, adaptedLoc, subject, flagArray); + return Boolean.TRUE.equals(result); + } + + private static boolean checkTownyPvP(@NonNull Location location) throws Exception { + Class townyApiClass = Class.forName("com.palmergames.bukkit.towny.TownyAPI"); + Object apiInstance = townyApiClass.getMethod("getInstance").invoke(null); + try { + Method isPvPMethod = townyApiClass.getMethod("isPvP", Location.class); + return (boolean) isPvPMethod.invoke(apiInstance, location); + } catch (NoSuchMethodException e) { + Object town = townyApiClass.getMethod("getTown", Location.class).invoke(apiInstance, location); + if (town == null) { + return true; + } + Method isPvp = town.getClass().getMethod("isPVP"); + return (boolean) isPvp.invoke(town); + } + } + + private static boolean checkGriefPreventionPvP(@NonNull Location location) throws Exception { + Class gpClass = Class.forName("me.ryanhamshire.GriefPrevention.GriefPrevention"); + Object gpInstance = gpClass.getField("instance").get(null); + Object dataStore = gpClass.getField("dataStore").get(gpInstance); + Method getClaim = dataStore.getClass().getMethod("getClaimAt", Location.class, boolean.class, Class.forName("me.ryanhamshire.GriefPrevention.Claim")); + Object claim = getClaim.invoke(dataStore, location, false, null); + if (claim == null) { + return true; + } + try { + Method pvpMethod = claim.getClass().getMethod("isPvpEnabled"); + return (boolean) pvpMethod.invoke(claim); + } catch (NoSuchMethodException e) { + return true; + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/Argument.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/Argument.java index d2b755d..08d4f74 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/Argument.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/Argument.java @@ -4,7 +4,10 @@ import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.suggestion.SuggestionProvider; import dev.oum.oumlib.scheduler.Scheduler; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; @@ -27,8 +30,8 @@ public final class Argument { private final BiFunction, T> extractor; private SuggestionProvider suggestionProvider; - public Argument(String name, ArgumentType brigadierType, - BiFunction, T> extractor) { + public Argument(@NonNull String name, @NonNull ArgumentType brigadierType, + @NonNull BiFunction, T> extractor) { this.name = name; this.brigadierType = brigadierType; this.extractor = extractor; @@ -41,12 +44,14 @@ public static void handleVelocityPluginMessage(@NonNull UUID playerUuid, @NonNul } } - public Argument suggests(SuggestionProvider provider) { + @Contract(value = "_ -> this", mutates = "this") + public @NonNull Argument suggests(@Nullable SuggestionProvider provider) { this.suggestionProvider = provider; return this; } - public Argument suggests(@NonNull Function> provider) { + @Contract(value = "_ -> this", mutates = "this") + public @NonNull Argument suggests(@NonNull Function> provider) { this.suggestionProvider = (ctx, builder) -> { var oumCtx = CommandContext.fromBrigadier(ctx); for (String s : provider.apply(oumCtx)) { @@ -57,7 +62,8 @@ public Argument suggests(@NonNull Function return this; } - public Argument suggestsRich(@NonNull Function> provider) { + @Contract(value = "_ -> this", mutates = "this") + public @NonNull Argument suggestsRich(@NonNull Function> provider) { this.suggestionProvider = (ctx, builder) -> { var oumCtx = CommandContext.fromBrigadier(ctx); for (RichSuggestion s : provider.apply(oumCtx)) { @@ -73,7 +79,8 @@ public Argument suggestsRich(@NonNull Function suggestsAsync(@NonNull Function>> provider) { + @Contract(value = "_ -> this", mutates = "this") + public @NonNull Argument suggestsAsync(@NonNull Function>> provider) { this.suggestionProvider = (ctx, builder) -> { var oumCtx = CommandContext.fromBrigadier(ctx); return provider.apply(oumCtx).thenApply(suggestions -> { @@ -86,7 +93,8 @@ public Argument suggestsAsync(@NonNull Function suggestsCached( + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull Argument suggestsCached( @NonNull Function> provider, @NonNull Duration cacheDuration ) { @@ -112,7 +120,8 @@ public Argument suggestsCached( return this; } - public Argument suggestsCachedAsync( + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull Argument suggestsCachedAsync( @NonNull Function>> provider, @NonNull Duration cacheDuration ) { @@ -139,7 +148,8 @@ public Argument suggestsCachedAsync( return this; } - public Argument suggestsVelocitySpigot(@NonNull String queryType) { + @Contract(value = "_ -> this", mutates = "this") + public @NonNull Argument suggestsVelocitySpigot(@NonNull String queryType) { this.suggestionProvider = (ctx, builder) -> { var oumCtx = CommandContext.fromBrigadier(ctx); if (!oumCtx.isPlayer()) { @@ -157,7 +167,7 @@ public Argument suggestsVelocitySpigot(@NonNull String queryType) { dos.writeUTF(queryType); dos.writeUTF(builder.getRemaining()); - Class proxyClass = Class.forName("dev.oum.oumlib.util.Proxy"); + Class proxyClass = Class.forName("dev.oum.oumlib.proxy.Proxy"); Class playerInterface = Class.forName("com.velocitypowered.api.proxy.Player"); proxyClass.getMethod("sendPluginMessage", playerInterface, String.class, byte[].class) .invoke(null, playerObj, "oumlib:autocomplete", baos.toByteArray()); @@ -183,20 +193,24 @@ public Argument suggestsVelocitySpigot(@NonNull String queryType) { return this; } - public String name() { + @CheckReturnValue + public @NonNull String name() { return name; } - public ArgumentType brigadierType() { + @CheckReturnValue + public @NonNull ArgumentType brigadierType() { return brigadierType; } - public BiFunction, T> extractor() { + @CheckReturnValue + public @NonNull BiFunction, T> extractor() { return extractor; } + @CheckReturnValue @SuppressWarnings("unchecked") - public SuggestionProvider suggestionProvider() { + public @Nullable SuggestionProvider suggestionProvider() { return (SuggestionProvider) suggestionProvider; } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/ArgumentMap.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/ArgumentMap.java index 110c5f5..99bd6d8 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/ArgumentMap.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/ArgumentMap.java @@ -27,6 +27,22 @@ public T get(@NonNull Argument argument) { }); } + public T getOrDefault(@NonNull Argument argument, T defaultValue) { + T val = get(argument); + return val != null ? val : defaultValue; + } + + @SuppressWarnings("unchecked") + public T get(@NonNull String name) { + return (T) cache.computeIfAbsent(name, k -> { + try { + return ctx.getArgument(k, Object.class); + } catch (IllegalArgumentException e) { + return null; + } + }); + } + @SuppressWarnings("unchecked") public T get(@NonNull String name, @NonNull Class clazz) { return (T) cache.computeIfAbsent(name, k -> { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/Arguments.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/Arguments.java index 94e2d3e..d3ca970 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/Arguments.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/Arguments.java @@ -2,7 +2,7 @@ import com.mojang.brigadier.arguments.*; import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.util.Format; +import dev.oum.oumlib.text.Format; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -55,11 +55,11 @@ private Arguments() { } @Contract("_ -> new") - @SuppressWarnings("DataFlowIssue") - public static @NonNull Argument player(String name) { + @SuppressWarnings({"DataFlowIssue", "unchecked"}) + public static @NonNull Argument player(String name) { try { Class.forName("io.papermc.paper.command.brigadier.argument.ArgumentTypes"); - return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") + return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") .getDeclaredMethod("createPlayerArgument", String.class) .invoke(null, name); } catch (Exception ignored) { @@ -68,21 +68,22 @@ private Arguments() { Class.forName("org.bukkit.Bukkit"); return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { String nameStr = (String) raw; - return Bukkit.getPlayer(nameStr); + return (T) Bukkit.getPlayer(nameStr); }); } catch (Exception ignored) { } return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { String nameStr = (String) raw; - return OumLib.proxy().getPlayer(nameStr).orElse(null); + return (T) OumLib.proxy().getPlayer(nameStr).orElse(null); }); } @Contract("_ -> new") - public static @NonNull Argument finePosition(String name) { + @SuppressWarnings("unchecked") + public static @NonNull Argument finePosition(String name) { try { Class.forName("io.papermc.paper.command.brigadier.argument.ArgumentTypes"); - return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") + return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") .getDeclaredMethod("createFinePositionArgument", String.class) .invoke(null, name); } catch (Exception ignored) { @@ -91,10 +92,11 @@ private Arguments() { } @Contract("_ -> new") - public static @NonNull Argument blockPosition(String name) { + @SuppressWarnings("unchecked") + public static @NonNull Argument blockPosition(String name) { try { Class.forName("io.papermc.paper.command.brigadier.argument.ArgumentTypes"); - return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") + return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") .getDeclaredMethod("createBlockPositionArgument", String.class) .invoke(null, name); } catch (Exception ignored) { @@ -103,10 +105,11 @@ private Arguments() { } @Contract("_ -> new") - public static @NonNull Argument players(String name) { + @SuppressWarnings("unchecked") + public static @NonNull Argument players(String name) { try { Class.forName("io.papermc.paper.command.brigadier.argument.ArgumentTypes"); - return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") + return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") .getDeclaredMethod("createPlayersArgument", String.class) .invoke(null, name); } catch (Exception ignored) { @@ -116,22 +119,23 @@ private Arguments() { return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { String nameStr = (String) raw; var player = Bukkit.getPlayer(nameStr); - return player != null ? List.of(player) : List.of(); + return (T) (player != null ? List.of(player) : List.of()); }); } catch (Exception ignored) { } return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { String nameStr = (String) raw; var player = OumLib.proxy().getPlayer(nameStr).orElse(null); - return player != null ? List.of(player) : List.of(); + return (T) (player != null ? List.of(player) : List.of()); }); } @Contract("_ -> new") - public static @NonNull Argument world(String name) { + @SuppressWarnings("unchecked") + public static @NonNull Argument world(String name) { try { Class.forName("io.papermc.paper.command.brigadier.argument.ArgumentTypes"); - return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") + return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") .getDeclaredMethod("createWorldArgument", String.class) .invoke(null, name); } catch (Exception ignored) { @@ -140,10 +144,11 @@ private Arguments() { } @Contract("_ -> new") - public static @NonNull Argument key(String name) { + @SuppressWarnings("unchecked") + public static @NonNull Argument key(String name) { try { Class.forName("io.papermc.paper.command.brigadier.argument.ArgumentTypes"); - return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") + return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") .getDeclaredMethod("createKeyArgument", String.class) .invoke(null, name); } catch (Exception ignored) { @@ -200,11 +205,11 @@ private Arguments() { } @Contract("_ -> new") - @SuppressWarnings("DataFlowIssue") - public static @NonNull Argument offlinePlayer(String name) { + @SuppressWarnings({"DataFlowIssue", "unchecked"}) + public static @NonNull Argument offlinePlayer(String name) { try { Class.forName("org.bukkit.Bukkit"); - return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { + return (Argument) new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> { String nameStr = (String) raw; return Bukkit.getOfflinePlayer(nameStr); }).suggests(context -> { @@ -220,15 +225,15 @@ private Arguments() { }); } catch (Exception ignored) { } - return new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> (String) raw); + return (Argument) new Argument<>(name, StringArgumentType.word(), (raw, ctx) -> (String) raw); } @Contract("_ -> new") - @SuppressWarnings("DataFlowIssue") - public static @NonNull Argument entity(String name) { + @SuppressWarnings("unchecked") + public static @NonNull Argument entity(String name) { try { Class.forName("io.papermc.paper.command.brigadier.argument.ArgumentTypes"); - return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") + return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") .getDeclaredMethod("createEntityArgument", String.class) .invoke(null, name); } catch (Exception ignored) { @@ -237,11 +242,11 @@ private Arguments() { } @Contract("_ -> new") - @SuppressWarnings("DataFlowIssue") - public static @NonNull Argument entities(String name) { + @SuppressWarnings("unchecked") + public static @NonNull Argument entities(String name) { try { Class.forName("io.papermc.paper.command.brigadier.argument.ArgumentTypes"); - return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") + return (Argument) Class.forName("dev.oum.oumlib.command.platform.PaperCommandHelper") .getDeclaredMethod("createEntitiesArgument", String.class) .invoke(null, name); } catch (Exception ignored) { diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/CommandBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/CommandBuilder.java index b7a63fd..4928413 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/CommandBuilder.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/CommandBuilder.java @@ -1,8 +1,8 @@ package dev.oum.oumlib.command; import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.util.Cooldown; -import dev.oum.oumlib.util.Permission; +import dev.oum.oumlib.bridge.permission.Permission; +import dev.oum.oumlib.cooldown.CooldownManager; import org.jetbrains.annotations.CheckReturnValue; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; @@ -11,6 +11,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; @@ -24,9 +25,10 @@ public final class CommandBuilder { private String description = ""; private String permission; private Permission permissionObject; - private String cooldownMessage = "Wait s before using this again."; + private String cooldownMessage = "Wait before using this again."; private Consumer executor; - private Cooldown cooldown; + private Duration cooldownDuration; + private CooldownManager cooldownManager; private Predicate cooldownBypass; private BiConsumer exceptionHandler; @@ -46,13 +48,13 @@ private CommandBuilder(String label) { return new CommandBuilder(label); } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull CommandBuilder description(@NonNull String description) { this.description = description; return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") @Deprecated(since = "1.0.5", forRemoval = false) public @NonNull CommandBuilder permission(@NonNull String permission) { this.permission = permission; @@ -60,39 +62,47 @@ private CommandBuilder(String label) { } @Contract(value = "_ -> this", mutates = "this") - @CheckReturnValue public @NonNull CommandBuilder permission(@NonNull Permission permission) { this.permissionObject = permission; this.permission = permission.name(); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull CommandBuilder aliases(String @NonNull ... a) { aliases.addAll(List.of(a)); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull CommandBuilder cooldown(@NonNull Duration duration) { - this.cooldown = new Cooldown(duration); + this.cooldownDuration = duration; + if (this.cooldownManager == null) { + this.cooldownManager = CooldownManager.create(); + } return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull CommandBuilder cooldown(@NonNull Duration duration, @NonNull CooldownManager manager) { + this.cooldownDuration = duration; + this.cooldownManager = manager; + return this; + } + + @Contract(value = "_ -> this", mutates = "this") public @NonNull CommandBuilder cooldownMessage(@NonNull String message) { this.cooldownMessage = message; return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull CommandBuilder argument(@NonNull Argument argument) { arguments.add(argument); return this; } - @Contract("_ -> this") - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull CommandBuilder subcommand(@NonNull Consumer<@NonNull SubcommandBuilder> configurer) { SubcommandBuilder sub = new SubcommandBuilder(); configurer.accept(sub); @@ -100,7 +110,7 @@ private CommandBuilder(String label) { return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull CommandBuilder executes(@NonNull Consumer<@NonNull CommandContext> executor) { this.executor = executor; return this; @@ -122,62 +132,79 @@ public void register() { } } + @CheckReturnValue public @NonNull String label() { return label; } + @CheckReturnValue public @NonNull String description() { return description; } + @CheckReturnValue public @Nullable String permission() { return permission; } + @CheckReturnValue public @Nullable Permission permissionObject() { return permissionObject; } + @CheckReturnValue public @NonNull String cooldownMessage() { return cooldownMessage; } + @CheckReturnValue public @NonNull List<@NonNull Argument> arguments() { return arguments; } + @CheckReturnValue public @NonNull List<@NonNull SubcommandBuilder> subcommands() { return subcommands; } + @CheckReturnValue public @Nullable Consumer<@NonNull CommandContext> executor() { return executor; } + @CheckReturnValue public @NonNull List<@NonNull String> aliases() { return aliases; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull CommandBuilder cooldownBypass(@NonNull Predicate<@NonNull CommandContext> bypassPredicate) { this.cooldownBypass = bypassPredicate; return this; } + @CheckReturnValue public @Nullable Predicate<@NonNull CommandContext> cooldownBypass() { return cooldownBypass; } - public @Nullable Cooldown cooldown() { - return cooldown; + @CheckReturnValue + public @Nullable Duration cooldownDuration() { + return cooldownDuration; } @CheckReturnValue + public @Nullable CooldownManager cooldownManager() { + return cooldownManager; + } + + @Contract(value = "_ -> this", mutates = "this") public @NonNull CommandBuilder onException(@NonNull BiConsumer handler) { this.exceptionHandler = handler; return this; } + @CheckReturnValue public @Nullable BiConsumer exceptionHandler() { return exceptionHandler; } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/SubcommandBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/SubcommandBuilder.java index e6fb639..8f3f274 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/SubcommandBuilder.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/SubcommandBuilder.java @@ -1,15 +1,19 @@ package dev.oum.oumlib.command; -import dev.oum.oumlib.util.Permission; -import org.jetbrains.annotations.CheckReturnValue; +import dev.oum.oumlib.bridge.permission.Permission; +import dev.oum.oumlib.cooldown.CooldownManager; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Unmodifiable; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.UUID; +import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Predicate; public final class SubcommandBuilder { @@ -19,9 +23,14 @@ public final class SubcommandBuilder { private String label; private String permission; private Permission permissionObject; + private String cooldownMessage = "Wait before using this again."; private Consumer executor; + private Duration cooldownDuration; + private CooldownManager cooldownManager; + private Predicate cooldownBypass; + private BiConsumer exceptionHandler; - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull SubcommandBuilder aliases(String @NonNull ... a) { this.aliases.addAll(List.of(a)); return this; @@ -34,34 +43,67 @@ public final class SubcommandBuilder { return List.copyOf(aliases); } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull SubcommandBuilder label(@NonNull String label) { this.label = label; return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") @Deprecated(since = "1.0.5") public @NonNull SubcommandBuilder permission(@NonNull String permission) { this.permission = permission; return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull SubcommandBuilder permission(@NonNull Permission permission) { this.permissionObject = permission; this.permission = permission.name(); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") + public @NonNull SubcommandBuilder cooldown(@NonNull Duration duration) { + this.cooldownDuration = duration; + if (this.cooldownManager == null) { + this.cooldownManager = CooldownManager.create(); + } + return this; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull SubcommandBuilder cooldown(@NonNull Duration duration, @NonNull CooldownManager manager) { + this.cooldownDuration = duration; + this.cooldownManager = manager; + return this; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull SubcommandBuilder cooldownMessage(@NonNull String message) { + this.cooldownMessage = message; + return this; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull SubcommandBuilder cooldownBypass(@NonNull Predicate<@NonNull CommandContext> predicate) { + this.cooldownBypass = predicate; + return this; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull SubcommandBuilder exceptionHandler(@NonNull BiConsumer<@NonNull CommandContext, @NonNull Throwable> handler) { + this.exceptionHandler = handler; + return this; + } + + @Contract(value = "_ -> this", mutates = "this") public @NonNull SubcommandBuilder argument(@NonNull Argument argument) { arguments.add(argument); return this; } - @Contract("_ -> this") - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull SubcommandBuilder subcommand(@NonNull Consumer<@NonNull SubcommandBuilder> configurer) { SubcommandBuilder sub = new SubcommandBuilder(); configurer.accept(sub); @@ -69,7 +111,7 @@ public final class SubcommandBuilder { return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull SubcommandBuilder executes(@NonNull Consumer<@NonNull CommandContext> executor) { this.executor = executor; return this; @@ -87,6 +129,26 @@ public final class SubcommandBuilder { return permissionObject; } + public @Nullable Duration cooldownDuration() { + return cooldownDuration; + } + + public @Nullable CooldownManager cooldownManager() { + return cooldownManager; + } + + public @NonNull String cooldownMessage() { + return cooldownMessage; + } + + public @Nullable Predicate cooldownBypass() { + return cooldownBypass; + } + + public @Nullable BiConsumer exceptionHandler() { + return exceptionHandler; + } + @Contract(pure = true) @NonNull @Unmodifiable diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/PaperCommandRegistrar.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/PaperCommandRegistrar.java index 19b01ae..57026af 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/PaperCommandRegistrar.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/PaperCommandRegistrar.java @@ -3,8 +3,9 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.bridge.permission.Permission; import dev.oum.oumlib.command.*; -import dev.oum.oumlib.util.Permission; +import dev.oum.oumlib.cooldown.CooldownManager; import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.Commands; import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; @@ -12,9 +13,12 @@ import org.bukkit.entity.Player; import org.jspecify.annotations.NonNull; +import java.time.Duration; import java.util.List; +import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Predicate; public final class PaperCommandRegistrar implements CommandRegistrar { @@ -45,7 +49,9 @@ public void register(CommandBuilder builder) { } if (builder.executor() != null) { - attachArguments(root, builder.arguments(), builder.executor(), builder); + attachArguments(root, builder.arguments(), builder.executor(), builder.label(), + builder.permission(), builder.cooldownManager(), builder.cooldownDuration(), + builder.cooldownMessage(), builder.cooldownBypass(), builder.exceptionHandler()); } return root; @@ -73,7 +79,15 @@ public void register(CommandBuilder builder) { } if (sub.executor() != null) { - attachArguments(subLiteral, sub.arguments(), sub.executor(), builder); + CooldownManager cdMgr = sub.cooldownManager() != null ? sub.cooldownManager() : builder.cooldownManager(); + Duration cdDur = sub.cooldownDuration() != null ? sub.cooldownDuration() : builder.cooldownDuration(); + String cdMsg = sub.cooldownDuration() != null ? sub.cooldownMessage() : builder.cooldownMessage(); + Predicate cdBypass = sub.cooldownBypass() != null ? sub.cooldownBypass() : builder.cooldownBypass(); + BiConsumer exHandler = sub.exceptionHandler() != null ? sub.exceptionHandler() : builder.exceptionHandler(); + String perm = sub.permission() != null ? sub.permission() : builder.permission(); + + attachArguments(subLiteral, sub.arguments(), sub.executor(), builder.label() + " " + sub.label(), + perm, cdMgr, cdDur, cdMsg, cdBypass, exHandler); } return subLiteral; } @@ -82,15 +96,23 @@ private void attachArguments( @NonNull LiteralArgumentBuilder node, @NonNull List> args, Consumer exec, - CommandBuilder builder + String fullLabel, + String permission, + CooldownManager cdMgr, + Duration cdDur, + String cdMsg, + Predicate cdBypass, + BiConsumer exHandler ) { node.executes(ctx -> { - handleExecution(ctx.getSource(), new ArgumentMap(ctx), exec, builder); + handleExecution(ctx.getSource(), new ArgumentMap(ctx), exec, fullLabel, + permission, cdMgr, cdDur, cdMsg, cdBypass, exHandler); return 1; }); if (!args.isEmpty()) { - RequiredArgumentBuilder first = buildArgChain(args, exec, builder); + RequiredArgumentBuilder first = buildArgChain(args, exec, fullLabel, + permission, cdMgr, cdDur, cdMsg, cdBypass, exHandler); node.then(first); } } @@ -99,7 +121,13 @@ private void attachArguments( private RequiredArgumentBuilder buildArgChain( @NonNull List> args, Consumer exec, - CommandBuilder builder + String fullLabel, + String permission, + CooldownManager cdMgr, + Duration cdDur, + String cdMsg, + Predicate cdBypass, + BiConsumer exHandler ) { RequiredArgumentBuilder head = null; RequiredArgumentBuilder prev = null; @@ -114,7 +142,8 @@ private void attachArguments( } if (i == args.size() - 1) { current.executes(ctx -> { - handleExecution((CommandSourceStack) ctx.getSource(), new ArgumentMap(ctx), exec, builder); + handleExecution((CommandSourceStack) ctx.getSource(), new ArgumentMap(ctx), exec, fullLabel, + permission, cdMgr, cdDur, cdMsg, cdBypass, exHandler); return 1; }); } @@ -133,36 +162,40 @@ private void handleExecution( @NonNull CommandSourceStack source, ArgumentMap map, Consumer exec, - @NonNull CommandBuilder builder + String label, + String permission, + CooldownManager cdMgr, + Duration cdDur, + String cdMsg, + Predicate cdBypass, + BiConsumer exHandler ) { var sender = source.getSender(); - CommandContext context = new CommandContext(source, sender, builder.label(), map); - if (builder.cooldown() != null && sender instanceof Player player) { - boolean bypassed; - if (builder.cooldownBypass() != null) { - bypassed = builder.cooldownBypass().test(context); - } else { - String bypassPerm = (builder.permission() != null ? builder.permission() : builder.label()) + ".bypass"; - bypassed = player.hasPermission(bypassPerm); - } - if (!bypassed && builder.cooldown().isOnCooldown(player.getUniqueId())) { - long remaining = builder.cooldown().remainingSeconds(player.getUniqueId()); - player.sendMessage(MiniMessage.miniMessage() - .deserialize(builder.cooldownMessage().replace("", String.valueOf(remaining)))); - return; + CommandContext context = new CommandContext(source, sender, label, map); + if (sender instanceof Player player) { + if (cdMgr != null && cdDur != null) { + boolean bypassed = (cdBypass != null) + ? cdBypass.test(context) + : player.hasPermission((permission != null ? permission : label.replace(' ', '.')) + ".bypass"); + if (!bypassed && cdMgr.isOnCooldown(player.getUniqueId())) { + String remaining = cdMgr.formatRemaining(player.getUniqueId()); + player.sendMessage(MiniMessage.miniMessage() + .deserialize(cdMsg.replace("", remaining))); + return; + } + cdMgr.apply(player.getUniqueId(), cdDur); } - builder.cooldown().set(player.getUniqueId()); } try { exec.accept(context); } catch (Throwable ex) { - BiConsumer handler = builder.exceptionHandler() != null - ? builder.exceptionHandler() + BiConsumer handler = exHandler != null + ? exHandler : OumLib.commandErrorHandler(); if (handler != null) { handler.accept(context, ex); } else { - OumLib.logError("Unhandled exception executing command /" + builder.label(), ex); + OumLib.logError("Unhandled exception executing command /" + label, ex); } } } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/VelocityCommandRegistrar.java b/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/VelocityCommandRegistrar.java index f177dc9..622e5fa 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/VelocityCommandRegistrar.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/command/platform/VelocityCommandRegistrar.java @@ -8,14 +8,18 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.Player; import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.bridge.permission.Permission; import dev.oum.oumlib.command.*; -import dev.oum.oumlib.util.Permission; +import dev.oum.oumlib.cooldown.CooldownManager; import net.kyori.adventure.text.minimessage.MiniMessage; import org.jspecify.annotations.NonNull; +import java.time.Duration; import java.util.List; +import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Predicate; public final class VelocityCommandRegistrar implements CommandRegistrar { @@ -49,7 +53,9 @@ public void register(@NonNull CommandBuilder builder) { } if (builder.executor() != null) { - attachArguments(root, builder.arguments(), builder.executor(), builder); + attachArguments(root, builder.arguments(), builder.executor(), builder.label(), + builder.permission(), builder.cooldownManager(), builder.cooldownDuration(), + builder.cooldownMessage(), builder.cooldownBypass(), builder.exceptionHandler()); } return root; @@ -77,7 +83,15 @@ public void register(@NonNull CommandBuilder builder) { } if (sub.executor() != null) { - attachArguments(subLiteral, sub.arguments(), sub.executor(), builder); + CooldownManager cdMgr = sub.cooldownManager() != null ? sub.cooldownManager() : builder.cooldownManager(); + Duration cdDur = sub.cooldownDuration() != null ? sub.cooldownDuration() : builder.cooldownDuration(); + String cdMsg = sub.cooldownDuration() != null ? sub.cooldownMessage() : builder.cooldownMessage(); + Predicate cdBypass = sub.cooldownBypass() != null ? sub.cooldownBypass() : builder.cooldownBypass(); + BiConsumer exHandler = sub.exceptionHandler() != null ? sub.exceptionHandler() : builder.exceptionHandler(); + String perm = sub.permission() != null ? sub.permission() : builder.permission(); + + attachArguments(subLiteral, sub.arguments(), sub.executor(), builder.label() + " " + sub.label(), + perm, cdMgr, cdDur, cdMsg, cdBypass, exHandler); } return subLiteral; } @@ -86,17 +100,25 @@ private void attachArguments( LiteralArgumentBuilder node, @NonNull List> args, Consumer exec, - CommandBuilder builder + String fullLabel, + String permission, + CooldownManager cdMgr, + Duration cdDur, + String cdMsg, + Predicate cdBypass, + BiConsumer exHandler ) { if (args.isEmpty()) { node.executes(ctx -> { - handleExecution(ctx.getSource(), new ArgumentMap(ctx), exec, builder); + handleExecution(ctx.getSource(), new ArgumentMap(ctx), exec, fullLabel, + permission, cdMgr, cdDur, cdMsg, cdBypass, exHandler); return 1; }); return; } - RequiredArgumentBuilder first = buildArgChain(args, exec, builder); + RequiredArgumentBuilder first = buildArgChain(args, exec, fullLabel, + permission, cdMgr, cdDur, cdMsg, cdBypass, exHandler); node.then(first); } @@ -104,7 +126,13 @@ private void attachArguments( private RequiredArgumentBuilder buildArgChain( @NonNull List> args, Consumer exec, - CommandBuilder builder + String fullLabel, + String permission, + CooldownManager cdMgr, + Duration cdDur, + String cdMsg, + Predicate cdBypass, + BiConsumer exHandler ) { RequiredArgumentBuilder head = null; RequiredArgumentBuilder prev = null; @@ -119,7 +147,8 @@ private void attachArguments( } if (i == args.size() - 1) { current.executes(ctx -> { - handleExecution((CommandSource) ctx.getSource(), new ArgumentMap(ctx), exec, builder); + handleExecution((CommandSource) ctx.getSource(), new ArgumentMap(ctx), exec, fullLabel, + permission, cdMgr, cdDur, cdMsg, cdBypass, exHandler); return 1; }); } @@ -138,35 +167,39 @@ private void handleExecution( CommandSource source, ArgumentMap map, Consumer exec, - @NonNull CommandBuilder builder + String label, + String permission, + CooldownManager cdMgr, + Duration cdDur, + String cdMsg, + Predicate cdBypass, + BiConsumer exHandler ) { - CommandContext context = new CommandContext(source, source, builder.label(), map); - if (builder.cooldown() != null && source instanceof Player player) { - boolean bypassed; - if (builder.cooldownBypass() != null) { - bypassed = builder.cooldownBypass().test(context); - } else { - String bypassPerm = (builder.permission() != null ? builder.permission() : builder.label()) + ".bypass"; - bypassed = player.hasPermission(bypassPerm); - } - if (!bypassed && builder.cooldown().isOnCooldown(player.getUniqueId())) { - long remaining = builder.cooldown().remainingSeconds(player.getUniqueId()); - player.sendMessage(MiniMessage.miniMessage() - .deserialize(builder.cooldownMessage().replace("", String.valueOf(remaining)))); - return; + CommandContext context = new CommandContext(source, source, label, map); + if (source instanceof Player player) { + if (cdMgr != null && cdDur != null) { + boolean bypassed = (cdBypass != null) + ? cdBypass.test(context) + : player.hasPermission((permission != null ? permission : label.replace(' ', '.')) + ".bypass"); + if (!bypassed && cdMgr.isOnCooldown(player.getUniqueId())) { + String remaining = cdMgr.formatRemaining(player.getUniqueId()); + player.sendMessage(MiniMessage.miniMessage() + .deserialize(cdMsg.replace("", remaining))); + return; + } + cdMgr.apply(player.getUniqueId(), cdDur); } - builder.cooldown().set(player.getUniqueId()); } try { exec.accept(context); } catch (Throwable ex) { - BiConsumer handler = builder.exceptionHandler() != null - ? builder.exceptionHandler() + BiConsumer handler = exHandler != null + ? exHandler : OumLib.commandErrorHandler(); if (handler != null) { handler.accept(context, ex); } else { - OumLib.logError("Unhandled exception executing command /" + builder.label(), ex); + OumLib.logError("Unhandled exception executing command /" + label, ex); } } } 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 f3951a6..731637c 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 @@ -304,7 +304,6 @@ private T load() { } } - // Collect unknown keys the user may have added — preserved on save. Map unknownKeys = new LinkedHashMap<>(); for (String yamlKey : yaml.keySet()) { boolean isKnown = false; diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/config/ConfigWatcher.java b/oumlib-core/src/main/java/dev/oum/oumlib/config/ConfigWatcher.java index b0d4e69..c86e415 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/config/ConfigWatcher.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/config/ConfigWatcher.java @@ -4,13 +4,7 @@ import org.jspecify.annotations.NonNull; import java.io.IOException; -import java.nio.file.ClosedWatchServiceException; -import java.nio.file.FileSystems; -import java.nio.file.Path; -import java.nio.file.StandardWatchEventKinds; -import java.nio.file.WatchEvent; -import java.nio.file.WatchKey; -import java.nio.file.WatchService; +import java.nio.file.*; import java.util.HashMap; import java.util.HashSet; import java.util.Map; diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/Cooldown.java b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/Cooldown.java new file mode 100644 index 0000000..27489a7 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/Cooldown.java @@ -0,0 +1,69 @@ +package dev.oum.oumlib.cooldown; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.time.Duration; +import java.time.Instant; + +public record Cooldown( + @NonNull K key, + @NonNull Instant startTime, + @NonNull Instant expireTime, + @Nullable Object metadata +) { + + public Cooldown(@NonNull K key, @NonNull Instant startTime, @NonNull Instant expireTime) { + this(key, startTime, expireTime, null); + } + + public static @NonNull Cooldown of(@NonNull K key, @NonNull Duration duration) { + Instant now = Instant.now(); + return new Cooldown<>(key, now, now.plus(duration)); + } + + public static @NonNull Cooldown of(@NonNull K key, @NonNull Duration duration, @Nullable Object metadata) { + Instant now = Instant.now(); + return new Cooldown<>(key, now, now.plus(duration), metadata); + } + + public boolean isExpired() { + return Instant.now().isAfter(expireTime); + } + + public boolean isActive() { + return !isExpired(); + } + + public long remainingMillis() { + long diff = expireTime.toEpochMilli() - System.currentTimeMillis(); + return Math.max(0L, diff); + } + + public @NonNull Duration remainingDuration() { + return Duration.ofMillis(remainingMillis()); + } + + public long totalDurationMillis() { + return Math.max(0L, expireTime.toEpochMilli() - startTime.toEpochMilli()); + } + + public @NonNull Duration totalDuration() { + return Duration.ofMillis(totalDurationMillis()); + } + + public double progress() { + long total = totalDurationMillis(); + if (total <= 0) return 1.0; + long elapsed = System.currentTimeMillis() - startTime.toEpochMilli(); + return Math.clamp((double) elapsed / (double) total, 0.0, 1.0); + } + + public @NonNull String formatRemaining() { + return CooldownFormatter.DEFAULT.format(remainingDuration()); + } + + public @NonNull String formatRemaining(@NonNull CooldownFormatter formatter) { + return formatter.format(remainingDuration()); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/CooldownFormatter.java b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/CooldownFormatter.java new file mode 100644 index 0000000..aa300ba --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/CooldownFormatter.java @@ -0,0 +1,111 @@ +package dev.oum.oumlib.cooldown; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.time.Duration; +import java.util.Locale; + +public final class CooldownFormatter { + + public static final CooldownFormatter DEFAULT = builder().build(); + public static final CooldownFormatter COMPACT = builder().compact(true).build(); + public static final CooldownFormatter PRECISE = builder().showFractionsUnderOneSecond(true).build(); + + private final boolean compact; + private final boolean showFractionsUnderOneSecond; + private final String daysSuffix; + private final String hoursSuffix; + private final String minutesSuffix; + private final String secondsSuffix; + + @Contract(pure = true) + private CooldownFormatter(@NonNull Builder builder) { + this.compact = builder.compact; + this.showFractionsUnderOneSecond = builder.showFractionsUnderOneSecond; + this.daysSuffix = builder.daysSuffix; + this.hoursSuffix = builder.hoursSuffix; + this.minutesSuffix = builder.minutesSuffix; + this.secondsSuffix = builder.secondsSuffix; + } + + public static @NonNull Builder builder() { + return new Builder(); + } + + public @NonNull String format(@NonNull Duration duration) { + long totalMillis = Math.max(0, duration.toMillis()); + if (totalMillis == 0) { + return "0" + secondsSuffix; + } + + if (showFractionsUnderOneSecond && totalMillis < 1000) { + double secs = totalMillis / 1000.0; + return String.format(Locale.ROOT, "%.1f%s", secs, secondsSuffix); + } + + long totalSeconds = (totalMillis + 999) / 1000; + long days = totalSeconds / 86400; + long hours = (totalSeconds % 86400) / 3600; + long minutes = (totalSeconds % 3600) / 60; + long seconds = totalSeconds % 60; + + if (compact) { + if (days > 0) { + return String.format(Locale.ROOT, "%d:%02d:%02d:%02d", days, hours, minutes, seconds); + } + if (hours > 0) { + return String.format(Locale.ROOT, "%02d:%02d:%02d", hours, minutes, seconds); + } + return String.format(Locale.ROOT, "%02d:%02d", minutes, seconds); + } + + StringBuilder sb = new StringBuilder(); + if (days > 0) { + sb.append(days).append(daysSuffix).append(' '); + } + if (hours > 0 || days > 0) { + sb.append(hours).append(hoursSuffix).append(' '); + } + if (minutes > 0 || hours > 0 || days > 0) { + sb.append(minutes).append(minutesSuffix).append(' '); + } + sb.append(seconds).append(secondsSuffix); + + return sb.toString().trim(); + } + + public static final class Builder { + private boolean compact = false; + private boolean showFractionsUnderOneSecond = false; + private String daysSuffix = "d"; + private String hoursSuffix = "h"; + private String minutesSuffix = "m"; + private String secondsSuffix = "s"; + + private Builder() { + } + + public @NonNull Builder compact(boolean compact) { + this.compact = compact; + return this; + } + + public @NonNull Builder showFractionsUnderOneSecond(boolean show) { + this.showFractionsUnderOneSecond = show; + return this; + } + + public @NonNull Builder suffixes(@NonNull String days, @NonNull String hours, @NonNull String minutes, @NonNull String seconds) { + this.daysSuffix = days; + this.hoursSuffix = hours; + this.minutesSuffix = minutes; + this.secondsSuffix = seconds; + return this; + } + + public @NonNull CooldownFormatter build() { + return new CooldownFormatter(this); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/CooldownManager.java b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/CooldownManager.java new file mode 100644 index 0000000..9044bf8 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/CooldownManager.java @@ -0,0 +1,209 @@ +package dev.oum.oumlib.cooldown; + +import dev.oum.oumlib.cooldown.store.CooldownStore; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +public class CooldownManager { + + private final Map> cache = new ConcurrentHashMap<>(); + private final List>> expireListeners = new ArrayList<>(); + private Predicate bypassPredicate = key -> false; + private CooldownFormatter defaultFormatter = CooldownFormatter.DEFAULT; + private CooldownStore store; + + protected CooldownManager() { + } + + public static @NonNull CooldownManager create() { + return new CooldownManager<>(); + } + + public @NonNull CooldownManager bypassPredicate(@NonNull Predicate predicate) { + this.bypassPredicate = Objects.requireNonNull(predicate); + return this; + } + + public @NonNull CooldownManager defaultFormatter(@NonNull CooldownFormatter formatter) { + this.defaultFormatter = Objects.requireNonNull(formatter); + return this; + } + + public @NonNull CooldownManager onExpire(@NonNull BiConsumer> listener) { + this.expireListeners.add(Objects.requireNonNull(listener)); + return this; + } + + public @NonNull CooldownManager store(@Nullable CooldownStore store) { + this.store = store; + return this; + } + + public @NonNull CompletableFuture loadAllFromStore() { + if (store == null) { + return CompletableFuture.completedFuture(null); + } + return store.loadAll().thenAccept(loaded -> { + loaded.forEach((k, cd) -> { + if (cd.isActive()) { + cache.put(k, cd); + } + }); + }); + } + + public boolean isBypassed(@NonNull K key) { + return bypassPredicate.test(key); + } + + public boolean isOnCooldown(@NonNull K key) { + if (isBypassed(key)) return false; + Cooldown cd = cache.get(key); + if (cd == null) return false; + if (cd.isExpired()) { + removeAndTriggerExpire(key, cd); + return false; + } + return true; + } + + public boolean test(@NonNull K key) { + return isOnCooldown(key); + } + + public boolean testAndApply(@NonNull K key, @NonNull Duration duration) { + return testAndApply(key, duration, null); + } + + public boolean testAndApply(@NonNull K key, @NonNull Duration duration, @Nullable Object metadata) { + if (isBypassed(key)) return true; + synchronized (this) { + if (isOnCooldown(key)) { + return false; + } + apply(key, duration, metadata); + return true; + } + } + + public @NonNull Cooldown apply(@NonNull K key, @NonNull Duration duration) { + return apply(key, duration, null); + } + + public @NonNull Cooldown apply(@NonNull K key, @NonNull Duration duration, @Nullable Object metadata) { + Cooldown cd = Cooldown.of(key, duration, metadata); + cache.put(key, cd); + if (store != null) { + store.save(key, cd); + } + return cd; + } + + public @NonNull Optional> get(@NonNull K key) { + if (isBypassed(key)) return Optional.empty(); + Cooldown cd = cache.get(key); + if (cd == null) return Optional.empty(); + if (cd.isExpired()) { + removeAndTriggerExpire(key, cd); + return Optional.empty(); + } + return Optional.of(cd); + } + + public long remainingMillis(@NonNull K key) { + return get(key).map(Cooldown::remainingMillis).orElse(0L); + } + + public @NonNull Duration remainingDuration(@NonNull K key) { + return get(key).map(Cooldown::remainingDuration).orElse(Duration.ZERO); + } + + public @NonNull String formatRemaining(@NonNull K key) { + return formatRemaining(key, defaultFormatter); + } + + public @NonNull String formatRemaining(@NonNull K key, @NonNull CooldownFormatter formatter) { + return get(key).map(cd -> cd.formatRemaining(formatter)).orElse("0s"); + } + + public boolean reset(@NonNull K key) { + Cooldown removed = cache.remove(key); + if (store != null) { + store.remove(key); + } + return removed != null; + } + + public boolean extend(@NonNull K key, @NonNull Duration amount) { + Cooldown existing = cache.get(key); + if (existing == null || existing.isExpired()) return false; + Instant newExpire = existing.expireTime().plus(amount); + Cooldown updated = new Cooldown<>(key, existing.startTime(), newExpire, existing.metadata()); + cache.put(key, updated); + if (store != null) { + store.save(key, updated); + } + return true; + } + + public boolean reduce(@NonNull K key, @NonNull Duration amount) { + Cooldown existing = cache.get(key); + if (existing == null || existing.isExpired()) return false; + Instant newExpire = existing.expireTime().minus(amount); + if (Instant.now().isAfter(newExpire)) { + removeAndTriggerExpire(key, existing); + return true; + } + Cooldown updated = new Cooldown<>(key, existing.startTime(), newExpire, existing.metadata()); + cache.put(key, updated); + if (store != null) { + store.save(key, updated); + } + return true; + } + + public void cleanUp() { + Iterator>> it = cache.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + if (entry.getValue().isExpired()) { + it.remove(); + if (store != null) { + store.remove(entry.getKey()); + } + for (BiConsumer> listener : expireListeners) { + listener.accept(entry.getKey(), entry.getValue()); + } + } + } + } + + public void clear() { + cache.clear(); + if (store != null) { + store.clear(); + } + } + + public @NonNull Map> asMap() { + return Collections.unmodifiableMap(cache); + } + + private void removeAndTriggerExpire(K key, Cooldown cd) { + cache.remove(key, cd); + if (store != null) { + store.remove(key); + } + for (BiConsumer> listener : expireListeners) { + listener.accept(key, cd); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/RateLimiter.java b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/RateLimiter.java new file mode 100644 index 0000000..23b2d9d --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/RateLimiter.java @@ -0,0 +1,135 @@ +package dev.oum.oumlib.cooldown; + +import org.jspecify.annotations.NonNull; + +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public final class RateLimiter { + + private final Strategy strategy; + private final int maxPermits; + private final long windowMillis; + private final Map windowMap; + private final Map bucketMap; + + private RateLimiter(Strategy strategy, int maxPermits, @NonNull Duration duration) { + this.strategy = strategy; + this.maxPermits = Math.max(1, maxPermits); + this.windowMillis = Math.max(1, duration.toMillis()); + this.windowMap = (strategy == Strategy.SLIDING_WINDOW) ? new ConcurrentHashMap<>() : null; + this.bucketMap = (strategy == Strategy.TOKEN_BUCKET) ? new ConcurrentHashMap<>() : null; + } + + public static @NonNull RateLimiter slidingWindow(int maxRequests, @NonNull Duration windowDuration) { + return new RateLimiter<>(Strategy.SLIDING_WINDOW, maxRequests, windowDuration); + } + + public static @NonNull RateLimiter tokenBucket(int capacity, @NonNull Duration refillDuration) { + return new RateLimiter<>(Strategy.TOKEN_BUCKET, capacity, refillDuration); + } + + public boolean tryAcquire(@NonNull K key) { + return tryAcquire(key, 1); + } + + public boolean tryAcquire(@NonNull K key, int permits) { + if (permits <= 0) return true; + Objects.requireNonNull(key); + + if (strategy == Strategy.SLIDING_WINDOW) { + return tryAcquireSlidingWindow(key, permits); + } else { + return tryAcquireTokenBucket(key, permits); + } + } + + private synchronized boolean tryAcquireSlidingWindow(K key, int permits) { + long now = System.currentTimeMillis(); + long cutoff = now - windowMillis; + WindowState state = windowMap.computeIfAbsent(key, k -> new WindowState()); + while (!state.timestamps.isEmpty() && state.timestamps.peekFirst() <= cutoff) { + state.timestamps.pollFirst(); + } + if (state.timestamps.size() + permits <= maxPermits) { + for (int i = 0; i < permits; i++) { + state.timestamps.addLast(now); + } + return true; + } + return false; + } + + private synchronized boolean tryAcquireTokenBucket(K key, int permits) { + long now = System.currentTimeMillis(); + BucketState state = bucketMap.computeIfAbsent(key, k -> new BucketState(maxPermits, now)); + long elapsed = now - state.lastRefill; + if (elapsed > 0) { + double tokensToAdd = ((double) elapsed / (double) windowMillis) * maxPermits; + state.tokens = Math.min((double) maxPermits, state.tokens + tokensToAdd); + state.lastRefill = now; + } + if (state.tokens >= permits) { + state.tokens -= permits; + return true; + } + return false; + } + + public synchronized int availablePermits(@NonNull K key) { + Objects.requireNonNull(key); + long now = System.currentTimeMillis(); + if (strategy == Strategy.SLIDING_WINDOW) { + WindowState state = windowMap.get(key); + if (state == null) return maxPermits; + long cutoff = now - windowMillis; + while (!state.timestamps.isEmpty() && state.timestamps.peekFirst() <= cutoff) { + state.timestamps.pollFirst(); + } + return Math.max(0, maxPermits - state.timestamps.size()); + } else { + BucketState state = bucketMap.get(key); + if (state == null) return maxPermits; + long elapsed = now - state.lastRefill; + double tokens = state.tokens; + if (elapsed > 0) { + double tokensToAdd = ((double) elapsed / (double) windowMillis) * maxPermits; + tokens = Math.min((double) maxPermits, tokens + tokensToAdd); + } + return (int) Math.floor(tokens); + } + } + + public void reset(@NonNull K key) { + if (windowMap != null) windowMap.remove(key); + if (bucketMap != null) bucketMap.remove(key); + } + + public void clear() { + if (windowMap != null) windowMap.clear(); + if (bucketMap != null) bucketMap.clear(); + } + + private enum Strategy { + SLIDING_WINDOW, + TOKEN_BUCKET + } + + private static final class WindowState { + private final Deque timestamps = new ArrayDeque<>(); + } + + private static final class BucketState { + private double tokens; + private long lastRefill; + + private BucketState(double tokens, long lastRefill) { + this.tokens = tokens; + this.lastRefill = lastRefill; + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/store/CooldownStore.java b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/store/CooldownStore.java new file mode 100644 index 0000000..4f66473 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/cooldown/store/CooldownStore.java @@ -0,0 +1,21 @@ +package dev.oum.oumlib.cooldown.store; + +import dev.oum.oumlib.cooldown.Cooldown; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public interface CooldownStore { + + @NonNull CompletableFuture save(@NonNull K key, @NonNull Cooldown cooldown); + + @NonNull CompletableFuture remove(@NonNull K key); + + @NonNull CompletableFuture<@Nullable Cooldown> load(@NonNull K key); + + @NonNull CompletableFuture>> loadAll(); + + @NonNull CompletableFuture clear(); +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/database/Database.java b/oumlib-core/src/main/java/dev/oum/oumlib/database/Database.java index 155c38b..6dfdb3b 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/database/Database.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/database/Database.java @@ -188,6 +188,71 @@ private static int parseVersion(@NonNull String filename) { return filename; } + private static @NonNull RowMapper buildMapper(@NonNull Class type) { + if (type.isRecord()) { + RecordComponent[] components = type.getRecordComponents(); + Class[] paramTypes = new Class[components.length]; + for (int i = 0; i < components.length; i++) { + paramTypes[i] = components[i].getType(); + } + Constructor ctor; + try { + ctor = type.getDeclaredConstructor(paramTypes); + ctor.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("No canonical constructor for record " + type.getName(), e); + } + return rs -> { + try { + Set available = columnLabels(rs); + Object[] args = new Object[components.length]; + for (int i = 0; i < components.length; i++) { + String name = components[i].getName(); + String label = available.contains(name) ? name + : (available.contains(toSnakeCase(name)) ? toSnakeCase(name) : null); + if (label == null) { + OumLib.logDebug("Unmapped record component '" + name + "' for " + type.getName()); + args[i] = null; + } else { + args[i] = getValueFromResultSet(rs, label, components[i].getType()); + } + } + return ctor.newInstance(args); + } catch (ReflectiveOperationException e) { + throw new SQLException("Failed to map row to record " + type.getName(), e); + } + }; + } + + Map fields = new HashMap<>(); + for (Field field : type.getDeclaredFields()) { + field.setAccessible(true); + fields.put(field.getName(), field); + } + return rs -> { + try { + Object instance = type.getDeclaredConstructor().newInstance(); + ResultSetMetaData md = rs.getMetaData(); + int columns = md.getColumnCount(); + for (int i = 1; i <= columns; i++) { + String label = md.getColumnLabel(i); + Field field = fields.get(label); + if (field == null) { + field = fields.get(toCamelCase(label)); + } + if (field == null) { + OumLib.logDebug("Unmapped column '" + label + "' for " + type.getName()); + continue; + } + field.set(instance, getValueFromResultSet(rs, label, field.getType())); + } + return instance; + } catch (ReflectiveOperationException e) { + throw new SQLException("Failed to map row to class " + type.getName(), e); + } + }; + } + public @NonNull Connection getConnection() throws SQLException { return dataSource.getConnection(); } @@ -210,7 +275,6 @@ public void setSlowQueryThresholdMs(long ms) { this.slowQueryThresholdMs = ms; } - @CheckReturnValue public @NonNull Promise executeUpdate(@NonNull String sql, Object... params) { return Promise.supplyVirtual(() -> { long start = System.currentTimeMillis(); @@ -231,9 +295,6 @@ public void setSlowQueryThresholdMs(long ms) { }); } - /** - * Executes a SELECT query asynchronously using virtual threads and returns mapped results. - */ @CheckReturnValue public @NonNull Promise>> executeQuery(@NonNull String sql, Object... params) { return Promise.supplyVirtual(() -> { @@ -302,72 +363,6 @@ public void setSlowQueryThresholdMs(long ms) { return (RowMapper) mapperCache.computeIfAbsent(type, Database::buildMapper); } - private static @NonNull RowMapper buildMapper(@NonNull Class type) { - if (type.isRecord()) { - RecordComponent[] components = type.getRecordComponents(); - Class[] paramTypes = new Class[components.length]; - for (int i = 0; i < components.length; i++) { - paramTypes[i] = components[i].getType(); - } - Constructor ctor; - try { - ctor = type.getDeclaredConstructor(paramTypes); - ctor.setAccessible(true); - } catch (NoSuchMethodException e) { - throw new IllegalStateException("No canonical constructor for record " + type.getName(), e); - } - return rs -> { - try { - Set available = columnLabels(rs); - Object[] args = new Object[components.length]; - for (int i = 0; i < components.length; i++) { - String name = components[i].getName(); - String label = available.contains(name) ? name - : (available.contains(toSnakeCase(name)) ? toSnakeCase(name) : null); - if (label == null) { - OumLib.logDebug("Unmapped record component '" + name + "' for " + type.getName()); - args[i] = null; - } else { - args[i] = getValueFromResultSet(rs, label, components[i].getType()); - } - } - return ctor.newInstance(args); - } catch (ReflectiveOperationException e) { - throw new SQLException("Failed to map row to record " + type.getName(), e); - } - }; - } - - Map fields = new HashMap<>(); - for (Field field : type.getDeclaredFields()) { - field.setAccessible(true); - fields.put(field.getName(), field); - } - return rs -> { - try { - Object instance = type.getDeclaredConstructor().newInstance(); - ResultSetMetaData md = rs.getMetaData(); - int columns = md.getColumnCount(); - for (int i = 1; i <= columns; i++) { - String label = md.getColumnLabel(i); - Field field = fields.get(label); - if (field == null) { - field = fields.get(toCamelCase(label)); - } - if (field == null) { - OumLib.logDebug("Unmapped column '" + label + "' for " + type.getName()); - continue; - } - field.set(instance, getValueFromResultSet(rs, label, field.getType())); - } - return instance; - } catch (ReflectiveOperationException e) { - throw new SQLException("Failed to map row to class " + type.getName(), e); - } - }; - } - - @CheckReturnValue public @NonNull Promise executeBatch(@NonNull String sql, @NonNull List parameterBatch) { return Promise.supplyVirtual(() -> { long start = System.currentTimeMillis(); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/effect/Effects.java b/oumlib-core/src/main/java/dev/oum/oumlib/effect/Effects.java index a1a05f9..d0f6a15 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/effect/Effects.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/effect/Effects.java @@ -2,6 +2,8 @@ import org.bukkit.Particle; import org.bukkit.Sound; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; public final class Effects { @@ -9,10 +11,14 @@ public final class Effects { private Effects() { } + @Contract("_ -> new") + @CheckReturnValue public static @NonNull SoundBuilder sound(@NonNull Sound sound) { return new SoundBuilder(sound); } + @Contract("_ -> new") + @CheckReturnValue public static @NonNull ParticleBuilder particle(@NonNull Particle particle) { return new ParticleBuilder(particle); } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/effect/ParticleBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/effect/ParticleBuilder.java index ba1af9f..87b149e 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/effect/ParticleBuilder.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/effect/ParticleBuilder.java @@ -4,6 +4,7 @@ import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.entity.Player; +import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -22,11 +23,13 @@ public ParticleBuilder(@NonNull Particle particle) { this.particle = particle; } + @Contract(value = "_ -> this", mutates = "this") public @NonNull ParticleBuilder count(int count) { this.count = count; return this; } + @Contract(value = "_, _, _ -> this", mutates = "this") public @NonNull ParticleBuilder offset(double x, double y, double z) { this.offsetX = x; this.offsetY = y; @@ -34,11 +37,18 @@ public ParticleBuilder(@NonNull Particle particle) { return this; } + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ParticleBuilder offset(double offset) { + return offset(offset, offset, offset); + } + + @Contract(value = "_ -> this", mutates = "this") public @NonNull ParticleBuilder speed(double speed) { this.speed = speed; return this; } + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ParticleBuilder color(@NonNull Color color, float size) { if (particle == Particle.DUST) { this.data = new Particle.DustOptions(color, size); @@ -46,6 +56,7 @@ public ParticleBuilder(@NonNull Particle particle) { return this; } + @Contract(value = "_, _, _ -> this", mutates = "this") public @NonNull ParticleBuilder transition(@NonNull Color from, @NonNull Color to, float size) { if (particle == Particle.DUST_COLOR_TRANSITION) { this.data = new Particle.DustTransition(from, to, size); @@ -53,6 +64,7 @@ public ParticleBuilder(@NonNull Particle particle) { return this; } + @Contract(value = "_ -> this", mutates = "this") public @NonNull ParticleBuilder data(@Nullable Object data) { this.data = data; return this; @@ -70,7 +82,32 @@ public void spawn(@NonNull Player player, @NonNull Location location) { public void spawn(@NonNull Collection players, @NonNull Location location) { for (Player p : players) { - p.spawnParticle(particle, location, count, offsetX, offsetY, offsetZ, speed, data); + spawn(p, location); + } + } + + public void ring(@NonNull Location center, double radius, int points) { + if (center.getWorld() == null) return; + double increment = (2 * Math.PI) / points; + for (int i = 0; i < points; i++) { + double angle = i * increment; + double x = center.getX() + radius * Math.cos(angle); + double z = center.getZ() + radius * Math.sin(angle); + Location point = new Location(center.getWorld(), x, center.getY(), z); + spawn(point); + } + } + + public void helix(@NonNull Location base, double radius, double height, int points, double rotations) { + if (base.getWorld() == null) return; + for (int i = 0; i < points; i++) { + double progress = (double) i / points; + double angle = progress * (2 * Math.PI * rotations); + double x = base.getX() + radius * Math.cos(angle); + double y = base.getY() + (progress * height); + double z = base.getZ() + radius * Math.sin(angle); + Location point = new Location(base.getWorld(), x, y, z); + spawn(point); } } } 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 4be4b47..dce074b 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 @@ -7,6 +7,8 @@ import org.bukkit.Registry; import org.bukkit.SoundCategory; import org.bukkit.entity.Player; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; import java.util.Collection; @@ -31,11 +33,13 @@ public SoundBuilder(org.bukkit.@NonNull Sound sound) { this.soundKey = Registry.SOUNDS.getKey(sound); } + @Contract(value = "_ -> this", mutates = "this") public @NonNull SoundBuilder source(@NonNull Source source) { this.source = source; return this; } + @Contract(value = "_ -> this", mutates = "this") public @NonNull SoundBuilder category(@NonNull SoundCategory category) { try { this.source = Source.valueOf(category.name()); @@ -45,16 +49,19 @@ public SoundBuilder(org.bukkit.@NonNull Sound sound) { return this; } + @Contract(value = "_ -> this", mutates = "this") public @NonNull SoundBuilder volume(float volume) { this.volume = volume; return this; } + @Contract(value = "_ -> this", mutates = "this") public @NonNull SoundBuilder pitch(float pitch) { this.pitch = pitch; return this; } + @Contract(value = "_ -> this", mutates = "this") public @NonNull SoundBuilder pitchVariance(float range) { this.pitchRange = range; return this; @@ -67,6 +74,8 @@ private float calculatePitch() { return (float) ThreadLocalRandom.current().nextDouble(min, max); } + @Contract("-> new") + @CheckReturnValue public net.kyori.adventure.sound.@NonNull Sound build() { return net.kyori.adventure.sound.Sound.sound(soundKey, source, volume, calculatePitch()); } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/AbstractVirtualDisplay.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/AbstractVirtualDisplay.java new file mode 100644 index 0000000..3edc2b4 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/AbstractVirtualDisplay.java @@ -0,0 +1,244 @@ +package dev.oum.oumlib.entity.display; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.util.Quaternion4f; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.util.Vector3f; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDestroyEntities; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityTeleport; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +public abstract class AbstractVirtualDisplay implements VirtualDisplay { + + private static final AtomicInteger ENTITY_ID_COUNTER = new AtomicInteger(1_500_000_000); + + protected final int entityId; + protected final UUID uniqueId; + protected final Set viewers = ConcurrentHashMap.newKeySet(); + protected Location location; + protected double viewDistance = 48.0; + protected Billboard billboard = Billboard.CENTER; + protected Vector scale = new Vector(1.0, 1.0, 1.0); + protected Vector translation = new Vector(0.0, 0.0, 0.0); + protected int interpolationDuration = 0; + protected int interpolationDelay = 0; + protected Predicate visibilityFilter; + + protected AbstractVirtualDisplay(@NonNull Location location) { + this.entityId = ENTITY_ID_COUNTER.incrementAndGet(); + this.uniqueId = UUID.randomUUID(); + this.location = location.clone(); + } + + protected abstract @NonNull EntityType getEntityType(); + + protected abstract void appendCustomMetadata(@NonNull Player player, @NonNull List> list); + + @Override + public int getEntityId() { + return entityId; + } + + @Override + public @NonNull UUID getUniqueId() { + return uniqueId; + } + + @Override + public @NonNull Location getLocation() { + return location.clone(); + } + + @Override + public void setLocation(@NonNull Location location) { + this.location = location.clone(); + } + + @Override + public void teleport(@NonNull Location location) { + this.location = location.clone(); + if (viewers.isEmpty()) return; + com.github.retrooper.packetevents.protocol.world.Location peLoc = + new com.github.retrooper.packetevents.protocol.world.Location( + location.getX(), location.getY(), location.getZ(), + location.getYaw(), location.getPitch()); + WrapperPlayServerEntityTeleport packet = new WrapperPlayServerEntityTeleport(entityId, peLoc, false); + for (UUID uid : viewers) { + Player p = Bukkit.getPlayer(uid); + if (p != null && p.isOnline()) { + sendPacket(p, packet); + } + } + } + + @Override + public double getViewDistance() { + return viewDistance; + } + + @Override + public void setViewDistance(double viewDistance) { + this.viewDistance = viewDistance; + } + + @Override + public @NonNull Billboard getBillboard() { + return billboard; + } + + @Override + public void setBillboard(@NonNull Billboard billboard) { + this.billboard = billboard; + } + + @Override + public @NonNull Vector getScale() { + return scale.clone(); + } + + @Override + public void setScale(@NonNull Vector scale) { + this.scale = scale.clone(); + } + + @Override + public @NonNull Vector getTranslation() { + return translation.clone(); + } + + @Override + public void setTranslation(@NonNull Vector translation) { + this.translation = translation.clone(); + } + + @Override + public int getInterpolationDuration() { + return interpolationDuration; + } + + @Override + public void setInterpolationDuration(int ticks) { + this.interpolationDuration = ticks; + } + + @Override + public int getInterpolationDelay() { + return interpolationDelay; + } + + @Override + public void setInterpolationDelay(int ticks) { + this.interpolationDelay = ticks; + } + + @Override + public void setVisibilityFilter(@Nullable Predicate filter) { + this.visibilityFilter = filter; + } + + @Override + public boolean isVisibleTo(@NonNull Player player) { + if (!player.isOnline()) return false; + if (location.getWorld() != null && !location.getWorld().equals(player.getWorld())) return false; + if (location.distanceSquared(player.getLocation()) > (viewDistance * viewDistance)) return false; + return visibilityFilter == null || visibilityFilter.test(player); + } + + @Override + public void spawn(@NonNull Player player) { + if (!isVisibleTo(player)) return; + com.github.retrooper.packetevents.protocol.world.Location peLoc = + new com.github.retrooper.packetevents.protocol.world.Location( + location.getX(), location.getY(), location.getZ(), + location.getYaw(), location.getPitch()); + WrapperPlayServerSpawnEntity spawnPacket = new WrapperPlayServerSpawnEntity( + entityId, + uniqueId, + getEntityType(), + peLoc, + location.getYaw(), + 0, + new Vector3d(0, 0, 0) + ); + sendPacket(player, spawnPacket); + viewers.add(player.getUniqueId()); + updateMetadata(player); + } + + @Override + public void destroy(@NonNull Player player) { + viewers.remove(player.getUniqueId()); + WrapperPlayServerDestroyEntities destroyPacket = new WrapperPlayServerDestroyEntities(entityId); + sendPacket(player, destroyPacket); + } + + @Override + public void spawnAll(@NonNull Collection players) { + for (Player player : players) { + spawn(player); + } + } + + @Override + public void destroyAll(@NonNull Collection players) { + for (Player player : players) { + destroy(player); + } + } + + @Override + public void updateMetadata() { + for (UUID uid : viewers) { + Player p = Bukkit.getPlayer(uid); + if (p != null && p.isOnline()) { + updateMetadata(p); + } + } + } + + @Override + public void updateMetadata(@NonNull Player player) { + List> dataList = new ArrayList<>(); + dataList.add(new EntityData<>(8, EntityDataTypes.INT, interpolationDelay)); + dataList.add(new EntityData<>(9, EntityDataTypes.INT, interpolationDuration)); + dataList.add(new EntityData<>(10, EntityDataTypes.INT, interpolationDuration)); + dataList.add(new EntityData<>(11, EntityDataTypes.VECTOR3F, new Vector3f((float) translation.getX(), (float) translation.getY(), (float) translation.getZ()))); + dataList.add(new EntityData<>(12, EntityDataTypes.VECTOR3F, new Vector3f((float) scale.getX(), (float) scale.getY(), (float) scale.getZ()))); + dataList.add(new EntityData<>(13, EntityDataTypes.QUATERNION, new Quaternion4f(0, 0, 0, 1))); + dataList.add(new EntityData<>(14, EntityDataTypes.QUATERNION, new Quaternion4f(0, 0, 0, 1))); + dataList.add(new EntityData<>(15, EntityDataTypes.BYTE, billboard.getId())); + + appendCustomMetadata(player, dataList); + + WrapperPlayServerEntityMetadata metadataPacket = new WrapperPlayServerEntityMetadata(entityId, dataList); + sendPacket(player, metadataPacket); + } + + @Override + public @NonNull Set getViewers() { + return Collections.unmodifiableSet(viewers); + } + + protected void sendPacket(@NonNull Player player, @NonNull Object packet) { + try { + PacketEvents.getAPI().getPlayerManager().sendPacket(player, (PacketWrapper) packet); + } catch (Throwable ignored) { + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/DisplayAnimation.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/DisplayAnimation.java new file mode 100644 index 0000000..f0ac2e2 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/DisplayAnimation.java @@ -0,0 +1,57 @@ +package dev.oum.oumlib.entity.display; + +import dev.oum.oumlib.scheduler.Scheduler; +import dev.oum.oumlib.scheduler.TaskHandle; +import org.bukkit.Location; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.CheckReturnValue; +import org.jspecify.annotations.NonNull; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +public final class DisplayAnimation { + + private DisplayAnimation() { + } + + @CheckReturnValue + public static @NonNull TaskHandle bobbing(@NonNull VirtualDisplay display, double amplitude, int periodTicks) { + Objects.requireNonNull(display); + AtomicLong tickCounter = new AtomicLong(0); + Vector baseTranslation = display.getTranslation(); + + return Scheduler.runRepeating(Duration.ZERO, Duration.ofMillis(50), () -> { + long t = tickCounter.incrementAndGet(); + double offset = Math.sin((2 * Math.PI * (t % periodTicks)) / periodTicks) * amplitude; + display.setTranslation(new Vector(baseTranslation.getX(), baseTranslation.getY() + offset, baseTranslation.getZ())); + display.updateMetadata(); + }); + } + + @CheckReturnValue + public static @NonNull TaskHandle spin(@NonNull VirtualDisplay display, float yawIncrementPerTick) { + Objects.requireNonNull(display); + return Scheduler.runRepeating(Duration.ZERO, Duration.ofMillis(50), () -> { + Location loc = display.getLocation(); + float newYaw = (loc.getYaw() + yawIncrementPerTick) % 360f; + loc.setYaw(newYaw); + display.teleport(loc); + }); + } + + @CheckReturnValue + public static @NonNull TaskHandle pulse(@NonNull VirtualDisplay display, double minScale, double maxScale, int periodTicks) { + Objects.requireNonNull(display); + AtomicLong tickCounter = new AtomicLong(0); + + return Scheduler.runRepeating(Duration.ZERO, Duration.ofMillis(50), () -> { + long t = tickCounter.incrementAndGet(); + double progress = (Math.sin((2 * Math.PI * (t % periodTicks)) / periodTicks) + 1.0) / 2.0; + double s = minScale + (maxScale - minScale) * progress; + display.setScale(new Vector(s, s, s)); + display.updateMetadata(); + }); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/DisplayBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/DisplayBuilder.java similarity index 97% rename from oumlib-core/src/main/java/dev/oum/oumlib/entity/DisplayBuilder.java rename to oumlib-core/src/main/java/dev/oum/oumlib/entity/display/DisplayBuilder.java index 065a5f3..1908662 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/entity/DisplayBuilder.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/DisplayBuilder.java @@ -1,7 +1,7 @@ -package dev.oum.oumlib.entity; +package dev.oum.oumlib.entity.display; +import dev.oum.oumlib.text.Text; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.block.data.BlockData; @@ -43,7 +43,7 @@ protected DisplayBuilder(Location location, EntityType type) { @Contract("_, _ -> new") public static @NonNull TextDisplayBuilder text(@NonNull Location location, @NonNull String miniMessage) { - return new TextDisplayBuilder(location, MiniMessage.miniMessage().deserialize(miniMessage)); + return new TextDisplayBuilder(location, Text.parse(miniMessage)); } @Contract("_, _ -> new") diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualBlockDisplay.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualBlockDisplay.java new file mode 100644 index 0000000..5f2d1c2 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualBlockDisplay.java @@ -0,0 +1,57 @@ +package dev.oum.oumlib.entity.display; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; + +import java.util.List; +import java.util.Objects; + +public class VirtualBlockDisplay extends AbstractVirtualDisplay { + + private BlockData blockData; + + public VirtualBlockDisplay(@NonNull Location location, @NonNull BlockData blockData) { + super(location); + this.blockData = Objects.requireNonNull(blockData); + } + + public VirtualBlockDisplay(@NonNull Location location, @NonNull Material material) { + this(location, Bukkit.createBlockData(material)); + } + + public static @NonNull VirtualBlockDisplay of(@NonNull Location location, @NonNull BlockData blockData) { + return new VirtualBlockDisplay(location, blockData); + } + + public static @NonNull VirtualBlockDisplay of(@NonNull Location location, @NonNull Material material) { + return new VirtualBlockDisplay(location, material); + } + + @Override + protected @NonNull EntityType getEntityType() { + return EntityTypes.BLOCK_DISPLAY; + } + + public @NonNull BlockData getBlockData() { + return blockData; + } + + public void setBlockData(@NonNull BlockData blockData) { + this.blockData = Objects.requireNonNull(blockData); + } + + @Override + protected void appendCustomMetadata(@NonNull Player player, @NonNull List> list) { + int blockStateId = SpigotConversionUtil.fromBukkitBlockData(blockData).getGlobalId(); + list.add(new EntityData<>(23, EntityDataTypes.INT, blockStateId)); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualDisplay.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualDisplay.java new file mode 100644 index 0000000..74a0169 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualDisplay.java @@ -0,0 +1,84 @@ +package dev.oum.oumlib.entity.display; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Collection; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; + +public interface VirtualDisplay { + + int getEntityId(); + + @NonNull UUID getUniqueId(); + + @NonNull Location getLocation(); + + void setLocation(@NonNull Location location); + + void teleport(@NonNull Location location); + + double getViewDistance(); + + void setViewDistance(double viewDistance); + + @NonNull Billboard getBillboard(); + + void setBillboard(@NonNull Billboard billboard); + + @NonNull Vector getScale(); + + void setScale(@NonNull Vector scale); + + @NonNull Vector getTranslation(); + + void setTranslation(@NonNull Vector translation); + + int getInterpolationDuration(); + + void setInterpolationDuration(int ticks); + + int getInterpolationDelay(); + + void setInterpolationDelay(int ticks); + + void setVisibilityFilter(@Nullable Predicate filter); + + boolean isVisibleTo(@NonNull Player player); + + void spawn(@NonNull Player player); + + void destroy(@NonNull Player player); + + void spawnAll(@NonNull Collection players); + + void destroyAll(@NonNull Collection players); + + void updateMetadata(); + + void updateMetadata(@NonNull Player player); + + @NonNull Set getViewers(); + + enum Billboard { + FIXED((byte) 0), + VERTICAL((byte) 1), + HORIZONTAL((byte) 2), + CENTER((byte) 3); + + private final byte id; + + Billboard(byte id) { + this.id = id; + } + + public byte getId() { + return id; + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualItemDisplay.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualItemDisplay.java new file mode 100644 index 0000000..9993a14 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualItemDisplay.java @@ -0,0 +1,89 @@ +package dev.oum.oumlib.entity.display; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import io.github.retrooper.packetevents.util.SpigotConversionUtil; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NonNull; + +import java.util.List; +import java.util.Objects; + +public class VirtualItemDisplay extends AbstractVirtualDisplay { + + private ItemStack itemStack; + private Transform transform = Transform.FIXED; + + public VirtualItemDisplay(@NonNull Location location, @NonNull ItemStack itemStack) { + super(location); + this.itemStack = Objects.requireNonNull(itemStack).clone(); + } + + public VirtualItemDisplay(@NonNull Location location, @NonNull Material material) { + this(location, new ItemStack(material)); + } + + public static @NonNull VirtualItemDisplay of(@NonNull Location location, @NonNull ItemStack itemStack) { + return new VirtualItemDisplay(location, itemStack); + } + + public static @NonNull VirtualItemDisplay of(@NonNull Location location, @NonNull Material material) { + return new VirtualItemDisplay(location, material); + } + + @Override + protected @NonNull EntityType getEntityType() { + return EntityTypes.ITEM_DISPLAY; + } + + public @NonNull ItemStack getItemStack() { + return itemStack.clone(); + } + + public void setItemStack(@NonNull ItemStack itemStack) { + this.itemStack = Objects.requireNonNull(itemStack).clone(); + } + + public @NonNull Transform getTransform() { + return transform; + } + + public void setTransform(@NonNull Transform transform) { + this.transform = Objects.requireNonNull(transform); + } + + @Override + protected void appendCustomMetadata(@NonNull Player player, @NonNull List> list) { + com.github.retrooper.packetevents.protocol.item.ItemStack peItem = + SpigotConversionUtil.fromBukkitItemStack(itemStack); + list.add(new EntityData<>(23, EntityDataTypes.ITEMSTACK, peItem)); + list.add(new EntityData<>(24, EntityDataTypes.BYTE, transform.getId())); + } + + public enum Transform { + NONE((byte) 0), + THIRDPERSON_LEFTHAND((byte) 1), + THIRDPERSON_RIGHTHAND((byte) 2), + FIRSTPERSON_LEFTHAND((byte) 3), + FIRSTPERSON_RIGHTHAND((byte) 4), + HEAD((byte) 5), + GUI((byte) 6), + GROUND((byte) 7), + FIXED((byte) 8); + + private final byte id; + + Transform(byte id) { + this.id = id; + } + + public byte getId() { + return id; + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualTextDisplay.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualTextDisplay.java new file mode 100644 index 0000000..e0ac9d5 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/display/VirtualTextDisplay.java @@ -0,0 +1,157 @@ +package dev.oum.oumlib.entity.display; + +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.protocol.entity.type.EntityType; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import dev.oum.oumlib.text.Text; +import net.kyori.adventure.text.Component; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +public class VirtualTextDisplay extends AbstractVirtualDisplay { + + private Component staticText; + private Function dynamicText; + private int lineWidth = 200; + private int backgroundColor = 0x40000000; + private byte textOpacity = -1; + private boolean shadow = false; + private boolean seeThrough = false; + private boolean defaultBackground = false; + private Alignment alignment = Alignment.CENTER; + + public VirtualTextDisplay(@NonNull Location location, @NonNull Component text) { + super(location); + this.staticText = Objects.requireNonNull(text); + } + + public VirtualTextDisplay(@NonNull Location location, @NonNull String miniMessage) { + super(location); + this.staticText = Text.parse(miniMessage); + } + + public VirtualTextDisplay(@NonNull Location location, @NonNull Function dynamicText) { + super(location); + this.dynamicText = Objects.requireNonNull(dynamicText); + } + + public static @NonNull VirtualTextDisplay of(@NonNull Location location, @NonNull Component text) { + return new VirtualTextDisplay(location, text); + } + + public static @NonNull VirtualTextDisplay of(@NonNull Location location, @NonNull String miniMessage) { + return new VirtualTextDisplay(location, miniMessage); + } + + public static @NonNull VirtualTextDisplay dynamic(@NonNull Location location, @NonNull Function dynamicText) { + return new VirtualTextDisplay(location, dynamicText); + } + + @Override + protected @NonNull EntityType getEntityType() { + return EntityTypes.TEXT_DISPLAY; + } + + public void setText(@NonNull Component text) { + this.staticText = Objects.requireNonNull(text); + this.dynamicText = null; + } + + public void setText(@NonNull String miniMessage) { + this.staticText = Text.parse(miniMessage); + this.dynamicText = null; + } + + public void setText(@NonNull Function dynamicText) { + this.dynamicText = Objects.requireNonNull(dynamicText); + this.staticText = null; + } + + public int getLineWidth() { + return lineWidth; + } + + public void setLineWidth(int lineWidth) { + this.lineWidth = lineWidth; + } + + public int getBackgroundColor() { + return backgroundColor; + } + + public void setBackgroundColor(int backgroundColor) { + this.backgroundColor = backgroundColor; + } + + public byte getTextOpacity() { + return textOpacity; + } + + public void setTextOpacity(byte textOpacity) { + this.textOpacity = textOpacity; + } + + public boolean hasShadow() { + return shadow; + } + + public void setShadow(boolean shadow) { + this.shadow = shadow; + } + + public boolean isSeeThrough() { + return seeThrough; + } + + public void setSeeThrough(boolean seeThrough) { + this.seeThrough = seeThrough; + } + + public boolean isDefaultBackground() { + return defaultBackground; + } + + public void setDefaultBackground(boolean defaultBackground) { + this.defaultBackground = defaultBackground; + } + + public @NonNull Alignment getAlignment() { + return alignment; + } + + public void setAlignment(@NonNull Alignment alignment) { + this.alignment = alignment; + } + + @Override + protected void appendCustomMetadata(@NonNull Player player, @NonNull List> list) { + Component text = dynamicText != null ? dynamicText.apply(player) : staticText; + if (text == null) { + text = Component.empty(); + } + list.add(new EntityData<>(23, EntityDataTypes.ADV_COMPONENT, text)); + list.add(new EntityData<>(24, EntityDataTypes.INT, lineWidth)); + list.add(new EntityData<>(25, EntityDataTypes.INT, backgroundColor)); + list.add(new EntityData<>(26, EntityDataTypes.BYTE, textOpacity)); + + byte flags = 0; + if (shadow) flags |= 0x01; + if (seeThrough) flags |= 0x02; + if (defaultBackground) flags |= 0x04; + if (alignment == Alignment.LEFT) flags |= 0x08; + if (alignment == Alignment.RIGHT) flags |= 0x10; + list.add(new EntityData<>(27, EntityDataTypes.BYTE, flags)); + } + + public enum Alignment { + CENTER, + LEFT, + RIGHT + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/hologram/Hologram.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/hologram/Hologram.java new file mode 100644 index 0000000..ba7e83b --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/hologram/Hologram.java @@ -0,0 +1,407 @@ +package dev.oum.oumlib.entity.hologram; + +import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.entity.display.VirtualBlockDisplay; +import dev.oum.oumlib.entity.display.VirtualDisplay; +import dev.oum.oumlib.entity.display.VirtualItemDisplay; +import dev.oum.oumlib.entity.display.VirtualTextDisplay; +import dev.oum.oumlib.text.Text; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Function; +import java.util.function.Predicate; + +public class Hologram { + + private final UUID id = UUID.randomUUID(); + private final List lines = new CopyOnWriteArrayList<>(); + private Location location; + private double lineSpacing = 0.28; + private double viewDistance = 48.0; + private VirtualDisplay.Billboard billboard = VirtualDisplay.Billboard.CENTER; + private Predicate visibilityFilter; + private ClickListener clickListener; + + public Hologram(@NonNull Location location) { + this.location = location.clone(); + } + + public static @NonNull Builder builder(@NonNull Location location) { + return new Builder(location); + } + + public @NonNull UUID getId() { + return id; + } + + public @NonNull Location getLocation() { + return location.clone(); + } + + public double getLineSpacing() { + return lineSpacing; + } + + public void setLineSpacing(double lineSpacing) { + this.lineSpacing = lineSpacing; + realignLines(); + } + + public double getViewDistance() { + return viewDistance; + } + + public void setViewDistance(double viewDistance) { + this.viewDistance = viewDistance; + for (HologramLine line : lines) { + line.getDisplay().setViewDistance(viewDistance); + } + } + + public VirtualDisplay.@NonNull Billboard getBillboard() { + return billboard; + } + + public void setBillboard(VirtualDisplay.@NonNull Billboard billboard) { + this.billboard = billboard; + for (HologramLine line : lines) { + line.getDisplay().setBillboard(billboard); + } + } + + public void setVisibilityFilter(@Nullable Predicate filter) { + this.visibilityFilter = filter; + for (HologramLine line : lines) { + line.getDisplay().setVisibilityFilter(filter); + } + } + + public @Nullable ClickListener getClickListener() { + return clickListener; + } + + public void setClickListener(@Nullable ClickListener clickListener) { + this.clickListener = clickListener; + } + + public int size() { + return lines.size(); + } + + public @NonNull List getLines() { + return Collections.unmodifiableList(lines); + } + + public @Nullable HologramLine getLine(int index) { + if (index < 0 || index >= lines.size()) return null; + return lines.get(index); + } + + public @NonNull Hologram addLine(@NonNull Component text) { + VirtualTextDisplay display = new VirtualTextDisplay(location, text); + return addDisplay(display, lineSpacing); + } + + public @NonNull Hologram addLine(@NonNull String miniMessage) { + VirtualTextDisplay display = new VirtualTextDisplay(location, miniMessage); + return addDisplay(display, lineSpacing); + } + + public @NonNull Hologram addDynamicLine(@NonNull Function dynamicText) { + VirtualTextDisplay display = new VirtualTextDisplay(location, dynamicText); + return addDisplay(display, lineSpacing); + } + + public @NonNull Hologram addItemLine(@NonNull ItemStack item) { + VirtualItemDisplay display = new VirtualItemDisplay(location, item); + return addDisplay(display, 0.45); + } + + public @NonNull Hologram addItemLine(@NonNull Material material) { + return addItemLine(new ItemStack(material)); + } + + public @NonNull Hologram addBlockLine(@NonNull BlockData blockData) { + VirtualBlockDisplay display = new VirtualBlockDisplay(location, blockData); + return addDisplay(display, 0.45); + } + + public @NonNull Hologram addBlockLine(@NonNull Material material) { + return addBlockLine(Bukkit.createBlockData(material)); + } + + public @NonNull Hologram addDisplay(@NonNull VirtualDisplay display, double heightOffset) { + display.setViewDistance(viewDistance); + display.setBillboard(billboard); + display.setVisibilityFilter(visibilityFilter); + + SimpleHologramLine line = new SimpleHologramLine(display, heightOffset); + lines.add(line); + realignLines(); + + for (Player p : Bukkit.getOnlinePlayers()) { + if (display.isVisibleTo(p)) { + display.spawn(p); + } + } + return this; + } + + public void removeLine(int index) { + if (index < 0 || index >= lines.size()) return; + HologramLine line = lines.remove(index); + for (Player p : Bukkit.getOnlinePlayers()) { + line.getDisplay().destroy(p); + } + realignLines(); + } + + public void clearLines() { + for (HologramLine line : lines) { + for (Player p : Bukkit.getOnlinePlayers()) { + line.getDisplay().destroy(p); + } + } + lines.clear(); + } + + public void setLine(int index, @NonNull Component text) { + if (index < 0 || index >= lines.size()) return; + HologramLine line = lines.get(index); + if (line.getDisplay() instanceof VirtualTextDisplay td) { + td.setText(text); + td.updateMetadata(); + } + } + + public void setLine(int index, @NonNull String miniMessage) { + setLine(index, Text.parse(miniMessage)); + } + + public void setLine(int index, @NonNull Function dynamicText) { + if (index < 0 || index >= lines.size()) return; + HologramLine line = lines.get(index); + if (line.getDisplay() instanceof VirtualTextDisplay td) { + td.setText(dynamicText); + td.updateMetadata(); + } + } + + public void teleport(@NonNull Location location) { + this.location = location.clone(); + realignLines(); + } + + public void update() { + for (HologramLine line : lines) { + line.getDisplay().updateMetadata(); + } + } + + public void update(@NonNull Player player) { + for (HologramLine line : lines) { + line.getDisplay().updateMetadata(player); + } + } + + public void spawn(@NonNull Player player) { + for (HologramLine line : lines) { + line.getDisplay().spawn(player); + } + } + + public void destroy(@NonNull Player player) { + for (HologramLine line : lines) { + line.getDisplay().destroy(player); + } + } + + public void spawnAll() { + for (HologramLine line : lines) { + line.getDisplay().spawnAll(Bukkit.getOnlinePlayers()); + } + } + + public void destroyAll() { + for (HologramLine line : lines) { + line.getDisplay().destroyAll(Bukkit.getOnlinePlayers()); + } + } + + public void realignLines() { + double currentY = location.getY(); + for (HologramLine line : lines) { + Location lineLoc = new Location(location.getWorld(), location.getX(), currentY, location.getZ(), location.getYaw(), location.getPitch()); + line.teleport(lineLoc); + currentY -= line.getHeightOffset(); + } + } + + public int getLineIndexByEntityId(int entityId) { + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).getDisplay().getEntityId() == entityId) { + return i; + } + } + return -1; + } + + public void register() { + OumLib.holograms().register(this); + } + + public void unregister() { + OumLib.holograms().unregister(this); + } + + public enum ClickType { + LEFT_CLICK, + RIGHT_CLICK + } + + @FunctionalInterface + public interface ClickListener { + void onClick(@NonNull Player player, @NonNull Hologram hologram, int lineIndex, @NonNull ClickType clickType); + } + + private static final class SimpleHologramLine implements HologramLine { + private final VirtualDisplay display; + private final double heightOffset; + + private SimpleHologramLine(VirtualDisplay display, double heightOffset) { + this.display = display; + this.heightOffset = heightOffset; + } + + @Override + public @NonNull VirtualDisplay getDisplay() { + return display; + } + + @Override + public double getHeightOffset() { + return heightOffset; + } + + @Override + public void setLocation(@NonNull Location location) { + display.setLocation(location); + } + + @Override + public void teleport(@NonNull Location location) { + display.teleport(location); + } + } + + public static final class Builder { + private final Location location; + private final List lineActions = new ArrayList<>(); + private double lineSpacing = 0.28; + private double viewDistance = 48.0; + private VirtualDisplay.Billboard billboard = VirtualDisplay.Billboard.CENTER; + private Predicate visibilityFilter; + private ClickListener clickListener; + + private Builder(Location location) { + this.location = location; + } + + public @NonNull Builder lineSpacing(double spacing) { + this.lineSpacing = spacing; + return this; + } + + public @NonNull Builder viewDistance(double distance) { + this.viewDistance = distance; + return this; + } + + public @NonNull Builder billboard(VirtualDisplay.@NonNull Billboard billboard) { + this.billboard = billboard; + return this; + } + + public @NonNull Builder visibilityFilter(@Nullable Predicate filter) { + this.visibilityFilter = filter; + return this; + } + + public @NonNull Builder onClick(@Nullable ClickListener listener) { + this.clickListener = listener; + return this; + } + + public @NonNull Builder line(@NonNull Component text) { + lineActions.add(h -> h.addLine(text)); + return this; + } + + public @NonNull Builder line(@NonNull String miniMessage) { + lineActions.add(h -> h.addLine(miniMessage)); + return this; + } + + public @NonNull Builder dynamicLine(@NonNull Function dynamicText) { + lineActions.add(h -> h.addDynamicLine(dynamicText)); + return this; + } + + public @NonNull Builder itemLine(@NonNull ItemStack item) { + lineActions.add(h -> h.addItemLine(item)); + return this; + } + + public @NonNull Builder itemLine(@NonNull Material material) { + lineActions.add(h -> h.addItemLine(material)); + return this; + } + + public @NonNull Builder blockLine(@NonNull BlockData blockData) { + lineActions.add(h -> h.addBlockLine(blockData)); + return this; + } + + public @NonNull Builder blockLine(@NonNull Material material) { + lineActions.add(h -> h.addBlockLine(material)); + return this; + } + + public @NonNull Hologram build() { + Hologram h = new Hologram(location); + h.setLineSpacing(lineSpacing); + h.setViewDistance(viewDistance); + h.setBillboard(billboard); + h.setVisibilityFilter(visibilityFilter); + h.setClickListener(clickListener); + for (ConsumerAction action : lineActions) { + action.accept(h); + } + return h; + } + + public @NonNull Hologram buildAndRegister() { + Hologram h = build(); + h.register(); + return h; + } + + @FunctionalInterface + private interface ConsumerAction { + void accept(Hologram hologram); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/hologram/HologramLine.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/hologram/HologramLine.java new file mode 100644 index 0000000..22632b6 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/hologram/HologramLine.java @@ -0,0 +1,16 @@ +package dev.oum.oumlib.entity.hologram; + +import dev.oum.oumlib.entity.display.VirtualDisplay; +import org.bukkit.Location; +import org.jspecify.annotations.NonNull; + +public interface HologramLine { + + @NonNull VirtualDisplay getDisplay(); + + double getHeightOffset(); + + void setLocation(@NonNull Location location); + + void teleport(@NonNull Location location); +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/entity/hologram/HologramRegistry.java b/oumlib-core/src/main/java/dev/oum/oumlib/entity/hologram/HologramRegistry.java new file mode 100644 index 0000000..25a2386 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/entity/hologram/HologramRegistry.java @@ -0,0 +1,128 @@ +package dev.oum.oumlib.entity.hologram; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketListenerAbstract; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity; +import dev.oum.oumlib.entity.display.VirtualDisplay; +import dev.oum.oumlib.scheduler.Scheduler; +import dev.oum.oumlib.scheduler.TaskHandle; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.UnmodifiableView; +import org.jspecify.annotations.NonNull; + +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class HologramRegistry extends PacketListenerAbstract { + + private final Set holograms = ConcurrentHashMap.newKeySet(); + private TaskHandle distanceTask; + private boolean listening = false; + + public HologramRegistry(@NonNull Plugin plugin) { + super(PacketListenerPriority.NORMAL); + } + + public void start() { + if (!listening) { + try { + PacketEvents.getAPI().getEventManager().registerListener(this); + } catch (Throwable ignored) { + } + listening = true; + } + if (distanceTask == null || distanceTask.isCancelled()) { + distanceTask = Scheduler.runRepeating(Duration.ZERO, Duration.ofMillis(500), this::tickDistances); + } + } + + public void stop() { + if (distanceTask != null) { + distanceTask.cancel(); + distanceTask = null; + } + if (listening) { + try { + PacketEvents.getAPI().getEventManager().unregisterListener(this); + } catch (Throwable ignored) { + } + listening = false; + } + for (Hologram h : holograms) { + h.destroyAll(); + } + holograms.clear(); + } + + public void register(@NonNull Hologram hologram) { + holograms.add(hologram); + for (Player p : Bukkit.getOnlinePlayers()) { + hologram.spawn(p); + } + } + + public void unregister(@NonNull Hologram hologram) { + holograms.remove(hologram); + hologram.destroyAll(); + } + + @Contract(pure = true) + public @NonNull @UnmodifiableView Set getAll() { + return Collections.unmodifiableSet(holograms); + } + + private void tickDistances() { + Collection online = Bukkit.getOnlinePlayers(); + for (Hologram h : holograms) { + for (Player p : online) { + for (HologramLine line : h.getLines()) { + VirtualDisplay display = line.getDisplay(); + boolean visible = display.isVisibleTo(p); + boolean viewing = display.getViewers().contains(p.getUniqueId()); + + if (visible && !viewing) { + display.spawn(p); + } else if (!visible && viewing) { + display.destroy(p); + } + } + } + } + } + + @Override + public void onPacketReceive(@NonNull PacketReceiveEvent event) { + if (event.getPacketType() != PacketType.Play.Client.INTERACT_ENTITY) { + return; + } + WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event); + int targetEntityId = packet.getEntityId(); + Object playerObj = event.getPlayer(); + if (!(playerObj instanceof Player player)) { + return; + } + + for (Hologram h : holograms) { + int lineIndex = h.getLineIndexByEntityId(targetEntityId); + if (lineIndex != -1) { + Hologram.ClickListener listener = h.getClickListener(); + if (listener != null) { + Hologram.ClickType type = (packet.getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK) + ? Hologram.ClickType.LEFT_CLICK + : Hologram.ClickType.RIGHT_CLICK; + Scheduler.run(() -> listener.onClick(player, h, lineIndex, type)); + } + break; + } + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/event/BukkitEvents.java b/oumlib-core/src/main/java/dev/oum/oumlib/event/BukkitEvents.java deleted file mode 100644 index 180637f..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/event/BukkitEvents.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.oum.oumlib.event; - -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.jspecify.annotations.NonNull; - -import java.lang.reflect.Method; -import java.util.UUID; - -@Deprecated(since = "1.0.7", forRemoval = true) -public final class BukkitEvents { - - private BukkitEvents() { - } - - public static EventBuilder listenFor(@NonNull Player player, Class type) { - UUID id = player.getUniqueId(); - return new EventBuilder<>(type).filter(event -> { - try { - Method m = event.getClass().getMethod("getPlayer"); - Object result = m.invoke(event); - return result instanceof Player p && p.getUniqueId().equals(id); - } catch (Exception e) { - return false; - } - }); - } -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/DataComponents.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/DataComponents.java new file mode 100644 index 0000000..73e0c53 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/DataComponents.java @@ -0,0 +1,114 @@ +package dev.oum.oumlib.inventory; + +import io.papermc.paper.datacomponent.DataComponentType; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.DamageResistant; +import io.papermc.paper.datacomponent.item.ItemLore; +import io.papermc.paper.registry.keys.tags.DamageTypeTagKeys; +import net.kyori.adventure.text.Component; +import org.bukkit.inventory.ItemRarity; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public final class DataComponents { + + private DataComponents() { + } + + @Contract("_, _, _ -> param1") + public static @NonNull ItemStack set(@NonNull ItemStack item, DataComponentType.@NonNull Valued type, @NonNull T value) { + Objects.requireNonNull(item); + Objects.requireNonNull(type); + Objects.requireNonNull(value); + item.setData(type, value); + return item; + } + + @CheckReturnValue + public static @NonNull Optional get(@NonNull ItemStack item, DataComponentType.@NonNull Valued type) { + Objects.requireNonNull(item); + Objects.requireNonNull(type); + return Optional.ofNullable(item.getData(type)); + } + + @CheckReturnValue + public static @NonNull T getOrDefault(@NonNull ItemStack item, DataComponentType.@NonNull Valued type, @NonNull T defaultValue) { + return get(item, type).orElse(defaultValue); + } + + @CheckReturnValue + public static boolean has(@NonNull ItemStack item, @NonNull DataComponentType type) { + Objects.requireNonNull(item); + Objects.requireNonNull(type); + return item.hasData(type); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack remove(@NonNull ItemStack item, @NonNull DataComponentType type) { + Objects.requireNonNull(item); + Objects.requireNonNull(type); + item.unsetData(type); + return item; + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack setMaxStackSize(@NonNull ItemStack item, int maxStackSize) { + return set(item, DataComponentTypes.MAX_STACK_SIZE, maxStackSize); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack setMaxDamage(@NonNull ItemStack item, int maxDamage) { + return set(item, DataComponentTypes.MAX_DAMAGE, maxDamage); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack setDamage(@NonNull ItemStack item, int damage) { + return set(item, DataComponentTypes.DAMAGE, damage); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack setItemName(@NonNull ItemStack item, @NonNull Component name) { + return set(item, DataComponentTypes.ITEM_NAME, name); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack setLore(@NonNull ItemStack item, @NonNull List lore) { + return set(item, DataComponentTypes.LORE, ItemLore.lore(lore)); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack setRarity(@NonNull ItemStack item, @NonNull ItemRarity rarity) { + return set(item, DataComponentTypes.RARITY, rarity); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack setRepairCost(@NonNull ItemStack item, int cost) { + return set(item, DataComponentTypes.REPAIR_COST, cost); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack setFireResistant(@NonNull ItemStack item, boolean resistant) { + if (resistant) { + return set(item, DataComponentTypes.DAMAGE_RESISTANT, + DamageResistant.damageResistant(DamageTypeTagKeys.IS_FIRE)); + } else { + return remove(item, DataComponentTypes.DAMAGE_RESISTANT); + } + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack setGlider(@NonNull ItemStack item, boolean glider) { + if (glider) { + item.setData(DataComponentTypes.GLIDER); + } else { + item.unsetData(DataComponentTypes.GLIDER); + } + return item; + } +} 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 4cb4d59..432bd3b 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 @@ -3,24 +3,25 @@ import com.destroystokyo.paper.profile.PlayerProfile; import com.google.gson.Gson; import dev.oum.oumlib.OumLib; -import dev.oum.oumlib.util.ItemSerializer; +import dev.oum.oumlib.bridge.item.ItemBridge; +import dev.oum.oumlib.pdc.DataKey; +import dev.oum.oumlib.pdc.PdcModel; +import dev.oum.oumlib.text.Text; import io.papermc.paper.datacomponent.DataComponentType; -import io.papermc.paper.datacomponent.DataComponentTypes; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.OfflinePlayer; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemRarity; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.persistence.PersistentDataType; import org.bukkit.profile.PlayerTextures; -import org.bukkit.tag.DamageTypeTags; import org.jetbrains.annotations.CheckReturnValue; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; @@ -30,12 +31,13 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.function.Consumer; import java.util.stream.Collectors; public final class ItemBuilder { - private static final MiniMessage MM = MiniMessage.miniMessage(); private static final Gson GSON = new Gson(); + private final List> dataModifiers = new ArrayList<>(); private final ItemMeta meta; private ItemStack stack; @@ -51,206 +53,273 @@ private ItemBuilder(@NonNull ItemStack item) { @Contract("_ -> new") @CheckReturnValue - public static @NonNull ItemBuilder of(Material material) { + public static @NonNull ItemBuilder of(@NonNull Material material) { return new ItemBuilder(material); } @Contract("_ -> new") @CheckReturnValue - public static @NonNull ItemBuilder of(ItemStack item) { + public static @NonNull ItemBuilder of(@NonNull ItemStack item) { return new ItemBuilder(item); } + @Contract("_ -> new") + @CheckReturnValue + public static @NonNull ItemBuilder from(@NonNull String identifier) { + return ItemBridge.getItem(identifier) + .map(ItemBuilder::of) + .orElseGet(() -> { + Material mat = Material.matchMaterial(identifier); + return of(mat != null ? mat : Material.STONE); + }); + } + @Contract("_, _, _ -> new") @CheckReturnValue public static @NonNull ItemStack quick(@NonNull Material material, @NonNull String miniMessageName, String @NonNull ... loreLines) { return of(material).name(miniMessageName).lore(loreLines).build(); } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder name(@NonNull String miniMessage) { - meta.displayName(MM.deserialize("" + miniMessage)); + meta.displayName(Text.parse("" + miniMessage)); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder name(@Nullable Component component) { meta.displayName(component); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder lore(String @NonNull ... lines) { meta.lore(Arrays.stream(lines) - .map(l -> MM.deserialize("" + l)) + .map(l -> Text.parse("" + l)) .collect(Collectors.toList())); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder lore(Component @NonNull ... lines) { + meta.lore(Arrays.asList(lines)); + return this; + } + + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder lore(@Nullable List lines) { meta.lore(lines); return this; } - @CheckReturnValue + @Contract(value = "-> this", mutates = "this") public @NonNull ItemBuilder clearLore() { meta.lore(null); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder addLore(String @NonNull ... lines) { List currentLore = meta.lore(); if (currentLore == null) currentLore = new ArrayList<>(); List newLines = Arrays.stream(lines) - .map(l -> MM.deserialize("" + l)) + .map(l -> Text.parse("" + l)) .toList(); currentLore.addAll(newLines); meta.lore(currentLore); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder addLore(Component @NonNull ... lines) { + List currentLore = meta.lore(); + if (currentLore == null) currentLore = new ArrayList<>(); + currentLore.addAll(Arrays.asList(lines)); + meta.lore(currentLore); + return this; + } + + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder amount(int amount) { stack.setAmount(amount); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder type(@NonNull Material material) { stack = stack.withType(material); return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ItemBuilder enchant(@NonNull Enchantment e, int level) { meta.addEnchant(e, level, true); return this; } - @CheckReturnValue + @Contract(value = "-> this", mutates = "this") public @NonNull ItemBuilder glow() { - meta.setEnchantmentGlintOverride(true); + return glow(true); + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder glow(boolean glow) { + meta.setEnchantmentGlintOverride(glow); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder flag(ItemFlag @NonNull ... flags) { meta.addItemFlags(flags); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") @SuppressWarnings("deprecation") public @NonNull ItemBuilder customModelData(@Nullable Integer data) { meta.setCustomModelData(data); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder modelData(int data) { + return customModelData(data); + } + + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder unbreakable(boolean value) { meta.setUnbreakable(value); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder itemModel(@NonNull Key key) { meta.setItemModel(NamespacedKey.fromString(key.asString())); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder itemModel(@NonNull NamespacedKey key) { meta.setItemModel(key); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder glintOverride(boolean glint) { meta.setEnchantmentGlintOverride(glint); return this; } - @CheckReturnValue + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder hideTooltip(boolean hide) { + meta.setHideTooltip(hide); + return this; + } + + @Contract(value = "_, _ -> this", mutates = "this") + @SuppressWarnings({"unchecked", "rawtypes"}) + public @NonNull ItemBuilder data(DataComponentType.@NonNull Valued type, @NonNull Object value) { + dataModifiers.add(s -> s.setData(type, value)); + return this; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder removeData(@NonNull DataComponentType type) { + dataModifiers.add(s -> s.unsetData(type)); + return this; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder rarity(@NonNull ItemRarity rarity) { + dataModifiers.add(s -> DataComponents.setRarity(s, rarity)); + return this; + } + + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder maxStackSize(int maxStackSize) { - meta.setMaxStackSize(maxStackSize); + dataModifiers.add(s -> DataComponents.setMaxStackSize(s, maxStackSize)); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder maxDamage(int maxDamage) { - stack.setData(DataComponentTypes.MAX_DAMAGE, maxDamage); + dataModifiers.add(s -> DataComponents.setMaxDamage(s, maxDamage)); return this; } - @CheckReturnValue - public @NonNull ItemBuilder fireResistant(boolean resistant) { - if (resistant) { - meta.setDamageResistant(DamageTypeTags.IS_FIRE); - } else { - meta.setDamageResistant(null); - } + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder damage(int damage) { + dataModifiers.add(s -> DataComponents.setDamage(s, damage)); return this; } - @CheckReturnValue - public @NonNull ItemBuilder hideTooltip(boolean hide) { - meta.setHideTooltip(hide); + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder itemName(@NonNull Component itemName) { + dataModifiers.add(s -> DataComponents.setItemName(s, itemName)); return this; } - @SuppressWarnings({"unchecked", "rawtypes"}) - @CheckReturnValue - public @NonNull ItemBuilder data(DataComponentType.@NonNull Valued type, @NonNull Object value) { - stack.setItemMeta(meta); - stack.setData(type, value); + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder repairCost(int cost) { + dataModifiers.add(s -> DataComponents.setRepairCost(s, cost)); return this; } - @CheckReturnValue - public @NonNull ItemBuilder removeData(@NonNull DataComponentType type) { - stack.setItemMeta(meta); - stack.unsetData(type); + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder fireResistant(boolean fireResistant) { + dataModifiers.add(s -> DataComponents.setFireResistant(s, fireResistant)); return this; } - @CheckReturnValue + @Contract(value = "_ -> this", mutates = "this") + public @NonNull ItemBuilder pdc(@NonNull T recordInstance) { + PdcModel.write(meta.getPersistentDataContainer(), recordInstance); + return this; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull ItemBuilder pdc(@NonNull DataKey key, @NonNull C value) { + meta.getPersistentDataContainer().set(key.key(), key.type(), value); + return this; + } + + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ItemBuilder pdc(@NonNull String key, @NonNull String value) { NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); meta.getPersistentDataContainer().set(nsk, PersistentDataType.STRING, value); return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ItemBuilder pdc(@NonNull String key, int value) { NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); meta.getPersistentDataContainer().set(nsk, PersistentDataType.INTEGER, value); return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ItemBuilder pdc(@NonNull String key, double value) { NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); meta.getPersistentDataContainer().set(nsk, PersistentDataType.DOUBLE, value); return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ItemBuilder pdc(@NonNull String key, boolean value) { NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); meta.getPersistentDataContainer().set(nsk, PersistentDataType.BYTE, (byte) (value ? 1 : 0)); return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ItemBuilder pdc(@NonNull String key, long value) { NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); meta.getPersistentDataContainer().set(nsk, PersistentDataType.LONG, value); return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ItemBuilder pdc(@NonNull String key, @Nullable List value) { NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); if (value == null) { @@ -261,7 +330,7 @@ private ItemBuilder(@NonNull ItemStack item) { return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ItemBuilder pdc(@NonNull String key, @Nullable ItemStack value) { NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); if (value == null) { @@ -272,7 +341,7 @@ private ItemBuilder(@NonNull ItemStack item) { return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") public @NonNull ItemBuilder pdc(@NonNull String key, ItemStack @Nullable [] value) { NamespacedKey nsk = new NamespacedKey(OumLib.plugin(), key); if (value == null) { @@ -283,19 +352,18 @@ private ItemBuilder(@NonNull ItemStack item) { return this; } - @CheckReturnValue + @Contract(value = "_, _ -> this", mutates = "this") 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)); + meta.getPersistentDataContainer().set(nsk, PersistentDataType.STRING, Text.serialize(value)); } return this; } - @CheckReturnValue - @SuppressWarnings("unused") + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder skull(@NonNull OfflinePlayer player) { if (meta instanceof SkullMeta skullMeta) { skullMeta.setOwningPlayer(player); @@ -303,8 +371,7 @@ private ItemBuilder(@NonNull ItemStack item) { return this; } - @CheckReturnValue - @SuppressWarnings("unused") + @Contract(value = "_ -> this", mutates = "this") public @NonNull ItemBuilder skull(@NonNull String textureValue) { if (meta instanceof SkullMeta skullMeta) { try { @@ -342,8 +409,12 @@ private ItemBuilder(@NonNull ItemStack item) { } @Contract(" -> new") + @CheckReturnValue public @NonNull ItemStack build() { stack.setItemMeta(meta); + for (Consumer modifier : dataModifiers) { + modifier.accept(stack); + } return stack; } } \ No newline at end of file diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/ItemSerializer.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemSerializer.java similarity index 72% rename from oumlib-core/src/main/java/dev/oum/oumlib/util/ItemSerializer.java rename to oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemSerializer.java index 1262611..e49d564 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/ItemSerializer.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/ItemSerializer.java @@ -1,4 +1,4 @@ -package dev.oum.oumlib.util; +package dev.oum.oumlib.inventory; import org.bukkit.inventory.ItemStack; import org.jspecify.annotations.NonNull; @@ -41,7 +41,7 @@ private ItemSerializer() { dos.writeInt(items.length); for (ItemStack item : items) { if (item == null || item.isEmpty()) { - dos.writeInt(-1); + dos.writeInt(0); } else { byte[] bytes = item.serializeAsBytes(); dos.writeInt(bytes.length); @@ -56,24 +56,21 @@ private ItemSerializer() { public static ItemStack @NonNull [] deserializeArray(@NonNull String base64) { if (base64.isEmpty()) return new ItemStack[0]; - try { - byte[] bytes = Base64.getDecoder().decode(base64); - try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - DataInputStream dis = new DataInputStream(bais)) { - int length = dis.readInt(); - ItemStack[] items = new ItemStack[length]; - for (int i = 0; i < length; i++) { - int size = dis.readInt(); - if (size == -1) { - items[i] = null; - } else { - byte[] itemBytes = new byte[size]; - dis.readFully(itemBytes); - items[i] = ItemStack.deserializeBytes(itemBytes); - } + try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(base64)); + DataInputStream dis = new DataInputStream(bais)) { + int length = dis.readInt(); + ItemStack[] items = new ItemStack[length]; + for (int i = 0; i < length; i++) { + int size = dis.readInt(); + if (size == 0) { + items[i] = null; + } else { + byte[] bytes = new byte[size]; + dis.readFully(bytes); + items[i] = ItemStack.deserializeBytes(bytes); } - return items; } + return items; } catch (Exception e) { throw new RuntimeException("Failed to deserialize ItemStack array", e); } 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 1f53887..9eff996 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 @@ -17,10 +17,7 @@ import org.jetbrains.annotations.Nullable; import org.jspecify.annotations.NonNull; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -36,6 +33,7 @@ public final class PaginatedMenu implements Menu { private final Function prevButton; private final Function nextButton; private final Function> itemsSupplier; + private final Function borderSupplier; private final PaginatedClickHandler clickHandler; private final Map pages = new ConcurrentHashMap<>(); private final Map open = new ConcurrentHashMap<>(); @@ -46,15 +44,31 @@ public final class PaginatedMenu implements Menu { private PaginatedMenu(@NonNull Builder builder) { this.title = builder.title; this.rows = builder.rows; - this.contentSlots = builder.contentSlots; + this.contentSlots = builder.contentSlots != null ? builder.contentSlots : defaultContentSlots(builder.rows); this.prevSlot = builder.prevSlot; this.nextSlot = builder.nextSlot; this.prevButton = builder.prevButton; this.nextButton = builder.nextButton; this.itemsSupplier = builder.itemsSupplier; + this.borderSupplier = builder.borderSupplier; this.clickHandler = builder.clickHandler; } + private static int[] defaultContentSlots(int rows) { + if (rows <= 2) { + int[] slots = new int[rows * 9]; + for (int i = 0; i < slots.length; i++) slots[i] = i; + return slots; + } + List slots = new ArrayList<>(); + for (int r = 1; r < rows - 1; r++) { + for (int c = 1; c < 8; c++) { + slots.add(r * 9 + c); + } + } + return slots.stream().mapToInt(Integer::intValue).toArray(); + } + @Contract(" -> new") @CheckReturnValue public static @NonNull Builder builder() { @@ -111,7 +125,8 @@ 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))); + .replace("", String.valueOf(totalPages(player))) + .replace("", String.valueOf(totalPages(player))); var titleComponent = MM.deserialize(resolvedTitle); Inventory inv = open.get(player.getUniqueId()); @@ -139,6 +154,22 @@ private void reopen(@NonNull Player player) { private void populateItems(@NonNull Player player, @NonNull Inventory inv, int page) { inv.clear(); + + if (borderSupplier != null) { + ItemStack border = borderSupplier.apply(player); + if (border != null) { + Set reserved = new HashSet<>(); + for (int slot : contentSlots) reserved.add(slot); + reserved.add(prevSlot); + reserved.add(nextSlot); + for (int s = 0; s < rows * 9; s++) { + if (!reserved.contains(s)) { + inv.setItem(s, border); + } + } + } + } + List list = itemsSupplier.apply(player); if (list == null) list = List.of(); int start = (page - 1) * contentSlots.length; @@ -146,12 +177,13 @@ private void populateItems(@NonNull Player player, @NonNull Inventory inv, int p int idx = start + i; if (idx < list.size()) inv.setItem(contentSlots[i], list.get(idx)); } + ItemStack prev = page > 1 ? prevButton.apply(page) - : ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name("Previous").build(); + : (borderSupplier != null ? borderSupplier.apply(player) : ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build()); ItemStack next = page < totalPages(player) ? nextButton.apply(page) - : ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name("Next").build(); + : (borderSupplier != null ? borderSupplier.apply(player) : ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).name(" ").build()); inv.setItem(prevSlot, prev); inv.setItem(nextSlot, next); } @@ -241,9 +273,10 @@ public interface PaginatedClickHandler { public static final class Builder { private Function> itemsSupplier = p -> new ArrayList<>(); + private Function borderSupplier; private String title = "Page /"; private int rows = 6; - private int[] contentSlots = {10, 11, 12, 13, 14, 15, 16}; + private int[] contentSlots; private int prevSlot = 45; private int nextSlot = 53; private Function prevButton = page -> ItemBuilder.of(Material.ARROW) @@ -253,7 +286,6 @@ public static final class Builder { private PaginatedClickHandler clickHandler; @CheckReturnValue - @SuppressWarnings("unused") public @NonNull Builder onClick(@NonNull PaginatedClickHandler handler) { this.clickHandler = handler; return this; @@ -277,17 +309,43 @@ public static final class Builder { return this; } + @CheckReturnValue + public @NonNull Builder border(@NonNull ItemStack item) { + this.borderSupplier = player -> item; + return this; + } + + @CheckReturnValue + public @NonNull Builder border(@NonNull Function<@NonNull Player, @NonNull ItemStack> supplier) { + this.borderSupplier = supplier; + return this; + } + @CheckReturnValue public @NonNull Builder previousButton(@NonNull Function<@NonNull Integer, @NonNull ItemStack> fn, int slot) { - prevButton = fn; - prevSlot = slot; + this.prevButton = fn; + this.prevSlot = slot; + return this; + } + + @CheckReturnValue + public @NonNull Builder previousButton(@NonNull ItemStack item, int slot) { + this.prevButton = page -> item; + this.prevSlot = slot; return this; } @CheckReturnValue public @NonNull Builder nextButton(@NonNull Function<@NonNull Integer, @NonNull ItemStack> fn, int slot) { - nextButton = fn; - nextSlot = slot; + this.nextButton = fn; + this.nextSlot = slot; + return this; + } + + @CheckReturnValue + public @NonNull Builder nextButton(@NonNull ItemStack item, int slot) { + this.nextButton = page -> item; + this.nextSlot = slot; return this; } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/PotionSerializer.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/PotionSerializer.java similarity index 97% rename from oumlib-core/src/main/java/dev/oum/oumlib/util/PotionSerializer.java rename to oumlib-core/src/main/java/dev/oum/oumlib/inventory/PotionSerializer.java index 5e11789..3df3545 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/PotionSerializer.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/PotionSerializer.java @@ -1,11 +1,10 @@ -package dev.oum.oumlib.util; +package dev.oum.oumlib.inventory; import org.bukkit.NamespacedKey; import org.bukkit.Registry; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/CookingRecipeBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/CookingRecipeBuilder.java new file mode 100644 index 0000000..bd55262 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/CookingRecipeBuilder.java @@ -0,0 +1,117 @@ +package dev.oum.oumlib.inventory.recipe; + +import dev.oum.oumlib.OumLib; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.*; +import org.bukkit.inventory.recipe.CookingBookCategory; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +public final class CookingRecipeBuilder { + + private final Type type; + private final NamespacedKey key; + private final ItemStack result; + private RecipeChoice source; + private float experience = 0.0f; + private int cookingTime; + private String group; + private CookingBookCategory category; + + public CookingRecipeBuilder(@NonNull Type type, @NonNull NamespacedKey key, @NonNull ItemStack result) { + this.type = Objects.requireNonNull(type); + this.key = Objects.requireNonNull(key); + this.result = Objects.requireNonNull(result); + this.cookingTime = type.defaultCookingTime; + } + + public static @NonNull CookingRecipeBuilder smelting(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return new CookingRecipeBuilder(Type.SMELTING, key, result); + } + + public static @NonNull CookingRecipeBuilder blasting(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return new CookingRecipeBuilder(Type.BLASTING, key, result); + } + + public static @NonNull CookingRecipeBuilder smoking(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return new CookingRecipeBuilder(Type.SMOKING, key, result); + } + + public static @NonNull CookingRecipeBuilder campfire(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return new CookingRecipeBuilder(Type.CAMPFIRE, key, result); + } + + public @NonNull CookingRecipeBuilder source(@NonNull Material material) { + this.source = new RecipeChoice.MaterialChoice(material); + return this; + } + + public @NonNull CookingRecipeBuilder source(@NonNull ItemStack item) { + this.source = new RecipeChoice.ExactChoice(item); + return this; + } + + public @NonNull CookingRecipeBuilder source(@NonNull RecipeChoice choice) { + this.source = Objects.requireNonNull(choice); + return this; + } + + public @NonNull CookingRecipeBuilder experience(float experience) { + this.experience = experience; + return this; + } + + public @NonNull CookingRecipeBuilder cookingTime(int cookingTime) { + this.cookingTime = cookingTime; + return this; + } + + public @NonNull CookingRecipeBuilder group(@Nullable String group) { + this.group = group; + return this; + } + + public @NonNull CookingRecipeBuilder category(@Nullable CookingBookCategory category) { + this.category = category; + return this; + } + + public @NonNull CookingRecipe build() { + if (source == null) { + throw new IllegalStateException("Source ingredient must be set for cooking recipe."); + } + CookingRecipe recipe = switch (type) { + case SMELTING -> new FurnaceRecipe(key, result, source, experience, cookingTime); + case BLASTING -> new BlastingRecipe(key, result, source, experience, cookingTime); + case SMOKING -> new SmokingRecipe(key, result, source, experience, cookingTime); + case CAMPFIRE -> new CampfireRecipe(key, result, source, experience, cookingTime); + }; + if (group != null) { + recipe.setGroup(group); + } + if (category != null) { + recipe.setCategory(category); + } + return recipe; + } + + public boolean register() { + return OumLib.recipes().register(build()); + } + + public enum Type { + SMELTING(200), + BLASTING(100), + SMOKING(100), + CAMPFIRE(600); + + private final int defaultCookingTime; + + Type(int defaultCookingTime) { + this.defaultCookingTime = defaultCookingTime; + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/RecipeDSL.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/RecipeDSL.java new file mode 100644 index 0000000..b4d35a8 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/RecipeDSL.java @@ -0,0 +1,148 @@ +package dev.oum.oumlib.inventory.recipe; + +import dev.oum.oumlib.OumLib; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NonNull; + +public final class RecipeDSL { + + private RecipeDSL() { + } + + public static @NonNull ShapedRecipeBuilder shaped(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return ShapedRecipeBuilder.of(key, result); + } + + public static @NonNull ShapedRecipeBuilder shaped(@NonNull String key, @NonNull ItemStack result) { + return shaped(createKey(key), result); + } + + public static @NonNull ShapedRecipeBuilder shaped(@NonNull NamespacedKey key, @NonNull Material result) { + return shaped(key, new ItemStack(result)); + } + + public static @NonNull ShapedRecipeBuilder shaped(@NonNull String key, @NonNull Material result) { + return shaped(createKey(key), new ItemStack(result)); + } + + public static @NonNull ShapelessRecipeBuilder shapeless(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return ShapelessRecipeBuilder.of(key, result); + } + + public static @NonNull ShapelessRecipeBuilder shapeless(@NonNull String key, @NonNull ItemStack result) { + return shapeless(createKey(key), result); + } + + public static @NonNull ShapelessRecipeBuilder shapeless(@NonNull NamespacedKey key, @NonNull Material result) { + return shapeless(key, new ItemStack(result)); + } + + public static @NonNull ShapelessRecipeBuilder shapeless(@NonNull String key, @NonNull Material result) { + return shapeless(createKey(key), new ItemStack(result)); + } + + public static @NonNull CookingRecipeBuilder smelting(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return CookingRecipeBuilder.smelting(key, result); + } + + public static @NonNull CookingRecipeBuilder smelting(@NonNull String key, @NonNull ItemStack result) { + return smelting(createKey(key), result); + } + + public static @NonNull CookingRecipeBuilder smelting(@NonNull NamespacedKey key, @NonNull Material result) { + return smelting(key, new ItemStack(result)); + } + + public static @NonNull CookingRecipeBuilder smelting(@NonNull String key, @NonNull Material result) { + return smelting(createKey(key), new ItemStack(result)); + } + + public static @NonNull CookingRecipeBuilder blasting(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return CookingRecipeBuilder.blasting(key, result); + } + + public static @NonNull CookingRecipeBuilder blasting(@NonNull String key, @NonNull ItemStack result) { + return blasting(createKey(key), result); + } + + public static @NonNull CookingRecipeBuilder blasting(@NonNull NamespacedKey key, @NonNull Material result) { + return blasting(key, new ItemStack(result)); + } + + public static @NonNull CookingRecipeBuilder blasting(@NonNull String key, @NonNull Material result) { + return blasting(createKey(key), new ItemStack(result)); + } + + public static @NonNull CookingRecipeBuilder smoking(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return CookingRecipeBuilder.smoking(key, result); + } + + public static @NonNull CookingRecipeBuilder smoking(@NonNull String key, @NonNull ItemStack result) { + return smoking(createKey(key), result); + } + + public static @NonNull CookingRecipeBuilder smoking(@NonNull NamespacedKey key, @NonNull Material result) { + return smoking(key, new ItemStack(result)); + } + + public static @NonNull CookingRecipeBuilder smoking(@NonNull String key, @NonNull Material result) { + return smoking(createKey(key), new ItemStack(result)); + } + + public static @NonNull CookingRecipeBuilder campfire(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return CookingRecipeBuilder.campfire(key, result); + } + + public static @NonNull CookingRecipeBuilder campfire(@NonNull String key, @NonNull ItemStack result) { + return campfire(createKey(key), result); + } + + public static @NonNull CookingRecipeBuilder campfire(@NonNull NamespacedKey key, @NonNull Material result) { + return campfire(key, new ItemStack(result)); + } + + public static @NonNull CookingRecipeBuilder campfire(@NonNull String key, @NonNull Material result) { + return campfire(createKey(key), new ItemStack(result)); + } + + public static @NonNull SmithingRecipeBuilder smithing(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return SmithingRecipeBuilder.of(key, result); + } + + public static @NonNull SmithingRecipeBuilder smithing(@NonNull String key, @NonNull ItemStack result) { + return smithing(createKey(key), result); + } + + public static @NonNull SmithingRecipeBuilder smithing(@NonNull NamespacedKey key, @NonNull Material result) { + return smithing(key, new ItemStack(result)); + } + + public static @NonNull SmithingRecipeBuilder smithing(@NonNull String key, @NonNull Material result) { + return smithing(createKey(key), new ItemStack(result)); + } + + public static @NonNull StonecutterRecipeBuilder stonecutting(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return StonecutterRecipeBuilder.of(key, result); + } + + public static @NonNull StonecutterRecipeBuilder stonecutting(@NonNull String key, @NonNull ItemStack result) { + return stonecutting(createKey(key), result); + } + + public static @NonNull StonecutterRecipeBuilder stonecutting(@NonNull NamespacedKey key, @NonNull Material result) { + return stonecutting(key, new ItemStack(result)); + } + + public static @NonNull StonecutterRecipeBuilder stonecutting(@NonNull String key, @NonNull Material result) { + return stonecutting(createKey(key), new ItemStack(result)); + } + + private static NamespacedKey createKey(String key) { + if (key.contains(":")) { + return NamespacedKey.fromString(key); + } + return new NamespacedKey(OumLib.plugin(), key); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/RecipeRegistry.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/RecipeRegistry.java new file mode 100644 index 0000000..315a2b9 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/RecipeRegistry.java @@ -0,0 +1,65 @@ +package dev.oum.oumlib.inventory.recipe; + +import org.bukkit.Bukkit; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Recipe; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.UnmodifiableView; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class RecipeRegistry { + + private final Set registeredKeys = ConcurrentHashMap.newKeySet(); + + public synchronized boolean register(@NonNull Recipe recipe) { + if (recipe instanceof Keyed keyed) { + NamespacedKey key = keyed.getKey(); + if (Bukkit.getRecipe(key) != null) { + Bukkit.removeRecipe(key); + } + boolean success = Bukkit.addRecipe(recipe); + if (success) { + registeredKeys.add(key); + } + return success; + } + return Bukkit.addRecipe(recipe); + } + + public synchronized boolean unregister(@NonNull NamespacedKey key) { + registeredKeys.remove(key); + return Bukkit.removeRecipe(key); + } + + public @Nullable Recipe get(@NonNull NamespacedKey key) { + return Bukkit.getRecipe(key); + } + + @Contract(pure = true) + public @NonNull @UnmodifiableView Set getRegisteredKeys() { + return Collections.unmodifiableSet(registeredKeys); + } + + public void discover(@NonNull Player player, @NonNull NamespacedKey... keys) { + player.discoverRecipes(Arrays.asList(keys)); + } + + public void undiscover(@NonNull Player player, @NonNull NamespacedKey... keys) { + player.undiscoverRecipes(Arrays.asList(keys)); + } + + public synchronized void unregisterAll() { + for (NamespacedKey key : registeredKeys) { + Bukkit.removeRecipe(key); + } + registeredKeys.clear(); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/ShapedRecipeBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/ShapedRecipeBuilder.java new file mode 100644 index 0000000..de6a6f2 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/ShapedRecipeBuilder.java @@ -0,0 +1,88 @@ +package dev.oum.oumlib.inventory.recipe; + +import dev.oum.oumlib.OumLib; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.RecipeChoice; +import org.bukkit.inventory.ShapedRecipe; +import org.bukkit.inventory.recipe.CraftingBookCategory; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public final class ShapedRecipeBuilder { + + private final NamespacedKey key; + private final ItemStack result; + private final Map ingredients = new HashMap<>(); + private String[] shape; + private String group; + private CraftingBookCategory category; + + public ShapedRecipeBuilder(@NonNull NamespacedKey key, @NonNull ItemStack result) { + this.key = Objects.requireNonNull(key); + this.result = Objects.requireNonNull(result); + } + + public static @NonNull ShapedRecipeBuilder of(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return new ShapedRecipeBuilder(key, result); + } + + public static @NonNull ShapedRecipeBuilder of(@NonNull NamespacedKey key, @NonNull Material result) { + return new ShapedRecipeBuilder(key, new ItemStack(result)); + } + + public @NonNull ShapedRecipeBuilder shape(@NonNull String... shape) { + this.shape = shape; + return this; + } + + public @NonNull ShapedRecipeBuilder set(char key, @NonNull Material material) { + this.ingredients.put(key, new RecipeChoice.MaterialChoice(material)); + return this; + } + + public @NonNull ShapedRecipeBuilder set(char key, @NonNull ItemStack item) { + this.ingredients.put(key, new RecipeChoice.ExactChoice(item)); + return this; + } + + public @NonNull ShapedRecipeBuilder set(char key, @NonNull RecipeChoice choice) { + this.ingredients.put(key, Objects.requireNonNull(choice)); + return this; + } + + public @NonNull ShapedRecipeBuilder group(@Nullable String group) { + this.group = group; + return this; + } + + public @NonNull ShapedRecipeBuilder category(@Nullable CraftingBookCategory category) { + this.category = category; + return this; + } + + public @NonNull ShapedRecipe build() { + if (shape == null || shape.length == 0) { + throw new IllegalStateException("Recipe shape must be defined."); + } + ShapedRecipe recipe = new ShapedRecipe(key, result); + recipe.shape(shape); + ingredients.forEach(recipe::setIngredient); + if (group != null) { + recipe.setGroup(group); + } + if (category != null) { + recipe.setCategory(category); + } + return recipe; + } + + public boolean register() { + return OumLib.recipes().register(build()); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/ShapelessRecipeBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/ShapelessRecipeBuilder.java new file mode 100644 index 0000000..45bc5ef --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/ShapelessRecipeBuilder.java @@ -0,0 +1,99 @@ +package dev.oum.oumlib.inventory.recipe; + +import dev.oum.oumlib.OumLib; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.RecipeChoice; +import org.bukkit.inventory.ShapelessRecipe; +import org.bukkit.inventory.recipe.CraftingBookCategory; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class ShapelessRecipeBuilder { + + private final NamespacedKey key; + private final ItemStack result; + private final List ingredients = new ArrayList<>(); + private String group; + private CraftingBookCategory category; + + public ShapelessRecipeBuilder(@NonNull NamespacedKey key, @NonNull ItemStack result) { + this.key = Objects.requireNonNull(key); + this.result = Objects.requireNonNull(result); + } + + public static @NonNull ShapelessRecipeBuilder of(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return new ShapelessRecipeBuilder(key, result); + } + + public static @NonNull ShapelessRecipeBuilder of(@NonNull NamespacedKey key, @NonNull Material result) { + return new ShapelessRecipeBuilder(key, new ItemStack(result)); + } + + public @NonNull ShapelessRecipeBuilder add(@NonNull Material material) { + return add(material, 1); + } + + public @NonNull ShapelessRecipeBuilder add(@NonNull Material material, int count) { + for (int i = 0; i < count; i++) { + this.ingredients.add(new RecipeChoice.MaterialChoice(material)); + } + return this; + } + + public @NonNull ShapelessRecipeBuilder add(@NonNull ItemStack item) { + return add(item, 1); + } + + public @NonNull ShapelessRecipeBuilder add(@NonNull ItemStack item, int count) { + for (int i = 0; i < count; i++) { + this.ingredients.add(new RecipeChoice.ExactChoice(item)); + } + return this; + } + + public @NonNull ShapelessRecipeBuilder add(@NonNull RecipeChoice choice) { + return add(choice, 1); + } + + public @NonNull ShapelessRecipeBuilder add(@NonNull RecipeChoice choice, int count) { + for (int i = 0; i < count; i++) { + this.ingredients.add(choice); + } + return this; + } + + public @NonNull ShapelessRecipeBuilder group(@Nullable String group) { + this.group = group; + return this; + } + + public @NonNull ShapelessRecipeBuilder category(@Nullable CraftingBookCategory category) { + this.category = category; + return this; + } + + public @NonNull ShapelessRecipe build() { + if (ingredients.isEmpty()) { + throw new IllegalStateException("Shapeless recipe must have at least one ingredient."); + } + ShapelessRecipe recipe = new ShapelessRecipe(key, result); + ingredients.forEach(recipe::addIngredient); + if (group != null) { + recipe.setGroup(group); + } + if (category != null) { + recipe.setCategory(category); + } + return recipe; + } + + public boolean register() { + return OumLib.recipes().register(build()); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/SmithingRecipeBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/SmithingRecipeBuilder.java new file mode 100644 index 0000000..91f3d76 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/SmithingRecipeBuilder.java @@ -0,0 +1,89 @@ +package dev.oum.oumlib.inventory.recipe; + +import dev.oum.oumlib.OumLib; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.RecipeChoice; +import org.bukkit.inventory.SmithingTransformRecipe; +import org.jspecify.annotations.NonNull; + +import java.util.Objects; + +public final class SmithingRecipeBuilder { + + private final NamespacedKey key; + private final ItemStack result; + private RecipeChoice template; + private RecipeChoice base; + private RecipeChoice addition; + + public SmithingRecipeBuilder(@NonNull NamespacedKey key, @NonNull ItemStack result) { + this.key = Objects.requireNonNull(key); + this.result = Objects.requireNonNull(result); + } + + public static @NonNull SmithingRecipeBuilder of(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return new SmithingRecipeBuilder(key, result); + } + + public static @NonNull SmithingRecipeBuilder of(@NonNull NamespacedKey key, @NonNull Material result) { + return new SmithingRecipeBuilder(key, new ItemStack(result)); + } + + public @NonNull SmithingRecipeBuilder template(@NonNull Material material) { + this.template = new RecipeChoice.MaterialChoice(material); + return this; + } + + public @NonNull SmithingRecipeBuilder template(@NonNull ItemStack item) { + this.template = new RecipeChoice.ExactChoice(item); + return this; + } + + public @NonNull SmithingRecipeBuilder template(@NonNull RecipeChoice choice) { + this.template = Objects.requireNonNull(choice); + return this; + } + + public @NonNull SmithingRecipeBuilder base(@NonNull Material material) { + this.base = new RecipeChoice.MaterialChoice(material); + return this; + } + + public @NonNull SmithingRecipeBuilder base(@NonNull ItemStack item) { + this.base = new RecipeChoice.ExactChoice(item); + return this; + } + + public @NonNull SmithingRecipeBuilder base(@NonNull RecipeChoice choice) { + this.base = Objects.requireNonNull(choice); + return this; + } + + public @NonNull SmithingRecipeBuilder addition(@NonNull Material material) { + this.addition = new RecipeChoice.MaterialChoice(material); + return this; + } + + public @NonNull SmithingRecipeBuilder addition(@NonNull ItemStack item) { + this.addition = new RecipeChoice.ExactChoice(item); + return this; + } + + public @NonNull SmithingRecipeBuilder addition(@NonNull RecipeChoice choice) { + this.addition = Objects.requireNonNull(choice); + return this; + } + + public @NonNull SmithingTransformRecipe build() { + if (template == null || base == null || addition == null) { + throw new IllegalStateException("Smithing recipe requires template, base, and addition ingredients."); + } + return new SmithingTransformRecipe(key, result, template, base, addition); + } + + public boolean register() { + return OumLib.recipes().register(build()); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/StonecutterRecipeBuilder.java b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/StonecutterRecipeBuilder.java new file mode 100644 index 0000000..5f372b5 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/inventory/recipe/StonecutterRecipeBuilder.java @@ -0,0 +1,68 @@ +package dev.oum.oumlib.inventory.recipe; + +import dev.oum.oumlib.OumLib; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.RecipeChoice; +import org.bukkit.inventory.StonecuttingRecipe; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +public final class StonecutterRecipeBuilder { + + private final NamespacedKey key; + private final ItemStack result; + private RecipeChoice source; + private String group; + + public StonecutterRecipeBuilder(@NonNull NamespacedKey key, @NonNull ItemStack result) { + this.key = Objects.requireNonNull(key); + this.result = Objects.requireNonNull(result); + } + + public static @NonNull StonecutterRecipeBuilder of(@NonNull NamespacedKey key, @NonNull ItemStack result) { + return new StonecutterRecipeBuilder(key, result); + } + + public static @NonNull StonecutterRecipeBuilder of(@NonNull NamespacedKey key, @NonNull Material result) { + return new StonecutterRecipeBuilder(key, new ItemStack(result)); + } + + public @NonNull StonecutterRecipeBuilder source(@NonNull Material material) { + this.source = new RecipeChoice.MaterialChoice(material); + return this; + } + + public @NonNull StonecutterRecipeBuilder source(@NonNull ItemStack item) { + this.source = new RecipeChoice.ExactChoice(item); + return this; + } + + public @NonNull StonecutterRecipeBuilder source(@NonNull RecipeChoice choice) { + this.source = Objects.requireNonNull(choice); + return this; + } + + public @NonNull StonecutterRecipeBuilder group(@Nullable String group) { + this.group = group; + return this; + } + + public @NonNull StonecuttingRecipe build() { + if (source == null) { + throw new IllegalStateException("Source ingredient must be set for stonecutting recipe."); + } + StonecuttingRecipe recipe = new StonecuttingRecipe(key, result, source); + if (group != null) { + recipe.setGroup(group); + } + return recipe; + } + + public boolean register() { + return OumLib.recipes().register(build()); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Locations.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Locations.java similarity index 93% rename from oumlib-core/src/main/java/dev/oum/oumlib/util/Locations.java rename to oumlib-core/src/main/java/dev/oum/oumlib/math/Locations.java index 6932084..3f43bc6 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Locations.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Locations.java @@ -1,13 +1,10 @@ -package dev.oum.oumlib.util; +package dev.oum.oumlib.math; -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; import org.bukkit.util.Vector; +import org.jetbrains.annotations.CheckReturnValue; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -18,6 +15,7 @@ private Locations() { } @Contract(pure = true) + @CheckReturnValue public static @NonNull String serialize(@NonNull Location loc) { return (loc.getWorld() != null ? loc.getWorld().getName() : "world") + "," + loc.getX() + "," + @@ -28,6 +26,7 @@ private Locations() { } @Contract(pure = true) + @CheckReturnValue public static @NonNull String serializeBlock(@NonNull Location loc) { return (loc.getWorld() != null ? loc.getWorld().getName() : "world") + "," + loc.getBlockX() + "," + @@ -36,6 +35,7 @@ private Locations() { } @Contract(pure = true) + @CheckReturnValue public static @Nullable Location deserialize(@NonNull String str) { String[] parts = str.split(","); if (parts.length < 4) return null; @@ -56,16 +56,19 @@ private Locations() { } @Contract(pure = true) + @CheckReturnValue public static @NonNull String serializeRegion(@NonNull Location loc) { return serialize(loc) + "," + (loc.getBlockX() >> 4) + "," + (loc.getBlockZ() >> 4); } @Contract(pure = true) + @CheckReturnValue public static @NonNull String serializeVector(@NonNull Vector vector) { return vector.getX() + "," + vector.getY() + "," + vector.getZ(); } @Contract(pure = true) + @CheckReturnValue public static @Nullable Vector deserializeVector(@NonNull String str) { String[] parts = str.split(","); if (parts.length < 3) return null; @@ -79,12 +82,14 @@ private Locations() { } } + @CheckReturnValue public static double distance2D(@NonNull Location a, @NonNull Location b) { double dx = a.getX() - b.getX(); double dz = a.getZ() - b.getZ(); return Math.sqrt(dx * dx + dz * dz); } + @CheckReturnValue public static @NonNull Location midpoint(@NonNull Location a, @NonNull Location b) { Vector3D v1 = Vector3D.fromLocation(a); Vector3D v2 = Vector3D.fromLocation(b); @@ -92,6 +97,7 @@ public static double distance2D(@NonNull Location a, @NonNull Location b) { return mid.toLocation(a.getWorld(), (a.getYaw() + b.getYaw()) / 2.0f, (a.getPitch() + b.getPitch()) / 2.0f); } + @CheckReturnValue public static @NonNull Location centerBlock(@NonNull Location block) { return new Location( block.getWorld(), @@ -103,6 +109,7 @@ public static double distance2D(@NonNull Location a, @NonNull Location b) { ); } + @CheckReturnValue public static boolean isWithinAABB(@NonNull Location loc, @NonNull Location min, @NonNull Location max) { 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())), @@ -111,6 +118,7 @@ public static boolean isWithinAABB(@NonNull Location loc, @NonNull Location min, return aabb.contains(Vector3D.fromLocation(loc)); } + @CheckReturnValue public static @NonNull Location randomInRadius(@NonNull Location center, double radius) { double angle = Chance.randomIn(0.0, 2.0 * Math.PI); double r = Chance.randomIn(0.0, radius); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/Vector2D.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/Vector2D.java new file mode 100644 index 0000000..d172376 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/Vector2D.java @@ -0,0 +1,140 @@ +package dev.oum.oumlib.math; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +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 Vector2D(double x, double z) { + + public static final Vector2D ZERO = new Vector2D(0.0, 0.0); + public static final Vector2D ONE = new Vector2D(1.0, 1.0); + + @Contract(value = "_, _ -> new", pure = true) + public static @NonNull Vector2D of(double x, double z) { + return new Vector2D(x, z); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector2D fromBukkit(@Nullable Vector vector) { + if (vector == null) return ZERO; + return new Vector2D(vector.getX(), vector.getZ()); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector2D fromLocation(@Nullable Location loc) { + if (loc == null) return ZERO; + return new Vector2D(loc.getX(), loc.getZ()); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector2D fromBlock(@Nullable Block block) { + if (block == null) return ZERO; + return new Vector2D(block.getX(), block.getZ()); + } + + @Contract(value = "_ -> new", pure = true) + public static @NonNull Vector2D read(@NonNull DataInputStream dis) throws IOException { + return new Vector2D(dis.readDouble(), dis.readDouble()); + } + + public void write(@NonNull DataOutputStream dos) throws IOException { + dos.writeDouble(x); + dos.writeDouble(z); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector2D add(@NonNull Vector2D other) { + return new Vector2D(x + other.x, z + other.z); + } + + @Contract(value = "_, _ -> new", pure = true) + public @NonNull Vector2D add(double x, double z) { + return new Vector2D(this.x + x, this.z + z); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector2D subtract(@NonNull Vector2D other) { + return new Vector2D(x - other.x, z - other.z); + } + + @Contract(value = "_, _ -> new", pure = true) + public @NonNull Vector2D subtract(double x, double z) { + return new Vector2D(this.x - x, this.z - z); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector2D multiply(double scalar) { + return new Vector2D(x * scalar, z * scalar); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector2D divide(double scalar) { + return new Vector2D(x / scalar, z / scalar); + } + + public double dot(@NonNull Vector2D other) { + return x * other.x + z * other.z; + } + + public double cross(@NonNull Vector2D other) { + return x * other.z - z * other.x; + } + + public double length() { + return Math.sqrt(x * x + z * z); + } + + public double lengthSquared() { + return x * x + z * z; + } + + public double distance(@NonNull Vector2D other) { + double dx = x - other.x; + double dz = z - other.z; + return Math.sqrt(dx * dx + dz * dz); + } + + public double distanceSquared(@NonNull Vector2D other) { + double dx = x - other.x; + double dz = z - other.z; + return dx * dx + dz * dz; + } + + @Contract(value = " -> new", pure = true) + public @NonNull Vector2D normalize() { + double len = length(); + if (len == 0) return ZERO; + return new Vector2D(x / len, z / len); + } + + @Contract(value = "_, _ -> new", pure = true) + public @NonNull Vector2D lerp(@NonNull Vector2D target, double t) { + return new Vector2D( + x + (target.x - x) * t, + z + (target.z - z) * t + ); + } + + @Contract(value = "_ -> new", pure = true) + public @NonNull Vector toBukkitVector(double y) { + return new Vector(x, y, z); + } + + @Contract(value = "_, _ -> new", pure = true) + public @NonNull Location toLocation(@Nullable World world, double y) { + return new Location(world, x, y, z); + } + + @Contract(value = "_, _, _, _ -> new", pure = true) + public @NonNull Location toLocation(@Nullable World world, double y, float yaw, float pitch) { + return new Location(world, x, y, z, yaw, pitch); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/region/CompoundRegion.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/CompoundRegion.java new file mode 100644 index 0000000..cc3704f --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/CompoundRegion.java @@ -0,0 +1,231 @@ +package dev.oum.oumlib.math.region; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +public class CompoundRegion implements Region { + + private final String worldName; + private final Mode mode; + private final List children; + + public CompoundRegion(@NonNull Mode mode, @NonNull List children) { + if (children.isEmpty()) { + throw new IllegalArgumentException("Compound region must contain at least one child region."); + } + this.mode = mode; + this.children = List.copyOf(children); + this.worldName = children.getFirst().getWorldName(); + for (Region r : children) { + if (!r.getWorldName().equalsIgnoreCase(this.worldName)) { + throw new IllegalArgumentException("All child regions must belong to the same world."); + } + } + } + + public static @NonNull CompoundRegion union(@NonNull Region... regions) { + return new CompoundRegion(Mode.UNION, List.of(regions)); + } + + public static @NonNull CompoundRegion intersection(@NonNull Region... regions) { + return new CompoundRegion(Mode.INTERSECTION, List.of(regions)); + } + + public @NonNull Mode getMode() { + return mode; + } + + public @NonNull List getChildren() { + return children; + } + + @Override + public @Nullable World getWorld() { + return Bukkit.getWorld(worldName); + } + + @Override + public @NonNull String getWorldName() { + return worldName; + } + + @Override + public boolean contains(@NonNull Location location) { + World w = location.getWorld(); + if (w != null && !w.getName().equalsIgnoreCase(worldName)) { + return false; + } + if (mode == Mode.UNION) { + for (Region r : children) { + if (r.contains(location)) return true; + } + return false; + } else { + for (Region r : children) { + if (!r.contains(location)) return false; + } + return true; + } + } + + @Override + public boolean contains(@NonNull Vector vector) { + if (mode == Mode.UNION) { + for (Region r : children) { + if (r.contains(vector)) return true; + } + return false; + } else { + for (Region r : children) { + if (!r.contains(vector)) return false; + } + return true; + } + } + + @Override + public boolean contains(double x, double y, double z) { + if (mode == Mode.UNION) { + for (Region r : children) { + if (r.contains(x, y, z)) return true; + } + return false; + } else { + for (Region r : children) { + if (!r.contains(x, y, z)) return false; + } + return true; + } + } + + @Override + public boolean contains(@NonNull Block block) { + if (mode == Mode.UNION) { + for (Region r : children) { + if (r.contains(block)) return true; + } + return false; + } else { + for (Region r : children) { + if (!r.contains(block)) return false; + } + return true; + } + } + + @Override + public boolean overlaps(@NonNull Region other) { + if (!worldName.equalsIgnoreCase(other.getWorldName())) return false; + for (Block b : other) { + if (contains(b)) return true; + } + return false; + } + + @Override + public @NonNull Location getCenter() { + return children.getFirst().getCenter(); + } + + @Override + public double getVolume() { + double sum = 0; + for (Region r : children) { + sum += r.getVolume(); + } + return sum; + } + + @Override + public long getBlockCount() { + long count = 0; + for (Block ignored : this) { + count++; + } + return count; + } + + @Override + public @NonNull Location getRandomLocation() { + return getRandomLocation(new Random()); + } + + @Override + public @NonNull Location getRandomLocation(@NonNull Random random) { + if (children.isEmpty()) return new Location(getWorld(), 0, 0, 0); + Region picked = children.get(random.nextInt(children.size())); + return picked.getRandomLocation(random); + } + + @Override + public @NonNull Set getIntersectingChunks() { + Set chunks = new HashSet<>(); + for (Region r : children) { + chunks.addAll(r.getIntersectingChunks()); + } + return chunks; + } + + @Override + public @NonNull Set getIntersectingChunkKeys() { + Set keys = new HashSet<>(); + for (Region r : children) { + keys.addAll(r.getIntersectingChunkKeys()); + } + return keys; + } + + @Override + public @NonNull Iterator iterator() { + Set uniqueBlocks = new LinkedHashSet<>(); + if (mode == Mode.UNION) { + for (Region r : children) { + for (Block b : r) { + uniqueBlocks.add(b); + } + } + } else { + if (!children.isEmpty()) { + for (Block b : children.getFirst()) { + boolean insideAll = true; + for (int i = 1; i < children.size(); i++) { + if (!children.get(i).contains(b)) { + insideAll = false; + break; + } + } + if (insideAll) { + uniqueBlocks.add(b); + } + } + } + } + return uniqueBlocks.iterator(); + } + + @Override + public @NonNull Map serialize() { + Map map = new LinkedHashMap<>(); + map.put("type", "compound"); + map.put("mode", mode.name().toLowerCase(Locale.ROOT)); + List> childList = new ArrayList<>(); + for (Region r : children) { + childList.add(r.serialize()); + } + map.put("children", childList); + return map; + } + + public enum Mode { + UNION, + INTERSECTION + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/region/CuboidRegion.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/CuboidRegion.java new file mode 100644 index 0000000..1996591 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/CuboidRegion.java @@ -0,0 +1,340 @@ +package dev.oum.oumlib.math.region; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +public class CuboidRegion implements Region { + + private final String worldName; + private final double minX; + private final double minY; + private final double minZ; + private final double maxX; + private final double maxY; + private final double maxZ; + + public CuboidRegion(@NonNull Location loc1, @NonNull Location loc2) { + World w = loc1.getWorld() != null ? loc1.getWorld() : loc2.getWorld(); + this.worldName = w != null ? w.getName() : "world"; + this.minX = Math.min(loc1.getX(), loc2.getX()); + this.minY = Math.min(loc1.getY(), loc2.getY()); + this.minZ = Math.min(loc1.getZ(), loc2.getZ()); + this.maxX = Math.max(loc1.getX(), loc2.getX()); + this.maxY = Math.max(loc1.getY(), loc2.getY()); + this.maxZ = Math.max(loc1.getZ(), loc2.getZ()); + } + + public CuboidRegion(@NonNull String worldName, double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + this.worldName = worldName; + this.minX = Math.min(minX, maxX); + this.minY = Math.min(minY, maxY); + this.minZ = Math.min(minZ, maxZ); + this.maxX = Math.max(minX, maxX); + this.maxY = Math.max(minY, maxY); + this.maxZ = Math.max(minZ, maxZ); + } + + public static @NonNull CuboidRegion of(@NonNull Location loc1, @NonNull Location loc2) { + return new CuboidRegion(loc1, loc2); + } + + public static @NonNull CuboidRegion centered(@NonNull Location center, double radiusX, double radiusY, double radiusZ) { + World w = center.getWorld(); + String wName = w != null ? w.getName() : "world"; + return new CuboidRegion(wName, + center.getX() - radiusX, center.getY() - radiusY, center.getZ() - radiusZ, + center.getX() + radiusX, center.getY() + radiusY, center.getZ() + radiusZ); + } + + public static @NonNull CuboidRegion deserialize(@NonNull Map map) { + String world = (String) map.getOrDefault("world", "world"); + double minX = ((Number) map.get("minX")).doubleValue(); + double minY = ((Number) map.get("minY")).doubleValue(); + double minZ = ((Number) map.get("minZ")).doubleValue(); + double maxX = ((Number) map.get("maxX")).doubleValue(); + double maxY = ((Number) map.get("maxY")).doubleValue(); + double maxZ = ((Number) map.get("maxZ")).doubleValue(); + return new CuboidRegion(world, minX, minY, minZ, maxX, maxY, maxZ); + } + + @Override + public @Nullable World getWorld() { + return Bukkit.getWorld(worldName); + } + + @Override + public @NonNull String getWorldName() { + return worldName; + } + + public double getMinX() { + return minX; + } + + public double getMinY() { + return minY; + } + + public double getMinZ() { + return minZ; + } + + public double getMaxX() { + return maxX; + } + + public double getMaxY() { + return maxY; + } + + public double getMaxZ() { + return maxZ; + } + + public int getMinBlockX() { + return (int) Math.floor(minX); + } + + public int getMinBlockY() { + return (int) Math.floor(minY); + } + + public int getMinBlockZ() { + return (int) Math.floor(minZ); + } + + public int getMaxBlockX() { + return (int) Math.floor(maxX); + } + + public int getMaxBlockY() { + return (int) Math.floor(maxY); + } + + public int getMaxBlockZ() { + return (int) Math.floor(maxZ); + } + + public @NonNull Location getMinimumPoint() { + return new Location(getWorld(), minX, minY, minZ); + } + + public @NonNull Location getMaximumPoint() { + return new Location(getWorld(), maxX, maxY, maxZ); + } + + public double getWidthX() { + return maxX - minX; + } + + public double getHeight() { + return maxY - minY; + } + + public double getLengthZ() { + return maxZ - minZ; + } + + @Override + public boolean contains(@NonNull Location location) { + World w = location.getWorld(); + if (w != null && !w.getName().equalsIgnoreCase(worldName)) { + return false; + } + return contains(location.getX(), location.getY(), location.getZ()); + } + + @Override + public boolean contains(@NonNull Vector vector) { + return contains(vector.getX(), vector.getY(), vector.getZ()); + } + + @Override + public boolean contains(double x, double y, double z) { + return x >= minX && x <= maxX + && y >= minY && y <= maxY + && z >= minZ && z <= maxZ; + } + + @Override + public boolean contains(@NonNull Block block) { + World w = block.getWorld(); + if (!w.getName().equalsIgnoreCase(worldName)) { + return false; + } + int bx = block.getX(); + int by = block.getY(); + int bz = block.getZ(); + return bx >= getMinBlockX() && bx <= getMaxBlockX() + && by >= getMinBlockY() && by <= getMaxBlockY() + && bz >= getMinBlockZ() && bz <= getMaxBlockZ(); + } + + @Override + public boolean overlaps(@NonNull Region other) { + if (!worldName.equalsIgnoreCase(other.getWorldName())) { + return false; + } + if (other instanceof CuboidRegion c) { + return this.minX <= c.maxX && this.maxX >= c.minX + && this.minY <= c.maxY && this.maxY >= c.minY + && this.minZ <= c.maxZ && this.maxZ >= c.minZ; + } + for (Block b : other) { + if (contains(b)) return true; + } + return false; + } + + @Override + public @NonNull Location getCenter() { + return new Location(getWorld(), + minX + (maxX - minX) / 2.0, + minY + (maxY - minY) / 2.0, + minZ + (maxZ - minZ) / 2.0); + } + + @Override + public double getVolume() { + return (maxX - minX) * (maxY - minY) * (maxZ - minZ); + } + + @Override + public long getBlockCount() { + long dx = (long) getMaxBlockX() - getMinBlockX() + 1; + long dy = (long) getMaxBlockY() - getMinBlockY() + 1; + long dz = (long) getMaxBlockZ() - getMinBlockZ() + 1; + return dx * dy * dz; + } + + @Override + public @NonNull Location getRandomLocation() { + return getRandomLocation(new Random()); + } + + @Override + public @NonNull Location getRandomLocation(@NonNull Random random) { + double rx = minX + (maxX - minX) * random.nextDouble(); + double ry = minY + (maxY - minY) * random.nextDouble(); + double rz = minZ + (maxZ - minZ) * random.nextDouble(); + return new Location(getWorld(), rx, ry, rz); + } + + @Override + public @NonNull Set getIntersectingChunks() { + World w = getWorld(); + if (w == null) return Collections.emptySet(); + Set chunks = new HashSet<>(); + int minChunkX = getMinBlockX() >> 4; + int maxChunkX = getMaxBlockX() >> 4; + int minChunkZ = getMinBlockZ() >> 4; + int maxChunkZ = getMaxBlockZ() >> 4; + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + chunks.add(w.getChunkAt(cx, cz)); + } + } + return chunks; + } + + @Override + public @NonNull Set getIntersectingChunkKeys() { + Set keys = new HashSet<>(); + int minChunkX = getMinBlockX() >> 4; + int maxChunkX = getMaxBlockX() >> 4; + int minChunkZ = getMinBlockZ() >> 4; + int maxChunkZ = getMaxBlockZ() >> 4; + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + keys.add(Chunk.getChunkKey(cx, cz)); + } + } + return keys; + } + + public @NonNull CuboidRegion expand(double amount) { + return expand(amount, amount, amount); + } + + public @NonNull CuboidRegion expand(double dx, double dy, double dz) { + return new CuboidRegion(worldName, minX - dx, minY - dy, minZ - dz, maxX + dx, maxY + dy, maxZ + dz); + } + + public @NonNull CuboidRegion contract(double amount) { + return expand(-amount, -amount, -amount); + } + + public @NonNull List getCorners() { + World w = getWorld(); + return List.of( + new Location(w, minX, minY, minZ), + new Location(w, maxX, minY, minZ), + new Location(w, minX, minY, maxZ), + new Location(w, maxX, minY, maxZ), + new Location(w, minX, maxY, minZ), + new Location(w, maxX, maxY, minZ), + new Location(w, minX, maxY, maxZ), + new Location(w, maxX, maxY, maxZ) + ); + } + + @Override + public @NonNull Iterator iterator() { + return new Iterator<>() { + private final World world = getWorld(); + private final int minBx = getMinBlockX(); + private final int minBy = getMinBlockY(); + private final int minBz = getMinBlockZ(); + private final int maxBx = getMaxBlockX(); + private final int maxBy = getMaxBlockY(); + private final int maxBz = getMaxBlockZ(); + + private int currentX = minBx; + private int currentY = minBy; + private int currentZ = minBz; + + @Override + public boolean hasNext() { + return world != null && currentX <= maxBx && currentY <= maxBy && currentZ <= maxBz; + } + + @Override + public Block next() { + if (!hasNext()) throw new NoSuchElementException(); + Block block = world.getBlockAt(currentX, currentY, currentZ); + currentX++; + if (currentX > maxBx) { + currentX = minBx; + currentZ++; + if (currentZ > maxBz) { + currentZ = minBz; + currentY++; + } + } + return block; + } + }; + } + + @Override + public @NonNull Map serialize() { + Map map = new LinkedHashMap<>(); + map.put("type", "cuboid"); + map.put("world", worldName); + map.put("minX", minX); + map.put("minY", minY); + map.put("minZ", minZ); + map.put("maxX", maxX); + map.put("maxY", maxY); + map.put("maxZ", maxZ); + return map; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/region/CylinderRegion.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/CylinderRegion.java new file mode 100644 index 0000000..e567841 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/CylinderRegion.java @@ -0,0 +1,255 @@ +package dev.oum.oumlib.math.region; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +public class CylinderRegion implements Region { + + private final String worldName; + private final double centerX; + private final double centerZ; + private final double radius; + private final double minY; + private final double maxY; + + public CylinderRegion(@NonNull Location center, double radius, double minY, double maxY) { + World w = center.getWorld(); + this.worldName = w != null ? w.getName() : "world"; + this.centerX = center.getX(); + this.centerZ = center.getZ(); + this.radius = Math.max(0.0, radius); + this.minY = Math.min(minY, maxY); + this.maxY = Math.max(minY, maxY); + } + + public CylinderRegion(@NonNull String worldName, double centerX, double centerZ, double radius, double minY, double maxY) { + this.worldName = worldName; + this.centerX = centerX; + this.centerZ = centerZ; + this.radius = Math.max(0.0, radius); + this.minY = Math.min(minY, maxY); + this.maxY = Math.max(minY, maxY); + } + + public static @NonNull CylinderRegion of(@NonNull Location center, double radius, double height) { + double half = height / 2.0; + return new CylinderRegion(center, radius, center.getY() - half, center.getY() + half); + } + + public static @NonNull CylinderRegion deserialize(@NonNull Map map) { + String world = (String) map.getOrDefault("world", "world"); + double centerX = ((Number) map.get("centerX")).doubleValue(); + double centerZ = ((Number) map.get("centerZ")).doubleValue(); + double radius = ((Number) map.get("radius")).doubleValue(); + double minY = ((Number) map.get("minY")).doubleValue(); + double maxY = ((Number) map.get("maxY")).doubleValue(); + return new CylinderRegion(world, centerX, centerZ, radius, minY, maxY); + } + + @Override + public @Nullable World getWorld() { + return Bukkit.getWorld(worldName); + } + + @Override + public @NonNull String getWorldName() { + return worldName; + } + + public double getCenterX() { + return centerX; + } + + public double getCenterZ() { + return centerZ; + } + + public double getRadius() { + return radius; + } + + public double getMinY() { + return minY; + } + + public double getMaxY() { + return maxY; + } + + @Override + public boolean contains(@NonNull Location location) { + World w = location.getWorld(); + if (w != null && !w.getName().equalsIgnoreCase(worldName)) { + return false; + } + return contains(location.getX(), location.getY(), location.getZ()); + } + + @Override + public boolean contains(@NonNull Vector vector) { + return contains(vector.getX(), vector.getY(), vector.getZ()); + } + + @Override + public boolean contains(double x, double y, double z) { + if (y < minY || y > maxY) return false; + double dx = x - centerX; + double dz = z - centerZ; + return (dx * dx + dz * dz) <= (radius * radius); + } + + @Override + public boolean contains(@NonNull Block block) { + World w = block.getWorld(); + if (!w.getName().equalsIgnoreCase(worldName)) { + return false; + } + return contains(block.getX() + 0.5, block.getY() + 0.5, block.getZ() + 0.5); + } + + @Override + public boolean overlaps(@NonNull Region other) { + if (!worldName.equalsIgnoreCase(other.getWorldName())) return false; + for (Block b : other) { + if (contains(b)) return true; + } + return false; + } + + @Override + public @NonNull Location getCenter() { + return new Location(getWorld(), centerX, minY + (maxY - minY) / 2.0, centerZ); + } + + @Override + public double getVolume() { + return Math.PI * radius * radius * (maxY - minY); + } + + @Override + public long getBlockCount() { + long count = 0; + for (Block ignored : this) { + count++; + } + return count; + } + + @Override + public @NonNull Location getRandomLocation() { + return getRandomLocation(new Random()); + } + + @Override + public @NonNull Location getRandomLocation(@NonNull Random random) { + double r = radius * Math.sqrt(random.nextDouble()); + double theta = random.nextDouble() * 2 * Math.PI; + double rx = centerX + r * Math.cos(theta); + double rz = centerZ + r * Math.sin(theta); + double ry = minY + (maxY - minY) * random.nextDouble(); + return new Location(getWorld(), rx, ry, rz); + } + + @Override + public @NonNull Set getIntersectingChunks() { + World w = getWorld(); + if (w == null) return Collections.emptySet(); + Set chunks = new HashSet<>(); + int minChunkX = (int) Math.floor(centerX - radius) >> 4; + int maxChunkX = (int) Math.floor(centerX + radius) >> 4; + int minChunkZ = (int) Math.floor(centerZ - radius) >> 4; + int maxChunkZ = (int) Math.floor(centerZ + radius) >> 4; + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + chunks.add(w.getChunkAt(cx, cz)); + } + } + return chunks; + } + + @Override + public @NonNull Set getIntersectingChunkKeys() { + Set keys = new HashSet<>(); + int minChunkX = (int) Math.floor(centerX - radius) >> 4; + int maxChunkX = (int) Math.floor(centerX + radius) >> 4; + int minChunkZ = (int) Math.floor(centerZ - radius) >> 4; + int maxChunkZ = (int) Math.floor(centerZ + radius) >> 4; + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + keys.add(Chunk.getChunkKey(cx, cz)); + } + } + return keys; + } + + @Override + public @NonNull Iterator iterator() { + return new Iterator<>() { + private final World world = getWorld(); + private final int minBx = (int) Math.floor(centerX - radius); + private final int maxBx = (int) Math.floor(centerX + radius); + private final int minBy = (int) Math.floor(minY); + private final int maxBy = (int) Math.floor(maxY); + private final int minBz = (int) Math.floor(centerZ - radius); + private final int maxBz = (int) Math.floor(centerZ + radius); + + private int currentX = minBx; + private int currentY = minBy; + private int currentZ = minBz; + private Block nextBlock = computeNext(); + + private Block computeNext() { + if (world == null) return null; + while (currentY <= maxBy) { + while (currentZ <= maxBz) { + while (currentX <= maxBx) { + int bx = currentX++; + if (contains(bx + 0.5, currentY + 0.5, currentZ + 0.5)) { + return world.getBlockAt(bx, currentY, currentZ); + } + } + currentX = minBx; + currentZ++; + } + currentZ = minBz; + currentY++; + } + return null; + } + + @Override + public boolean hasNext() { + return nextBlock != null; + } + + @Override + public Block next() { + if (nextBlock == null) throw new NoSuchElementException(); + Block cur = nextBlock; + nextBlock = computeNext(); + return cur; + } + }; + } + + @Override + public @NonNull Map serialize() { + Map map = new LinkedHashMap<>(); + map.put("type", "cylinder"); + map.put("world", worldName); + map.put("centerX", centerX); + map.put("centerZ", centerZ); + map.put("radius", radius); + map.put("minY", minY); + map.put("maxY", maxY); + return map; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/region/PolygonRegion.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/PolygonRegion.java new file mode 100644 index 0000000..2fcdbf0 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/PolygonRegion.java @@ -0,0 +1,309 @@ +package dev.oum.oumlib.math.region; + +import dev.oum.oumlib.math.Vector2D; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +public class PolygonRegion implements Region { + + private final String worldName; + private final List points; + private final double minY; + private final double maxY; + private final double minX; + private final double maxX; + private final double minZ; + private final double maxZ; + + public PolygonRegion(@NonNull String worldName, @NonNull List points, double minY, double maxY) { + if (points.size() < 3) { + throw new IllegalArgumentException("A polygon region must have at least 3 points."); + } + this.worldName = worldName; + this.points = List.copyOf(points); + this.minY = Math.min(minY, maxY); + this.maxY = Math.max(minY, maxY); + + double calcMinX = Double.POSITIVE_INFINITY; + double calcMaxX = Double.NEGATIVE_INFINITY; + double calcMinZ = Double.POSITIVE_INFINITY; + double calcMaxZ = Double.NEGATIVE_INFINITY; + + for (Vector2D pt : points) { + calcMinX = Math.min(calcMinX, pt.x()); + calcMaxX = Math.max(calcMaxX, pt.x()); + calcMinZ = Math.min(calcMinZ, pt.z()); + calcMaxZ = Math.max(calcMaxZ, pt.z()); + } + + this.minX = calcMinX; + this.maxX = calcMaxX; + this.minZ = calcMinZ; + this.maxZ = calcMaxZ; + } + + public static @NonNull PolygonRegion of(@NonNull String worldName, @NonNull List points, double minY, double maxY) { + return new PolygonRegion(worldName, points, minY, maxY); + } + + @SuppressWarnings("unchecked") + public static @NonNull PolygonRegion deserialize(@NonNull Map map) { + String world = (String) map.getOrDefault("world", "world"); + double minY = ((Number) map.get("minY")).doubleValue(); + double maxY = ((Number) map.get("maxY")).doubleValue(); + List> ptList = (List>) map.get("points"); + List points = new ArrayList<>(); + if (ptList != null) { + for (Map p : ptList) { + points.add(Vector2D.of(p.get("x").doubleValue(), p.get("z").doubleValue())); + } + } + return new PolygonRegion(world, points, minY, maxY); + } + + @Override + public @Nullable World getWorld() { + return Bukkit.getWorld(worldName); + } + + @Override + public @NonNull String getWorldName() { + return worldName; + } + + public @NonNull List getPoints() { + return points; + } + + public double getMinY() { + return minY; + } + + public double getMaxY() { + return maxY; + } + + public double getMinX() { + return minX; + } + + public double getMaxX() { + return maxX; + } + + public double getMinZ() { + return minZ; + } + + public double getMaxZ() { + return maxZ; + } + + @Override + public boolean contains(@NonNull Location location) { + World w = location.getWorld(); + if (w != null && !w.getName().equalsIgnoreCase(worldName)) { + return false; + } + return contains(location.getX(), location.getY(), location.getZ()); + } + + @Override + public boolean contains(@NonNull Vector vector) { + return contains(vector.getX(), vector.getY(), vector.getZ()); + } + + @Override + public boolean contains(double x, double y, double z) { + if (y < minY || y > maxY) return false; + if (x < minX || x > maxX || z < minZ || z > maxZ) return false; + + boolean inside = false; + int n = points.size(); + for (int i = 0, j = n - 1; i < n; j = i++) { + Vector2D pi = points.get(i); + Vector2D pj = points.get(j); + + boolean intersect = ((pi.z() > z) != (pj.z() > z)) + && (x < (pj.x() - pi.x()) * (z - pi.z()) / (pj.z() - pi.z()) + pi.x()); + if (intersect) { + inside = !inside; + } + } + return inside; + } + + @Override + public boolean contains(@NonNull Block block) { + World w = block.getWorld(); + if (!w.getName().equalsIgnoreCase(worldName)) { + return false; + } + return contains(block.getX() + 0.5, block.getY() + 0.5, block.getZ() + 0.5); + } + + @Override + public boolean overlaps(@NonNull Region other) { + if (!worldName.equalsIgnoreCase(other.getWorldName())) return false; + for (Block b : other) { + if (contains(b)) return true; + } + return false; + } + + @Override + public @NonNull Location getCenter() { + double sumX = 0; + double sumZ = 0; + for (Vector2D pt : points) { + sumX += pt.x(); + sumZ += pt.z(); + } + return new Location(getWorld(), sumX / points.size(), minY + (maxY - minY) / 2.0, sumZ / points.size()); + } + + @Override + public double getVolume() { + double area = 0.0; + int n = points.size(); + for (int i = 0; i < n; i++) { + Vector2D p1 = points.get(i); + Vector2D p2 = points.get((i + 1) % n); + area += (p1.x() * p2.z()) - (p2.x() * p1.z()); + } + area = Math.abs(area) / 2.0; + return area * (maxY - minY); + } + + @Override + public long getBlockCount() { + long count = 0; + for (Block ignored : this) { + count++; + } + return count; + } + + @Override + public @NonNull Location getRandomLocation() { + return getRandomLocation(new Random()); + } + + @Override + public @NonNull Location getRandomLocation(@NonNull Random random) { + for (int attempts = 0; attempts < 1000; attempts++) { + double rx = minX + (maxX - minX) * random.nextDouble(); + double rz = minZ + (maxZ - minZ) * random.nextDouble(); + double ry = minY + (maxY - minY) * random.nextDouble(); + if (contains(rx, ry, rz)) { + return new Location(getWorld(), rx, ry, rz); + } + } + return getCenter(); + } + + @Override + public @NonNull Set getIntersectingChunks() { + World w = getWorld(); + if (w == null) return Collections.emptySet(); + Set chunks = new HashSet<>(); + int minChunkX = (int) Math.floor(minX) >> 4; + int maxChunkX = (int) Math.floor(maxX) >> 4; + int minChunkZ = (int) Math.floor(minZ) >> 4; + int maxChunkZ = (int) Math.floor(maxZ) >> 4; + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + chunks.add(w.getChunkAt(cx, cz)); + } + } + return chunks; + } + + @Override + public @NonNull Set getIntersectingChunkKeys() { + Set keys = new HashSet<>(); + int minChunkX = (int) Math.floor(minX) >> 4; + int maxChunkX = (int) Math.floor(maxX) >> 4; + int minChunkZ = (int) Math.floor(minZ) >> 4; + int maxChunkZ = (int) Math.floor(maxZ) >> 4; + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + keys.add(Chunk.getChunkKey(cx, cz)); + } + } + return keys; + } + + @Override + public @NonNull Iterator iterator() { + return new Iterator<>() { + private final World world = getWorld(); + private final int minBx = (int) Math.floor(minX); + private final int maxBx = (int) Math.floor(maxX); + private final int minBy = (int) Math.floor(minY); + private final int maxBy = (int) Math.floor(maxY); + private final int minBz = (int) Math.floor(minZ); + private final int maxBz = (int) Math.floor(maxZ); + + private int currentX = minBx; + private int currentY = minBy; + private int currentZ = minBz; + private Block nextBlock = computeNext(); + + private Block computeNext() { + if (world == null) return null; + while (currentY <= maxBy) { + while (currentZ <= maxBz) { + while (currentX <= maxBx) { + int bx = currentX++; + if (contains(bx + 0.5, currentY + 0.5, currentZ + 0.5)) { + return world.getBlockAt(bx, currentY, currentZ); + } + } + currentX = minBx; + currentZ++; + } + currentZ = minBz; + currentY++; + } + return null; + } + + @Override + public boolean hasNext() { + return nextBlock != null; + } + + @Override + public Block next() { + if (nextBlock == null) throw new NoSuchElementException(); + Block cur = nextBlock; + nextBlock = computeNext(); + return cur; + } + }; + } + + @Override + public @NonNull Map serialize() { + Map map = new LinkedHashMap<>(); + map.put("type", "polygon"); + map.put("world", worldName); + map.put("minY", minY); + map.put("maxY", maxY); + List> ptList = new ArrayList<>(); + for (Vector2D pt : points) { + ptList.add(Map.of("x", pt.x(), "z", pt.z())); + } + map.put("points", ptList); + return map; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/region/Region.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/Region.java new file mode 100644 index 0000000..580599b --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/Region.java @@ -0,0 +1,67 @@ +package dev.oum.oumlib.math.region; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Iterator; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public interface Region extends Iterable { + + @Nullable World getWorld(); + + @NonNull String getWorldName(); + + boolean contains(@NonNull Location location); + + boolean contains(@NonNull Vector vector); + + boolean contains(double x, double y, double z); + + boolean contains(@NonNull Block block); + + boolean overlaps(@NonNull Region other); + + @NonNull Location getCenter(); + + double getVolume(); + + long getBlockCount(); + + @NonNull Location getRandomLocation(); + + @NonNull Location getRandomLocation(@NonNull Random random); + + @NonNull Set getIntersectingChunks(); + + @NonNull Set getIntersectingChunkKeys(); + + @Override + @NonNull Iterator iterator(); + + default @NonNull Iterable getBlocks() { + return this; + } + + default @NonNull Stream streamBlocks() { + return StreamSupport.stream(spliterator(), false); + } + + default void forEachBlock(@NonNull Consumer action) { + for (Block block : this) { + action.accept(block); + } + } + + @NonNull Map serialize(); +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/region/RegionTracker.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/RegionTracker.java new file mode 100644 index 0000000..0e99b21 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/RegionTracker.java @@ -0,0 +1,198 @@ +package dev.oum.oumlib.math.region; + +import dev.oum.oumlib.math.region.event.PlayerEnterRegionEvent; +import dev.oum.oumlib.math.region.event.PlayerLeaveRegionEvent; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.*; +import org.bukkit.plugin.Plugin; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public final class RegionTracker implements Listener { + + private final Map regions = new ConcurrentHashMap<>(); + private final Map> chunkGrid = new ConcurrentHashMap<>(); + private final Map> playerRegions = new ConcurrentHashMap<>(); + private final Plugin plugin; + private boolean listening = false; + + public RegionTracker(@NonNull Plugin plugin) { + this.plugin = plugin; + } + + public void start() { + if (!listening) { + Bukkit.getPluginManager().registerEvents(this, plugin); + listening = true; + } + } + + public void stop() { + if (listening) { + HandlerList.unregisterAll(this); + listening = false; + } + playerRegions.clear(); + } + + public synchronized void register(@NonNull String id, @NonNull Region region) { + regions.put(id, region); + for (long chunkKey : region.getIntersectingChunkKeys()) { + chunkGrid.computeIfAbsent(chunkKey, k -> ConcurrentHashMap.newKeySet()).add(id); + } + } + + public synchronized @Nullable Region unregister(@NonNull String id) { + Region removed = regions.remove(id); + if (removed != null) { + for (long chunkKey : removed.getIntersectingChunkKeys()) { + Set set = chunkGrid.get(chunkKey); + if (set != null) { + set.remove(id); + if (set.isEmpty()) { + chunkGrid.remove(chunkKey); + } + } + } + playerRegions.values().forEach(set -> set.remove(id)); + } + return removed; + } + + public @NonNull Optional get(@NonNull String id) { + return Optional.ofNullable(regions.get(id)); + } + + public @NonNull Map getAll() { + return Collections.unmodifiableMap(regions); + } + + public @NonNull Set getRegionsAt(@NonNull Location loc) { + int cx = loc.getBlockX() >> 4; + int cz = loc.getBlockZ() >> 4; + long chunkKey = Chunk.getChunkKey(cx, cz); + Set candidates = chunkGrid.get(chunkKey); + if (candidates == null || candidates.isEmpty()) { + return Collections.emptySet(); + } + Set inside = new HashSet<>(); + for (String id : candidates) { + Region r = regions.get(id); + if (r != null && r.contains(loc)) { + inside.add(id); + } + } + return inside; + } + + public @NonNull Set getPlayerRegions(@NonNull Player player) { + Set set = playerRegions.get(player.getUniqueId()); + return set != null ? Collections.unmodifiableSet(set) : Collections.emptySet(); + } + + public boolean isInside(@NonNull Player player, @NonNull String regionId) { + Set set = playerRegions.get(player.getUniqueId()); + return set != null && set.contains(regionId); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onMove(PlayerMoveEvent event) { + Location from = event.getFrom(); + Location to = event.getTo(); + if (to == null) return; + if (from.getBlockX() == to.getBlockX() && from.getBlockY() == to.getBlockY() && from.getBlockZ() == to.getBlockZ()) { + return; + } + handleMovement(event.getPlayer(), from, to); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTeleport(PlayerTeleportEvent event) { + Location from = event.getFrom(); + Location to = event.getTo(); + if (to == null) return; + handleMovement(event.getPlayer(), from, to); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + Set inside = getRegionsAt(player.getLocation()); + playerRegions.put(player.getUniqueId(), ConcurrentHashMap.newKeySet()); + Set current = playerRegions.get(player.getUniqueId()); + for (String id : inside) { + Region r = regions.get(id); + if (r != null) { + PlayerEnterRegionEvent enterEvent = new PlayerEnterRegionEvent(player, id, r, player.getLocation(), player.getLocation()); + Bukkit.getPluginManager().callEvent(enterEvent); + if (!enterEvent.isCancelled()) { + current.add(id); + } + } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onQuit(PlayerQuitEvent event) { + UUID uid = event.getPlayer().getUniqueId(); + playerRegions.remove(uid); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onRespawn(PlayerRespawnEvent event) { + Player player = event.getPlayer(); + Location to = event.getRespawnLocation(); + handleMovement(player, player.getLocation(), to); + } + + private void handleMovement(Player player, Location from, Location to) { + Set current = playerRegions.computeIfAbsent(player.getUniqueId(), k -> ConcurrentHashMap.newKeySet()); + Set toRegions = getRegionsAt(to); + + for (String id : new HashSet<>(current)) { + if (!toRegions.contains(id)) { + Region r = regions.get(id); + if (r != null) { + PlayerLeaveRegionEvent leaveEvent = new PlayerLeaveRegionEvent(player, id, r, from, to); + Bukkit.getPluginManager().callEvent(leaveEvent); + if (leaveEvent.isCancelled()) { + player.teleport(from); + return; + } + } + current.remove(id); + } + } + + for (String id : toRegions) { + if (!current.contains(id)) { + Region r = regions.get(id); + if (r != null) { + PlayerEnterRegionEvent enterEvent = new PlayerEnterRegionEvent(player, id, r, from, to); + Bukkit.getPluginManager().callEvent(enterEvent); + if (enterEvent.isCancelled()) { + player.teleport(from); + return; + } + current.add(id); + } + } + } + } + + public void clear() { + regions.clear(); + chunkGrid.clear(); + playerRegions.clear(); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/region/SphereRegion.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/SphereRegion.java new file mode 100644 index 0000000..ae9c125 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/SphereRegion.java @@ -0,0 +1,249 @@ +package dev.oum.oumlib.math.region; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; + +public class SphereRegion implements Region { + + private final String worldName; + private final double centerX; + private final double centerY; + private final double centerZ; + private final double radius; + + public SphereRegion(@NonNull Location center, double radius) { + World w = center.getWorld(); + this.worldName = w != null ? w.getName() : "world"; + this.centerX = center.getX(); + this.centerY = center.getY(); + this.centerZ = center.getZ(); + this.radius = Math.max(0.0, radius); + } + + public SphereRegion(@NonNull String worldName, double centerX, double centerY, double centerZ, double radius) { + this.worldName = worldName; + this.centerX = centerX; + this.centerY = centerY; + this.centerZ = centerZ; + this.radius = Math.max(0.0, radius); + } + + public static @NonNull SphereRegion of(@NonNull Location center, double radius) { + return new SphereRegion(center, radius); + } + + public static @NonNull SphereRegion deserialize(@NonNull Map map) { + String world = (String) map.getOrDefault("world", "world"); + double centerX = ((Number) map.get("centerX")).doubleValue(); + double centerY = ((Number) map.get("centerY")).doubleValue(); + double centerZ = ((Number) map.get("centerZ")).doubleValue(); + double radius = ((Number) map.get("radius")).doubleValue(); + return new SphereRegion(world, centerX, centerY, centerZ, radius); + } + + @Override + public @Nullable World getWorld() { + return Bukkit.getWorld(worldName); + } + + @Override + public @NonNull String getWorldName() { + return worldName; + } + + public double getCenterX() { + return centerX; + } + + public double getCenterY() { + return centerY; + } + + public double getCenterZ() { + return centerZ; + } + + public double getRadius() { + return radius; + } + + @Override + public boolean contains(@NonNull Location location) { + World w = location.getWorld(); + if (w != null && !w.getName().equalsIgnoreCase(worldName)) { + return false; + } + return contains(location.getX(), location.getY(), location.getZ()); + } + + @Override + public boolean contains(@NonNull Vector vector) { + return contains(vector.getX(), vector.getY(), vector.getZ()); + } + + @Override + public boolean contains(double x, double y, double z) { + double dx = x - centerX; + double dy = y - centerY; + double dz = z - centerZ; + return (dx * dx + dy * dy + dz * dz) <= (radius * radius); + } + + @Override + public boolean contains(@NonNull Block block) { + World w = block.getWorld(); + if (!w.getName().equalsIgnoreCase(worldName)) { + return false; + } + return contains(block.getX() + 0.5, block.getY() + 0.5, block.getZ() + 0.5); + } + + @Override + public boolean overlaps(@NonNull Region other) { + if (!worldName.equalsIgnoreCase(other.getWorldName())) return false; + for (Block b : other) { + if (contains(b)) return true; + } + return false; + } + + @Override + public @NonNull Location getCenter() { + return new Location(getWorld(), centerX, centerY, centerZ); + } + + @Override + public double getVolume() { + return (4.0 / 3.0) * Math.PI * radius * radius * radius; + } + + @Override + public long getBlockCount() { + long count = 0; + for (Block ignored : this) { + count++; + } + return count; + } + + @Override + public @NonNull Location getRandomLocation() { + return getRandomLocation(new Random()); + } + + @Override + public @NonNull Location getRandomLocation(@NonNull Random random) { + double u = random.nextDouble(); + double v = random.nextDouble(); + double theta = u * 2.0 * Math.PI; + double phi = Math.acos(2.0 * v - 1.0); + double r = Math.cbrt(random.nextDouble()) * radius; + double sinPhi = Math.sin(phi); + double rx = centerX + r * sinPhi * Math.cos(theta); + double ry = centerY + r * Math.cos(phi); + double rz = centerZ + r * sinPhi * Math.sin(theta); + return new Location(getWorld(), rx, ry, rz); + } + + @Override + public @NonNull Set getIntersectingChunks() { + World w = getWorld(); + if (w == null) return Collections.emptySet(); + Set chunks = new HashSet<>(); + int minChunkX = (int) Math.floor(centerX - radius) >> 4; + int maxChunkX = (int) Math.floor(centerX + radius) >> 4; + int minChunkZ = (int) Math.floor(centerZ - radius) >> 4; + int maxChunkZ = (int) Math.floor(centerZ + radius) >> 4; + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + chunks.add(w.getChunkAt(cx, cz)); + } + } + return chunks; + } + + @Override + public @NonNull Set getIntersectingChunkKeys() { + Set keys = new HashSet<>(); + int minChunkX = (int) Math.floor(centerX - radius) >> 4; + int maxChunkX = (int) Math.floor(centerX + radius) >> 4; + int minChunkZ = (int) Math.floor(centerZ - radius) >> 4; + int maxChunkZ = (int) Math.floor(centerZ + radius) >> 4; + for (int cx = minChunkX; cx <= maxChunkX; cx++) { + for (int cz = minChunkZ; cz <= maxChunkZ; cz++) { + keys.add(Chunk.getChunkKey(cx, cz)); + } + } + return keys; + } + + @Override + public @NonNull Iterator iterator() { + return new Iterator<>() { + private final World world = getWorld(); + private final int minBx = (int) Math.floor(centerX - radius); + private final int maxBx = (int) Math.floor(centerX + radius); + private final int minBy = (int) Math.floor(centerY - radius); + private final int maxBy = (int) Math.floor(centerY + radius); + private final int minBz = (int) Math.floor(centerZ - radius); + private final int maxBz = (int) Math.floor(centerZ + radius); + + private int currentX = minBx; + private int currentY = minBy; + private int currentZ = minBz; + private Block nextBlock = computeNext(); + + private Block computeNext() { + if (world == null) return null; + while (currentY <= maxBy) { + while (currentZ <= maxBz) { + while (currentX <= maxBx) { + int bx = currentX++; + if (contains(bx + 0.5, currentY + 0.5, currentZ + 0.5)) { + return world.getBlockAt(bx, currentY, currentZ); + } + } + currentX = minBx; + currentZ++; + } + currentZ = minBz; + currentY++; + } + return null; + } + + @Override + public boolean hasNext() { + return nextBlock != null; + } + + @Override + public Block next() { + if (nextBlock == null) throw new NoSuchElementException(); + Block cur = nextBlock; + nextBlock = computeNext(); + return cur; + } + }; + } + + @Override + public @NonNull Map serialize() { + Map map = new LinkedHashMap<>(); + map.put("type", "sphere"); + map.put("world", worldName); + map.put("centerX", centerX); + map.put("centerY", centerY); + map.put("centerZ", centerZ); + map.put("radius", radius); + return map; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/region/event/PlayerEnterRegionEvent.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/event/PlayerEnterRegionEvent.java new file mode 100644 index 0000000..aba17db --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/event/PlayerEnterRegionEvent.java @@ -0,0 +1,68 @@ +package dev.oum.oumlib.math.region.event; + +import dev.oum.oumlib.math.region.Region; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jspecify.annotations.NonNull; + +public class PlayerEnterRegionEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final Player player; + private final String regionId; + private final Region region; + private final Location from; + private final Location to; + private boolean cancelled = false; + + public PlayerEnterRegionEvent(@NonNull Player player, @NonNull String regionId, @NonNull Region region, @NonNull Location from, @NonNull Location to) { + this.player = player; + this.regionId = regionId; + this.region = region; + this.from = from; + this.to = to; + } + + public static @NonNull HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NonNull HandlerList getHandlers() { + return HANDLERS; + } + + public @NonNull Player getPlayer() { + return player; + } + + public @NonNull String getRegionId() { + return regionId; + } + + public @NonNull Region getRegion() { + return region; + } + + public @NonNull Location getFrom() { + return from; + } + + public @NonNull Location getTo() { + return to; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/math/region/event/PlayerLeaveRegionEvent.java b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/event/PlayerLeaveRegionEvent.java new file mode 100644 index 0000000..ace7d55 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/math/region/event/PlayerLeaveRegionEvent.java @@ -0,0 +1,68 @@ +package dev.oum.oumlib.math.region.event; + +import dev.oum.oumlib.math.region.Region; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jspecify.annotations.NonNull; + +public class PlayerLeaveRegionEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final Player player; + private final String regionId; + private final Region region; + private final Location from; + private final Location to; + private boolean cancelled = false; + + public PlayerLeaveRegionEvent(@NonNull Player player, @NonNull String regionId, @NonNull Region region, @NonNull Location from, @NonNull Location to) { + this.player = player; + this.regionId = regionId; + this.region = region; + this.from = from; + this.to = to; + } + + public static @NonNull HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NonNull HandlerList getHandlers() { + return HANDLERS; + } + + public @NonNull Player getPlayer() { + return player; + } + + public @NonNull String getRegionId() { + return regionId; + } + + public @NonNull Region getRegion() { + return region; + } + + public @NonNull Location getFrom() { + return from; + } + + public @NonNull Location getTo() { + return to; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/DataKey.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/DataKey.java new file mode 100644 index 0000000..5d93046 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/DataKey.java @@ -0,0 +1,277 @@ +package dev.oum.oumlib.pdc; + +import com.google.gson.Gson; +import dev.oum.oumlib.OumLib; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.persistence.PersistentDataAdapterContext; +import org.bukkit.persistence.PersistentDataType; +import org.jspecify.annotations.NonNull; + +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public record DataKey( + @NonNull NamespacedKey key, + @NonNull PersistentDataType type +) { + + public static final PersistentDataType BOOLEAN_TYPE = new PersistentDataType<>() { + @Override + public @NonNull Class getPrimitiveType() { + return Byte.class; + } + + @Override + public @NonNull Class getComplexType() { + return Boolean.class; + } + + @Override + public @NonNull Byte toPrimitive(@NonNull Boolean complex, @NonNull PersistentDataAdapterContext context) { + return (byte) (complex ? 1 : 0); + } + + @Override + public @NonNull Boolean fromPrimitive(@NonNull Byte primitive, @NonNull PersistentDataAdapterContext context) { + return primitive == 1; + } + }; + public static final PersistentDataType UUID_TYPE = new PersistentDataType<>() { + @Override + public @NonNull Class getPrimitiveType() { + return byte[].class; + } + + @Override + public @NonNull Class getComplexType() { + return UUID.class; + } + + @Override + public byte @NonNull [] toPrimitive(@NonNull UUID complex, @NonNull PersistentDataAdapterContext context) { + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(complex.getMostSignificantBits()); + bb.putLong(complex.getLeastSignificantBits()); + return bb.array(); + } + + @Override + public @NonNull UUID fromPrimitive(byte @NonNull [] primitive, @NonNull PersistentDataAdapterContext context) { + ByteBuffer bb = ByteBuffer.wrap(primitive); + return new UUID(bb.getLong(), bb.getLong()); + } + }; + public static final PersistentDataType INSTANT_TYPE = new PersistentDataType<>() { + @Override + public @NonNull Class getPrimitiveType() { + return Long.class; + } + + @Override + public @NonNull Class getComplexType() { + return Instant.class; + } + + @Override + public @NonNull Long toPrimitive(@NonNull Instant complex, @NonNull PersistentDataAdapterContext context) { + return complex.toEpochMilli(); + } + + @Override + public @NonNull Instant fromPrimitive(@NonNull Long primitive, @NonNull PersistentDataAdapterContext context) { + return Instant.ofEpochMilli(primitive); + } + }; + public static final PersistentDataType LOCATION_TYPE = new PersistentDataType<>() { + @Override + public @NonNull Class getPrimitiveType() { + return String.class; + } + + @Override + public @NonNull Class getComplexType() { + return Location.class; + } + + @Override + public @NonNull String toPrimitive(@NonNull Location complex, @NonNull PersistentDataAdapterContext context) { + String world = complex.getWorld() != null ? complex.getWorld().getName() : "world"; + return world + ";" + complex.getX() + ";" + complex.getY() + ";" + complex.getZ() + ";" + complex.getYaw() + ";" + complex.getPitch(); + } + + @Override + public @NonNull Location fromPrimitive(@NonNull String primitive, @NonNull PersistentDataAdapterContext context) { + String[] parts = primitive.split(";"); + World w = Bukkit.getWorld(parts[0]); + double x = Double.parseDouble(parts[1]); + double y = Double.parseDouble(parts[2]); + double z = Double.parseDouble(parts[3]); + float yaw = parts.length > 4 ? Float.parseFloat(parts[4]) : 0f; + float pitch = parts.length > 5 ? Float.parseFloat(parts[5]) : 0f; + return new Location(w, x, y, z, yaw, pitch); + } + }; + private static final Gson GSON = new Gson(); + public static final PersistentDataType> STRING_LIST_TYPE = new PersistentDataType<>() { + @Override + public @NonNull Class getPrimitiveType() { + return String.class; + } + + @Override + @SuppressWarnings("unchecked") + public @NonNull Class> getComplexType() { + return (Class>) (Class) List.class; + } + + @Override + public @NonNull String toPrimitive(@NonNull List complex, @NonNull PersistentDataAdapterContext context) { + return GSON.toJson(complex); + } + + @Override + @SuppressWarnings("unchecked") + public @NonNull List fromPrimitive(@NonNull String primitive, @NonNull PersistentDataAdapterContext context) { + return GSON.fromJson(primitive, List.class); + } + }; + + public static @NonNull DataKey string(@NonNull NamespacedKey key) { + return new DataKey<>(key, PersistentDataType.STRING); + } + + public static @NonNull DataKey string(@NonNull String key) { + return string(createKey(key)); + } + + public static @NonNull DataKey integer(@NonNull NamespacedKey key) { + return new DataKey<>(key, PersistentDataType.INTEGER); + } + + public static @NonNull DataKey integer(@NonNull String key) { + return integer(createKey(key)); + } + + public static @NonNull DataKey doubles(@NonNull NamespacedKey key) { + return new DataKey<>(key, PersistentDataType.DOUBLE); + } + + public static @NonNull DataKey doubles(@NonNull String key) { + return doubles(createKey(key)); + } + + public static @NonNull DataKey floats(@NonNull NamespacedKey key) { + return new DataKey<>(key, PersistentDataType.FLOAT); + } + + public static @NonNull DataKey floats(@NonNull String key) { + return floats(createKey(key)); + } + + public static @NonNull DataKey longs(@NonNull NamespacedKey key) { + return new DataKey<>(key, PersistentDataType.LONG); + } + + public static @NonNull DataKey longs(@NonNull String key) { + return longs(createKey(key)); + } + + public static @NonNull DataKey bytes(@NonNull NamespacedKey key) { + return new DataKey<>(key, PersistentDataType.BYTE); + } + + public static @NonNull DataKey bytes(@NonNull String key) { + return bytes(createKey(key)); + } + + public static @NonNull DataKey bool(@NonNull NamespacedKey key) { + return new DataKey<>(key, BOOLEAN_TYPE); + } + + public static @NonNull DataKey bool(@NonNull String key) { + return bool(createKey(key)); + } + + public static @NonNull DataKey uuid(@NonNull NamespacedKey key) { + return new DataKey<>(key, UUID_TYPE); + } + + public static @NonNull DataKey uuid(@NonNull String key) { + return uuid(createKey(key)); + } + + public static @NonNull DataKey instant(@NonNull NamespacedKey key) { + return new DataKey<>(key, INSTANT_TYPE); + } + + public static @NonNull DataKey instant(@NonNull String key) { + return instant(createKey(key)); + } + + public static @NonNull DataKey location(@NonNull NamespacedKey key) { + return new DataKey<>(key, LOCATION_TYPE); + } + + public static @NonNull DataKey location(@NonNull String key) { + return location(createKey(key)); + } + + public static @NonNull DataKey> stringList(@NonNull NamespacedKey key) { + return new DataKey<>(key, STRING_LIST_TYPE); + } + + public static @NonNull DataKey> stringList(@NonNull String key) { + return stringList(createKey(key)); + } + + public static @NonNull DataKey json(@NonNull NamespacedKey key, @NonNull Class clazz) { + return new DataKey<>(key, new JsonDataType<>(clazz)); + } + + public static @NonNull DataKey json(@NonNull String key, @NonNull Class clazz) { + return json(createKey(key), clazz); + } + + public static @NonNull DataKey custom(@NonNull NamespacedKey key, @NonNull PersistentDataType type) { + return new DataKey<>(key, type); + } + + public static @NonNull DataKey custom(@NonNull String key, @NonNull PersistentDataType type) { + return custom(createKey(key), type); + } + + private static NamespacedKey createKey(@NonNull String key) { + if (key.contains(":")) { + return NamespacedKey.fromString(key); + } + return new NamespacedKey(OumLib.plugin(), key); + } + + private record JsonDataType(Class clazz) implements PersistentDataType { + + @Override + public @NonNull Class getPrimitiveType() { + return String.class; + } + + @Override + public @NonNull Class getComplexType() { + return clazz; + } + + @Override + public @NonNull String toPrimitive(@NonNull T complex, @NonNull PersistentDataAdapterContext context) { + return GSON.toJson(complex); + } + + @Override + public @NonNull T fromPrimitive(@NonNull String primitive, @NonNull PersistentDataAdapterContext context) { + return GSON.fromJson(primitive, clazz); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PDC.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PDC.java new file mode 100644 index 0000000..8af32da --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PDC.java @@ -0,0 +1,314 @@ +package dev.oum.oumlib.pdc; + +import org.bukkit.NamespacedKey; +import org.bukkit.block.BlockState; +import org.bukkit.block.TileState; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataHolder; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +public final class PDC { + + private static final Map> listeners = new ConcurrentHashMap<>(); + + private PDC() { + } + + public static void registerListener(@NonNull NamespacedKey key, @NonNull PdcChangeListener listener) { + listeners.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(listener); + } + + public static void registerListener(@NonNull DataKey key, @NonNull PdcChangeListener listener) { + registerListener(key.key(), listener); + } + + public static void unregisterListener(@NonNull NamespacedKey key, @NonNull PdcChangeListener listener) { + List list = listeners.get(key); + if (list != null) { + list.remove(listener); + } + } + + public static void unregisterListener(@NonNull DataKey key, @NonNull PdcChangeListener listener) { + unregisterListener(key.key(), listener); + } + + public static void triggerListeners(@NonNull Object target, @NonNull NamespacedKey key, + @Nullable Object oldValue, @Nullable Object newValue) { + List list = listeners.get(key); + if (list != null) { + for (PdcChangeListener listener : list) { + try { + listener.onChange(target, key, oldValue, newValue); + } catch (Exception ignored) { + } + } + } + } + + @Contract("_ -> new") + @CheckReturnValue + public static @NonNull PdcHolder of(@NonNull PersistentDataHolder holder) { + return new PdcHolder(holder); + } + + @Contract("_ -> new") + @CheckReturnValue + public static @NonNull PdcItem of(@NonNull ItemStack item) { + return new PdcItem(item); + } + + @Contract("_ -> new") + @CheckReturnValue + public static @NonNull PdcTree tree(@NonNull PersistentDataHolder holder) { + return PdcTree.of(holder); + } + + @Contract("_ -> new") + @CheckReturnValue + public static @NonNull PdcTree tree(@NonNull PersistentDataContainer container) { + return PdcTree.of(container); + } + + @Contract("_ -> new") + @CheckReturnValue + public static @NonNull PdcFlags flags(@NonNull PersistentDataHolder holder) { + return new PdcFlags(holder); + } + + @Contract("_, _ -> new") + @CheckReturnValue + public static @NonNull PdcFlags flags(@NonNull PersistentDataHolder holder, @NonNull NamespacedKey key) { + return new PdcFlags(holder, key); + } + + @Contract("_, _ -> new") + @CheckReturnValue + public static @NonNull PdcProperty property(@NonNull PersistentDataHolder holder, @NonNull DataKey key) { + return new PdcProperty<>(holder, key, null); + } + + @Contract("_, _, _ -> new") + @CheckReturnValue + public static @NonNull PdcProperty property(@NonNull PersistentDataHolder holder, @NonNull DataKey key, @Nullable C defaultValue) { + return new PdcProperty<>(holder, key, defaultValue); + } + + public static void write(@NonNull PersistentDataHolder holder, @NonNull T recordInstance) { + PdcModel.write(holder.getPersistentDataContainer(), recordInstance); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack write(@NonNull ItemStack item, @NonNull T recordInstance) { + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + PdcModel.write(meta.getPersistentDataContainer(), recordInstance); + item.setItemMeta(meta); + } + return item; + } + + @CheckReturnValue + public static @NonNull Optional read(@NonNull PersistentDataHolder holder, @NonNull Class recordClass) { + return PdcModel.read(holder.getPersistentDataContainer(), recordClass); + } + + @CheckReturnValue + public static @NonNull Optional read(@NonNull ItemStack item, @NonNull Class recordClass) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return Optional.empty(); + return PdcModel.read(meta.getPersistentDataContainer(), recordClass); + } + + public static void set(@NonNull PersistentDataHolder holder, @NonNull DataKey key, @NonNull C value) { + Objects.requireNonNull(holder); + Objects.requireNonNull(key); + Objects.requireNonNull(value); + C oldValue = holder.getPersistentDataContainer().get(key.key(), key.type()); + holder.getPersistentDataContainer().set(key.key(), key.type(), value); + triggerListeners(holder, key.key(), oldValue, value); + } + + public static void set(@NonNull PersistentDataHolder holder, @NonNull NamespacedKey key, @NonNull PersistentDataType type, @NonNull C value) { + Objects.requireNonNull(holder); + Objects.requireNonNull(key); + Objects.requireNonNull(type); + Objects.requireNonNull(value); + C oldValue = holder.getPersistentDataContainer().get(key, type); + holder.getPersistentDataContainer().set(key, type, value); + triggerListeners(holder, key, oldValue, value); + } + + @CheckReturnValue + public static @NonNull Optional get(@NonNull PersistentDataHolder holder, @NonNull DataKey key) { + Objects.requireNonNull(holder); + Objects.requireNonNull(key); + return Optional.ofNullable(holder.getPersistentDataContainer().get(key.key(), key.type())); + } + + @CheckReturnValue + public static @NonNull Optional get(@NonNull PersistentDataHolder holder, @NonNull NamespacedKey key, @NonNull PersistentDataType type) { + Objects.requireNonNull(holder); + Objects.requireNonNull(key); + Objects.requireNonNull(type); + return Optional.ofNullable(holder.getPersistentDataContainer().get(key, type)); + } + + @CheckReturnValue + public static @NonNull C getOrDefault(@NonNull PersistentDataHolder holder, @NonNull DataKey key, @NonNull C defaultValue) { + return get(holder, key).orElse(defaultValue); + } + + @CheckReturnValue + public static boolean has(@NonNull PersistentDataHolder holder, @NonNull DataKey key) { + Objects.requireNonNull(holder); + Objects.requireNonNull(key); + return holder.getPersistentDataContainer().has(key.key(), key.type()); + } + + @CheckReturnValue + public static boolean has(@NonNull PersistentDataHolder holder, @NonNull NamespacedKey key, @NonNull PersistentDataType type) { + Objects.requireNonNull(holder); + Objects.requireNonNull(key); + Objects.requireNonNull(type); + return holder.getPersistentDataContainer().has(key, type); + } + + @CheckReturnValue + public static boolean has(@NonNull PersistentDataHolder holder, @NonNull NamespacedKey key) { + Objects.requireNonNull(holder); + Objects.requireNonNull(key); + return holder.getPersistentDataContainer().has(key); + } + + public static void remove(@NonNull PersistentDataHolder holder, @NonNull DataKey key) { + Objects.requireNonNull(holder); + Objects.requireNonNull(key); + holder.getPersistentDataContainer().remove(key.key()); + triggerListeners(holder, key.key(), null, null); + } + + public static void remove(@NonNull PersistentDataHolder holder, @NonNull NamespacedKey key) { + Objects.requireNonNull(holder); + Objects.requireNonNull(key); + holder.getPersistentDataContainer().remove(key); + triggerListeners(holder, key, null, null); + } + + @CheckReturnValue + public static @NonNull Set getKeys(@NonNull PersistentDataHolder holder) { + Objects.requireNonNull(holder); + return holder.getPersistentDataContainer().getKeys(); + } + + @Contract("_, _, _ -> param1") + public static @NonNull ItemStack set(@NonNull ItemStack item, @NonNull DataKey key, @NonNull C value) { + Objects.requireNonNull(item); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + C oldValue = meta.getPersistentDataContainer().get(key.key(), key.type()); + meta.getPersistentDataContainer().set(key.key(), key.type(), value); + item.setItemMeta(meta); + triggerListeners(item, key.key(), oldValue, value); + } + return item; + } + + @Contract("_, _, _, _ -> param1") + public static @NonNull ItemStack set(@NonNull ItemStack item, @NonNull NamespacedKey key, @NonNull PersistentDataType type, @NonNull C value) { + Objects.requireNonNull(item); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + C oldValue = meta.getPersistentDataContainer().get(key, type); + meta.getPersistentDataContainer().set(key, type, value); + item.setItemMeta(meta); + triggerListeners(item, key, oldValue, value); + } + return item; + } + + @CheckReturnValue + public static @NonNull Optional get(@NonNull ItemStack item, @NonNull DataKey key) { + Objects.requireNonNull(item); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return Optional.empty(); + return get(meta, key); + } + + @CheckReturnValue + public static @NonNull Optional get(@NonNull ItemStack item, @NonNull NamespacedKey key, @NonNull PersistentDataType type) { + Objects.requireNonNull(item); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return Optional.empty(); + return get(meta, key, type); + } + + @CheckReturnValue + public static @NonNull C getOrDefault(@NonNull ItemStack item, @NonNull DataKey key, @NonNull C defaultValue) { + return get(item, key).orElse(defaultValue); + } + + @CheckReturnValue + public static boolean has(@NonNull ItemStack item, @NonNull DataKey key) { + Objects.requireNonNull(item); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + return has(meta, key); + } + + @CheckReturnValue + public static boolean has(@NonNull ItemStack item, @NonNull NamespacedKey key) { + Objects.requireNonNull(item); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + return has(meta, key); + } + + @Contract("_, _ -> param1") + public static @NonNull ItemStack remove(@NonNull ItemStack item, @NonNull DataKey key) { + Objects.requireNonNull(item); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + remove(meta, key); + item.setItemMeta(meta); + triggerListeners(item, key.key(), null, null); + } + return item; + } + + public static void set(@NonNull BlockState blockState, @NonNull DataKey key, @NonNull C value) { + if (blockState instanceof TileState tileState) { + set((PersistentDataHolder) tileState, key, value); + tileState.update(); + } + } + + @CheckReturnValue + public static @NonNull Optional get(@NonNull BlockState blockState, @NonNull DataKey key) { + if (blockState instanceof TileState tileState) { + return get((PersistentDataHolder) tileState, key); + } + return Optional.empty(); + } + + @CheckReturnValue + public static @NonNull PersistentDataContainer container(@NonNull PersistentDataHolder holder) { + return holder.getPersistentDataContainer(); + } + + @CheckReturnValue + public static @Nullable PersistentDataContainer container(@NonNull ItemStack item) { + ItemMeta meta = item.getItemMeta(); + return meta != null ? meta.getPersistentDataContainer() : null; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcChangeListener.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcChangeListener.java new file mode 100644 index 0000000..5137424 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcChangeListener.java @@ -0,0 +1,10 @@ +package dev.oum.oumlib.pdc; + +import org.bukkit.NamespacedKey; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +@FunctionalInterface +public interface PdcChangeListener { + void onChange(@NonNull Object target, @NonNull NamespacedKey key, @Nullable Object oldValue, @Nullable Object newValue); +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcFlags.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcFlags.java new file mode 100644 index 0000000..b9ebd3d --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcFlags.java @@ -0,0 +1,119 @@ +package dev.oum.oumlib.pdc; + +import dev.oum.oumlib.OumLib; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataHolder; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public final class PdcFlags { + + private static final Map FLAG_INDICES = new ConcurrentHashMap<>(); + private static final Map INDEX_FLAGS = new ConcurrentHashMap<>(); + private static int nextIndex = 0; + + private final PersistentDataContainer container; + private final NamespacedKey key; + + public PdcFlags(@NonNull PersistentDataHolder holder) { + this(holder.getPersistentDataContainer(), new NamespacedKey(OumLib.plugin(), "pdc_flags")); + } + + public PdcFlags(@NonNull PersistentDataHolder holder, @NonNull NamespacedKey key) { + this(holder.getPersistentDataContainer(), key); + } + + public PdcFlags(@NonNull PersistentDataContainer container, @NonNull NamespacedKey key) { + this.container = Objects.requireNonNull(container); + this.key = Objects.requireNonNull(key); + } + + private static synchronized int indexFor(@NonNull String flag) { + return FLAG_INDICES.computeIfAbsent(flag.toLowerCase(Locale.ROOT), f -> { + if (nextIndex >= 64) { + return Math.abs(f.hashCode()) % 64; + } + int idx = nextIndex++; + INDEX_FLAGS.put(idx, f); + return idx; + }); + } + + private long bitmask() { + Long val = container.get(key, PersistentDataType.LONG); + return val != null ? val : 0L; + } + + private void save(long mask) { + if (mask == 0L) { + container.remove(key); + } else { + container.set(key, PersistentDataType.LONG, mask); + } + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcFlags add(String @NonNull ... flags) { + long mask = bitmask(); + for (String flag : flags) { + int idx = indexFor(flag); + mask |= (1L << idx); + } + save(mask); + return this; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcFlags remove(String @NonNull ... flags) { + long mask = bitmask(); + for (String flag : flags) { + int idx = indexFor(flag); + mask &= ~(1L << idx); + } + save(mask); + return this; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcFlags toggle(@NonNull String flag) { + long mask = bitmask(); + int idx = indexFor(flag); + mask ^= (1L << idx); + save(mask); + return this; + } + + @CheckReturnValue + public boolean has(@NonNull String flag) { + long mask = bitmask(); + int idx = indexFor(flag); + return (mask & (1L << idx)) != 0L; + } + + @CheckReturnValue + public @NonNull Set all() { + long mask = bitmask(); + Set set = new HashSet<>(); + for (int i = 0; i < 64; i++) { + if ((mask & (1L << i)) != 0L) { + String name = INDEX_FLAGS.get(i); + if (name != null) { + set.add(name); + } + } + } + return set; + } + + @Contract(value = "-> this", mutates = "this") + public @NonNull PdcFlags clear() { + container.remove(key); + return this; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcHolder.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcHolder.java new file mode 100644 index 0000000..ea8784c --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcHolder.java @@ -0,0 +1,439 @@ +package dev.oum.oumlib.pdc; + +import com.google.gson.Gson; +import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.inventory.ItemSerializer; +import dev.oum.oumlib.text.Text; +import net.kyori.adventure.text.Component; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataHolder; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public final class PdcHolder { + + private static final Gson GSON = new Gson(); + + private final PersistentDataHolder holder; + private final PersistentDataContainer pdc; + private final String prefix; + + public PdcHolder(@NonNull PersistentDataHolder holder) { + this(holder, null); + } + + public PdcHolder(@NonNull PersistentDataHolder holder, @Nullable String prefix) { + this.holder = holder; + this.pdc = holder.getPersistentDataContainer(); + this.prefix = prefix; + } + + @Contract(value = "_ -> new", pure = true) + @CheckReturnValue + public @NonNull PdcHolder namespaced(@NonNull String subNamespace) { + return new PdcHolder(holder, prefix == null ? subNamespace : prefix + "_" + subNamespace); + } + + @CheckReturnValue + public @NonNull NamespacedKey nsk(@NonNull String key) { + String finalKey = prefix == null ? key : prefix + "_" + key; + return new NamespacedKey(OumLib.plugin(), finalKey); + } + + @CheckReturnValue + public @NonNull PersistentDataHolder holder() { + return holder; + } + + @CheckReturnValue + public @NonNull PersistentDataContainer container() { + return pdc; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder set(@NonNull DataKey key, @Nullable C value) { + C oldValue = pdc.get(key.key(), key.type()); + if (value == null) { + pdc.remove(key.key()); + } else { + pdc.set(key.key(), key.type(), value); + } + PDC.triggerListeners(holder, key.key(), oldValue, value); + return this; + } + + @CheckReturnValue + public @NonNull Optional get(@NonNull DataKey key) { + return Optional.ofNullable(pdc.get(key.key(), key.type())); + } + + @CheckReturnValue + public @NonNull C getOrDefault(@NonNull DataKey key, @NonNull C def) { + C val = pdc.get(key.key(), key.type()); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder set(@NonNull String key, @Nullable String value) { + return set(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder set(@NonNull NamespacedKey key, @Nullable String value) { + String oldValue = pdc.get(key, PersistentDataType.STRING); + if (value == null) { + pdc.remove(key); + } else { + pdc.set(key, PersistentDataType.STRING, value); + } + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable String get(@NonNull String key) { + return get(nsk(key)); + } + + @CheckReturnValue + public @Nullable String get(@NonNull NamespacedKey key) { + return pdc.get(key, PersistentDataType.STRING); + } + + @CheckReturnValue + public @NonNull String getOrDefault(@NonNull String key, @NonNull String def) { + return getOrDefault(nsk(key), def); + } + + @CheckReturnValue + public @NonNull String getOrDefault(@NonNull NamespacedKey key, @NonNull String def) { + String val = get(key); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setInt(@NonNull String key, int value) { + return setInt(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setInt(@NonNull NamespacedKey key, int value) { + Integer oldValue = pdc.get(key, PersistentDataType.INTEGER); + pdc.set(key, PersistentDataType.INTEGER, value); + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable Integer getInt(@NonNull String key) { + return getInt(nsk(key)); + } + + @CheckReturnValue + public @Nullable Integer getInt(@NonNull NamespacedKey key) { + return pdc.get(key, PersistentDataType.INTEGER); + } + + @CheckReturnValue + public int getIntOrDefault(@NonNull String key, int def) { + return getIntOrDefault(nsk(key), def); + } + + @CheckReturnValue + public int getIntOrDefault(@NonNull NamespacedKey key, int def) { + Integer val = getInt(key); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setDouble(@NonNull String key, double value) { + return setDouble(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setDouble(@NonNull NamespacedKey key, double value) { + Double oldValue = pdc.get(key, PersistentDataType.DOUBLE); + pdc.set(key, PersistentDataType.DOUBLE, value); + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable Double getDouble(@NonNull String key) { + return getDouble(nsk(key)); + } + + @CheckReturnValue + public @Nullable Double getDouble(@NonNull NamespacedKey key) { + return pdc.get(key, PersistentDataType.DOUBLE); + } + + @CheckReturnValue + public double getDoubleOrDefault(@NonNull String key, double def) { + return getDoubleOrDefault(nsk(key), def); + } + + @CheckReturnValue + public double getDoubleOrDefault(@NonNull NamespacedKey key, double def) { + Double val = getDouble(key); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setBoolean(@NonNull String key, boolean value) { + return setBoolean(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setBoolean(@NonNull NamespacedKey key, boolean value) { + Byte b = pdc.get(key, PersistentDataType.BYTE); + Boolean oldValue = b != null ? b != 0 : null; + pdc.set(key, PersistentDataType.BYTE, (byte) (value ? 1 : 0)); + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public boolean getBoolean(@NonNull String key) { + return getBoolean(nsk(key)); + } + + @CheckReturnValue + public boolean getBoolean(@NonNull NamespacedKey key) { + Byte b = pdc.get(key, PersistentDataType.BYTE); + return b != null && b != 0; + } + + @CheckReturnValue + public boolean getBooleanOrDefault(@NonNull String key, boolean def) { + return getBooleanOrDefault(nsk(key), def); + } + + @CheckReturnValue + public boolean getBooleanOrDefault(@NonNull NamespacedKey key, boolean def) { + Byte b = pdc.get(key, PersistentDataType.BYTE); + return b != null ? b != 0 : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setLong(@NonNull String key, long value) { + return setLong(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setLong(@NonNull NamespacedKey key, long value) { + Long oldValue = pdc.get(key, PersistentDataType.LONG); + pdc.set(key, PersistentDataType.LONG, value); + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable Long getLong(@NonNull String key) { + return getLong(nsk(key)); + } + + @CheckReturnValue + public @Nullable Long getLong(@NonNull NamespacedKey key) { + return pdc.get(key, PersistentDataType.LONG); + } + + @CheckReturnValue + public long getLongOrDefault(@NonNull String key, long def) { + return getLongOrDefault(nsk(key), def); + } + + @CheckReturnValue + public long getLongOrDefault(@NonNull NamespacedKey key, long def) { + Long val = getLong(key); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setComponent(@NonNull String key, @Nullable Component value) { + return setComponent(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + 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, Text.serialize(value)); + } + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable Component getComponent(@NonNull String key) { + return getComponent(nsk(key)); + } + + @CheckReturnValue + public @Nullable Component getComponent(@NonNull NamespacedKey key) { + String val = pdc.get(key, PersistentDataType.STRING); + return val != null ? Text.parse(val) : null; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setList(@NonNull String key, @Nullable List value) { + return setList(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setList(@NonNull NamespacedKey key, @Nullable List value) { + List oldValue = getList(key); + if (value == null) { + pdc.remove(key); + } else { + pdc.set(key, PersistentDataType.STRING, GSON.toJson(value)); + } + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable List getList(@NonNull String key) { + return getList(nsk(key)); + } + + @CheckReturnValue + public @Nullable List getList(@NonNull NamespacedKey key) { + String raw = pdc.get(key, PersistentDataType.STRING); + if (raw == null) return null; + if (raw.isEmpty()) return List.of(); + return Arrays.asList(GSON.fromJson(raw, String[].class)); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setObject(@NonNull String key, @Nullable T value) { + return setObject(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setObject(@NonNull NamespacedKey key, @Nullable T value) { + Object oldValue = pdc.get(key, PersistentDataType.STRING); + if (value == null) { + pdc.remove(key); + } else { + pdc.set(key, PersistentDataType.STRING, GSON.toJson(value)); + } + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable T getObject(@NonNull String key, @NonNull Class type) { + return getObject(nsk(key), type); + } + + @CheckReturnValue + public @Nullable T getObject(@NonNull NamespacedKey key, @NonNull Class type) { + String raw = pdc.get(key, PersistentDataType.STRING); + if (raw == null) return null; + return GSON.fromJson(raw, type); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setItem(@NonNull String key, @Nullable ItemStack value) { + return setItem(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setItem(@NonNull NamespacedKey key, @Nullable ItemStack value) { + String oldValue = pdc.get(key, PersistentDataType.STRING); + if (value == null) { + pdc.remove(key); + } else { + pdc.set(key, PersistentDataType.STRING, ItemSerializer.serialize(value)); + } + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable ItemStack getItem(@NonNull String key) { + return getItem(nsk(key)); + } + + @CheckReturnValue + public @Nullable ItemStack getItem(@NonNull NamespacedKey key) { + String base64 = pdc.get(key, PersistentDataType.STRING); + if (base64 == null) return null; + return ItemSerializer.deserialize(base64); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setItemArray(@NonNull String key, ItemStack @Nullable [] value) { + return setItemArray(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcHolder setItemArray(@NonNull NamespacedKey key, ItemStack @Nullable [] value) { + String oldValue = pdc.get(key, PersistentDataType.STRING); + if (value == null) { + pdc.remove(key); + } else { + pdc.set(key, PersistentDataType.STRING, ItemSerializer.serializeArray(value)); + } + PDC.triggerListeners(holder, key, oldValue, value); + return this; + } + + @CheckReturnValue + public ItemStack @Nullable [] getItemArray(@NonNull String key) { + return getItemArray(nsk(key)); + } + + @CheckReturnValue + public ItemStack @Nullable [] getItemArray(@NonNull NamespacedKey key) { + String base64 = pdc.get(key, PersistentDataType.STRING); + if (base64 == null) return null; + return ItemSerializer.deserializeArray(base64); + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcHolder remove(@NonNull String key) { + return remove(nsk(key)); + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcHolder remove(@NonNull NamespacedKey key) { + pdc.remove(key); + PDC.triggerListeners(holder, key, null, null); + return this; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcHolder remove(@NonNull DataKey key) { + pdc.remove(key.key()); + PDC.triggerListeners(holder, key.key(), null, null); + return this; + } + + @CheckReturnValue + public boolean has(@NonNull String key) { + return has(nsk(key)); + } + + @CheckReturnValue + public boolean has(@NonNull NamespacedKey key) { + return pdc.has(key); + } + + @CheckReturnValue + public boolean has(@NonNull DataKey key) { + return pdc.has(key.key(), key.type()); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcItem.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcItem.java new file mode 100644 index 0000000..06bcdf9 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcItem.java @@ -0,0 +1,484 @@ +package dev.oum.oumlib.pdc; + +import com.google.gson.Gson; +import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.inventory.ItemSerializer; +import dev.oum.oumlib.text.Text; +import net.kyori.adventure.text.Component; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +public final class PdcItem { + + private static final Gson GSON = new Gson(); + + private final ItemStack item; + private final String prefix; + + public PdcItem(@NonNull ItemStack item) { + this(item, null); + } + + public PdcItem(@NonNull ItemStack item, @Nullable String prefix) { + this.item = item; + this.prefix = prefix; + } + + @Contract(value = "_ -> new", pure = true) + @CheckReturnValue + public @NonNull PdcItem namespaced(@NonNull String subNamespace) { + return new PdcItem(item, prefix == null ? subNamespace : prefix + "_" + subNamespace); + } + + @CheckReturnValue + public @NonNull NamespacedKey nsk(@NonNull String key) { + String finalKey = prefix == null ? key : prefix + "_" + key; + return new NamespacedKey(OumLib.plugin(), finalKey); + } + + @CheckReturnValue + public @NonNull ItemStack item() { + return item; + } + + private boolean updateMeta(Consumer consumer) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + consumer.accept(meta); + return item.setItemMeta(meta); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem set(@NonNull DataKey key, @Nullable C value) { + C oldValue = get(key).orElse(null); + updateMeta(meta -> { + if (value == null) { + meta.getPersistentDataContainer().remove(key.key()); + } else { + meta.getPersistentDataContainer().set(key.key(), key.type(), value); + } + }); + PDC.triggerListeners(item, key.key(), oldValue, value); + return this; + } + + @CheckReturnValue + public @NonNull Optional get(@NonNull DataKey key) { + if (!item.hasItemMeta()) return Optional.empty(); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return Optional.empty(); + return Optional.ofNullable(meta.getPersistentDataContainer().get(key.key(), key.type())); + } + + @CheckReturnValue + public @NonNull C getOrDefault(@NonNull DataKey key, @NonNull C def) { + return get(key).orElse(def); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem set(@NonNull String key, @Nullable String value) { + return set(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem set(@NonNull NamespacedKey key, @Nullable String value) { + String oldValue = get(key); + updateMeta(meta -> { + if (value == null) { + meta.getPersistentDataContainer().remove(key); + } else { + meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, value); + } + }); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable String get(@NonNull String key) { + return get(nsk(key)); + } + + @CheckReturnValue + public @Nullable String get(@NonNull NamespacedKey key) { + if (!item.hasItemMeta()) return null; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + return meta.getPersistentDataContainer().get(key, PersistentDataType.STRING); + } + + @CheckReturnValue + public @NonNull String getOrDefault(@NonNull String key, @NonNull String def) { + return getOrDefault(nsk(key), def); + } + + @CheckReturnValue + public @NonNull String getOrDefault(@NonNull NamespacedKey key, @NonNull String def) { + String val = get(key); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setInt(@NonNull String key, int value) { + return setInt(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setInt(@NonNull NamespacedKey key, int value) { + Integer oldValue = getInt(key); + updateMeta(meta -> meta.getPersistentDataContainer().set(key, PersistentDataType.INTEGER, value)); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable Integer getInt(@NonNull String key) { + return getInt(nsk(key)); + } + + @CheckReturnValue + public @Nullable Integer getInt(@NonNull NamespacedKey key) { + if (!item.hasItemMeta()) return null; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + return meta.getPersistentDataContainer().get(key, PersistentDataType.INTEGER); + } + + @CheckReturnValue + public int getIntOrDefault(@NonNull String key, int def) { + return getIntOrDefault(nsk(key), def); + } + + @CheckReturnValue + public int getIntOrDefault(@NonNull NamespacedKey key, int def) { + Integer val = getInt(key); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setDouble(@NonNull String key, double value) { + return setDouble(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setDouble(@NonNull NamespacedKey key, double value) { + Double oldValue = getDouble(key); + updateMeta(meta -> meta.getPersistentDataContainer().set(key, PersistentDataType.DOUBLE, value)); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable Double getDouble(@NonNull String key) { + return getDouble(nsk(key)); + } + + @CheckReturnValue + public @Nullable Double getDouble(@NonNull NamespacedKey key) { + if (!item.hasItemMeta()) return null; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + return meta.getPersistentDataContainer().get(key, PersistentDataType.DOUBLE); + } + + @CheckReturnValue + public double getDoubleOrDefault(@NonNull String key, double def) { + return getDoubleOrDefault(nsk(key), def); + } + + @CheckReturnValue + public double getDoubleOrDefault(@NonNull NamespacedKey key, double def) { + Double val = getDouble(key); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setBoolean(@NonNull String key, boolean value) { + return setBoolean(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setBoolean(@NonNull NamespacedKey key, boolean value) { + Boolean oldValue = getBoolean(key); + updateMeta(meta -> meta.getPersistentDataContainer().set(key, PersistentDataType.BYTE, (byte) (value ? 1 : 0))); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public boolean getBoolean(@NonNull String key) { + return getBoolean(nsk(key)); + } + + @CheckReturnValue + public boolean getBoolean(@NonNull NamespacedKey key) { + if (!item.hasItemMeta()) return false; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + Byte b = meta.getPersistentDataContainer().get(key, PersistentDataType.BYTE); + return b != null && b != 0; + } + + @CheckReturnValue + public boolean getBooleanOrDefault(@NonNull String key, boolean def) { + return getBooleanOrDefault(nsk(key), def); + } + + @CheckReturnValue + public boolean getBooleanOrDefault(@NonNull NamespacedKey key, boolean def) { + if (!item.hasItemMeta()) return def; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return def; + Byte b = meta.getPersistentDataContainer().get(key, PersistentDataType.BYTE); + return b != null ? b != 0 : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setLong(@NonNull String key, long value) { + return setLong(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setLong(@NonNull NamespacedKey key, long value) { + Long oldValue = getLong(key); + updateMeta(meta -> meta.getPersistentDataContainer().set(key, PersistentDataType.LONG, value)); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable Long getLong(@NonNull String key) { + return getLong(nsk(key)); + } + + @CheckReturnValue + public @Nullable Long getLong(@NonNull NamespacedKey key) { + if (!item.hasItemMeta()) return null; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + return meta.getPersistentDataContainer().get(key, PersistentDataType.LONG); + } + + @CheckReturnValue + public long getLongOrDefault(@NonNull String key, long def) { + return getLongOrDefault(nsk(key), def); + } + + @CheckReturnValue + public long getLongOrDefault(@NonNull NamespacedKey key, long def) { + Long val = getLong(key); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setList(@NonNull String key, @Nullable List value) { + return setList(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setList(@NonNull NamespacedKey key, @Nullable List value) { + List oldValue = getList(key); + updateMeta(meta -> { + if (value == null) { + meta.getPersistentDataContainer().remove(key); + } else { + meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, GSON.toJson(value)); + } + }); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable List getList(@NonNull String key) { + return getList(nsk(key)); + } + + @CheckReturnValue + public @Nullable List getList(@NonNull NamespacedKey key) { + if (!item.hasItemMeta()) return null; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return null; + String raw = meta.getPersistentDataContainer().get(key, PersistentDataType.STRING); + if (raw == null) return null; + if (raw.isEmpty()) return List.of(); + return Arrays.asList(GSON.fromJson(raw, String[].class)); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setComponent(@NonNull String key, @Nullable Component value) { + return setComponent(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + 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, Text.serialize(value)); + } + }); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable Component getComponent(@NonNull String key) { + return getComponent(nsk(key)); + } + + @CheckReturnValue + 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 ? Text.parse(val) : null; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setObject(@NonNull String key, @Nullable T value) { + return setObject(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setObject(@NonNull NamespacedKey key, @Nullable T value) { + Object oldValue = get(key); + updateMeta(meta -> { + if (value == null) { + meta.getPersistentDataContainer().remove(key); + } else { + meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, GSON.toJson(value)); + } + }); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable T getObject(@NonNull String key, @NonNull Class type) { + return getObject(nsk(key), type); + } + + @CheckReturnValue + public @Nullable T getObject(@NonNull NamespacedKey key, @NonNull Class type) { + String raw = get(key); + if (raw == null) return null; + return GSON.fromJson(raw, type); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setItem(@NonNull String key, @Nullable ItemStack value) { + return setItem(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setItem(@NonNull NamespacedKey key, @Nullable ItemStack value) { + Object oldValue = get(key); + updateMeta(meta -> { + if (value == null) { + meta.getPersistentDataContainer().remove(key); + } else { + meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, ItemSerializer.serialize(value)); + } + }); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public @Nullable ItemStack getItem(@NonNull String key) { + return getItem(nsk(key)); + } + + @CheckReturnValue + public @Nullable ItemStack getItem(@NonNull NamespacedKey key) { + String base64 = get(key); + if (base64 == null) return null; + return ItemSerializer.deserialize(base64); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setItemArray(@NonNull String key, ItemStack @Nullable [] value) { + return setItemArray(nsk(key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcItem setItemArray(@NonNull NamespacedKey key, ItemStack @Nullable [] value) { + Object oldValue = get(key); + updateMeta(meta -> { + if (value == null) { + meta.getPersistentDataContainer().remove(key); + } else { + meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, ItemSerializer.serializeArray(value)); + } + }); + PDC.triggerListeners(item, key, oldValue, value); + return this; + } + + @CheckReturnValue + public ItemStack @Nullable [] getItemArray(@NonNull String key) { + return getItemArray(nsk(key)); + } + + @CheckReturnValue + public ItemStack @Nullable [] getItemArray(@NonNull NamespacedKey key) { + String base64 = get(key); + if (base64 == null) return null; + return ItemSerializer.deserializeArray(base64); + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcItem remove(@NonNull String key) { + return remove(nsk(key)); + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcItem remove(@NonNull NamespacedKey key) { + updateMeta(meta -> meta.getPersistentDataContainer().remove(key)); + PDC.triggerListeners(item, key, null, null); + return this; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcItem remove(@NonNull DataKey key) { + updateMeta(meta -> meta.getPersistentDataContainer().remove(key.key())); + PDC.triggerListeners(item, key.key(), null, null); + return this; + } + + @CheckReturnValue + public boolean has(@NonNull String key) { + return has(nsk(key)); + } + + @CheckReturnValue + public boolean has(@NonNull NamespacedKey key) { + if (!item.hasItemMeta()) return false; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + return meta.getPersistentDataContainer().has(key); + } + + @CheckReturnValue + public boolean has(@NonNull DataKey key) { + if (!item.hasItemMeta()) return false; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + return meta.getPersistentDataContainer().has(key.key(), key.type()); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcModel.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcModel.java new file mode 100644 index 0000000..404de68 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcModel.java @@ -0,0 +1,219 @@ +package dev.oum.oumlib.pdc; + +import com.google.gson.Gson; +import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.inventory.ItemSerializer; +import dev.oum.oumlib.math.Locations; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.CheckReturnValue; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Constructor; +import java.lang.reflect.RecordComponent; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.*; + +public final class PdcModel { + + private static final Gson GSON = new Gson(); + + private PdcModel() { + } + + public static void write(@NonNull PersistentDataContainer container, @NonNull T recordInstance) { + Objects.requireNonNull(container); + Objects.requireNonNull(recordInstance); + Class recordClass = recordInstance.getClass(); + RecordComponent[] components = recordClass.getRecordComponents(); + if (components == null) return; + + for (RecordComponent comp : components) { + String name = toKebab(comp.getName()); + NamespacedKey key = new NamespacedKey(OumLib.plugin(), name); + try { + Object value = comp.getAccessor().invoke(recordInstance); + if (value == null) { + container.remove(key); + } else { + writeField(container, key, comp.getType(), value); + } + } catch (Exception e) { + OumLib.logError("Failed to write record field " + comp.getName() + " to PDC", e); + } + } + } + + @CheckReturnValue + public static @NonNull Optional read(@NonNull PersistentDataContainer container, @NonNull Class recordClass) { + Objects.requireNonNull(container); + Objects.requireNonNull(recordClass); + RecordComponent[] components = recordClass.getRecordComponents(); + if (components == null) return Optional.empty(); + + Class[] paramTypes = new Class[components.length]; + Object[] paramValues = new Object[components.length]; + boolean hasAny = false; + + for (int i = 0; i < components.length; i++) { + RecordComponent comp = components[i]; + paramTypes[i] = comp.getType(); + String name = toKebab(comp.getName()); + NamespacedKey key = new NamespacedKey(OumLib.plugin(), name); + try { + Object value = readField(container, key, comp.getType()); + paramValues[i] = value != null ? value : defaultValue(comp.getType()); + if (value != null) { + hasAny = true; + } + } catch (Exception e) { + paramValues[i] = defaultValue(comp.getType()); + } + } + + if (!hasAny) { + return Optional.empty(); + } + + try { + Constructor constructor = recordClass.getDeclaredConstructor(paramTypes); + constructor.setAccessible(true); + return Optional.of(constructor.newInstance(paramValues)); + } catch (Exception e) { + OumLib.logError("Failed to construct record " + recordClass.getName() + " from PDC", e); + return Optional.empty(); + } + } + + private static void writeField(@NonNull PersistentDataContainer container, @NonNull NamespacedKey key, + @NonNull Class type, @NonNull Object value) { + if (type == String.class) { + container.set(key, PersistentDataType.STRING, (String) value); + } else if (type == int.class || type == Integer.class) { + container.set(key, PersistentDataType.INTEGER, (Integer) value); + } else if (type == double.class || type == Double.class) { + container.set(key, PersistentDataType.DOUBLE, (Double) value); + } else if (type == float.class || type == Float.class) { + container.set(key, PersistentDataType.FLOAT, (Float) value); + } else if (type == long.class || type == Long.class) { + container.set(key, PersistentDataType.LONG, (Long) value); + } else if (type == byte.class || type == Byte.class) { + container.set(key, PersistentDataType.BYTE, (Byte) value); + } else if (type == short.class || type == Short.class) { + container.set(key, PersistentDataType.SHORT, (Short) value); + } else if (type == boolean.class || type == Boolean.class) { + container.set(key, PersistentDataType.BYTE, (byte) ((Boolean) value ? 1 : 0)); + } else if (type == UUID.class) { + UUID uuid = (UUID) value; + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + container.set(key, PersistentDataType.BYTE_ARRAY, bb.array()); + } else if (type == Instant.class) { + container.set(key, PersistentDataType.LONG, ((Instant) value).toEpochMilli()); + } else if (type == Location.class) { + container.set(key, PersistentDataType.STRING, Locations.serialize((Location) value)); + } else if (type == ItemStack.class) { + container.set(key, PersistentDataType.STRING, ItemSerializer.serialize((ItemStack) value)); + } else if (Record.class.isAssignableFrom(type)) { + PersistentDataContainer nested = container.getAdapterContext().newPersistentDataContainer(); + @SuppressWarnings("unchecked") + Class recordType = (Class) type; + writeNested(nested, (Record) value); + container.set(key, PersistentDataType.TAG_CONTAINER, nested); + } else if (List.class.isAssignableFrom(type)) { + container.set(key, PersistentDataType.STRING, GSON.toJson(value)); + } else { + container.set(key, PersistentDataType.STRING, GSON.toJson(value)); + } + } + + private static void writeNested(@NonNull PersistentDataContainer container, @NonNull Record record) { + write(container, record); + } + + private static @Nullable Object readField(@NonNull PersistentDataContainer container, @NonNull NamespacedKey key, + @NonNull Class type) { + if (!container.has(key)) return null; + + if (type == String.class) { + return container.get(key, PersistentDataType.STRING); + } else if (type == int.class || type == Integer.class) { + return container.get(key, PersistentDataType.INTEGER); + } else if (type == double.class || type == Double.class) { + return container.get(key, PersistentDataType.DOUBLE); + } else if (type == float.class || type == Float.class) { + return container.get(key, PersistentDataType.FLOAT); + } else if (type == long.class || type == Long.class) { + return container.get(key, PersistentDataType.LONG); + } else if (type == byte.class || type == Byte.class) { + return container.get(key, PersistentDataType.BYTE); + } else if (type == short.class || type == Short.class) { + return container.get(key, PersistentDataType.SHORT); + } else if (type == boolean.class || type == Boolean.class) { + Byte b = container.get(key, PersistentDataType.BYTE); + return b != null && b != 0; + } else if (type == UUID.class) { + byte[] bytes = container.get(key, PersistentDataType.BYTE_ARRAY); + if (bytes == null || bytes.length < 16) return null; + ByteBuffer bb = ByteBuffer.wrap(bytes); + return new UUID(bb.getLong(), bb.getLong()); + } else if (type == Instant.class) { + Long epoch = container.get(key, PersistentDataType.LONG); + return epoch != null ? Instant.ofEpochMilli(epoch) : null; + } else if (type == Location.class) { + String raw = container.get(key, PersistentDataType.STRING); + return raw != null ? Locations.deserialize(raw) : null; + } else if (type == ItemStack.class) { + String raw = container.get(key, PersistentDataType.STRING); + return raw != null ? ItemSerializer.deserialize(raw) : null; + } else if (Record.class.isAssignableFrom(type)) { + PersistentDataContainer nested = container.get(key, PersistentDataType.TAG_CONTAINER); + if (nested == null) return null; + @SuppressWarnings("unchecked") + Class recordType = (Class) type; + return read(nested, recordType).orElse(null); + } else if (List.class.isAssignableFrom(type)) { + String raw = container.get(key, PersistentDataType.STRING); + if (raw == null) return null; + return Arrays.asList(GSON.fromJson(raw, String[].class)); + } else { + String raw = container.get(key, PersistentDataType.STRING); + if (raw == null) return null; + return GSON.fromJson(raw, type); + } + } + + private static @Nullable Object defaultValue(@NonNull Class type) { + if (type == int.class) return 0; + if (type == double.class) return 0.0; + if (type == float.class) return 0.0f; + if (type == long.class) return 0L; + if (type == byte.class) return (byte) 0; + if (type == short.class) return (short) 0; + if (type == boolean.class) return false; + if (type == List.class) return List.of(); + return null; + } + + private static @NonNull String toKebab(@NonNull String str) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (Character.isUpperCase(c)) { + if (i > 0) sb.append('-'); + sb.append(Character.toLowerCase(c)); + } else if (c == '_') { + sb.append('-'); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcProperty.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcProperty.java new file mode 100644 index 0000000..86d831f --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcProperty.java @@ -0,0 +1,81 @@ +package dev.oum.oumlib.pdc; + +import org.bukkit.persistence.PersistentDataHolder; +import org.jetbrains.annotations.CheckReturnValue; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; + +public final class PdcProperty { + + private final PersistentDataHolder holder; + private final DataKey key; + private final C defaultValue; + private final List> observers = new CopyOnWriteArrayList<>(); + + public PdcProperty(@NonNull PersistentDataHolder holder, @NonNull DataKey key, @Nullable C defaultValue) { + this.holder = Objects.requireNonNull(holder); + this.key = Objects.requireNonNull(key); + this.defaultValue = defaultValue; + } + + @CheckReturnValue + public @Nullable C get() { + C value = holder.getPersistentDataContainer().get(key.key(), key.type()); + return value != null ? value : defaultValue; + } + + @CheckReturnValue + public @NonNull Optional getOptional() { + return Optional.ofNullable(get()); + } + + public void set(@Nullable C newValue) { + C oldValue = get(); + if (newValue == null) { + holder.getPersistentDataContainer().remove(key.key()); + } else { + holder.getPersistentDataContainer().set(key.key(), key.type(), newValue); + } + PDC.triggerListeners(holder, key.key(), oldValue, newValue); + for (BiConsumer observer : observers) { + try { + observer.accept(oldValue, newValue); + } catch (Exception ignored) { + } + } + } + + public void update(@NonNull UnaryOperator updater) { + set(updater.apply(get())); + } + + public @NonNull PdcProperty observe(@NonNull BiConsumer observer) { + observers.add(Objects.requireNonNull(observer)); + return this; + } + + public @NonNull PdcProperty bindTo(@NonNull Consumer consumer) { + Objects.requireNonNull(consumer); + consumer.accept(get()); + observers.add((oldVal, newVal) -> consumer.accept(newVal)); + return this; + } + + @CheckReturnValue + public @NonNull PersistentDataHolder holder() { + return holder; + } + + @CheckReturnValue + public @NonNull DataKey key() { + return key; + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcTree.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcTree.java new file mode 100644 index 0000000..6f32ea4 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/PdcTree.java @@ -0,0 +1,183 @@ +package dev.oum.oumlib.pdc; + +import dev.oum.oumlib.OumLib; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataHolder; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; +import java.util.Optional; + +public final class PdcTree { + + private final PersistentDataContainer current; + private final PdcTree parent; + private final NamespacedKey keyInParent; + + public PdcTree(@NonNull PersistentDataContainer container) { + this(container, null, null); + } + + private PdcTree(@NonNull PersistentDataContainer container, @Nullable PdcTree parent, @Nullable NamespacedKey keyInParent) { + this.current = Objects.requireNonNull(container); + this.parent = parent; + this.keyInParent = keyInParent; + } + + @Contract("_ -> new") + @CheckReturnValue + public static @NonNull PdcTree of(@NonNull PersistentDataHolder holder) { + return new PdcTree(holder.getPersistentDataContainer()); + } + + @Contract("_ -> new") + @CheckReturnValue + public static @NonNull PdcTree of(@NonNull PersistentDataContainer container) { + return new PdcTree(container); + } + + @Contract("_ -> new") + @CheckReturnValue + public @NonNull PdcTree branch(@NonNull String name) { + return branch(new NamespacedKey(OumLib.plugin(), name)); + } + + @Contract("_ -> new") + @CheckReturnValue + public @NonNull PdcTree branch(@NonNull NamespacedKey key) { + PersistentDataContainer child = current.get(key, PersistentDataType.TAG_CONTAINER); + if (child == null) { + child = current.getAdapterContext().newPersistentDataContainer(); + current.set(key, PersistentDataType.TAG_CONTAINER, child); + } + return new PdcTree(child, this, key); + } + + @CheckReturnValue + public @NonNull PdcTree parent() { + if (parent != null && keyInParent != null) { + parent.current.set(keyInParent, PersistentDataType.TAG_CONTAINER, current); + return parent; + } + return this; + } + + @CheckReturnValue + public @NonNull PdcTree root() { + PdcTree node = this; + while (node.parent != null) { + node = node.parent(); + } + return node; + } + + @CheckReturnValue + public @NonNull PersistentDataContainer container() { + return current; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcTree set(@NonNull DataKey key, @NonNull C value) { + current.set(key.key(), key.type(), value); + saveToParent(); + return this; + } + + @CheckReturnValue + public @NonNull Optional get(@NonNull DataKey key) { + return Optional.ofNullable(current.get(key.key(), key.type())); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcTree set(@NonNull String key, @NonNull String value) { + return set(new NamespacedKey(OumLib.plugin(), key), value); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcTree set(@NonNull NamespacedKey key, @NonNull String value) { + current.set(key, PersistentDataType.STRING, value); + saveToParent(); + return this; + } + + @CheckReturnValue + public @Nullable String get(@NonNull String key) { + return current.get(new NamespacedKey(OumLib.plugin(), key), PersistentDataType.STRING); + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcTree setInt(@NonNull String key, int value) { + current.set(new NamespacedKey(OumLib.plugin(), key), PersistentDataType.INTEGER, value); + saveToParent(); + return this; + } + + @CheckReturnValue + public int getIntOrDefault(@NonNull String key, int def) { + Integer val = current.get(new NamespacedKey(OumLib.plugin(), key), PersistentDataType.INTEGER); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcTree setDouble(@NonNull String key, double value) { + current.set(new NamespacedKey(OumLib.plugin(), key), PersistentDataType.DOUBLE, value); + saveToParent(); + return this; + } + + @CheckReturnValue + public double getDoubleOrDefault(@NonNull String key, double def) { + Double val = current.get(new NamespacedKey(OumLib.plugin(), key), PersistentDataType.DOUBLE); + return val != null ? val : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcTree setBoolean(@NonNull String key, boolean value) { + current.set(new NamespacedKey(OumLib.plugin(), key), PersistentDataType.BYTE, (byte) (value ? 1 : 0)); + saveToParent(); + return this; + } + + @CheckReturnValue + public boolean getBooleanOrDefault(@NonNull String key, boolean def) { + Byte val = current.get(new NamespacedKey(OumLib.plugin(), key), PersistentDataType.BYTE); + return val != null ? val != 0 : def; + } + + @Contract(value = "_, _ -> this", mutates = "this") + public @NonNull PdcTree setLong(@NonNull String key, long value) { + current.set(new NamespacedKey(OumLib.plugin(), key), PersistentDataType.LONG, value); + saveToParent(); + return this; + } + + @CheckReturnValue + public long getLongOrDefault(@NonNull String key, long def) { + Long val = current.get(new NamespacedKey(OumLib.plugin(), key), PersistentDataType.LONG); + return val != null ? val : def; + } + + @Contract(value = "_ -> this", mutates = "this") + public @NonNull PdcTree remove(@NonNull String key) { + current.remove(new NamespacedKey(OumLib.plugin(), key)); + saveToParent(); + return this; + } + + @CheckReturnValue + public boolean has(@NonNull String key) { + return current.has(new NamespacedKey(OumLib.plugin(), key)); + } + + private void saveToParent() { + if (parent != null && keyInParent != null) { + parent.current.set(keyInParent, PersistentDataType.TAG_CONTAINER, current); + parent.saveToParent(); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/pdc/metadata/VolatileData.java b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/metadata/VolatileData.java new file mode 100644 index 0000000..ff35349 --- /dev/null +++ b/oumlib-core/src/main/java/dev/oum/oumlib/pdc/metadata/VolatileData.java @@ -0,0 +1,170 @@ +package dev.oum.oumlib.pdc.metadata; + +import dev.oum.oumlib.OumLib; +import dev.oum.oumlib.pdc.DataKey; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.jetbrains.annotations.CheckReturnValue; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public final class VolatileData implements Listener { + + private static final Map>> STORE = new ConcurrentHashMap<>(); + private static boolean registered = false; + + private VolatileData() { + } + + public static void initialize() { + if (registered) return; + registered = true; + if (OumLib.isPaper()) { + Bukkit.getPluginManager().registerEvents(new VolatileData(), OumLib.plugin()); + } + } + + public static void set(@NonNull Object target, @NonNull DataKey key, @NonNull C value) { + set(target, key, value, null); + } + + public static void set(@NonNull Object target, @NonNull DataKey key, @NonNull C value, @Nullable Duration ttl) { + set(target, key.key(), value, ttl); + } + + public static void set(@NonNull Object target, @NonNull String key, @NonNull Object value) { + set(target, resolveKey(key), value, null); + } + + public static void set(@NonNull Object target, @NonNull String key, @NonNull Object value, @Nullable Duration ttl) { + set(target, resolveKey(key), value, ttl); + } + + public static void set(@NonNull Object target, @NonNull NamespacedKey key, @NonNull Object value, @Nullable Duration ttl) { + Objects.requireNonNull(target); + Objects.requireNonNull(key); + Objects.requireNonNull(value); + initialize(); + + Instant expiresAt = ttl != null ? Instant.now().plus(ttl) : null; + STORE.computeIfAbsent(target, t -> new ConcurrentHashMap<>()) + .put(key, new Entry<>(value, expiresAt)); + } + + @CheckReturnValue + @SuppressWarnings("unchecked") + public static @NonNull Optional get(@NonNull Object target, @NonNull DataKey key) { + return get(target, key.key()); + } + + @CheckReturnValue + public static @NonNull Optional get(@NonNull Object target, @NonNull String key) { + return get(target, resolveKey(key)); + } + + @CheckReturnValue + @SuppressWarnings("unchecked") + public static @NonNull Optional get(@NonNull Object target, @NonNull NamespacedKey key) { + Objects.requireNonNull(target); + Objects.requireNonNull(key); + Map> targetMap = STORE.get(target); + if (targetMap == null) return Optional.empty(); + + Entry entry = targetMap.get(key); + if (entry == null) return Optional.empty(); + + if (entry.isExpired()) { + targetMap.remove(key); + return Optional.empty(); + } + + return Optional.of((T) entry.value()); + } + + @CheckReturnValue + public static @NonNull C getOrDefault(@NonNull Object target, @NonNull DataKey key, @NonNull C defaultValue) { + return get(target, key).orElse(defaultValue); + } + + @CheckReturnValue + public static @NonNull T getOrDefault(@NonNull Object target, @NonNull String key, @NonNull T defaultValue) { + return get(target, resolveKey(key)).map(val -> (T) val).orElse(defaultValue); + } + + @CheckReturnValue + public static boolean has(@NonNull Object target, @NonNull DataKey key) { + return get(target, key).isPresent(); + } + + @CheckReturnValue + public static boolean has(@NonNull Object target, @NonNull String key) { + return get(target, key).isPresent(); + } + + public static void remove(@NonNull Object target, @NonNull DataKey key) { + remove(target, key.key()); + } + + public static void remove(@NonNull Object target, @NonNull String key) { + remove(target, resolveKey(key)); + } + + public static void remove(@NonNull Object target, @NonNull NamespacedKey key) { + Objects.requireNonNull(target); + Objects.requireNonNull(key); + Map> targetMap = STORE.get(target); + if (targetMap != null) { + targetMap.remove(key); + } + } + + public static void clear(@NonNull Object target) { + Objects.requireNonNull(target); + STORE.remove(target); + } + + public static void clearAll() { + STORE.clear(); + } + + private static @NonNull NamespacedKey resolveKey(@NonNull String key) { + if (key.contains(":")) { + return NamespacedKey.fromString(key); + } + return new NamespacedKey(OumLib.plugin(), key); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerQuit(@NonNull PlayerQuitEvent event) { + clear(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityDeath(@NonNull EntityDeathEvent event) { + clear(event.getEntity()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onChunkUnload(@NonNull ChunkUnloadEvent event) { + clear(event.getChunk()); + } + + private record Entry(T value, Instant expiresAt) { + boolean isExpired() { + return expiresAt != null && Instant.now().isAfter(expiresAt); + } + } +} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Proxy.java b/oumlib-core/src/main/java/dev/oum/oumlib/proxy/Proxy.java similarity index 93% rename from oumlib-core/src/main/java/dev/oum/oumlib/util/Proxy.java rename to oumlib-core/src/main/java/dev/oum/oumlib/proxy/Proxy.java index a7456ee..9f10ffb 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Proxy.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/proxy/Proxy.java @@ -1,4 +1,4 @@ -package dev.oum.oumlib.util; +package dev.oum.oumlib.proxy; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; @@ -10,9 +10,10 @@ import dev.oum.oumlib.OumLib; import dev.oum.oumlib.event.Events; import dev.oum.oumlib.scheduler.Promise; -import net.kyori.adventure.text.minimessage.MiniMessage; +import dev.oum.oumlib.text.Text; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.title.Title; +import org.jetbrains.annotations.CheckReturnValue; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -61,12 +62,14 @@ public static void registerFallbackRouter(@NonNull Object plugin, @NonNull List< }); } + @CheckReturnValue public static int getPlayerCount(@NonNull String serverName) { return OumLib.proxy().getServer(serverName) .map(server -> server.getPlayersConnected().size()) .orElse(0); } + @CheckReturnValue public static int getPlayerCount(@NonNull List serverNames) { int count = 0; for (String name : serverNames) { @@ -85,16 +88,10 @@ public static boolean sendPluginMessage(@NonNull Player player, @NonNull String .orElse(false); } - /** - * Registers a new backend server dynamically on the proxy. - */ public static void registerServer(@NonNull String name, @NonNull String ipAddress, int port) { OumLib.proxy().registerServer(new ServerInfo(name, new InetSocketAddress(ipAddress, port))); } - /** - * Unregisters a backend server dynamically by name. - */ public static boolean unregisterServer(@NonNull String name) { return OumLib.proxy().getServer(name) .map(server -> { @@ -104,9 +101,7 @@ public static boolean unregisterServer(@NonNull String name) { .orElse(false); } - /** - * Pings a server asynchronously to determine if it is online. - */ + @CheckReturnValue public static @NonNull Promise isOnline(@NonNull String serverName) { return Promise.fromCompletableFuture( OumLib.proxy().getServer(serverName) @@ -117,25 +112,19 @@ public static boolean unregisterServer(@NonNull String name) { ); } - /** - * Sends a MiniMessage-formatted message to all players on a specific backend server. - */ public static void broadcastTo(@NonNull String serverName, @NonNull String miniMessage, @NonNull TagResolver... resolvers) { OumLib.proxy().getServer(serverName).ifPresent(server -> { - var component = MiniMessage.miniMessage().deserialize(miniMessage, resolvers); + var component = Text.parse(miniMessage, resolvers); for (Player player : server.getPlayersConnected()) { player.sendMessage(component); } }); } - /** - * Sends a MiniMessage-formatted title to all players on a specific backend server. - */ public static void sendTitleTo(@NonNull String serverName, @NonNull String title, @NonNull String subtitle, @NonNull TagResolver... resolvers) { OumLib.proxy().getServer(serverName).ifPresent(server -> { - var titleComp = MiniMessage.miniMessage().deserialize(title, resolvers); - var subtitleComp = MiniMessage.miniMessage().deserialize(subtitle, resolvers); + var titleComp = Text.parse(title, resolvers); + var subtitleComp = Text.parse(subtitle, resolvers); var titleObject = Title.title(titleComp, subtitleComp); for (Player player : server.getPlayersConnected()) { player.showTitle(titleObject); @@ -143,9 +132,7 @@ public static void sendTitleTo(@NonNull String serverName, @NonNull String title }); } - /** - * Finds the least populated online server from the provided list. - */ + @CheckReturnValue public static @NonNull Promise> getBestServer(@NonNull List serverNames) { var futures = serverNames.stream() .map(name -> OumLib.proxy().getServer(name)) @@ -180,6 +167,7 @@ public static void registerGroup(@NonNull String groupName, @NonNull List getGroupServers(@NonNull String groupName) { List names = SERVER_GROUPS.get(groupName.toLowerCase(Locale.ROOT)); if (names == null) return List.of(); @@ -262,6 +250,7 @@ public static void broadcastPluginMessage(@NonNull String channelName, @NonNull broadcastPluginMessage(channelName, out.toByteArray()); } + @CheckReturnValue public static @NonNull Promise ping(@NonNull String serverName) { return Promise.fromCompletableFuture( OumLib.proxy().getServer(serverName) @@ -278,18 +267,22 @@ public static void broadcastPluginMessage(@NonNull String channelName, @NonNull ); } + @CheckReturnValue public static @NonNull Optional getPlayer(@NonNull String name) { return OumLib.proxy().getPlayer(name); } + @CheckReturnValue public static @NonNull Optional getPlayer(@NonNull UUID uuid) { return OumLib.proxy().getPlayer(uuid); } + @CheckReturnValue public static @NonNull Collection getPlayers() { return OumLib.proxy().getAllPlayers(); } + @CheckReturnValue public static @NonNull Collection getPlayersOn(@NonNull String serverName) { return OumLib.proxy().getServer(serverName) .map(RegisteredServer::getPlayersConnected) diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/BukkitScheduler.java b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/BukkitScheduler.java deleted file mode 100644 index a6ab6aa..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/BukkitScheduler.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.oum.oumlib.scheduler; - -import dev.oum.oumlib.scheduler.platform.BukkitSchedulerAdapter; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.entity.Entity; -import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.NonNull; - -public final class BukkitScheduler { - - private BukkitScheduler() { - } - - @Contract("_, _ -> new") - public static @NonNull TaskHandle runAt(Location location, Runnable task) { - return BukkitSchedulerAdapter.get().runAt(location, task); - } - - @Contract("_, _ -> new") - public static @NonNull TaskHandle runFor(Entity entity, Runnable task) { - return BukkitSchedulerAdapter.get().runFor(entity, task); - } - - public static void assertMainThread() { - if (!isMainThread()) throw new IllegalStateException("Must be called on the main thread."); - } - - public static void assertAsync() { - if (isMainThread()) throw new IllegalStateException("Must not be called on the main thread."); - } - - public static boolean isMainThread() { - return Bukkit.isPrimaryThread(); - } -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Countdown.java b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Countdown.java similarity index 95% rename from oumlib-core/src/main/java/dev/oum/oumlib/util/Countdown.java rename to oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Countdown.java index ff87360..fc89c49 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Countdown.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/scheduler/Countdown.java @@ -1,11 +1,10 @@ -package dev.oum.oumlib.util; +package dev.oum.oumlib.scheduler; import dev.oum.oumlib.effect.Sounds; -import dev.oum.oumlib.scheduler.Scheduler; -import dev.oum.oumlib.scheduler.TaskHandle; +import dev.oum.oumlib.text.Format; +import dev.oum.oumlib.text.Text; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.title.Title; import org.bukkit.entity.Player; import org.jetbrains.annotations.Contract; @@ -50,6 +49,10 @@ private Countdown(@NonNull Builder builder) { return new Builder(); } + public static @NonNull Builder builder(@NonNull Audience audience, int seconds) { + return new Builder().audience(audience).seconds(seconds); + } + public @NonNull TaskHandle start() { if (task != null) { return task; @@ -66,7 +69,7 @@ private Countdown(@NonNull Builder builder) { if (displayFilter.test(secondsRemaining)) { String formatted = formatFunction.apply(secondsRemaining); - Component message = MiniMessage.miniMessage().deserialize(formatted); + Component message = Text.parse(formatted); if (displayMode == Display.TITLE) { audience.showTitle(Title.title(message, Component.empty())); 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 f90e205..cad86fa 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 @@ -92,10 +92,18 @@ private Promise(CompletableFuture future) { return new Promise<>(future.thenApply(mapper)); } + public @NonNull Promise thenApply(@NonNull Function mapper) { + return map(mapper); + } + public @NonNull Promise flatMap(@NonNull Function> mapper) { return new Promise<>(future.thenCompose(value -> mapper.apply(value).toCompletableFuture())); } + public @NonNull Promise thenCompose(@NonNull Function> mapper) { + return flatMap(mapper); + } + public @NonNull Promise exceptionally(@NonNull Function recover) { return new Promise<>(future.exceptionally(recover)); } @@ -236,4 +244,8 @@ private Promise(CompletableFuture future) { public @NonNull CompletableFuture toCompletableFuture() { return future; } + + public T join() { + return future.join(); + } } 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 def548a..bb30a63 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 @@ -123,21 +123,41 @@ public static void runVirtual(Runnable task) { return adapter().runFor(entity, task); } + @Contract("_, _, _ -> new") + public static @NonNull TaskHandle runLaterFor(Object entity, Duration delay, Runnable task) { + return adapter().runLaterFor(entity, delay, task, null); + } + @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) { + return adapter().runLaterFor(entity, ticks, task, null); + } + @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) { + return adapter().runRepeatingFor(entity, initialDelay, period, task, null); + } + @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) { + return adapter().runRepeatingFor(entity, initialTicks, periodTicks, task, null); + } + @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); 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 d514d99..143f43d 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 @@ -81,7 +81,9 @@ public static BukkitSchedulerAdapter get() { @Override public @NonNull TaskHandle runRepeating(long initialTicks, long periodTicks, Runnable task) { if (FOLIA) { - var scheduled = Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, t -> task.run(), initialTicks, periodTicks); + long delay = Math.max(1L, initialTicks); + long period = Math.max(1L, periodTicks); + var scheduled = Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, t -> task.run(), delay, period); return new TaskHandle(scheduled::cancel, scheduled::isCancelled); } var scheduled = scheduler.runTaskTimer(plugin, task, initialTicks, periodTicks); @@ -140,7 +142,9 @@ public static BukkitSchedulerAdapter get() { 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); + long delay = Math.max(1L, initialTicks); + long period = Math.max(1L, periodTicks); + var scheduled = Bukkit.getRegionScheduler().runAtFixedRate(plugin, loc, t -> task.run(), delay, period); return new TaskHandle(scheduled::cancel, scheduled::isCancelled); } } @@ -164,7 +168,8 @@ public static BukkitSchedulerAdapter get() { public @NonNull TaskHandle runLaterFor(Object entity, long ticks, Runnable task, Runnable retired) { if (entity instanceof Entity ent) { if (FOLIA) { - var scheduled = ent.getScheduler().runDelayed(plugin, t -> task.run(), retired, ticks); + long delay = Math.max(1L, ticks); + var scheduled = ent.getScheduler().runDelayed(plugin, t -> task.run(), retired, delay); if (scheduled != null) { return new TaskHandle(scheduled::cancel, scheduled::isCancelled); } @@ -203,7 +208,9 @@ public static BukkitSchedulerAdapter get() { 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); + long delay = Math.max(1L, initialTicks); + long period = Math.max(1L, periodTicks); + var scheduled = ent.getScheduler().runAtFixedRate(plugin, t -> task.run(), retired, delay, period); if (scheduled != null) { return new TaskHandle(scheduled::cancel, scheduled::isCancelled); } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Format.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/Format.java similarity index 99% rename from oumlib-core/src/main/java/dev/oum/oumlib/util/Format.java rename to oumlib-core/src/main/java/dev/oum/oumlib/text/Format.java index 97238fa..6aa2503 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Format.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/Format.java @@ -1,4 +1,4 @@ -package dev.oum.oumlib.util; +package dev.oum.oumlib.text; import dev.oum.oumlib.math.FormatMath; import org.jspecify.annotations.NonNull; diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java index 8a93226..5c9f0e5 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/Localization.java @@ -5,14 +5,15 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; - import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import java.io.File; import java.io.InputStream; import java.nio.file.Files; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -35,14 +36,24 @@ public static void load(@NonNull String defaultLanguageCode) { langFolder.mkdirs(); } - String defaultFileName = "lang/" + defaultLang + ".yml"; - File defaultFile = new File(dataFolder, defaultFileName); - if (!defaultFile.exists()) { - try (InputStream in = Localization.class.getClassLoader().getResourceAsStream(defaultFileName)) { - if (in != null) { - Files.copy(in, defaultFile.toPath()); + List bundledLangs = List.of("en", "es", "ko", "de", "fr", "ja", "it"); + for (String lang : bundledLangs) { + String filePath = "lang/" + lang + ".yml"; + File targetFile = new File(dataFolder, filePath); + if (!targetFile.exists()) { + InputStream rawIn = null; + if (OumLib.isPaper() && OumLib.plugin() != null) { + rawIn = OumLib.plugin().getResource(filePath); + } + if (rawIn == null) { + rawIn = Localization.class.getClassLoader().getResourceAsStream(filePath); + } + if (rawIn != null) { + try (InputStream in = rawIn) { + Files.copy(in, targetFile.toPath()); + } catch (Exception ignored) { + } } - } catch (Exception ignored) { } } @@ -70,6 +81,11 @@ private static void flatten(@NonNull String prefix, @NonNull Map Object value = entry.getValue(); if (value instanceof Map subMap) { flatten(key, (Map) subMap, target); + } else if (value instanceof List list) { + target.put(key, String.valueOf(value)); + for (int i = 0; i < list.size(); i++) { + target.put(key + "." + i, String.valueOf(list.get(i))); + } } else if (value != null) { target.put(key, String.valueOf(value)); } @@ -88,33 +104,50 @@ private static void flatten(@NonNull String prefix, @NonNull Map return MiniMessage.miniMessage().deserialize(message, resolvers); } - @SuppressWarnings("unchecked") - public static @NonNull Component translateFor(@NonNull Object playerObj, @NonNull String key, TagResolver... resolvers) { - String locale = defaultLang; - try { - Class bukkitPlayerClass = Class.forName("org.bukkit.entity.Player"); - if (bukkitPlayerClass.isInstance(playerObj)) { - Locale loc = (Locale) playerObj.getClass().getMethod("locale").invoke(playerObj); - if (loc != null) { - locale = loc.getLanguage(); - } - } else { - Class velocityPlayerClass = Class.forName("com.velocitypowered.api.proxy.Player"); - if (velocityPlayerClass.isInstance(playerObj)) { - Object profile = playerObj.getClass().getMethod("getPlayerProfile").invoke(playerObj); - if (profile != null) { - Optional optLocale = (Optional) profile.getClass().getMethod("getLocale").invoke(profile); - if (optLocale != null && optLocale.isPresent()) { - locale = optLocale.get().getLanguage(); - } - } - } - } - } catch (Exception ignored) { - } + public static @NonNull Component translateFor(@Nullable Object playerObj, @NonNull String key, TagResolver... resolvers) { + String locale = resolveLocale(playerObj); return translateFor(locale, key, resolvers); } + public static @NonNull List translateList(@NonNull String key, TagResolver... resolvers) { + return translateListFor(defaultLang, key, resolvers); + } + + public static @NonNull List translateListFor(@Nullable Object playerObj, @NonNull String key, TagResolver... resolvers) { + String locale = resolveLocale(playerObj); + List result = new ArrayList<>(); + int index = 0; + while (true) { + String raw = getRaw(locale, key + "." + index); + if (raw == null) break; + result.add(MiniMessage.miniMessage().deserialize(raw, resolvers)); + index++; + } + return result; + } + + public static @Nullable String getRaw(@NonNull String key) { + return getRaw(defaultLang, key); + } + + public static @Nullable String getRaw(@Nullable Object playerObj, @NonNull String key) { + if (playerObj instanceof String langStr) { + return getRaw(langStr, key); + } + String locale = resolveLocale(playerObj); + return getRaw(locale, key); + } + + public static @NonNull String getRawOrDefault(@Nullable Object playerObj, @NonNull String key, @NonNull String fallback) { + String val = getRaw(playerObj, key); + return val != null ? val : fallback; + } + + public static @NonNull String getRawOrDefault(@NonNull String key, @NonNull String fallback) { + String val = getRaw(defaultLang, key); + return val != null ? val : fallback; + } + public static @Nullable String getRaw(@NonNull String lang, @NonNull String key) { String langCode = lang.toLowerCase(); Map map = translations.get(langCode); @@ -122,9 +155,17 @@ private static void flatten(@NonNull String prefix, @NonNull Map return map.get(key); } Map defaultMap = translations.get(defaultLang); - if (defaultMap != null) { + if (defaultMap != null && defaultMap.containsKey(key)) { return defaultMap.get(key); } + Map enMap = translations.get("en"); + if (enMap != null && enMap.containsKey(key)) { + return enMap.get(key); + } return null; } + + private static @NonNull String resolveLocale(@Nullable Object playerObj) { + return defaultLang; + } } diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/Pagination.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/Pagination.java index 8dfb15a..358d8f1 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/Pagination.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/Pagination.java @@ -84,6 +84,11 @@ public Builder entry(Function renderer) { return this; } + public Builder entryRenderer(Function renderer) { + this.entryRenderer = renderer; + return this; + } + public Builder footer(String footer) { this.footer = footer; return this; diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/Placeholders.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/Placeholders.java index 400a810..68a0f5b 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/Placeholders.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/Placeholders.java @@ -3,6 +3,7 @@ import dev.oum.oumlib.config.ConfigSection; import org.jspecify.annotations.NonNull; +import java.lang.reflect.RecordComponent; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -13,6 +14,19 @@ public final class Placeholders { private static final Pattern PLACEHOLDER = Pattern.compile("%(.*?)%"); private static final Map registered = new ConcurrentHashMap<>(); + private Placeholders() { + } + + @FunctionalInterface + public interface PlaceholderSupplier { + String get(Object player); + } + + @FunctionalInterface + public interface ConfigPlaceholderSupplier { + String get(Object player, T config); + } + public static void register(@NonNull String key, @NonNull PlaceholderSupplier supplier) { Objects.requireNonNull(key, "key"); Objects.requireNonNull(supplier, "supplier"); @@ -58,7 +72,7 @@ private static String replaceGeneral(@NonNull Object player, @NonNull String tex private static String replaceConfigPlaceholders(String text, T config) { String result = text; try { - for (java.lang.reflect.RecordComponent comp : config.getClass().getRecordComponents()) { + for (RecordComponent comp : config.getClass().getRecordComponents()) { Object val = comp.getAccessor().invoke(config); String valStr = val != null ? String.valueOf(val) : ""; result = result.replace("%config_" + comp.getName() + "%", valStr); @@ -75,14 +89,4 @@ private static String replaceConfigPlaceholders(String text, .replaceAll("([A-Z]+)([A-Z][a-z])", "$1-$2") .toLowerCase(); } - - @FunctionalInterface - public interface PlaceholderSupplier { - String get(@NonNull Object player); - } - - @FunctionalInterface - public interface ConfigPlaceholderSupplier { - String get(@NonNull Object player, T config); - } } \ No newline at end of file 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 d5329f4..016b241 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 @@ -15,6 +15,7 @@ import org.jetbrains.annotations.CheckReturnValue; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.lang.reflect.RecordComponent; import java.time.Duration; @@ -30,65 +31,99 @@ public final class Text { private Text() { } - public static void send(@NonNull Audience audience, String message, Object... pairs) { + @Contract("_ -> new") + @CheckReturnValue + public static @NonNull TextBuilder of(@NonNull String message) { + return new TextBuilder(message); + } + + public static void send(@NonNull Audience audience, @NonNull String message, Object... pairs) { audience.sendMessage(parse(resolve(message, audience), createResolvers(pairs))); } - public static void send(@NonNull Audience audience, String message) { + public static void send(@NonNull Audience audience, @NonNull String message) { audience.sendMessage(parse(resolve(message, audience))); } - public static void send(@NonNull Audience audience, String message, Record data) { + public static void send(@NonNull Audience audience, @NonNull String message, @NonNull Record data) { audience.sendMessage(parse(resolve(message, audience), createResolvers(data))); } - public static void sendLines(Audience audience, @NonNull List lines, Object... pairs) { + public static void sendLines(@NonNull Audience audience, @NonNull List lines, Object... pairs) { TagResolver[] resolvers = createResolvers(pairs); lines.forEach(line -> audience.sendMessage(parse(resolve(line, audience), resolvers))); } - public static @NonNull Component parse(String message) { + @Contract(pure = true) + @CheckReturnValue + public static @NonNull Component parse(@NonNull String message) { return MM.deserialize(message); } - public static @NonNull Component parse(String message, TagResolver... resolvers) { + @Contract(pure = true) + @CheckReturnValue + public static @NonNull Component parse(@NonNull String message, TagResolver... resolvers) { return MM.deserialize(message, resolvers); } - public static @NonNull String strip(String message) { + @Contract(pure = true) + @CheckReturnValue + public static @NonNull Component deserialize(@NonNull String message) { + return MM.deserialize(message); + } + + @Contract(pure = true) + @CheckReturnValue + public static @NonNull Component deserialize(@NonNull String message, TagResolver... resolvers) { + return MM.deserialize(message, resolvers); + } + + @Contract(pure = true) + @CheckReturnValue + public static @NonNull String serialize(@NonNull Component component) { + return MM.serialize(component); + } + + @Contract(pure = true) + @CheckReturnValue + public static @NonNull String strip(@NonNull String message) { return MM.stripTags(message); } - public static void actionBar(@NonNull Audience audience, String message, Object... pairs) { + public static void actionBar(@NonNull Audience audience, @NonNull String message, Object... pairs) { audience.sendActionBar(parse(resolve(message, audience), createResolvers(pairs))); } - public static void title(@NonNull Audience audience, String title, String subtitle, - Duration fadeIn, Duration stay, Duration fadeOut) { + public static void title(@NonNull Audience audience, @NonNull String title, @NonNull String subtitle, + @NonNull Duration fadeIn, @NonNull Duration stay, @NonNull Duration fadeOut) { audience.showTitle(Title.title(parse(title), parse(subtitle), Title.Times.times(fadeIn, stay, fadeOut))); } - public static void title(@NonNull Audience audience, String title, String subtitle) { + public static void title(@NonNull Audience audience, @NonNull String title, @NonNull String subtitle) { title(audience, title, subtitle, Duration.ofMillis(500), Duration.ofMillis(3000), Duration.ofMillis(500)); } - public static void broadcast(String message, Object... pairs) { + public static void broadcast(@NonNull Component component) { + OumLib.players().sendMessage(component); + } + + public static void broadcast(@NonNull String message, Object... pairs) { OumLib.players().sendMessage(parse(resolve(message, null), createResolvers(pairs))); } - public static void broadcast(String message, Record data) { + public static void broadcast(@NonNull String message, @NonNull Record data) { OumLib.players().sendMessage(parse(resolve(message, null), createResolvers(data))); } - public static void broadcastActionBar(String message, Object... pairs) { + public static void broadcastActionBar(@NonNull String message, Object... pairs) { OumLib.players().sendActionBar(parse(resolve(message, null), createResolvers(pairs))); } - public static void broadcastTitle(String title, String subtitle, Duration fadeIn, Duration stay, Duration fadeOut) { + public static void broadcastTitle(@NonNull String title, @NonNull String subtitle, @NonNull Duration fadeIn, @NonNull Duration stay, @NonNull Duration fadeOut) { OumLib.players().showTitle(Title.title(parse(title), parse(subtitle), Title.Times.times(fadeIn, stay, fadeOut))); } - public static void broadcastTitle(String title, String subtitle) { + public static void broadcastTitle(@NonNull String title, @NonNull String subtitle) { broadcastTitle(title, subtitle, Duration.ofMillis(500), Duration.ofMillis(3000), Duration.ofMillis(500)); } @@ -98,7 +133,9 @@ public static void broadcastTitle(String title, String subtitle) { return new TextBuilder(message); } - public static Component clickable(String text, ClickEvent clickEvent, String hoverText) { + @Contract(pure = true) + @CheckReturnValue + public static @NonNull Component clickable(@NonNull String text, @NonNull ClickEvent clickEvent, @Nullable String hoverText) { Component c = parse(text).clickEvent(clickEvent); if (hoverText != null) c = c.hoverEvent(HoverEvent.showText(parse(hoverText))); return c; diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/placeholder/PlaceholderRegistry.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/placeholder/PlaceholderRegistry.java index 49059e6..c1e5f0c 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/placeholder/PlaceholderRegistry.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/placeholder/PlaceholderRegistry.java @@ -32,6 +32,17 @@ public PlaceholderRegistry add(String key, Function fn) { return this; } + public

PlaceholderRegistry add(String key, Class

playerClass, Function fn) { + ensureNamespace(); + namespaces.get(currentNamespace).put(key, PlaceholderSupplier.ofPlayer(obj -> { + if (playerClass.isInstance(obj)) { + return fn.apply(playerClass.cast(obj)); + } + return ""; + })); + return this; + } + public PlaceholderRegistry add(String key, BiFunction, String> fn) { ensureNamespace(); namespaces.get(currentNamespace).put(key, PlaceholderSupplier.ofParam(fn)); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/text/placeholder/PlaceholderResolver.java b/oumlib-core/src/main/java/dev/oum/oumlib/text/placeholder/PlaceholderResolver.java index 191db4c..076238d 100644 --- a/oumlib-core/src/main/java/dev/oum/oumlib/text/placeholder/PlaceholderResolver.java +++ b/oumlib-core/src/main/java/dev/oum/oumlib/text/placeholder/PlaceholderResolver.java @@ -1,7 +1,9 @@ package dev.oum.oumlib.text.placeholder; import dev.oum.oumlib.OumLib; +import org.jetbrains.annotations.CheckReturnValue; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.util.Map; import java.util.regex.Matcher; @@ -9,13 +11,13 @@ public final class PlaceholderResolver { - // Matches and — namespace and key are alphanumeric/underscore only. private static final Pattern PATTERN = Pattern.compile("<([a-z0-9]+)_([a-z0-9_]+)(?::([a-zA-Z0-9_.-]+))?>"); private PlaceholderResolver() { } - public static @NonNull String resolveInternal(String input, Object player) { + @CheckReturnValue + public static @NonNull String resolveInternal(@NonNull String input, @Nullable Object player) { PlaceholderRegistry registry = OumLib.globalRegistry(); Matcher m = PATTERN.matcher(input); StringBuilder sb = new StringBuilder(); diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Cooldown.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/Cooldown.java deleted file mode 100644 index 007b9a9..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Cooldown.java +++ /dev/null @@ -1,57 +0,0 @@ -package dev.oum.oumlib.util; - -import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.NonNull; - -import java.time.Duration; -import java.time.Instant; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -public final class Cooldown { - - private final Map timestamps = new ConcurrentHashMap<>(); - private final Duration duration; - - public Cooldown(Duration duration) { - this.duration = duration; - } - - @Contract("_ -> new") - public static @NonNull Cooldown of(Duration duration) { - return new Cooldown(duration); - } - - public boolean isOnCooldown(UUID uuid) { - Instant last = timestamps.get(uuid); - return last != null && Instant.now().isBefore(last.plus(duration)); - } - - public Duration remaining(UUID uuid) { - Instant last = timestamps.get(uuid); - if (last == null) return Duration.ZERO; - Duration rem = Duration.between(Instant.now(), last.plus(duration)); - return rem.isNegative() ? Duration.ZERO : rem; - } - - public long remainingSeconds(UUID uuid) { - return remaining(uuid).toSeconds(); - } - - public double remainingSecondsDouble(UUID uuid) { - return remaining(uuid).toMillis() / 1000.0; - } - - public void set(UUID uuid) { - timestamps.put(uuid, Instant.now()); - } - - public void remove(UUID uuid) { - timestamps.remove(uuid); - } - - public void clear() { - timestamps.clear(); - } -} 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 deleted file mode 100644 index 9d0054d..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Pdc.java +++ /dev/null @@ -1,749 +0,0 @@ -package dev.oum.oumlib.util; - -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; -import org.bukkit.persistence.PersistentDataContainer; -import org.bukkit.persistence.PersistentDataHolder; -import org.bukkit.persistence.PersistentDataType; -import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -public final class Pdc { - - private static final Gson GSON = new Gson(); - private static final Map> listeners = new ConcurrentHashMap<>(); - - private Pdc() { - } - - public static void registerListener(@NonNull NamespacedKey key, @NonNull PdcChangeListener listener) { - listeners.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(listener); - } - - public static void unregisterListener(@NonNull NamespacedKey key, @NonNull PdcChangeListener listener) { - List list = listeners.get(key); - if (list != null) { - list.remove(listener); - } - } - - private static void triggerListeners(@NonNull Object target, @NonNull NamespacedKey key, - @Nullable Object oldValue, @Nullable Object newValue) { - List list = listeners.get(key); - if (list != null) { - for (PdcChangeListener listener : list) { - try { - listener.onChange(target, key, oldValue, newValue); - } catch (Exception ignored) { - } - } - } - } - - @Contract("_ -> new") - public static @NonNull PdcHolder of(@NonNull PersistentDataHolder holder) { - return new PdcHolder(holder); - } - - @Contract("_ -> new") - public static @NonNull PdcItem of(@NonNull ItemStack item) { - return new PdcItem(item); - } - - public interface PdcChangeListener { - void onChange(@NonNull Object target, @NonNull NamespacedKey key, - @Nullable Object oldValue, @Nullable Object newValue); - } - - public static final class PdcHolder { - private final PersistentDataHolder holder; - private final PersistentDataContainer pdc; - private final String prefix; - - private PdcHolder(@NonNull PersistentDataHolder holder) { - this(holder, null); - } - - private PdcHolder(@NonNull PersistentDataHolder holder, @Nullable String prefix) { - this.holder = holder; - this.pdc = holder.getPersistentDataContainer(); - this.prefix = prefix; - } - - public @NonNull PdcHolder namespaced(@NonNull String subNamespace) { - return new PdcHolder(holder, prefix == null ? subNamespace : prefix + "_" + subNamespace); - } - - private @NonNull NamespacedKey nsk(String key) { - String finalKey = prefix == null ? key : prefix + "_" + key; - return new NamespacedKey(OumLib.plugin(), finalKey); - } - - public @NonNull PersistentDataHolder holder() { - return holder; - } - - public @NonNull PdcHolder set(@NonNull String key, @Nullable String value) { - return set(nsk(key), value); - } - - public @NonNull PdcHolder set(@NonNull NamespacedKey key, @Nullable String value) { - String oldValue = pdc.get(key, PersistentDataType.STRING); - if (value == null) { - pdc.remove(key); - } else { - pdc.set(key, PersistentDataType.STRING, value); - } - triggerListeners(holder, key, oldValue, value); - return this; - } - - public @Nullable String get(@NonNull String key) { - return get(nsk(key)); - } - - public @Nullable String get(@NonNull NamespacedKey key) { - return pdc.get(key, PersistentDataType.STRING); - } - - public @NonNull String getOrDefault(@NonNull String key, @NonNull String def) { - return getOrDefault(nsk(key), def); - } - - public @NonNull String getOrDefault(@NonNull NamespacedKey key, @NonNull String def) { - String val = get(key); - return val != null ? val : def; - } - - public @NonNull PdcHolder setInt(@NonNull String key, int value) { - return setInt(nsk(key), value); - } - - public @NonNull PdcHolder setInt(@NonNull NamespacedKey key, int value) { - Integer oldValue = pdc.get(key, PersistentDataType.INTEGER); - pdc.set(key, PersistentDataType.INTEGER, value); - triggerListeners(holder, key, oldValue, value); - return this; - } - - public @Nullable Integer getInt(@NonNull String key) { - return getInt(nsk(key)); - } - - public @Nullable Integer getInt(@NonNull NamespacedKey key) { - return pdc.get(key, PersistentDataType.INTEGER); - } - - public int getIntOrDefault(@NonNull String key, int def) { - return getIntOrDefault(nsk(key), def); - } - - public int getIntOrDefault(@NonNull NamespacedKey key, int def) { - Integer val = getInt(key); - return val != null ? val : def; - } - - public @NonNull PdcHolder setDouble(@NonNull String key, double value) { - return setDouble(nsk(key), value); - } - - public @NonNull PdcHolder setDouble(@NonNull NamespacedKey key, double value) { - Double oldValue = pdc.get(key, PersistentDataType.DOUBLE); - pdc.set(key, PersistentDataType.DOUBLE, value); - triggerListeners(holder, key, oldValue, value); - return this; - } - - public @Nullable Double getDouble(@NonNull String key) { - return getDouble(nsk(key)); - } - - public @Nullable Double getDouble(@NonNull NamespacedKey key) { - return pdc.get(key, PersistentDataType.DOUBLE); - } - - public double getDoubleOrDefault(@NonNull String key, double def) { - return getDoubleOrDefault(nsk(key), def); - } - - public double getDoubleOrDefault(@NonNull NamespacedKey key, double def) { - Double val = getDouble(key); - return val != null ? val : def; - } - - public @NonNull PdcHolder setBoolean(@NonNull String key, boolean value) { - return setBoolean(nsk(key), value); - } - - public @NonNull PdcHolder setBoolean(@NonNull NamespacedKey key, boolean value) { - Byte b = pdc.get(key, PersistentDataType.BYTE); - Boolean oldValue = b != null ? b != 0 : null; - pdc.set(key, PersistentDataType.BYTE, (byte) (value ? 1 : 0)); - triggerListeners(holder, key, oldValue, value); - return this; - } - - public boolean getBoolean(@NonNull String key) { - return getBoolean(nsk(key)); - } - - public boolean getBoolean(@NonNull NamespacedKey key) { - Byte b = pdc.get(key, PersistentDataType.BYTE); - return b != null && b != 0; - } - - public boolean getBooleanOrDefault(@NonNull String key, boolean def) { - return getBooleanOrDefault(nsk(key), def); - } - - public boolean getBooleanOrDefault(@NonNull NamespacedKey key, boolean def) { - Byte b = pdc.get(key, PersistentDataType.BYTE); - return b != null ? b != 0 : def; - } - - public @NonNull PdcHolder setLong(@NonNull String key, long value) { - return setLong(nsk(key), value); - } - - public @NonNull PdcHolder setLong(@NonNull NamespacedKey key, long value) { - Long oldValue = pdc.get(key, PersistentDataType.LONG); - pdc.set(key, PersistentDataType.LONG, value); - triggerListeners(holder, key, oldValue, value); - return this; - } - - public @Nullable Long getLong(@NonNull String key) { - return getLong(nsk(key)); - } - - public @Nullable Long getLong(@NonNull NamespacedKey key) { - return pdc.get(key, PersistentDataType.LONG); - } - - public long getLongOrDefault(@NonNull String key, long def) { - return getLongOrDefault(nsk(key), def); - } - - public long getLongOrDefault(@NonNull NamespacedKey key, long def) { - Long val = getLong(key); - 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); - } - - public @NonNull PdcHolder setList(@NonNull NamespacedKey key, @Nullable List value) { - List oldValue = getList(key); - if (value == null) { - pdc.remove(key); - } else { - pdc.set(key, PersistentDataType.STRING, GSON.toJson(value)); - } - triggerListeners(holder, key, oldValue, value); - return this; - } - - public @Nullable List getList(@NonNull String key) { - return getList(nsk(key)); - } - - public @Nullable List getList(@NonNull NamespacedKey key) { - String raw = pdc.get(key, PersistentDataType.STRING); - if (raw == null) return null; - if (raw.isEmpty()) return List.of(); - return Arrays.asList(GSON.fromJson(raw, String[].class)); - } - - public @NonNull PdcHolder setObject(@NonNull String key, @Nullable T value) { - return setObject(nsk(key), value); - } - - public @NonNull PdcHolder setObject(@NonNull NamespacedKey key, @Nullable T value) { - Object oldValue = pdc.get(key, PersistentDataType.STRING); - if (value == null) { - pdc.remove(key); - } else { - pdc.set(key, PersistentDataType.STRING, GSON.toJson(value)); - } - triggerListeners(holder, key, oldValue, value); - return this; - } - - public @Nullable T getObject(@NonNull String key, @NonNull Class type) { - return getObject(nsk(key), type); - } - - public @Nullable T getObject(@NonNull NamespacedKey key, @NonNull Class type) { - String raw = pdc.get(key, PersistentDataType.STRING); - if (raw == null) return null; - return GSON.fromJson(raw, type); - } - - public @NonNull PdcHolder setItem(@NonNull String key, @Nullable ItemStack value) { - return setItem(nsk(key), value); - } - - public @NonNull PdcHolder setItem(@NonNull NamespacedKey key, @Nullable ItemStack value) { - String oldValue = pdc.get(key, PersistentDataType.STRING); - if (value == null) { - pdc.remove(key); - } else { - pdc.set(key, PersistentDataType.STRING, ItemSerializer.serialize(value)); - } - triggerListeners(holder, key, oldValue, value); - return this; - } - - public @Nullable ItemStack getItem(@NonNull String key) { - return getItem(nsk(key)); - } - - public @Nullable ItemStack getItem(@NonNull NamespacedKey key) { - String base64 = pdc.get(key, PersistentDataType.STRING); - if (base64 == null) return null; - return ItemSerializer.deserialize(base64); - } - - public @NonNull PdcHolder setItemArray(@NonNull String key, ItemStack @Nullable [] value) { - return setItemArray(nsk(key), value); - } - - public @NonNull PdcHolder setItemArray(@NonNull NamespacedKey key, ItemStack @Nullable [] value) { - String oldValue = pdc.get(key, PersistentDataType.STRING); - if (value == null) { - pdc.remove(key); - } else { - pdc.set(key, PersistentDataType.STRING, ItemSerializer.serializeArray(value)); - } - triggerListeners(holder, key, oldValue, value); - return this; - } - - public ItemStack @Nullable [] getItemArray(@NonNull String key) { - return getItemArray(nsk(key)); - } - - public ItemStack @Nullable [] getItemArray(@NonNull NamespacedKey key) { - String base64 = pdc.get(key, PersistentDataType.STRING); - if (base64 == null) return null; - return ItemSerializer.deserializeArray(base64); - } - - public @NonNull PdcHolder remove(@NonNull String key) { - return remove(nsk(key)); - } - - public @NonNull PdcHolder remove(@NonNull NamespacedKey key) { - pdc.remove(key); - triggerListeners(holder, key, null, null); - return this; - } - - public boolean has(@NonNull String key) { - return has(nsk(key)); - } - - public boolean has(@NonNull NamespacedKey key) { - return pdc.has(key); - } - } - - public static final class PdcItem { - private final ItemStack item; - private final String prefix; - - private PdcItem(@NonNull ItemStack item) { - this(item, null); - } - - private PdcItem(@NonNull ItemStack item, @Nullable String prefix) { - this.item = item; - this.prefix = prefix; - } - - public @NonNull PdcItem namespaced(@NonNull String subNamespace) { - return new PdcItem(item, prefix == null ? subNamespace : prefix + "_" + subNamespace); - } - - private @NonNull NamespacedKey nsk(String key) { - String finalKey = prefix == null ? key : prefix + "_" + key; - return new NamespacedKey(OumLib.plugin(), finalKey); - } - - public @NonNull ItemStack item() { - return item; - } - - private boolean updateMeta(Consumer consumer) { - ItemMeta meta = item.getItemMeta(); - if (meta == null) return false; - consumer.accept(meta); - return item.setItemMeta(meta); - } - - public @NonNull PdcItem set(@NonNull String key, @Nullable String value) { - return set(nsk(key), value); - } - - public @NonNull PdcItem set(@NonNull NamespacedKey key, @Nullable String value) { - String oldValue = get(key); - updateMeta(meta -> { - if (value == null) { - meta.getPersistentDataContainer().remove(key); - } else { - meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, value); - } - }); - triggerListeners(item, key, oldValue, value); - return this; - } - - public @Nullable String get(@NonNull String key) { - return get(nsk(key)); - } - - public @Nullable String get(@NonNull NamespacedKey key) { - if (!item.hasItemMeta()) return null; - ItemMeta meta = item.getItemMeta(); - if (meta == null) return null; - return meta.getPersistentDataContainer().get(key, PersistentDataType.STRING); - } - - public @NonNull String getOrDefault(@NonNull String key, @NonNull String def) { - return getOrDefault(nsk(key), def); - } - - public @NonNull String getOrDefault(@NonNull NamespacedKey key, @NonNull String def) { - String val = get(key); - return val != null ? val : def; - } - - public @NonNull PdcItem setInt(@NonNull String key, int value) { - return setInt(nsk(key), value); - } - - public @NonNull PdcItem setInt(@NonNull NamespacedKey key, int value) { - Integer oldValue = getInt(key); - updateMeta(meta -> meta.getPersistentDataContainer().set(key, PersistentDataType.INTEGER, value)); - triggerListeners(item, key, oldValue, value); - return this; - } - - public @Nullable Integer getInt(@NonNull String key) { - return getInt(nsk(key)); - } - - public @Nullable Integer getInt(@NonNull NamespacedKey key) { - if (!item.hasItemMeta()) return null; - ItemMeta meta = item.getItemMeta(); - if (meta == null) return null; - return meta.getPersistentDataContainer().get(key, PersistentDataType.INTEGER); - } - - public int getIntOrDefault(@NonNull String key, int def) { - return getIntOrDefault(nsk(key), def); - } - - public int getIntOrDefault(@NonNull NamespacedKey key, int def) { - Integer val = getInt(key); - return val != null ? val : def; - } - - public @NonNull PdcItem setDouble(@NonNull String key, double value) { - return setDouble(nsk(key), value); - } - - public @NonNull PdcItem setDouble(@NonNull NamespacedKey key, double value) { - Double oldValue = getDouble(key); - updateMeta(meta -> meta.getPersistentDataContainer().set(key, PersistentDataType.DOUBLE, value)); - triggerListeners(item, key, oldValue, value); - return this; - } - - public @Nullable Double getDouble(@NonNull String key) { - return getDouble(nsk(key)); - } - - public @Nullable Double getDouble(@NonNull NamespacedKey key) { - if (!item.hasItemMeta()) return null; - ItemMeta meta = item.getItemMeta(); - if (meta == null) return null; - return meta.getPersistentDataContainer().get(key, PersistentDataType.DOUBLE); - } - - public double getDoubleOrDefault(@NonNull String key, double def) { - return getDoubleOrDefault(nsk(key), def); - } - - public double getDoubleOrDefault(@NonNull NamespacedKey key, double def) { - Double val = getDouble(key); - return val != null ? val : def; - } - - public @NonNull PdcItem setBoolean(@NonNull String key, boolean value) { - return setBoolean(nsk(key), value); - } - - public @NonNull PdcItem setBoolean(@NonNull NamespacedKey key, boolean value) { - Boolean oldValue = getBoolean(key); - updateMeta(meta -> meta.getPersistentDataContainer().set(key, PersistentDataType.BYTE, (byte) (value ? 1 : 0))); - triggerListeners(item, key, oldValue, value); - return this; - } - - public boolean getBoolean(@NonNull String key) { - return getBoolean(nsk(key)); - } - - public boolean getBoolean(@NonNull NamespacedKey key) { - if (!item.hasItemMeta()) return false; - ItemMeta meta = item.getItemMeta(); - if (meta == null) return false; - Byte b = meta.getPersistentDataContainer().get(key, PersistentDataType.BYTE); - return b != null && b != 0; - } - - public boolean getBooleanOrDefault(@NonNull String key, boolean def) { - return getBooleanOrDefault(nsk(key), def); - } - - public boolean getBooleanOrDefault(@NonNull NamespacedKey key, boolean def) { - if (!item.hasItemMeta()) return def; - ItemMeta meta = item.getItemMeta(); - if (meta == null) return def; - Byte b = meta.getPersistentDataContainer().get(key, PersistentDataType.BYTE); - return b != null ? b != 0 : def; - } - - public @NonNull PdcItem setLong(@NonNull String key, long value) { - return setLong(nsk(key), value); - } - - public @NonNull PdcItem setLong(@NonNull NamespacedKey key, long value) { - Long oldValue = getLong(key); - updateMeta(meta -> meta.getPersistentDataContainer().set(key, PersistentDataType.LONG, value)); - triggerListeners(item, key, oldValue, value); - return this; - } - - public @Nullable Long getLong(@NonNull String key) { - return getLong(nsk(key)); - } - - public @Nullable Long getLong(@NonNull NamespacedKey key) { - if (!item.hasItemMeta()) return null; - ItemMeta meta = item.getItemMeta(); - if (meta == null) return null; - return meta.getPersistentDataContainer().get(key, PersistentDataType.LONG); - } - - public long getLongOrDefault(@NonNull String key, long def) { - return getLongOrDefault(nsk(key), def); - } - - public long getLongOrDefault(@NonNull NamespacedKey key, long def) { - Long val = getLong(key); - return val != null ? val : def; - } - - public @NonNull PdcItem setList(@NonNull String key, @Nullable List value) { - return setList(nsk(key), value); - } - - public @NonNull PdcItem setList(@NonNull NamespacedKey key, @Nullable List value) { - List oldValue = getList(key); - updateMeta(meta -> { - if (value == null) { - meta.getPersistentDataContainer().remove(key); - } else { - meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, GSON.toJson(value)); - } - }); - triggerListeners(item, key, oldValue, value); - return this; - } - - public @Nullable List getList(@NonNull String key) { - return getList(nsk(key)); - } - - public @Nullable List getList(@NonNull NamespacedKey key) { - if (!item.hasItemMeta()) return null; - ItemMeta meta = item.getItemMeta(); - if (meta == null) return null; - String raw = meta.getPersistentDataContainer().get(key, PersistentDataType.STRING); - if (raw == null) return null; - if (raw.isEmpty()) return List.of(); - 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); - } - - public @NonNull PdcItem setObject(@NonNull NamespacedKey key, @Nullable T value) { - Object oldValue = get(key); - updateMeta(meta -> { - if (value == null) { - meta.getPersistentDataContainer().remove(key); - } else { - meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, GSON.toJson(value)); - } - }); - triggerListeners(item, key, oldValue, value); - return this; - } - - public @Nullable T getObject(@NonNull String key, @NonNull Class type) { - return getObject(nsk(key), type); - } - - public @Nullable T getObject(@NonNull NamespacedKey key, @NonNull Class type) { - String raw = get(key); - if (raw == null) return null; - return GSON.fromJson(raw, type); - } - - public @NonNull PdcItem setItem(@NonNull String key, @Nullable ItemStack value) { - return setItem(nsk(key), value); - } - - public @NonNull PdcItem setItem(@NonNull NamespacedKey key, @Nullable ItemStack value) { - Object oldValue = get(key); - updateMeta(meta -> { - if (value == null) { - meta.getPersistentDataContainer().remove(key); - } else { - meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, ItemSerializer.serialize(value)); - } - }); - triggerListeners(item, key, oldValue, value); - return this; - } - - public @Nullable ItemStack getItem(@NonNull String key) { - return getItem(nsk(key)); - } - - public @Nullable ItemStack getItem(@NonNull NamespacedKey key) { - String base64 = get(key); - if (base64 == null) return null; - return ItemSerializer.deserialize(base64); - } - - public @NonNull PdcItem setItemArray(@NonNull String key, ItemStack @Nullable [] value) { - return setItemArray(nsk(key), value); - } - - public @NonNull PdcItem setItemArray(@NonNull NamespacedKey key, ItemStack @Nullable [] value) { - Object oldValue = get(key); - updateMeta(meta -> { - if (value == null) { - meta.getPersistentDataContainer().remove(key); - } else { - meta.getPersistentDataContainer().set(key, PersistentDataType.STRING, ItemSerializer.serializeArray(value)); - } - }); - triggerListeners(item, key, oldValue, value); - return this; - } - - public ItemStack @Nullable [] getItemArray(@NonNull String key) { - return getItemArray(nsk(key)); - } - - public ItemStack @Nullable [] getItemArray(@NonNull NamespacedKey key) { - String base64 = get(key); - if (base64 == null) return null; - return ItemSerializer.deserializeArray(base64); - } - - public @NonNull PdcItem remove(@NonNull String key) { - return remove(nsk(key)); - } - - public @NonNull PdcItem remove(@NonNull NamespacedKey key) { - updateMeta(meta -> meta.getPersistentDataContainer().remove(key)); - triggerListeners(item, key, null, null); - return this; - } - - public boolean has(@NonNull String key) { - return has(nsk(key)); - } - - public boolean has(@NonNull NamespacedKey key) { - if (!item.hasItemMeta()) return false; - ItemMeta meta = item.getItemMeta(); - if (meta == null) return false; - return meta.getPersistentDataContainer().has(key); - } - } -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/util/Players.java b/oumlib-core/src/main/java/dev/oum/oumlib/util/Players.java deleted file mode 100644 index 2379b0f..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/util/Players.java +++ /dev/null @@ -1,34 +0,0 @@ -package dev.oum.oumlib.util; - -import org.bukkit.block.Block; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.util.RayTraceResult; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -@Deprecated(since = "1.0.7", forRemoval = true) -public final class Players { - - private Players() { - } - - public static @Nullable Block getTargetBlock(@NonNull Player player, int maxDistance) { - RayTraceResult result = player.getWorld().rayTraceBlocks( - player.getEyeLocation(), - player.getLocation().getDirection(), - maxDistance - ); - return result != null ? result.getHitBlock() : null; - } - - public static @Nullable Entity getTargetEntity(@NonNull Player player, int maxDistance) { - RayTraceResult result = player.getWorld().rayTraceEntities( - player.getEyeLocation(), - player.getLocation().getDirection(), - maxDistance, - entity -> !entity.equals(player) - ); - return result != null ? result.getHitEntity() : null; - } -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/web/Webhook.java b/oumlib-core/src/main/java/dev/oum/oumlib/web/Webhook.java deleted file mode 100644 index 84d2295..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/web/Webhook.java +++ /dev/null @@ -1,346 +0,0 @@ -package dev.oum.oumlib.web; - -import dev.oum.oumlib.scheduler.Promise; -import dev.oum.oumlib.scheduler.Scheduler; -import dev.oum.oumlib.scheduler.TaskHandle; -import org.jetbrains.annotations.CheckReturnValue; -import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.Consumer; - -@SuppressWarnings("unused") -public final class Webhook { - - private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); - - private static final Map> messageBuffers = new ConcurrentHashMap<>(); - private static final Map> embedBuffers = new ConcurrentHashMap<>(); - private static final Map flushTasks = new ConcurrentHashMap<>(); - - private final String url; - private final String username; - private final String avatarUrl; - private final String content; - private final List embeds; - - @Contract(pure = true) - private Webhook(@NonNull Builder builder) { - this.url = builder.url; - this.username = builder.username; - this.avatarUrl = builder.avatarUrl; - this.content = builder.content; - this.embeds = List.copyOf(builder.embeds); - } - - @CheckReturnValue - public static @NonNull Builder url(@NonNull String url) { - return new Builder(url); - } - - public static void queueMessage(@NonNull String url, @NonNull String content) { - messageBuffers.computeIfAbsent(url, k -> new ConcurrentLinkedQueue<>()).add(content); - flushTasks.compute(url + "_message", (k, existing) -> { - if (existing != null) { - existing.cancel(); - } - return Scheduler.runLater(Duration.ofMillis(1000), () -> flushQueue(url)); - }); - } - - public static void queueEmbed(@NonNull String url, @NonNull WebhookEmbed embed) { - embedBuffers.computeIfAbsent(url, k -> new ConcurrentLinkedQueue<>()).add(embed); - flushTasks.compute(url + "_embed", (k, existing) -> { - if (existing != null) { - existing.cancel(); - } - return Scheduler.runLater(Duration.ofMillis(1000), () -> flushEmbedsQueue(url)); - }); - } - - private static void flushQueue(@NonNull String url) { - Queue queue = messageBuffers.get(url); - if (queue == null || queue.isEmpty()) return; - - StringBuilder combinedContent = new StringBuilder(); - String msg; - while ((msg = queue.poll()) != null) { - if (!combinedContent.isEmpty()) { - combinedContent.append("\n"); - } - combinedContent.append(msg); - } - - if (!combinedContent.isEmpty()) { - Webhook.url(url) - .content(combinedContent.toString()) - .sendAsync(); - } - } - - private static void flushEmbedsQueue(@NonNull String url) { - Queue queue = embedBuffers.get(url); - if (queue == null || queue.isEmpty()) return; - - List batched = new ArrayList<>(); - WebhookEmbed embed; - while ((embed = queue.poll()) != null) { - batched.add(embed); - if (batched.size() >= 10) { - sendEmbedBatch(url, batched); - batched = new ArrayList<>(); - } - } - - if (!batched.isEmpty()) { - sendEmbedBatch(url, batched); - } - } - - private static void sendEmbedBatch(String url, List embeds) { - Builder builder = Webhook.url(url); - for (WebhookEmbed em : embeds) { - builder.embed(em); - } - builder.sendAsync(); - } - - private static @NonNull String escapeJson(String value) { - if (value == null) return ""; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < value.length(); i++) { - char ch = value.charAt(i); - switch (ch) { - case '"' -> sb.append("\\\""); - case '\\' -> sb.append("\\\\"); - case '\b' -> sb.append("\\b"); - case '\f' -> sb.append("\\f"); - case '\n' -> sb.append("\\n"); - case '\r' -> sb.append("\\r"); - case '\t' -> sb.append("\\t"); - default -> { - if (ch < ' ') { - String t = "000" + Integer.toHexString(ch); - sb.append("\\u").append(t.substring(t.length() - 4)); - } else { - sb.append(ch); - } - } - } - } - return sb.toString(); - } - - private static void sendWithRetry(String url, String json, int attempt) { - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(json)) - .build(); - - HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() == 429 && attempt < 5) { - long retryAfterMs = 1000; - String retryAfterHeader = response.headers().firstValue("Retry-After").orElse(null); - if (retryAfterHeader != null) { - try { - double val = Double.parseDouble(retryAfterHeader); - retryAfterMs = (long) (val * 1000.0); - } catch (NumberFormatException ignored) { - } - } else { - retryAfterMs = (long) (1000 * Math.pow(2, attempt)); - } - Thread.sleep(retryAfterMs); - sendWithRetry(url, json, attempt + 1); - } else if ((response.statusCode() < 200 || response.statusCode() >= 300) && attempt < 5) { - Thread.sleep((long) (1000 * Math.pow(2, attempt))); - sendWithRetry(url, json, attempt + 1); - } else if (response.statusCode() < 200 || response.statusCode() >= 300) { - throw new RuntimeException("Webhook request failed after 5 retries with status: " + response.statusCode() + " - " + response.body()); - } - } catch (Exception e) { - if (attempt < 5) { - try { - Thread.sleep((long) (1000 * Math.pow(2, attempt))); - } catch (InterruptedException ignored) { - } - sendWithRetry(url, json, attempt + 1); - } else { - throw new RuntimeException("Failed to send webhook request after 5 retries", e); - } - } - } - - public @NonNull Promise sendAsync() { - return Promise.supplyAsync(() -> { - sendWithRetry(url, toJson(), 0); - return null; - }); - } - - private @NonNull String toJson() { - StringBuilder sb = new StringBuilder(); - sb.append("{"); - - boolean first = true; - if (username != null) { - sb.append("\"username\":\"").append(escapeJson(username)).append("\""); - first = false; - } - if (avatarUrl != null) { - if (!first) sb.append(","); - sb.append("\"avatar_url\":\"").append(escapeJson(avatarUrl)).append("\""); - first = false; - } - if (content != null) { - if (!first) sb.append(","); - sb.append("\"content\":\"").append(escapeJson(content)).append("\""); - first = false; - } - - if (!embeds.isEmpty()) { - if (!first) sb.append(","); - sb.append("\"embeds\":["); - for (int i = 0; i < embeds.size(); i++) { - if (i > 0) sb.append(","); - appendEmbedJson(sb, embeds.get(i)); - } - sb.append("]"); - } - - sb.append("}"); - return sb.toString(); - } - - private void appendEmbedJson(@NonNull StringBuilder sb, @NonNull WebhookEmbed embed) { - sb.append("{"); - boolean first = true; - - if (embed.title() != null) { - sb.append("\"title\":\"").append(escapeJson(embed.title())).append("\""); - first = false; - } - if (embed.description() != null) { - if (!first) sb.append(","); - sb.append("\"description\":\"").append(escapeJson(embed.description())).append("\""); - first = false; - } - if (embed.color() != null) { - if (!first) sb.append(","); - sb.append("\"color\":").append(embed.color()); - first = false; - } - - if (embed.thumbnailUrl() != null) { - if (!first) sb.append(","); - sb.append("\"thumbnail\":{\"url\":\"").append(escapeJson(embed.thumbnailUrl())).append("\"}"); - first = false; - } - - if (embed.footerText() != null) { - if (!first) sb.append(","); - sb.append("\"footer\":{"); - sb.append("\"text\":\"").append(escapeJson(embed.footerText())).append("\""); - if (embed.footerIcon() != null) { - sb.append(",\"icon_url\":\"").append(escapeJson(embed.footerIcon())).append("\""); - } - sb.append("}"); - first = false; - } - - if (embed.authorName() != null) { - if (!first) sb.append(","); - sb.append("\"author\":{"); - sb.append("\"name\":\"").append(escapeJson(embed.authorName())).append("\""); - if (embed.authorUrl() != null) { - sb.append(",\"url\":\"").append(escapeJson(embed.authorUrl())).append("\""); - } - if (embed.authorIcon() != null) { - sb.append(",\"icon_url\":\"").append(escapeJson(embed.authorIcon())).append("\""); - } - sb.append("}"); - first = false; - } - - List fields = embed.fields(); - if (!fields.isEmpty()) { - if (!first) sb.append(","); - sb.append("\"fields\":["); - for (int i = 0; i < fields.size(); i++) { - if (i > 0) sb.append(","); - WebhookEmbedField f = fields.get(i); - sb.append("{"); - sb.append("\"name\":\"").append(escapeJson(f.name())).append("\","); - sb.append("\"value\":\"").append(escapeJson(f.value())).append("\","); - sb.append("\"inline\":").append(f.inline()); - sb.append("}"); - } - sb.append("]"); - } - - sb.append("}"); - } - - public static final class Builder { - private final String url; - private final List embeds = new ArrayList<>(); - private String username; - private String avatarUrl; - private String content; - - private Builder(String url) { - this.url = url; - } - - @CheckReturnValue - public @NonNull Builder username(@Nullable String username) { - this.username = username; - return this; - } - - @CheckReturnValue - public @NonNull Builder avatarUrl(@Nullable String avatarUrl) { - this.avatarUrl = avatarUrl; - return this; - } - - @CheckReturnValue - public @NonNull Builder content(@Nullable String content) { - this.content = content; - return this; - } - - @CheckReturnValue - public @NonNull Builder embed(@NonNull WebhookEmbed embed) { - this.embeds.add(embed); - return this; - } - - @CheckReturnValue - public @NonNull Builder embed(@NonNull Consumer builderConsumer) { - WebhookEmbed.Builder builder = WebhookEmbed.builder(); - builderConsumer.accept(builder); - this.embeds.add(builder.build()); - return this; - } - - @CheckReturnValue - public @NonNull Promise sendAsync() { - return new Webhook(this).sendAsync(); - } - } -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbed.java b/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbed.java deleted file mode 100644 index da1729a..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbed.java +++ /dev/null @@ -1,148 +0,0 @@ -package dev.oum.oumlib.web; - -import org.jetbrains.annotations.CheckReturnValue; -import org.jetbrains.annotations.Contract; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; - -public final class WebhookEmbed { - - private final String title; - private final String description; - private final Integer color; - private final List fields; - private final String footerText; - private final String footerIcon; - private final String thumbnailUrl; - private final String authorName; - private final String authorUrl; - private final String authorIcon; - - @Contract(pure = true) - private WebhookEmbed(@NonNull Builder builder) { - this.title = builder.title; - this.description = builder.description; - this.color = builder.color; - this.fields = List.copyOf(builder.fields); - this.footerText = builder.footerText; - this.footerIcon = builder.footerIcon; - this.thumbnailUrl = builder.thumbnailUrl; - this.authorName = builder.authorName; - this.authorUrl = builder.authorUrl; - this.authorIcon = builder.authorIcon; - } - - @CheckReturnValue - public static @NonNull Builder builder() { - return new Builder(); - } - - public @Nullable String title() { - return title; - } - - public @Nullable String description() { - return description; - } - - public @Nullable Integer color() { - return color; - } - - public @NonNull List fields() { - return fields; - } - - public @Nullable String footerText() { - return footerText; - } - - public @Nullable String footerIcon() { - return footerIcon; - } - - public @Nullable String thumbnailUrl() { - return thumbnailUrl; - } - - public @Nullable String authorName() { - return authorName; - } - - public @Nullable String authorUrl() { - return authorUrl; - } - - public @Nullable String authorIcon() { - return authorIcon; - } - - public static final class Builder { - private final List fields = new ArrayList<>(); - private String title; - private String description; - private Integer color; - private String footerText; - private String footerIcon; - private String thumbnailUrl; - private String authorName; - private String authorUrl; - private String authorIcon; - - private Builder() { - } - - @CheckReturnValue - public @NonNull Builder title(@Nullable String title) { - this.title = title; - return this; - } - - @CheckReturnValue - public @NonNull Builder description(@Nullable String description) { - this.description = description; - return this; - } - - @CheckReturnValue - public @NonNull Builder color(@Nullable Integer color) { - this.color = color; - return this; - } - - @CheckReturnValue - public @NonNull Builder field(@NonNull String name, @NonNull String value, boolean inline) { - this.fields.add(new WebhookEmbedField(name, value, inline)); - return this; - } - - @CheckReturnValue - public @NonNull Builder footer(@Nullable String text, @Nullable String iconUrl) { - this.footerText = text; - this.footerIcon = iconUrl; - return this; - } - - @CheckReturnValue - public @NonNull Builder thumbnail(@Nullable String url) { - this.thumbnailUrl = url; - return this; - } - - @CheckReturnValue - public @NonNull Builder author(@Nullable String name, @Nullable String url, @Nullable String iconUrl) { - this.authorName = name; - this.authorUrl = url; - this.authorIcon = iconUrl; - return this; - } - - @Contract(" -> new") - public @NonNull WebhookEmbed build() { - return new WebhookEmbed(this); - } - } -} diff --git a/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbedField.java b/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbedField.java deleted file mode 100644 index 3860168..0000000 --- a/oumlib-core/src/main/java/dev/oum/oumlib/web/WebhookEmbedField.java +++ /dev/null @@ -1,4 +0,0 @@ -package dev.oum.oumlib.web; - -public record WebhookEmbedField(String name, String value, boolean inline) { -} diff --git a/pom.xml b/pom.xml index 75d4429..0643ed6 100644 --- a/pom.xml +++ b/pom.xml @@ -7,12 +7,11 @@ dev.oum oumlib - 1.0.8 + 1.0.9 pom oumlib-core - example-plugin @@ -34,6 +33,10 @@ miniplaceholders https://repo.miniplaceholders.me/releases + + codemc-releases + https://repo.codemc.io/repository/maven-releases/ +