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://jitpack.io/#sun-mc-dev/oumlib)
[](https://adoptium.net/)
[](https://github.com/PaperMC/Folia)
+[](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