diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..c9183e5
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,30 @@
+name: Build Action
+
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
+ types: [ opened, synchronize, reopened ]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ packages: write
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build and Publish zCrates
+ uses: GroupeZ-dev/actions/.github/workflows/build.yml@main
+ with:
+ project-name: "zCrates"
+ publish: true
+ project-to-publish: "api:publish"
+ discord-avatar-url: "https://groupez.dev/storage/images/337.png"
+ secrets:
+ WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}
+ MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
+ MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 7bc07ec..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Environment-dependent path to Maven home directory
-/mavenHomeManager.xml
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml
deleted file mode 100644
index 4ea72a9..0000000
--- a/.idea/copilot.data.migration.agent.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml
deleted file mode 100644
index 7ef04e2..0000000
--- a/.idea/copilot.data.migration.ask.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml
deleted file mode 100644
index 1f2ea11..0000000
--- a/.idea/copilot.data.migration.ask2agent.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml
deleted file mode 100644
index 8648f94..0000000
--- a/.idea/copilot.data.migration.edit.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/discord.xml b/.idea/discord.xml
deleted file mode 100644
index 104c42f..0000000
--- a/.idea/discord.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 8627761..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml
deleted file mode 100644
index a8bdd19..0000000
--- a/.idea/material_theme_project_new.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index f16dea7..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MARKET.md b/MARKET.md
new file mode 100644
index 0000000..9e9ab63
--- /dev/null
+++ b/MARKET.md
@@ -0,0 +1,288 @@
+# 🎁 zCrates
+
+[](https://github.com/GroupeZ-dev/zCrates)
+[](https://www.spigotmc.org/)
+[](https://www.oracle.com/java/)
+[](https://groupez.dev)
+
+A modern, JavaScript-powered Minecraft crate plugin with stunning animations and smart reward systems.
+
+## 📋 Description
+
+**zCrates** brings a new level of customization to your Minecraft server's crate system. Built with cutting-edge technology and powered by **JavaScript**, create unique opening experiences that keep your players engaged and coming back for more.
+
+## ✨ Key Features
+
+### 🎯 What Makes zCrates Special
+
+- **🎬 JavaScript-Powered** - Create custom animations and reward algorithms without touching Java code
+- **🎨 Beautiful Animations** - 4 built-in animations (instant, roulette, cascade, simple) with full customization
+- **🎲 Smart Algorithms** - Pity system, progressive luck, and weighted random distribution
+- **💎 Flexible Rewards** - Items, commands, or both - with weight-based probability
+- **🔐 Advanced Conditions** - Permission, cooldown, and PlaceholderAPI support
+- **🗄️ Database Support** - SQLite, MySQL, or MariaDB for persistent data
+- **🎮 Beautiful GUI** - Powered by zMenu for stunning inventory interfaces
+- **⚡ Hot Reload** - Update configurations without restarting your server
+
+### 🎁 Reward Types
+
+Create exciting rewards for your players:
+
+- **ITEM** - Give single items with custom enchantments and names
+- **ITEMS** - Award multiple items at once (perfect for armor sets!)
+- **COMMAND** - Execute any console command
+- **COMMANDS** - Run multiple commands together
+
+All rewards support **weight-based probability** - a reward with weight 10 in a pool of 100 total weight = 10% chance!
+
+### 🔐 Opening Conditions
+
+Control who can open your crates:
+
+- **PERMISSION** - Require specific permissions
+- **COOLDOWN** - Set time restrictions (hourly, daily, etc.)
+- **PLACEHOLDER** - Advanced PlaceholderAPI comparisons (level, balance, world, etc.)
+
+Mix multiple conditions together - all must pass for the crate to open!
+
+### 🎬 Animation System
+
+**4 Built-in Animations:**
+
+- **instant** - Quick reward reveal (500ms)
+- **roulette** - Classic spinning wheel (11100ms)
+- **cascade** - Progressive fill effect (3900ms)
+- **simple** - Basic display (600ms)
+
+Want more? **Create your own** with JavaScript - full control over timing, effects, and display!
+
+### 🎲 Smart Reward Algorithms
+
+**3 Intelligent Systems:**
+
+| Algorithm | How It Works | Best For |
+|----------------------|--------------------------------------|---------------------|
+| **weighted** | Standard probability-based selection | General crates |
+| **pity_system** | Guarantees legendary after 10 tries | Premium/paid crates |
+| **progressive_luck** | Increases rare chances over time | Event crates |
+
+**Create custom algorithms** with JavaScript to implement your own reward logic!
+
+### 🔑 Key Management
+
+Choose your key type:
+
+- **VIRTUAL** - Stored in database (no inventory clutter)
+- **PHYSIC** - Real items players can trade
+
+### 🖼️ Display Options
+
+**6 Ways to Display Your Crates:**
+
+- BLOCK - Classic block display
+- ENTITY - Animated mob display
+- MYTHIC_MOB - MythicMobs integration
+- ITEMS_ADDER - ItemsAdder custom items
+- ORAXEN - Oraxen custom items
+- NEXO - Nexo custom items
+
+## 🎬 Showcase
+
+> **Note:** GIFs are sped up for demonstration purposes - actual animations run smoother in-game!
+
+### Virtual Crate Opening
+Experience seamless crate openings with virtual keys - no inventory clutter, just pure excitement! Watch as the roulette animation reveals your reward.
+
+
+
+### Physical Crate Opening
+Prefer tangible keys? Physical keys give players tradeable items they can hold, share, or collect before opening their crates.
+
+
+
+### Reroll System
+Don't like your reward? The reroll feature lets players try their luck again for a chance at something better!
+
+
+
+### Block Display Placement
+Place crates as interactive blocks anywhere in your world - perfect for spawn areas, shops, or event locations.
+
+
+
+### Entity Display Placement
+Make your crates stand out with animated entity displays - floating, rotating, and eye-catching!
+
+
+
+### MythicMobs Display Placement
+Integrate with MythicMobs for custom creature displays - turn your crates into unique, custom mob presentations!
+
+
+
+## 📦 Requirements
+
+- **Server**: Spigot or Paper 1.21+
+- **Java**: Version 21 or higher
+- **Required**: [zMenu](https://www.spigotmc.org/resources/zmenu.109103/)
+
+**Optional Integrations:**
+- [PlaceholderAPI](https://www.spigotmc.org/resources/placeholderapi.6245/) - For PLACEHOLDER conditions
+- [MythicMobs](https://www.spigotmc.org/resources/mythicmobs.5702/) - For MYTHIC_MOB display
+- [ItemsAdder](https://www.spigotmc.org/resources/itemsadder.73355/) - For custom item displays
+- [Oraxen](https://www.spigotmc.org/resources/oraxen.72448/) - For custom item displays
+- [Nexo](https://www.spigotmc.org/resources/nexo.115448/) - For custom item displays
+
+## 🚀 Quick Start
+
+1. Download zCrates and install **zMenu** (required)
+2. Place both JARs in your `plugins/` folder
+3. Restart your server
+4. Edit configurations in `plugins/zCrates/`
+5. Create your first crate and start rewarding players!
+
+## ⚙️ Configuration Example
+
+Simple crate configuration:
+
+```yaml
+id: my_crate
+animation: roulette
+algorithm: weighted
+display-name: "Epic Crate"
+
+key:
+ type: VIRTUAL
+ name: epic_key
+
+conditions:
+ - type: PERMISSION
+ permission: "crates.epic"
+ - type: COOLDOWN
+ cooldown: 3600000 # 1 hour
+ - type: PLACEHOLDER
+ placeholder: "%player_level%"
+ comparison: GREATER_THAN_OR_EQUALS
+ result: "10"
+
+rewards:
+ - type: ITEM
+ id: diamond_sword
+ weight: 20.0
+ display-item:
+ material: DIAMOND_SWORD
+ name: "Legendary Sword"
+ item:
+ material: DIAMOND_SWORD
+ enchantments:
+ - SHARPNESS:5
+ - UNBREAKING:3
+
+ - type: COMMAND
+ id: vip_rank
+ weight: 5.0
+ display-item:
+ material: GOLD_INGOT
+ name: "VIP Rank"
+ command: "lp user %player% parent set vip"
+
+ - type: ITEMS
+ id: armor_set
+ weight: 10.0
+ display-item:
+ material: DIAMOND_CHESTPLATE
+ name: "Full Armor Set"
+ items:
+ - material: DIAMOND_HELMET
+ - material: DIAMOND_CHESTPLATE
+ - material: DIAMOND_LEGGINGS
+ - material: DIAMOND_BOOTS
+```
+
+## 🎮 Commands
+
+| Command | Description |
+|-----------------------------------------------|----------------------------|
+| `/zcrates` | Show plugin info |
+| `/zcrates reload` | Reload configurations |
+| `/zcrates place ` | Place crate at location |
+| `/zcrates remove` | Remove crate at target |
+| `/zcrates purge` | Remove all crates in chunk |
+| `/zcrates givekeys ` | Give keys to player |
+| `/zcrates open [force]` | Force crate opening |
+
+**Aliases:** `/zc`, `/crates`
+
+## 🎨 Customization
+
+### Create Custom Animations
+
+Write your own animations in JavaScript:
+
+```javascript
+animations.register("my-animation", {
+ phases: [
+ {
+ name: "spinning",
+ duration: 3000,
+ interval: 50,
+ speedCurve: "EASE_OUT",
+ onTick: (context, tickData) => {
+ // Your animation logic
+ }
+ }
+ ]
+});
+```
+
+### Create Custom Algorithms
+
+Implement your own reward logic:
+
+```javascript
+algorithms.register("my-algorithm", (context) => {
+ // Your reward selection logic
+ return context.rewards().weightedRandom();
+});
+```
+
+## 💡 Why Choose zCrates?
+
+✅ **Easy to Use** - YAML configuration with clear examples
+✅ **Highly Customizable** - JavaScript support for unlimited possibilities
+✅ **Performance** - Built with Java 21 and optimized for large servers
+✅ **Modern** - MiniMessage support for beautiful text formatting
+✅ **Reliable** - Database persistence ensures data safety
+✅ **Supported** - Active development and updates
+
+## 🤝 Support & Links
+
+- **Website**: [groupez.dev](https://groupez.dev)
+- **Author**: Traqueur_
+- **Report Issues**: [GitHub Issues](https://github.com/GroupeZ-dev/zCrates/issues)
+
+---
+
+## 🔄 What's Included
+
+✅ 4 reward types (ITEM, ITEMS, COMMAND, COMMANDS)
+✅ 3 condition types (PERMISSION, COOLDOWN, PLACEHOLDER)
+✅ JavaScript animation system with 4 built-in animations
+✅ 3 smart reward algorithms
+✅ Virtual and physical key support
+✅ 6 display type integrations
+✅ Full database persistence
+✅ Beautiful GUI with zMenu
+✅ Hot reload system
+✅ MiniMessage formatting
+✅ PlaceholderAPI integration
+
+---
+
+**Ready to elevate your server's crate experience?**
+
+Download now and start creating unforgettable moments for your players! 🎉
+
+---
+
+Developed with ❤️ by [Traqueur_](https://groupez.dev)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..eb36af4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,277 @@
+# zCrates
+
+A modern, feature-rich crate/loot box plugin for Minecraft Paper servers with JavaScript-powered animations and algorithms.
+
+## Features
+
+- **JavaScript Animations** - Create custom animations using ES6 JavaScript
+- **Custom Algorithms** - Define reward selection logic with JavaScript
+- **Multiple Key Types** - Virtual (database) and physical (item) keys
+- **Flexible Rewards** - Items, commands, or multiple of each
+- **Opening Conditions** - Permissions, cooldowns, and custom conditions
+- **Reroll System** - Allow players to reroll for a different reward
+- **Multi-Display Support** - Blocks, entities, MythicMobs, ItemsAdder, Nexo, Oraxen
+- **PlaceholderAPI** - Built-in placeholders for keys and statistics
+- **Database Support** - SQLite, MySQL, MariaDB
+
+## Requirements
+
+- **Minecraft**: 1.21+
+- **Server**: Paper/Purpur
+- **Java**: 21+
+- **Dependencies**: zMenu (bundled)
+
+## Installation
+
+1. Download `zCrates.jar`
+2. Place in `plugins/` folder
+3. Restart server
+4. Configure crates in `plugins/zCrates/crates/`
+
+## Quick Start
+
+### Creating a Crate
+
+Create `plugins/zCrates/crates/example.yml`:
+
+```yaml
+id: example
+animation: roulette
+algorithm: weighted
+display-name: "Example Crate"
+max-rerolls: 1
+
+key:
+ type: VIRTUAL
+ name: "example-key"
+
+related-menu: crate-menu-example
+
+rewards:
+ - type: ITEM
+ id: diamond-reward
+ weight: 10.0
+ display-item:
+ material: DIAMOND
+ name: "Diamonds"
+ item:
+ material: DIAMOND
+ amount: 5
+
+ - type: COMMAND
+ id: money-reward
+ weight: 20.0
+ display-item:
+ material: GOLD_INGOT
+ name: "$1000"
+ command: "eco give %player% 1000"
+```
+
+### Placing a Crate
+
+```bash
+/zcrates place example BLOCK CHEST
+```
+
+### Giving Keys
+
+```bash
+/zcrates give example 5
+```
+
+## Commands
+
+| Command | Description | Permission |
+|-------------------------------------------|--------------------------|-------------------------|
+| `/zcrates reload` | Reload configurations | `crates.command.reload` |
+| `/zcrates open [force]` | Open a crate | `crates.command.open` |
+| `/zcrates give ` | Give keys | `crates.command.give` |
+| `/zcrates place [value]` | Place a crate | `crates.command.place` |
+| `/zcrates remove` | Remove placed crate | `crates.command.remove` |
+| `/zcrates purge` | Remove all placed crates | `crates.command.purge` |
+
+## Key Types
+
+### Virtual Key
+Stored in database, no physical item.
+```yaml
+key:
+ type: VIRTUAL
+ name: "my-key"
+```
+
+### Physical Key
+Item in player inventory.
+```yaml
+key:
+ type: PHYSIC
+ name: "my-key"
+ item:
+ material: TRIPWIRE_HOOK
+ name: "Legendary Key"
+ glow: true
+```
+
+## Reward Types
+
+| Type | Description |
+|------------|-----------------------|
+| `ITEM` | Single item reward |
+| `ITEMS` | Multiple items reward |
+| `COMMAND` | Single command |
+| `COMMANDS` | Multiple commands |
+
+## Display Types
+
+| Type | Description |
+|---------------|-----------------------------------|
+| `BLOCK` | Placed block (CHEST, ENDER_CHEST) |
+| `ENTITY` | Spawned entity (ARMOR_STAND) |
+| `MYTHIC_MOB` | MythicMobs entity |
+| `ITEMS_ADDER` | ItemsAdder furniture |
+| `NEXO` | Nexo furniture |
+| `ORAXEN` | Oraxen furniture |
+
+## JavaScript API
+
+### Custom Animation
+
+Create `plugins/zCrates/animations/my-animation.js`:
+
+```javascript
+animations.register("my-animation", {
+ phases: [
+ {
+ name: "spin",
+ duration: 3000,
+ interval: 2,
+ speedCurve: "EASE_OUT",
+ onTick: function(context, tickData) {
+ context.inventory().randomizeSlots();
+ }
+ }
+ ],
+ onComplete: function(context) {
+ context.inventory().close(60);
+ }
+});
+```
+
+### Custom Algorithm
+
+Create `plugins/zCrates/algorithms/my-algorithm.js`:
+
+```javascript
+algorithms.register("my-algorithm", function(context) {
+ var rewards = context.rewards();
+ var history = context.history();
+
+ // Custom selection logic
+ return rewards.weightedRandom();
+});
+```
+
+## Opening Conditions
+
+```yaml
+conditions:
+ - type: PERMISSION
+ permission: "zcrates.open.vip"
+ - type: COOLDOWN
+ cooldown: 3600000 # 1 hour in ms
+ - type: PLACEHOLDER # Requires PlaceholderAPI hook
+ placeholder: "%player_level%"
+ comparison: GREATER_THAN_OR_EQUALS # EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS
+ result: "10"
+```
+
+**Comparison Types for PLACEHOLDER:**
+- `EQUALS` - String/numeric equality (default)
+- `NOT_EQUALS` - String/numeric inequality
+- `GREATER_THAN` - Numeric comparison (>)
+- `LESS_THAN` - Numeric comparison (<)
+- `GREATER_THAN_OR_EQUALS` - Numeric comparison (>=)
+- `LESS_THAN_OR_EQUALS` - Numeric comparison (<=)
+
+## PlaceholderAPI
+
+Numbers are formatted in compact notation (1.2K, 3.5M, etc.). Use `-raw` suffix for raw numbers.
+
+| Placeholder | Description | Example |
+|--------------------------------|---------------------------------------|-----------|
+| `%zcrates__keys%` | Key count (formatted) | `1.2K` |
+| `%zcrates__keys-raw%` | Key count (raw) | `1234` |
+| `%zcrates__opened%` | Crate openings (formatted) | `3.5M` |
+| `%zcrates__opened-raw%` | Crate openings (raw) | `3500000` |
+| `%zcrates_crates_opened%` | Total openings all crates (formatted) | `15K` |
+| `%zcrates_crates_opened_raw%` | Total openings all crates (raw) | `15000` |
+
+**Examples:**
+```
+%zcrates_legendary_keys% → 5
+%zcrates_legendary_opened% → 1.2K
+%zcrates_crates_opened% → 3.5M
+```
+
+## API Usage
+
+### Maven Dependency
+
+```xml
+
+ fr.traqueur
+ zcrates-api
+ 1.0.0
+ provided
+
+```
+
+### Accessing the API
+
+```java
+CratesPlugin plugin = (CratesPlugin) Bukkit.getPluginManager().getPlugin("zCrates");
+CratesManager cratesManager = plugin.getManager(CratesManager.class);
+
+// Open a crate
+Crate crate = Registry.get(CratesRegistry.class).getById("example");
+OpenResult result = cratesManager.tryOpenCrate(player, crate);
+
+// Give keys
+UsersManager usersManager = plugin.getManager(UsersManager.class);
+User user = usersManager.getUser(player.getUniqueId());
+user.addKeys("example-key", 5);
+```
+
+### Listening to Events
+
+```java
+@EventHandler
+public void onRewardGiven(RewardGivenEvent event) {
+ Player player = event.getPlayer();
+ Reward reward = event.getReward();
+ // Log, broadcast, etc.
+}
+```
+
+## Documentation
+
+- **[User Guide](docs/USER_GUIDE.md)** - Complete configuration guide for server administrators
+- **[Developer Guide](docs/DEVELOPER_GUIDE.md)** - Extension and integration guide
+- **[Architecture](docs/ARCHITECTURE.md)** - High-level architecture overview
+
+## Building
+
+```bash
+./gradlew build
+```
+
+Output: `target/zCrates.jar`
+
+## License
+
+Proprietary - All rights reserved.
+
+## Support
+
+- **Discord**: [https://discord.gg/PTSYTC53d3]
+- **GitHub**: [https://github.com/GroupeZ-dev/zCrates]
\ No newline at end of file
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index 8650400..1e68b52 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -1,3 +1,7 @@
+plugins {
+ id("re.alwyn974.groupez.publish") version "1.0.0"
+}
+
rootProject.extra.properties["sha"]?.let { sha ->
version = sha
}
diff --git a/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java b/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java
index 3eeb2b7..a61ef84 100644
--- a/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java
+++ b/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java
@@ -1,9 +1,25 @@
package fr.traqueur.crates.api;
+import fr.maxlego08.menu.api.InventoryManager;
import fr.traqueur.crates.api.managers.Manager;
+import org.bukkit.event.Listener;
import org.bukkit.plugin.ServicePriority;
import org.bukkit.plugin.java.JavaPlugin;
+/**
+ * Base class for zItems plugins, providing manager registration and retrieval.
+ *
+ *
This class extends {@link JavaPlugin} and offers methods to register
+ * and retrieve various manager implementations used within the zItems ecosystem.
+ * Managers are registered as services with Bukkit's {@code ServicesManager},
+ * allowing for easy access by other plugins.
+ *
+ *
Plugins extending this class can leverage the provided methods to
+ * manage effects, items, and other functionalities through their respective managers.
+ *
+ * @see Manager
+ * @see org.bukkit.plugin.ServicesManager
+ */
public abstract class CratesPlugin extends JavaPlugin {
/**
@@ -67,4 +83,19 @@ public I getManager(Class clazz) {
return rsp.getProvider();
}
+ /**
+ * Registers an event listener with the plugin's event system.
+ *
+ * @param listener the listener to register
+ */
+ public void registerListener(Listener listener) {
+ this.getServer().getPluginManager().registerEvents(listener, this);
+ }
+
+ /**
+ * Retrieves the InventoryManager associated with this plugin.
+ *
+ * @return the InventoryManager instance
+ */
+ public abstract InventoryManager getInventoryManager();
}
diff --git a/api/src/main/java/fr/traqueur/crates/api/annotations/AutoHook.java b/api/src/main/java/fr/traqueur/crates/api/annotations/AutoHook.java
new file mode 100644
index 0000000..5214702
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/annotations/AutoHook.java
@@ -0,0 +1,24 @@
+package fr.traqueur.crates.api.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used to mark an {@link fr.traqueur.crates.api.hooks.Hook} for automatic registration.
+ * When the specified plugin is present, the hook will be enabled automatically.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface AutoHook {
+
+ /**
+ * The name of the plugin that this hook integrates with.
+ * This should match the exact plugin name as it appears in the plugin.yml.
+ * The hook will only be enabled if a plugin with this name is present.
+ *
+ * @return the plugin name
+ */
+ String value();
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java
new file mode 100644
index 0000000..12d0ec0
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java
@@ -0,0 +1,34 @@
+package fr.traqueur.crates.api.events;
+
+import fr.traqueur.crates.api.models.crates.Crate;
+import org.bukkit.entity.Player;
+import org.bukkit.event.player.PlayerEvent;
+
+/**
+ * Base class for all crate-related events.
+ */
+public abstract class CrateEvent extends PlayerEvent {
+
+ /** The crate involved in this event. */
+ protected final Crate crate;
+
+ /**
+ * Constructs a CrateEvent with the specified player and crate.
+ *
+ * @param player the player involved in the event
+ * @param crate the crate involved in the event
+ */
+ public CrateEvent(Player player, Crate crate) {
+ super(player);
+ this.crate = crate;
+ }
+
+ /**
+ * Gets the crate involved in this event.
+ *
+ * @return the crate
+ */
+ public Crate getCrate() {
+ return crate;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CrateOpenEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CrateOpenEvent.java
new file mode 100644
index 0000000..032a782
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateOpenEvent.java
@@ -0,0 +1,59 @@
+package fr.traqueur.crates.api.events;
+
+import fr.traqueur.crates.api.models.animations.Animation;
+import fr.traqueur.crates.api.models.crates.Crate;
+import org.bukkit.entity.Player;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called when a player opens a crate (after key is consumed, menu is about to open).
+ * This event is not cancellable.
+ */
+public class CrateOpenEvent extends CrateEvent {
+
+ /** The handler list for this event. */
+ private static final HandlerList HANDLERS = new HandlerList();
+ /** The animation that will be played when the crate is opened. */
+ private final Animation animation;
+
+ /**
+ * Constructs a CrateOpenEvent with the specified player, crate, and animation.
+ *
+ * @param player the player opening the crate
+ * @param crate the crate being opened
+ * @param animation the animation that will be played
+ */
+ public CrateOpenEvent(Player player, Crate crate, Animation animation) {
+ super(player, crate);
+ this.animation = animation;
+ }
+
+ /**
+ * Gets the animation that will be played.
+ *
+ * @return the animation
+ */
+ public Animation getAnimation() {
+ return animation;
+ }
+
+ /**
+ * Gets the handler list for this event.
+ *
+ * @return the handler list
+ */
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ /**
+ * Gets the static handler list for this event.
+ *
+ * @return the handler list
+ */
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CratePreOpenEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CratePreOpenEvent.java
new file mode 100644
index 0000000..975672b
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/events/CratePreOpenEvent.java
@@ -0,0 +1,54 @@
+package fr.traqueur.crates.api.events;
+
+import fr.traqueur.crates.api.models.crates.Crate;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called before a player opens a crate.
+ * This event is cancellable - if cancelled, the crate will not be opened
+ * and the key will not be consumed.
+ */
+public class CratePreOpenEvent extends CrateEvent implements Cancellable {
+
+ /** The handler list for this event. */
+ private static final HandlerList HANDLERS = new HandlerList();
+ /** Indicates whether the event has been cancelled. */
+ private boolean cancelled = false;
+
+ /**
+ * Constructs a CratePreOpenEvent with the specified player and crate.
+ *
+ * @param player the player attempting to open the crate
+ * @param crate the crate being opened
+ */
+ public CratePreOpenEvent(Player player, Crate crate) {
+ super(player, crate);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.cancelled = cancel;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ /**
+ * Gets the static handler list for this event.
+ *
+ * @return the handler list
+ */
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CrateRerollEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CrateRerollEvent.java
new file mode 100644
index 0000000..e5aa85b
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateRerollEvent.java
@@ -0,0 +1,76 @@
+package fr.traqueur.crates.api.events;
+
+import fr.traqueur.crates.api.models.crates.Crate;
+import fr.traqueur.crates.api.models.crates.Reward;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called when a player attempts to reroll their reward.
+ * This event is cancellable - if cancelled, the reroll will not occur.
+ */
+public class CrateRerollEvent extends CrateEvent implements Cancellable {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+ private boolean cancelled = false;
+ private final Reward currentReward;
+ private final int rerollsRemaining;
+
+ /**
+ * Constructs a CrateRerollEvent with the specified player, crate, current reward, and rerolls remaining.
+ *
+ * @param player the player attempting the reroll
+ * @param crate the crate being rerolled
+ * @param currentReward the current reward that will be replaced if reroll succeeds
+ * @param rerollsRemaining the number of rerolls remaining after this one
+ */
+ public CrateRerollEvent(Player player, Crate crate, Reward currentReward, int rerollsRemaining) {
+ super(player, crate);
+ this.currentReward = currentReward;
+ this.rerollsRemaining = rerollsRemaining;
+ }
+
+ /**
+ * Gets the current reward that will be replaced if reroll succeeds.
+ *
+ * @return the current reward
+ */
+ public Reward getCurrentReward() {
+ return currentReward;
+ }
+
+ /**
+ * Gets the number of rerolls remaining after this reroll (if it succeeds).
+ *
+ * @return rerolls remaining after this one
+ */
+ public int getRerollsRemaining() {
+ return rerollsRemaining;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.cancelled = cancel;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ /**
+ * Gets the static handler list for this event.
+ *
+ * @return the handler list
+ */
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/events/RewardGeneratedEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/RewardGeneratedEvent.java
new file mode 100644
index 0000000..95e860c
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/events/RewardGeneratedEvent.java
@@ -0,0 +1,64 @@
+package fr.traqueur.crates.api.events;
+
+import fr.traqueur.crates.api.models.crates.Crate;
+import fr.traqueur.crates.api.models.crates.Reward;
+import org.bukkit.entity.Player;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called when a reward is generated for a player (after animation starts).
+ * This can be used to track reward distribution or modify logging.
+ */
+public class RewardGeneratedEvent extends CrateEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+ private final Reward reward;
+ private final boolean isReroll;
+
+ /**
+ * Constructs a new RewardGeneratedEvent.
+ *
+ * @param player the player receiving the reward
+ * @param crate the crate from which the reward is generated
+ * @param reward the reward that was generated
+ * @param isReroll whether this reward was generated as a result of a reroll
+ */
+ public RewardGeneratedEvent(Player player, Crate crate, Reward reward, boolean isReroll) {
+ super(player, crate);
+ this.reward = reward;
+ this.isReroll = isReroll;
+ }
+
+ /**
+ * Gets the reward that was generated.
+ *
+ * @return the reward
+ */
+ public Reward getReward() {
+ return reward;
+ }
+
+ /**
+ * Whether this reward was generated as a result of a reroll.
+ *
+ * @return true if this is a reroll
+ */
+ public boolean isReroll() {
+ return isReroll;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ /**
+ * Gets the handler list for this event.
+ *
+ * @return the handler list
+ */
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/events/RewardGivenEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/RewardGivenEvent.java
new file mode 100644
index 0000000..7c94b59
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/events/RewardGivenEvent.java
@@ -0,0 +1,52 @@
+package fr.traqueur.crates.api.events;
+
+import fr.traqueur.crates.api.models.crates.Crate;
+import fr.traqueur.crates.api.models.crates.Reward;
+import org.bukkit.entity.Player;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called when a reward is given to a player (when they close the inventory).
+ * This event fires after the reward has been given.
+ */
+public class RewardGivenEvent extends CrateEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+ private final Reward reward;
+
+ /**
+ * Constructs a new RewardGivenEvent.
+ *
+ * @param player the player receiving the reward
+ * @param crate the crate from which the reward is given
+ * @param reward the reward that was given
+ */
+ public RewardGivenEvent(Player player, Crate crate, Reward reward) {
+ super(player, crate);
+ this.reward = reward;
+ }
+
+ /**
+ * Gets the reward that was given to the player.
+ *
+ * @return the reward
+ */
+ public Reward getReward() {
+ return reward;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ /**
+ * Gets the handler list for this event.
+ *
+ * @return the handler list
+ */
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/hooks/Hook.java b/api/src/main/java/fr/traqueur/crates/api/hooks/Hook.java
new file mode 100644
index 0000000..8f37532
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/hooks/Hook.java
@@ -0,0 +1,13 @@
+package fr.traqueur.crates.api.hooks;
+
+/**
+ * Interface representing a hook that can be enabled.
+ */
+public interface Hook {
+
+ /**
+ * Method to be called when the hook is enabled.
+ */
+ void onEnable();
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/managers/CratesManager.java b/api/src/main/java/fr/traqueur/crates/api/managers/CratesManager.java
new file mode 100644
index 0000000..b71cddd
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/managers/CratesManager.java
@@ -0,0 +1,263 @@
+package fr.traqueur.crates.api.managers;
+
+import fr.traqueur.crates.api.models.crates.Crate;
+import fr.traqueur.crates.api.models.crates.OpenResult;
+import fr.traqueur.crates.api.models.animations.Animation;
+import fr.traqueur.crates.api.models.crates.Reward;
+import fr.traqueur.crates.api.models.placedcrates.DisplayType;
+import fr.traqueur.crates.api.models.placedcrates.PlacedCrate;
+import org.bukkit.Chunk;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Manager responsible for all crate-related operations including opening crates,
+ * managing animations, handling rerolls, and managing placed crates in the world.
+ *
+ *
This is the main entry point for interacting with the crate system programmatically.
+ * Obtain an instance via {@code plugin.getManager(CratesManager.class)}.
+ *
+ * @see OpenResult
+ * @see Crate
+ * @see PlacedCrate
+ */
+public non-sealed interface CratesManager extends Manager {
+
+ // ==================== Crate Opening ====================
+
+ /**
+ * Attempts to open a crate for a player with all validation checks.
+ *
+ *
This method performs the following checks in order:
+ *
+ *
Verifies the player has the required key
+ *
Checks all configured conditions (permissions, cooldowns, etc.)
+ *
+ *
+ * @param player the player attempting to open the crate
+ * @param crate the crate to open
+ * @return an {@link OpenResult} indicating success or the reason for failure
+ */
+ OpenResult tryOpenCrate(Player player, Crate crate);
+
+ /**
+ * Force opens a crate for a player, bypassing key checks and conditions.
+ *
+ *
Use this method for administrative purposes or when you've already
+ * validated access. This will still fire the {@link fr.traqueur.crates.api.events.CrateOpenEvent}.
+ *
+ * @param player the player to open the crate for
+ * @param crate the crate to open
+ * @param animation the animation to play
+ */
+ void openCrate(Player player, Crate crate, Animation animation);
+
+ /**
+ * Opens the preview menu for a crate, showing all possible rewards.
+ *
+ *
The preview menu allows players to see what rewards they can win
+ * without consuming a key.
+ *
+ * @param player the player to show the preview to
+ * @param crate the crate to preview
+ */
+ void openPreview(Player player, Crate crate);
+
+ /**
+ * Gets the crate a player is currently previewing, if any.
+ *
+ * @param player the player to check
+ * @return an Optional containing the crate being previewed, or empty if not previewing
+ */
+ Optional getPreviewingCrate(Player player);
+
+ /**
+ * Closes the preview for a player and cleans up associated state.
+ *
+ * @param player the player whose preview to close
+ */
+ void closePreview(Player player);
+
+ /**
+ * Starts the animation for a player who has an open crate menu.
+ *
+ *
This method is typically called by the animation button in the crate menu.
+ * It generates the reward and begins the animation phases.
+ *
+ * @param player the player whose animation to start
+ * @param inventory the inventory to animate in
+ * @param slots the slots to use for the animation
+ */
+ void startAnimation(Player player, Inventory inventory, List slots);
+
+ // ==================== Reroll System ====================
+
+ /**
+ * Checks if a player can reroll their current reward.
+ *
+ *
A player can reroll if:
+ *
+ *
They have an active crate opening
+ *
The animation has completed
+ *
They have remaining rerolls
+ *
+ *
+ * @param player the player to check
+ * @return true if the player can reroll, false otherwise
+ */
+ boolean canReroll(Player player);
+
+ /**
+ * Gets the number of rerolls remaining for a player's current crate opening.
+ *
+ * @param player the player to check
+ * @return the number of remaining rerolls, or 0 if not opening a crate
+ */
+ int getRerollsRemaining(Player player);
+
+ /**
+ * Gets the current reward for a player's active crate opening.
+ *
+ * @param player the player to check
+ * @return an Optional containing the current reward, or empty if not opening
+ */
+ Optional getCurrentReward(Player player);
+
+ /**
+ * Performs a reroll for a player, generating a new reward and restarting the animation.
+ *
+ *
This method fires {@link fr.traqueur.crates.api.events.CrateRerollEvent} which can be cancelled.
+ *
+ * @param player the player to reroll for
+ * @return true if the reroll was successful, false if cancelled or not allowed
+ */
+ boolean reroll(Player player);
+
+ /**
+ * Checks if a player's animation has completed.
+ *
+ * @param player the player to check
+ * @return true if animation is complete, false otherwise
+ */
+ boolean isAnimationCompleted(Player player);
+
+ // ==================== Crate Lifecycle ====================
+
+ /**
+ * Stops all active crate openings, cancelling animations and closing inventories.
+ *
+ *
This is typically called during plugin shutdown.
+ */
+ void stopAllOpening();
+
+ /**
+ * Closes a crate opening for a player and gives them their reward.
+ *
+ *
If the animation was completed, the current reward is given to the player
+ * and {@link fr.traqueur.crates.api.events.RewardGivenEvent} is fired.
+ *
+ * @param player the player whose crate to close
+ */
+ void closeCrate(Player player);
+
+ /**
+ * Ensures all inventory files exist for registered crates.
+ *
+ *
Creates default inventory files if they don't exist.
+ */
+ void ensureInventoriesExist();
+
+ // ==================== Placed Crates Management ====================
+
+ /**
+ * Places a crate at a location with the specified display.
+ *
+ *
The crate data is persisted in the chunk's PDC and the display
+ * entity/block is spawned immediately.
+ *
+ * @param crateId the ID of the crate to place
+ * @param location the location to place the crate at
+ * @param displayType the type of display (BLOCK, ENTITY, etc.)
+ * @param displayValue the display value (material name, entity type, etc.)
+ * @param yaw the rotation of the display
+ * @return the created PlacedCrate instance
+ * @throws IllegalArgumentException if no display factory exists for the type
+ */
+ PlacedCrate placeCrate(String crateId, Location location, DisplayType displayType, String displayValue, float yaw);
+
+ /**
+ * Removes a placed crate from the world.
+ *
+ *
This removes both the display and the persisted data.
+ *
+ * @param placedCrate the placed crate to remove
+ */
+ void removePlacedCrate(PlacedCrate placedCrate);
+
+ /**
+ * Finds a placed crate by the block at a location.
+ *
+ * @param block the block to search for
+ * @return an Optional containing the placed crate, or empty if not found
+ */
+ Optional findPlacedCrateByBlock(Block block);
+
+ /**
+ * Finds a placed crate by its display entity.
+ *
+ * @param entity the entity to search for
+ * @return an Optional containing the placed crate, or empty if not found
+ */
+ Optional findPlacedCrateByEntity(Entity entity);
+
+ /**
+ * Loads all placed crates from a chunk's PDC and spawns their displays.
+ *
+ *
Called automatically on chunk load events.
+ *
+ * @param chunk the chunk to load from
+ */
+ void loadPlacedCratesFromChunk(Chunk chunk);
+
+ /**
+ * Unloads placed crates from a chunk, removing their displays.
+ *
+ *
Called automatically on chunk unload events. Data is preserved in PDC.
+ *
+ * @param chunk the chunk to unload
+ */
+ void unloadPlacedCratesFromChunk(Chunk chunk);
+
+ /**
+ * Loads all placed crates from all loaded chunks in all worlds.
+ *
+ *
+ */
+ void unloadAllPlacedCrates();
+
+ /**
+ * Gets all placed crates in a specific world.
+ *
+ * @param world the world to search in
+ * @return a list of placed crates in the world
+ */
+ List getPlacedCratesInWorld(World world);
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java b/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java
index 7ec79e2..3c33fcf 100644
--- a/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java
+++ b/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java
@@ -3,8 +3,19 @@
import fr.traqueur.crates.api.CratesPlugin;
import org.bukkit.plugin.java.JavaPlugin;
-public interface Manager {
+/**
+ * Base interface for all manager classes in the zCrates plugin.
+ * Managers are responsible for handling specific aspects of the plugin's functionality.
+ */
+public sealed interface Manager permits CratesManager, UsersManager {
+ /** Initializes the manager. This method is called during the plugin's startup sequence. */
+ void init();
+
+ /**
+ * Gets the main CratesPlugin instance.
+ * @return the CratesPlugin instance
+ */
default CratesPlugin getPlugin() {
return JavaPlugin.getPlugin(CratesPlugin.class);
}
diff --git a/api/src/main/java/fr/traqueur/crates/api/managers/UsersManager.java b/api/src/main/java/fr/traqueur/crates/api/managers/UsersManager.java
new file mode 100644
index 0000000..aa85b9f
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/managers/UsersManager.java
@@ -0,0 +1,74 @@
+package fr.traqueur.crates.api.managers;
+
+import fr.traqueur.crates.api.models.CrateOpening;
+import fr.traqueur.crates.api.models.User;
+
+import java.util.UUID;
+
+/**
+ * Manager responsible for player data including virtual keys and opening history.
+ *
+ *
This manager handles the lifecycle of user data:
+ *
+ *
Loading user data when a player joins
+ *
Caching user data in memory for performance
+ *
Persisting changes to the database
+ *
Unloading user data when a player leaves
+ *
+ *
+ *
Usage example:
+ *
{@code
+ * UsersManager usersManager = plugin.getManager(UsersManager.class);
+ * User user = usersManager.getUser(player.getUniqueId());
+ *
+ * // Modify user data
+ * user.addKeys("legendary-key", 5);
+ *
+ * // Changes are automatically persisted
+ * }
+ *
+ * @see User
+ * @see CrateOpening
+ */
+public non-sealed interface UsersManager extends Manager {
+
+ /**
+ * Loads a user's data from the database into cache.
+ *
+ *
This is called automatically when a player joins the server.
+ * The operation is asynchronous and loads both keys and opening history.
+ *
+ * @param uuid the player's UUID
+ */
+ void loadUser(UUID uuid);
+
+ /**
+ * Unloads a user's data from cache and saves any pending changes.
+ *
+ *
This is called automatically when a player leaves the server.
+ *
+ * @param uuid the player's UUID
+ */
+ void unloadUser(UUID uuid);
+
+ /**
+ * Gets a user from the cache.
+ *
+ *
The user must have been loaded first via {@link #loadUser(UUID)}.
+ * Returns null if the user is not in cache.
+ *
+ * @param uuid the player's UUID
+ * @return the cached User object, or null if not loaded
+ */
+ User getUser(UUID uuid);
+
+ /**
+ * Persists a crate opening record to the database.
+ *
+ *
This is called internally when a reward is given to a player.
+ * The operation is asynchronous.
+ *
+ * @param opening the crate opening record to persist
+ */
+ void persistCrateOpening(CrateOpening opening);
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/CrateOpening.java b/api/src/main/java/fr/traqueur/crates/api/models/CrateOpening.java
new file mode 100644
index 0000000..d1a0909
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/CrateOpening.java
@@ -0,0 +1,21 @@
+package fr.traqueur.crates.api.models;
+
+import java.util.UUID;
+
+/**
+ * Represents a record of a crate opening by a player.
+ *
+ * @param id The unique identifier of the crate opening.
+ * @param playerUuid The UUID of the player who opened the crate.
+ * @param crateId The identifier of the crate that was opened.
+ * @param rewardId The identifier of the reward obtained from the crate.
+ * @param timestamp The timestamp when the crate was opened.
+ */
+public record CrateOpening(
+ UUID id,
+ UUID playerUuid,
+ String crateId,
+ String rewardId,
+ long timestamp
+) {
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/User.java b/api/src/main/java/fr/traqueur/crates/api/models/User.java
new file mode 100644
index 0000000..8ea73ad
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/User.java
@@ -0,0 +1,101 @@
+package fr.traqueur.crates.api.models;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Represents a player's crate-related data including virtual keys and opening history.
+ *
+ *
User data is persisted in the database and cached in memory for performance.
+ * The {@link fr.traqueur.crates.api.managers.UsersManager} handles loading and saving.
+ *
+ *
Usage example:
+ *
{@code
+ * UsersManager usersManager = plugin.getManager(UsersManager.class);
+ * User user = usersManager.getUser(player.getUniqueId());
+ *
+ * // Check and use keys
+ * if (user.hasKey("legendary-key")) {
+ * user.removeKeys("legendary-key", 1);
+ * }
+ *
+ * // Give keys
+ * user.addKeys("common-key", 5);
+ * }
+ *
+ * @see fr.traqueur.crates.api.managers.UsersManager
+ * @see CrateOpening
+ */
+public interface User {
+
+ /**
+ * Gets the UUID of this user.
+ *
+ * @return the player's unique identifier
+ */
+ UUID uuid();
+
+ /**
+ * Gets the number of keys a user has for a specific key type.
+ *
+ * @param keyName the key name
+ * @return the number of keys (0 if none)
+ */
+ int getKeyCount(String keyName);
+
+ /**
+ * Adds keys to this user's balance.
+ *
+ * @param keyName the key name
+ * @param amount the amount to add (must be positive)
+ */
+ void addKeys(String keyName, int amount);
+
+ /**
+ * Removes keys from this user's balance.
+ *
+ *
The balance will not go below 0.
+ *
+ * @param keyName the key name
+ * @param amount the amount to remove (must be positive)
+ */
+ void removeKeys(String keyName, int amount);
+
+ /**
+ * Checks if this user has at least one key of the specified type.
+ *
+ * @param keyName the key name
+ * @return true if the user has at least one key
+ */
+ boolean hasKey(String keyName);
+
+ /**
+ * Gets all keys and their counts for this user.
+ *
+ * @return an unmodifiable map of key names to counts
+ */
+ Map getAllKeys();
+
+ /**
+ * Gets all crate openings for this user.
+ *
+ *
Used by algorithms that need player history (e.g., pity systems).
+ *
+ * @return the list of crate openings, most recent first
+ */
+ List crateOpenings();
+
+ /**
+ * Adds a crate opening to this user's history.
+ *
+ *
Called automatically when a reward is given. The opening is persisted
+ * to the database asynchronously.
+ *
+ * @param crateId the crate ID
+ * @param rewardId the reward ID
+ * @return the created CrateOpening record
+ */
+ CrateOpening addCrateOpening(String crateId, String rewardId);
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/Wrapper.java b/api/src/main/java/fr/traqueur/crates/api/models/Wrapper.java
new file mode 100644
index 0000000..c4bd2ba
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/Wrapper.java
@@ -0,0 +1,22 @@
+package fr.traqueur.crates.api.models;
+
+/**
+ * A generic wrapper class that holds a delegate object of type T.
+ *
+ * @param the type of the delegate object
+ */
+public abstract class Wrapper {
+
+ /** The delegate object being wrapped */
+ protected final T delegate;
+
+ /**
+ * Constructs a Wrapper with the specified delegate.
+ *
+ * @param delegate the object to be wrapped
+ */
+ public Wrapper(T delegate) {
+ this.delegate = delegate;
+ }
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/algorithms/AlgorithmContext.java b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/AlgorithmContext.java
new file mode 100644
index 0000000..0fa384f
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/AlgorithmContext.java
@@ -0,0 +1,26 @@
+package fr.traqueur.crates.api.models.algorithms;
+
+import fr.traqueur.crates.api.models.CrateOpening;
+import fr.traqueur.crates.api.models.Wrapper;
+import fr.traqueur.crates.api.models.crates.Reward;
+
+import java.util.List;
+
+/**
+ * Context provided to random algorithms when selecting a reward.
+ * Contains wrapped objects with helper methods for algorithm development.
+ *
+ * This follows the same pattern as AnimationContext, using Wrapper objects
+ * to expose safe and convenient APIs to JavaScript.
+ * @param rewards Wrapped list of possible rewards.
+ * @param history Wrapped list of history of crate openings for the player.
+ * @param crateId Identifier of the crate being opened.
+ * @param playerUuid UUID of the player opening the crate.
+ */
+public record AlgorithmContext(
+ Wrapper> rewards,
+ Wrapper> history,
+ String crateId,
+ String playerUuid
+) {
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/algorithms/RandomAlgorithm.java b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/RandomAlgorithm.java
new file mode 100644
index 0000000..a316267
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/RandomAlgorithm.java
@@ -0,0 +1,34 @@
+package fr.traqueur.crates.api.models.algorithms;
+
+import fr.traqueur.crates.api.models.crates.Reward;
+
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Interface for reward selection algorithms.
+ * Algorithms can be implemented in JavaScript to provide custom logic
+ * for selecting rewards from crates (e.g., pity systems, guaranteed rewards).
+ */
+public interface RandomAlgorithm {
+
+ /**
+ * Unique identifier for this algorithm
+ * @return Algorithm ID
+ */
+ String id();
+
+ /**
+ * Source file where this algorithm was defined
+ * @return Source file path
+ */
+ String sourceFile();
+
+ /**
+ * Function that selects a reward based on the context
+ * The function receives an AlgorithmContext and returns the selected Reward
+ * @return Function that selects a reward
+ */
+ Function selector();
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/animations/Animation.java b/api/src/main/java/fr/traqueur/crates/api/models/animations/Animation.java
new file mode 100644
index 0000000..2b6cd59
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/animations/Animation.java
@@ -0,0 +1,117 @@
+package fr.traqueur.crates.api.models.animations;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Represents an animation that plays when a crate is opened.
+ *
+ *
Animations are defined in JavaScript files in {@code plugins/zCrates/animations/}
+ * and consist of multiple phases with timing and visual effects.
Referenced by crate configurations in the {@code animation} field.
+ *
+ * @return the animation ID (e.g., "roulette", "csgo")
+ */
+ String id();
+
+ /**
+ * Gets the source JavaScript file path.
+ *
+ * @return the relative path to the animation script
+ */
+ String sourceFile();
+
+ /**
+ * Gets all phases of this animation.
+ *
+ *
Phases execute sequentially, each with its own duration and callbacks.
+ *
+ * @return the list of animation phases
+ * @see AnimationPhase
+ */
+ List phases();
+
+ /**
+ * Gets a phase by its index.
+ *
+ * @param index the phase index (0-based)
+ * @return the animation phase
+ * @throws IndexOutOfBoundsException if index is invalid
+ */
+ default AnimationPhase phase(int index) {
+ if(index < 0 || index >= phases().size()) {
+ throw new IndexOutOfBoundsException("Invalid phase index: " + index);
+ }
+ return phases().get(index);
+ }
+
+ /**
+ * Gets a phase by its name.
+ *
+ * @param name the phase name
+ * @return the animation phase
+ * @throws IllegalArgumentException if no phase with that name exists
+ */
+ default AnimationPhase phase(String name) {
+ return phases().stream()
+ .filter(phase -> phase.name().equals(name))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("No phase found with id: " + name));
+ }
+
+ /**
+ * Gets the total duration of all phases combined.
+ *
+ * @return the total duration in milliseconds
+ */
+ default long duration() {
+ return phases().stream().mapToLong(AnimationPhase::duration).sum();
+ }
+
+ /**
+ * Gets the callback executed when the animation completes successfully.
+ *
+ *
Called after all phases finish and the reward is ready to be claimed.
+ *
+ * @return the completion callback
+ */
+ Consumer onComplete();
+
+ /**
+ * Gets the callback executed when the animation is cancelled.
+ *
+ *
Called when the player closes the menu or the animation is interrupted.
+ *
+ * @return the cancellation callback
+ */
+ Consumer onCancel();
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationContext.java b/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationContext.java
new file mode 100644
index 0000000..88a206c
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationContext.java
@@ -0,0 +1,15 @@
+package fr.traqueur.crates.api.models.animations;
+
+import fr.traqueur.crates.api.models.crates.Crate;
+import fr.traqueur.crates.api.models.Wrapper;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+
+/**
+ * Context information for crate opening animations.
+ *
+ * @param player The player involved in the animation.
+ * @param inventory The inventory associated with the animation.
+ * @param crate The crate being opened in the animation.
+ */
+public record AnimationContext(Wrapper player, Wrapper inventory, Wrapper crate) { }
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationPhase.java b/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationPhase.java
new file mode 100644
index 0000000..0f2fee1
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationPhase.java
@@ -0,0 +1,73 @@
+package fr.traqueur.crates.api.models.animations;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Represents a phase in an animation sequence.
+ *
+ * @param name The name of the animation phase.
+ * @param duration The total duration of the phase in milliseconds.
+ * @param interval The interval between ticks in milliseconds.
+ * @param speedCurve The speed curve to apply during the phase.
+ * @param onStart A consumer that is called when the phase starts.
+ * @param onTick A bi-consumer that is called on each tick with the animation context and tick data.
+ * @param onComplete A consumer that is called when the phase completes.
+ */
+public record AnimationPhase(String name, long duration, long interval, SpeedCurve speedCurve,
+ Consumer onStart,
+ BiConsumer onTick,
+ Consumer onComplete) {
+
+ /**
+ * Data provided on each tick of the animation phase.
+ *
+ * @param tickNumber The current tick number.
+ * @param progress The progress of the phase as a value between 0.0 and 1.0.
+ * @param elapsedTime The elapsed time since the start of the phase in milliseconds.
+ */
+ public record TickData(int tickNumber, double progress, long elapsedTime) { }
+
+ /**
+ * Represents different speed curves for animation phases.
+ */
+ public enum SpeedCurve {
+ /** A linear speed curve. */
+ LINEAR(progress -> progress),
+ /** An ease-in speed curve. */
+ EASE_IN(progress -> progress * progress),
+ /** An ease-out speed curve. */
+ EASE_OUT(progress -> 1 - Math.pow(1 - progress, 2)),
+ /** An ease-in-out speed curve. */
+ EASE_IN_OUT(progress -> {
+ if (progress < 0.5) {
+ return 2 * progress * progress;
+ } else {
+ return 1 - Math.pow(-2 * progress + 2, 2) / 2;
+ }
+ });
+
+ /** The function defining the speed curve. */
+ private final Function curveFunction;
+
+ /**
+ * Constructs a SpeedCurve with the given curve function.
+ *
+ * @param curveFunction A function that defines the speed curve.
+ */
+ SpeedCurve(Function curveFunction) {
+ this.curveFunction = curveFunction;
+ }
+
+ /**
+ * Applies the speed curve to the given progress value.
+ *
+ * @param progress The progress value between 0.0 and 1.0.
+ * @return The adjusted progress value according to the speed curve.
+ */
+ public double apply(double progress) {
+ return curveFunction.apply(progress);
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/Crate.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/Crate.java
new file mode 100644
index 0000000..299cb2c
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/Crate.java
@@ -0,0 +1,153 @@
+package fr.traqueur.crates.api.models.crates;
+
+import fr.traqueur.crates.api.models.User;
+import fr.traqueur.crates.api.models.algorithms.RandomAlgorithm;
+import fr.traqueur.crates.api.models.animations.Animation;
+import fr.traqueur.crates.api.settings.models.ItemStackWrapper;
+
+import java.util.List;
+
+/**
+ * Represents a crate configuration that can be opened by players to receive rewards.
+ *
+ *
A crate defines all aspects of the loot box experience including:
+ *
+ *
The key required to open it
+ *
The animation played during opening
+ *
The algorithm used to select rewards
+ *
The list of possible rewards with their weights
+ *
Opening conditions (permissions, cooldowns, etc.)
+ *
+ *
+ *
Crates are loaded from YAML files in the {@code plugins/zCrates/crates/} directory.
This ID is used in commands, configurations, and internal references.
+ *
+ * @return the crate ID (e.g., "legendary", "common")
+ */
+ String id();
+
+ /**
+ * Gets the display name of this crate.
+ *
+ *
Supports MiniMessage formatting for colors and styles.
+ *
+ * @return the formatted display name
+ */
+ String displayName();
+
+ /**
+ * Gets the key required to open this crate.
+ *
+ * @return the key configuration (virtual or physical)
+ * @see Key
+ */
+ Key key();
+
+ /**
+ * Gets the animation to play when opening this crate.
+ *
+ * @return the animation configuration
+ * @see Animation
+ */
+ Animation animation();
+
+ /**
+ * Gets the algorithm used to select rewards from this crate.
+ *
+ * @return the random selection algorithm
+ * @see RandomAlgorithm
+ */
+ RandomAlgorithm algorithm();
+
+ /**
+ * Gets the zMenu inventory name associated with this crate.
+ *
+ *
This refers to an inventory file in {@code plugins/zCrates/inventories/}.
+ *
+ * @return the menu identifier
+ */
+ String relatedMenu();
+
+ /**
+ * Gets all possible rewards from this crate.
+ *
+ * @return an unmodifiable list of rewards
+ * @see Reward
+ */
+ List rewards();
+
+ /**
+ * Gets the maximum number of rerolls allowed for this crate.
+ *
+ *
A value of 0 disables rerolling. When rerolling is enabled, players
+ * can re-spin the animation to get a different reward.
+ *
+ * @return the maximum reroll count
+ */
+ int maxRerolls();
+
+ /**
+ * Gets the conditions that must be met to open this crate.
+ *
+ *
All conditions must pass before the crate can be opened.
+ * Common conditions include permissions and cooldowns.
+ *
+ * @return the list of conditions, empty if no conditions
+ * @see OpenCondition
+ */
+ List conditions();
+
+ /**
+ * Gets a random item to display in menus (preview filler).
+ *
+ *
This is typically used in animations to show random reward items
+ * before the final reward is revealed.
+ *
+ * @return a random display item from the rewards list
+ */
+ ItemStackWrapper randomDisplay();
+
+ /**
+ * Generates a reward for a user using the configured algorithm.
+ *
+ *
This method uses the crate's {@link RandomAlgorithm} to select
+ * a reward based on weights and the user's history.
+ *
+ * @param user the user opening the crate (for history-based algorithms)
+ * @return the selected reward
+ * @see RandomAlgorithm
+ */
+ Reward generateReward(User user);
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/Key.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/Key.java
new file mode 100644
index 0000000..018f1c3
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/Key.java
@@ -0,0 +1,93 @@
+package fr.traqueur.crates.api.models.crates;
+
+import fr.traqueur.structura.annotations.Polymorphic;
+import fr.traqueur.structura.api.Loadable;
+import org.bukkit.entity.Player;
+
+/**
+ * Represents a key required to open a crate.
+ *
+ *
Keys use polymorphic deserialization based on the "type" field in YAML.
+ * Two key types are available:
+ *
+ *
{@code VIRTUAL} - Stored in database, no physical item
+ *
{@code PHYSIC} - Physical item in player's inventory
+ *
+ * @see Crate
+ */
+@Polymorphic
+public interface Key extends Loadable {
+
+ /**
+ * Gets the unique name of this key.
+ *
+ *
For virtual keys, this is the database key identifier.
+ * For physical keys, this is used for matching.
+ *
+ * @return the key name
+ */
+ String name();
+
+ /**
+ * Checks if a player has this key.
+ *
+ *
For virtual keys, checks the database.
+ * For physical keys, searches the player's inventory.
+ *
+ * @param player the player to check
+ * @return true if the player has at least one key
+ */
+ boolean has(Player player);
+
+ /**
+ * Removes one key from the player.
+ *
+ *
For virtual keys, decrements the database count.
+ * For physical keys, removes one matching item from inventory.
+ *
+ * @param player the player to remove the key from
+ */
+ void remove(Player player);
+
+ /**
+ * Gives one key to the player.
+ *
+ *
For virtual keys, increments the database count.
+ * For physical keys, adds the key item to inventory.
+ *
+ * @param player the player to give the key to
+ */
+ void give(Player player);
+
+ /**
+ * Counts how many keys the player has.
+ *
+ *
For virtual keys, retrieves the count from the database.
+ * For physical keys, counts matching items in inventory.
+ *
+ * @param player the player to count keys for
+ * @return the number of keys the player has
+ */
+ int count(Player player);
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenCondition.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenCondition.java
new file mode 100644
index 0000000..57e2115
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenCondition.java
@@ -0,0 +1,40 @@
+package fr.traqueur.crates.api.models.crates;
+
+import fr.traqueur.structura.annotations.Polymorphic;
+import fr.traqueur.structura.api.Loadable;
+import org.bukkit.entity.Player;
+
+/**
+ * Represents a condition that must be met before opening a crate.
+ * Implementations can check permissions, cooldowns, or any other requirement.
+ */
+@Polymorphic
+public interface OpenCondition extends Loadable {
+
+ /**
+ * Checks if the player meets this condition.
+ *
+ * @param player the player to check
+ * @param crate the crate being opened
+ * @return true if the condition is met, false otherwise
+ */
+ boolean check(Player player, Crate crate);
+
+ /**
+ * Called when the player successfully opens the crate.
+ * Use this for side effects like setting cooldowns.
+ *
+ * @param player the player who opened the crate
+ * @param crate the crate that was opened
+ */
+ default void onOpen(Player player, Crate crate) {
+ // Default: no-op
+ }
+
+ /**
+ * Gets the error message key to display when the condition is not met.
+ *
+ * @return the error message key for messages.yml
+ */
+ String errorMessageKey();
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenResult.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenResult.java
new file mode 100644
index 0000000..ed1b12e
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenResult.java
@@ -0,0 +1,74 @@
+package fr.traqueur.crates.api.models.crates;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Result of attempting to open a crate.
+ * @param status The status of the open attempt.
+ * @param failedCondition The condition that failed, if any.
+ */
+public record OpenResult(Status status, @Nullable OpenCondition failedCondition) {
+
+ /**
+ * Possible statuses for crate opening attempts.
+ */
+ public enum Status {
+ /**
+ * The crate was opened successfully.
+ */
+ SUCCESS,
+ /**
+ * The player does not have the required key to open the crate.
+ */
+ NO_KEY,
+ /**
+ * A condition required to open the crate was not met.
+ */
+ CONDITION_FAILED,
+ /**
+ * The crate opening was cancelled by an event.
+ */
+ EVENT_CANCELLED
+ }
+
+ /**
+ * Creates a successful OpenResult.
+ * @return An OpenResult indicating success.
+ */
+ public static OpenResult success() {
+ return new OpenResult(Status.SUCCESS, null);
+ }
+
+ /**
+ * Creates an OpenResult indicating the player lacks the required key.
+ * @return An OpenResult indicating no key.
+ */
+ public static OpenResult noKey() {
+ return new OpenResult(Status.NO_KEY, null);
+ }
+
+ /**
+ * Creates an OpenResult indicating a condition failed.
+ * @param condition The condition that failed.
+ * @return An OpenResult indicating a condition failure.
+ */
+ public static OpenResult conditionFailed(OpenCondition condition) {
+ return new OpenResult(Status.CONDITION_FAILED, condition);
+ }
+
+ /**
+ * Creates an OpenResult indicating the event was cancelled.
+ * @return An OpenResult indicating event cancellation.
+ */
+ public static OpenResult eventCancelled() {
+ return new OpenResult(Status.EVENT_CANCELLED, null);
+ }
+
+ /**
+ * Checks if the open attempt resulted in an error.
+ * @return True if there was an error, false if successful.
+ */
+ public boolean isError() {
+ return status != Status.SUCCESS;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/Reward.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/Reward.java
new file mode 100644
index 0000000..ffb8de7
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/Reward.java
@@ -0,0 +1,98 @@
+package fr.traqueur.crates.api.models.crates;
+
+import fr.traqueur.crates.api.settings.models.ItemStackWrapper;
+import fr.traqueur.structura.annotations.Polymorphic;
+import fr.traqueur.structura.api.Loadable;
+import org.bukkit.entity.Player;
+
+/**
+ * Represents a reward that can be won from a crate.
+ *
+ *
Rewards use polymorphic deserialization based on the "type" field in YAML.
+ * The following reward types are available:
+ *
+ * @see Crate
+ */
+@Polymorphic()
+public interface Reward extends Loadable {
+
+ /**
+ * Gets the unique identifier for this reward within the crate.
+ *
+ *
Used for logging, history tracking, and algorithm references.
+ *
+ * @return the reward ID
+ */
+ String id();
+
+ /**
+ * Gets the weight of this reward for random selection.
+ *
+ *
Higher weight means higher probability of being selected.
+ * The probability is calculated as: {@code weight / totalWeights}.
+ *
+ *
Example: With rewards weighted 10, 20, 70:
+ *
+ *
10 weight = 10% chance
+ *
20 weight = 20% chance
+ *
70 weight = 70% chance
+ *
+ *
+ * @return the weight value (higher = more common)
+ */
+ double weight();
+
+ /**
+ * Gets the item to display in preview menus and animations.
+ *
+ *
This is what players see before receiving the reward.
+ * It may differ from the actual reward item.
+ *
+ * @return the display item configuration
+ */
+ ItemStackWrapper displayItem();
+
+ /**
+ * Gives this reward to a player.
+ *
+ *
The implementation depends on the reward type:
+ *
+ *
ITEM/ITEMS: Adds items to inventory (drops if full)
+ *
COMMAND/COMMANDS: Executes console commands with %player% placeholder
+ *
+ *
+ * @param player the player to give the reward to
+ */
+ void give(Player player);
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplay.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplay.java
new file mode 100644
index 0000000..7e14106
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplay.java
@@ -0,0 +1,32 @@
+package fr.traqueur.crates.api.models.placedcrates;
+
+import org.bukkit.Location;
+
+/**
+ * Represents a display for a crate in the game world.
+ *
+ * @param The type of element that the crate display represents.
+ */
+public interface CrateDisplay {
+
+ /** Spawns the crate display in the game world. */
+ void spawn();
+
+ /** Removes the crate display from the game world. */
+ void remove();
+
+ /**
+ * Checks if the given element matches the crate display.
+ *
+ * @param element The element to check.
+ * @return true if the element matches, false otherwise.
+ */
+ boolean matches(T element);
+
+ /**
+ * Gets the location of the crate display in the game world.
+ *
+ * @return The location of the crate display.
+ */
+ Location getLocation();
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplayFactory.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplayFactory.java
new file mode 100644
index 0000000..f3853fe
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplayFactory.java
@@ -0,0 +1,38 @@
+package fr.traqueur.crates.api.models.placedcrates;
+
+import org.bukkit.Location;
+
+import java.util.List;
+
+/**
+ * Factory interface for creating crate displays.
+ *
+ * @param The type of element that the crate display represents.
+ */
+public interface CrateDisplayFactory {
+
+ /**
+ * Creates a crate display at the specified location with the given value and yaw.
+ *
+ * @param location the location to create the display
+ * @param value the value representing the display content
+ * @param yaw the yaw orientation of the display
+ * @return the created crate display
+ */
+ CrateDisplay create(Location location, String value, float yaw);
+
+ /**
+ * Validates if the given value is valid for this display type.
+ *
+ * @param value the value to validate
+ * @return true if valid, false otherwise
+ */
+ boolean isValidValue(String value);
+
+ /**
+ * Returns a list of suggested values for tab completion.
+ *
+ * @return list of suggested values
+ */
+ List getSuggestions();
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/DisplayType.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/DisplayType.java
new file mode 100644
index 0000000..7f49577
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/DisplayType.java
@@ -0,0 +1,19 @@
+package fr.traqueur.crates.api.models.placedcrates;
+
+/**
+ * Enum representing different types of displays for placed crates.
+ */
+public enum DisplayType {
+ /** Display type for blocks. */
+ BLOCK,
+ /** Display type for entities. */
+ ENTITY,
+ /** Display type for holograms. */
+ MYTHIC_MOB,
+ /** Display type for items adder. */
+ ITEMS_ADDER,
+ /** Display type for oraxen. */
+ ORAXEN,
+ /** Display type for nexo. */
+ NEXO;
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/PlacedCrate.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/PlacedCrate.java
new file mode 100644
index 0000000..601a72b
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/PlacedCrate.java
@@ -0,0 +1,46 @@
+package fr.traqueur.crates.api.models.placedcrates;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+
+import java.util.UUID;
+
+/**
+ * Record representing a placed crate in the game world.
+ *
+ * @param id Unique identifier for the placed crate.
+ * @param crateId Identifier of the crate type.
+ * @param worldName Name of the world where the crate is placed.
+ * @param x X coordinate of the crate's location.
+ * @param y Y coordinate of the crate's location.
+ * @param z Z coordinate of the crate's location.
+ * @param displayType Type of display for the crate.
+ * @param displayValue Value associated with the display type.
+ * @param yaw Yaw orientation of the crate.
+ */
+public record PlacedCrate(
+ UUID id,
+ String crateId,
+ String worldName,
+ int x,
+ int y,
+ int z,
+ DisplayType displayType,
+ String displayValue,
+ float yaw
+) {
+
+ /**
+ * Converts the PlacedCrate to a Bukkit Location object.
+ *
+ * @return Location object representing the crate's location, or null if the world does not exist.
+ */
+ public Location getLocation() {
+ World world = Bukkit.getWorld(worldName);
+ if (world == null) {
+ return null;
+ }
+ return new Location(world, x, y, z, yaw, 0);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/providers/ItemsProvider.java b/api/src/main/java/fr/traqueur/crates/api/providers/ItemsProvider.java
new file mode 100644
index 0000000..ab5b1bf
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/providers/ItemsProvider.java
@@ -0,0 +1,20 @@
+package fr.traqueur.crates.api.providers;
+
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * Functional interface for providing ItemStack instances based on a player and item ID.
+ */
+@FunctionalInterface
+public interface ItemsProvider {
+
+ /**
+ * Retrieves an ItemStack for the given player and item ID.
+ *
+ * @param player the player for whom the item is being retrieved
+ * @param itemId the identifier of the item
+ * @return the corresponding ItemStack
+ */
+ ItemStack item(Player player, String itemId);
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/placeholders/PlaceholderParser.java b/api/src/main/java/fr/traqueur/crates/api/providers/PlaceholderProvider.java
similarity index 86%
rename from api/src/main/java/fr/traqueur/crates/api/placeholders/PlaceholderParser.java
rename to api/src/main/java/fr/traqueur/crates/api/providers/PlaceholderProvider.java
index c76972b..b17ce6a 100644
--- a/api/src/main/java/fr/traqueur/crates/api/placeholders/PlaceholderParser.java
+++ b/api/src/main/java/fr/traqueur/crates/api/providers/PlaceholderProvider.java
@@ -1,4 +1,4 @@
-package fr.traqueur.crates.api.placeholders;
+package fr.traqueur.crates.api.providers;
import org.bukkit.entity.Player;
@@ -21,7 +21,7 @@
*
PAPIHook: Integrates with PlaceholderAPI for full placeholder support
*
*/
-public interface PlaceholderParser {
+public interface PlaceholderProvider {
/**
* The singleton instance holder.
@@ -33,14 +33,14 @@ class Holder {
*/
private Holder() {}
- private static PlaceholderParser instance = new EmptyParser();
+ private static PlaceholderProvider instance = new EmptyProvider();
/**
* Sets the global placeholder parser instance.
*
* @param parser The parser implementation to use
*/
- public static void setInstance(PlaceholderParser parser) {
+ public static void setInstance(PlaceholderProvider parser) {
instance = parser;
}
@@ -49,7 +49,7 @@ public static void setInstance(PlaceholderParser parser) {
*
* @return The active parser (never null, defaults to EmptyParser)
*/
- public static PlaceholderParser getInstance() {
+ public static PlaceholderProvider getInstance() {
return instance;
}
}
@@ -81,12 +81,12 @@ static String parsePlaceholders(Player player, String text) {
*
This implementation is used as the default when no placeholder system
* (like PlaceholderAPI) is available.
*/
- class EmptyParser implements PlaceholderParser {
+ class EmptyProvider implements PlaceholderProvider {
/**
* Constructs an EmptyParser instance.
*/
- private EmptyParser() {}
+ private EmptyProvider() {}
@Override
public String parse(Player player, String text) {
diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/AnimationsRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/AnimationsRegistry.java
new file mode 100644
index 0000000..a8e9bf3
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/registries/AnimationsRegistry.java
@@ -0,0 +1,20 @@
+package fr.traqueur.crates.api.registries;
+
+import fr.traqueur.crates.api.CratesPlugin;
+import fr.traqueur.crates.api.models.animations.Animation;
+
+/**
+ * Registry for managing animations loaded from files.
+ */
+public abstract class AnimationsRegistry extends FileBasedRegistry {
+
+ /**
+ * Constructs an AnimationsRegistry with the specified plugin and resource folder.
+ *
+ * @param plugin The CratesPlugin instance.
+ * @param resourceFolder The folder where animation files are located.
+ */
+ protected AnimationsRegistry(CratesPlugin plugin, String resourceFolder) {
+ super(plugin, resourceFolder, "animations", ".js");
+ }
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/CrateDisplayFactoriesRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/CrateDisplayFactoriesRegistry.java
new file mode 100644
index 0000000..ffd88ae
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/registries/CrateDisplayFactoriesRegistry.java
@@ -0,0 +1,22 @@
+package fr.traqueur.crates.api.registries;
+
+import fr.traqueur.crates.api.models.placedcrates.CrateDisplayFactory;
+import fr.traqueur.crates.api.models.placedcrates.DisplayType;
+
+/**
+ * Registry for managing crate display factories.
+ */
+public interface CrateDisplayFactoriesRegistry extends Registry> {
+
+ /**
+ * Registers a generic crate display factory for the specified display type.
+ *
+ * @param type The display type.
+ * @param factory The crate display factory.
+ * @param The type of element that the crate display represents.
+ */
+ default void registerGeneric(DisplayType type, CrateDisplayFactory factory) {
+ this.register(type, factory);
+ }
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/CratesRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/CratesRegistry.java
new file mode 100644
index 0000000..db5acf5
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/registries/CratesRegistry.java
@@ -0,0 +1,30 @@
+package fr.traqueur.crates.api.registries;
+
+import fr.traqueur.crates.api.CratesPlugin;
+import fr.traqueur.crates.api.models.crates.Crate;
+
+/**
+ * Abstract registry for managing crate configurations stored in files.
+ *
+ *
This class extends {@link FileBasedRegistry} to provide functionality
+ * for loading and managing crate definitions from YAML files located in a
+ * specified resource folder.
+ *
+ *
Concrete implementations of this class should specify the resource folder
+ * where crate configuration files are stored.
+ *
+ * @see FileBasedRegistry
+ * @see Crate
+ */
+public abstract class CratesRegistry extends FileBasedRegistry {
+
+ /**
+ * Constructs a new {@code CratesRegistry} with the specified plugin and resource folder.
+ *
+ * @param plugin the main plugin instance
+ * @param resourceFolder the folder where crate configuration files are located
+ */
+ protected CratesRegistry(CratesPlugin plugin, String resourceFolder) {
+ super(plugin, resourceFolder, "crates", ".yml", ".yaml");
+ }
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java
index c7fbff8..1edb0dd 100644
--- a/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java
+++ b/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java
@@ -11,7 +11,12 @@
import java.util.*;
import java.util.stream.Stream;
-
+/**
+ * A registry that loads items from files in a specified folder structure.
+ *
+ * @param the type of the identifier for registered items
+ * @param the type of items being registered
+ */
public abstract class FileBasedRegistry implements Registry {
/** The plugin instance */
@@ -23,6 +28,9 @@ public abstract class FileBasedRegistry implements Registry {
/** The name used in logging messages */
private final String logName;
+ /** The supported file types for loading items */
+ private final Set supportedFileTypes;
+
/** The root folder of the loaded folder structure */
private Folder rootFolder;
@@ -32,19 +40,21 @@ public abstract class FileBasedRegistry implements Registry {
* @param plugin the ItemsPlugin instance
* @param resourceFolder the folder in resources containing example files
* @param logName the name used in logging messages
+ * @param supportedFileTypes the supported file extensions (e.g., ".yml", ".json")
*/
- protected FileBasedRegistry(CratesPlugin plugin, String resourceFolder, String logName) {
+ protected FileBasedRegistry(CratesPlugin plugin, String resourceFolder, String logName, String... supportedFileTypes) {
this.plugin = plugin;
this.storage = new HashMap<>();
this.resourceFolder = resourceFolder;
this.logName = logName;
+ this.supportedFileTypes = Set.of(supportedFileTypes);
}
/**
* Loads items from the specified folder, building the folder structure.
*/
public void loadFromFolder() {
- Path folder = this.plugin.getDataPath().resolve(this.resourceFolder);
+ Path folder = this.plugin.getDataFolder().toPath().resolve(this.resourceFolder);
if (!ensureFolderExists(folder)) {
return;
}
@@ -78,7 +88,7 @@ private Folder buildFolderStructure(Path currentPath, Path rootPath) {
if (!sub.isEmpty()) {
subFolders.add(sub);
}
- } else if (isYamlFile(path)) {
+ } else if (isFile(path.toString().toLowerCase())) {
T element = loadFile(path);
if (element != null) {
elements.add(element);
@@ -140,16 +150,6 @@ protected boolean ensureFolderExists(Path folder) {
return true;
}
- /** Checks if the given path points to a YAML file.
- *
- * @param path the path to check
- * @return true if the file is a YAML file, false otherwise
- */
- private boolean isYamlFile(Path path) {
- String name = path.toString().toLowerCase();
- return name.endsWith(".yml") || name.endsWith(".yaml");
- }
-
/** Copies example files from the resource folder to the plugin's data folder. */
private void copyExampleFiles() {
Logger.info("Copying example " + logName + " files...");
@@ -237,7 +237,12 @@ private List listResourceFiles(String folder) throws IOException {
*/
private boolean isFile(String fileName) {
String lower = fileName.toLowerCase();
- return lower.endsWith(".yml") || lower.endsWith(".yaml") || lower.endsWith(".properties");
+ for (String ext : supportedFileTypes) {
+ if (lower.endsWith(ext)) {
+ return true;
+ }
+ }
+ return fileName.endsWith(".properties");
}
/** Loads an item from the specified file.
@@ -258,8 +263,8 @@ public T getById(ID id) {
}
@Override
- public Collection getAll() {
- return storage.values();
+ public List getAll() {
+ return new ArrayList<>(storage.values());
}
@Override
diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java
new file mode 100644
index 0000000..636bc94
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java
@@ -0,0 +1,25 @@
+package fr.traqueur.crates.api.registries;
+
+import fr.traqueur.crates.api.CratesPlugin;
+import fr.traqueur.crates.api.hooks.Hook;
+import org.bukkit.plugin.java.JavaPlugin;
+
+/**
+ * Registry interface for managing hooks within the application.
+ */
+public interface HooksRegistry extends Registry {
+
+ /**
+ * Enables all registered hooks.
+ */
+ void enableAll();
+
+ /**
+ * Scans the specified package for hook implementations and registers them.
+ *
+ * @param plugin the JavaPlugin instance
+ * @param packageName the package name to scan for hooks
+ */
+ void scanPackage(CratesPlugin plugin, String packageName);
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/ItemsProvidersRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/ItemsProvidersRegistry.java
new file mode 100644
index 0000000..e252097
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/registries/ItemsProvidersRegistry.java
@@ -0,0 +1,9 @@
+package fr.traqueur.crates.api.registries;
+
+import fr.traqueur.crates.api.providers.ItemsProvider;
+
+/**
+ * Registry interface for managing ItemsProvider instances within the application.
+ */
+public interface ItemsProvidersRegistry extends Registry {
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/RandomAlgorithmsRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/RandomAlgorithmsRegistry.java
new file mode 100644
index 0000000..c1c412f
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/registries/RandomAlgorithmsRegistry.java
@@ -0,0 +1,20 @@
+package fr.traqueur.crates.api.registries;
+
+import fr.traqueur.crates.api.CratesPlugin;
+import fr.traqueur.crates.api.models.algorithms.RandomAlgorithm;
+
+/**
+ * Registry for random algorithms used in crates.
+ */
+public abstract class RandomAlgorithmsRegistry extends FileBasedRegistry {
+
+ /**
+ * Constructor for RandomAlgorithmsRegistry.
+ *
+ * @param plugin the CratesPlugin instance
+ * @param resourceFolder the resource folder path
+ */
+ protected RandomAlgorithmsRegistry(CratesPlugin plugin, String resourceFolder) {
+ super(plugin, resourceFolder, "algorithms", ".js");
+ }
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java b/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java
index d413096..e65fe33 100644
--- a/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java
+++ b/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java
@@ -4,6 +4,7 @@
import com.google.common.collect.MutableClassToInstanceMap;
import java.util.Collection;
+import java.util.List;
/**
* A generic registry interface for managing items identified by unique IDs.
@@ -60,7 +61,7 @@ public interface Registry {
*
* @return A collection of all registered items.
*/
- Collection getAll();
+ List getAll();
/**
* Clear all registered items in this registry.
diff --git a/api/src/main/java/fr/traqueur/crates/api/serialization/Keys.java b/api/src/main/java/fr/traqueur/crates/api/serialization/Keys.java
new file mode 100644
index 0000000..fc73dcc
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/serialization/Keys.java
@@ -0,0 +1,168 @@
+package fr.traqueur.crates.api.serialization;
+
+import fr.traqueur.crates.api.models.placedcrates.PlacedCrate;
+import org.bukkit.NamespacedKey;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+import org.bukkit.plugin.Plugin;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Registry of all data keys used in the zCrates plugin.
+ * Each key automatically uses its field name as the key identifier.
+ */
+public class Keys {
+
+ /** Key for the name of the crate */
+ public static final DataKey KEY_NAME = new DataKey<>(PersistentDataType.STRING);
+ /** Key indicating whether the crate is a placed crate entity */
+ public static final DataKey PLACED_CRATE_ENTITY = new DataKey<>(PersistentDataType.BOOLEAN);
+ /** Key for the list of placed crates */
+ public static final DataKey> PLACED_CRATES = new DataKey<>(PersistentDataType.LIST.listTypeFrom(PlacedCrateDataType.INSTANCE));
+
+ // Internal keys for PlacedCrate PDC serialization
+
+ /** Key for the unique ID of the placed crate */
+ public static final DataKey INTERNAL_PLACED_CRATE_ID = new DataKey<>(PersistentDataType.STRING);
+ /** Key for the crate ID associated with the placed crate */
+ public static final DataKey INTERNAL_PLACED_CRATE_CRATE_ID = new DataKey<>(PersistentDataType.STRING);
+ /** Key for the world name where the placed crate is located */
+ public static final DataKey INTERNAL_PLACED_CRATE_WORLD_NAME = new DataKey<>(PersistentDataType.STRING);
+ /** Key for the X coordinate of the placed crate */
+ public static final DataKey INTERNAL_PLACED_CRATE_X = new DataKey<>(PersistentDataType.INTEGER);
+ /** Key for the Y coordinate of the placed crate */
+ public static final DataKey INTERNAL_PLACED_CRATE_Y = new DataKey<>(PersistentDataType.INTEGER);
+ /** Key for the Z coordinate of the placed crate */
+ public static final DataKey INTERNAL_PLACED_CRATE_Z = new DataKey<>(PersistentDataType.INTEGER);
+ /** Key indicating whether the placed crate is currently active */
+ public static final DataKey INTERNAL_PLACED_CRATE_DISPLAY_TYPE = new DataKey<>(PersistentDataType.STRING);
+ /** Key for the display value of the placed crate */
+ public static final DataKey INTERNAL_PLACED_CRATE_DISPLAY_VALUE = new DataKey<>(PersistentDataType.STRING);
+ /** Key for the yaw rotation of the placed crate */
+ public static final DataKey INTERNAL_PLACED_CRATE_YAW = new DataKey<>(PersistentDataType.FLOAT);
+
+ private static Plugin PLUGIN;
+
+ private Keys() {
+ }
+
+ /**
+ * Initializes the Keys registry with the given plugin instance.
+ * This must be called before using any DataKey.
+ * Initializes dependent data types as well.
+ *
+ * @param plugin the plugin instance
+ */
+ public static void initialize(Plugin plugin) {
+ PLUGIN = plugin;
+ }
+
+ /**
+ * Generic typed persistent data key that automatically resolves its name from the static field name.
+ *
+ * @param the type of data this key stores
+ */
+ public static class DataKey {
+
+ /** Cache of resolved key names to avoid repeated reflection lookups */
+ private static final Map, String> KEY_NAMES = new HashMap<>();
+
+ /** The PersistentDataType associated with this key */
+ private final PersistentDataType, T> type;
+ /** The NamespacedKey for this DataKey, lazily initialized */
+ private NamespacedKey namespacedKey;
+
+ /**
+ * Creates a new DataKey with the specified {@link PersistentDataType}.
+ * The key name is automatically derived from the static field name.
+ *
+ * @param type the {@link PersistentDataType} for this key
+ */
+ public DataKey(PersistentDataType, T> type) {
+ this.type = type;
+ }
+
+ /**
+ * Gets the NamespacedKey for this DataKey, resolving the field name if needed.
+ * @return the {@link NamespacedKey} for this DataKey
+ */
+ public NamespacedKey getNamespacedKey() {
+ if (namespacedKey == null) {
+ String keyName = resolveFieldName();
+ namespacedKey = new NamespacedKey(PLUGIN, keyName.toLowerCase());
+ }
+ return namespacedKey;
+ }
+
+ /**
+ * Resolves the field name by scanning the Keys class for this instance.
+ */
+ private String resolveFieldName() {
+ String cachedName = KEY_NAMES.get(this);
+ if (cachedName != null) {
+ return cachedName;
+ }
+
+ try {
+ Field[] fields = Keys.class.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers()) &&
+ Modifier.isFinal(field.getModifiers()) &&
+ DataKey.class.isAssignableFrom(field.getType())) {
+
+ field.setAccessible(true);
+ Object fieldValue = field.get(null);
+
+ if (fieldValue == this) {
+ String fieldName = field.getName();
+ KEY_NAMES.put(this, fieldName);
+ return fieldName;
+ }
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Failed to resolve field name for DataKey", e);
+ }
+
+ throw new RuntimeException("Could not resolve field name for DataKey instance");
+ }
+
+ /**
+ * Retrieves the value associated with this key from the given {@link PersistentDataContainer}.
+ *
+ * @param container the {@link PersistentDataContainer} from which to retrieve the value
+ * @return an {@link Optional} containing the value if it exists, or empty if it does not
+ */
+ public Optional get(PersistentDataContainer container) {
+ return Optional.ofNullable(container.get(getNamespacedKey(), type));
+ }
+
+ /**
+ * Retrieves the value associated with this key from the given {@link PersistentDataContainer}.
+ * If the value does not exist, the provided default value is returned.
+ *
+ * @param container the {@link PersistentDataContainer} from which to retrieve the value
+ * @param defaultValue the default value to return if the key does not exist
+ * @return the value associated with this key, or the default value if it does not exist
+ */
+ public T get(PersistentDataContainer container, T defaultValue) {
+ return container.getOrDefault(getNamespacedKey(), type, defaultValue);
+ }
+
+ /**
+ * Sets the value associated with this key in the given {@link PersistentDataContainer}.
+ *
+ * @param container the {@link PersistentDataContainer} in which to store the value
+ * @param value the value to store
+ */
+ public void set(PersistentDataContainer container, T value) {
+ container.set(getNamespacedKey(), type, value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/serialization/PlacedCrateDataType.java b/api/src/main/java/fr/traqueur/crates/api/serialization/PlacedCrateDataType.java
new file mode 100644
index 0000000..c66454e
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/serialization/PlacedCrateDataType.java
@@ -0,0 +1,35 @@
+package fr.traqueur.crates.api.serialization;
+
+import fr.traqueur.crates.api.models.placedcrates.DisplayType;
+import fr.traqueur.crates.api.models.placedcrates.PlacedCrate;
+import org.bukkit.persistence.PersistentDataAdapterContext;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Abstract class for PlacedCrate data type serialization.
+ */
+public abstract class PlacedCrateDataType implements PersistentDataType {
+
+ /** Singleton instance of PlacedCrateDataType */
+ public static PlacedCrateDataType INSTANCE;
+
+ /**
+ * Constructor for PlacedCrateDataType.
+ */
+ protected PlacedCrateDataType() {
+ }
+
+ @Override
+ public @NotNull Class getPrimitiveType() {
+ return PersistentDataContainer.class;
+ }
+
+ @Override
+ public @NotNull Class getComplexType() {
+ return PlacedCrate.class;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/services/ItemsService.java b/api/src/main/java/fr/traqueur/crates/api/services/ItemsService.java
new file mode 100644
index 0000000..295e8b6
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/services/ItemsService.java
@@ -0,0 +1,201 @@
+package fr.traqueur.crates.api.services;
+
+import fr.traqueur.crates.api.PlatformType;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.TextDecoration;
+
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for creating and modifying ItemStacks with Paper/Spigot compatibility.
+ * Works with Adventure Components throughout the code and handles conversion only at the final step.
+ */
+public class ItemsService {
+
+ private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacySection();
+
+ /*
+ * Private constructor to prevent instantiation
+ */
+ private ItemsService() {
+ throw new UnsupportedOperationException("Utility class");
+ }
+
+ /**
+ * Sets the display name of an ItemStack using a Component.
+ * Handles Paper (native) vs Spigot (legacy conversion) automatically.
+ *
+ * @param itemStack The ItemStack to modify
+ * @param displayName The display name as a Component
+ */
+ public static void setDisplayName(ItemStack itemStack, Component displayName) {
+ if (itemStack == null || displayName == null) {
+ return;
+ }
+
+ ItemMeta meta = itemStack.getItemMeta();
+ if (meta == null) {
+ return;
+ }
+
+ Component processedDisplayName = displayName.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE);
+
+
+ if (PlatformType.isPaper()) {
+ // Use Paper's native Adventure API
+ meta.displayName(processedDisplayName);
+ } else {
+ // Convert Component to legacy format for Spigot
+ String legacy = LEGACY_SERIALIZER.serialize(processedDisplayName);
+ meta.setDisplayName(legacy);
+ }
+
+ itemStack.setItemMeta(meta);
+ }
+
+ /**
+ * Sets the lore of an ItemStack using a list of Components.
+ * Handles Paper (native) vs Spigot (legacy conversion) automatically.
+ * Automatically disables italic decoration for all lore lines.
+ *
+ * @param itemStack The ItemStack to modify
+ * @param lore The lore lines as Components
+ */
+ public static void setLore(ItemStack itemStack, List lore) {
+ if (itemStack == null || lore == null) {
+ return;
+ }
+
+ ItemMeta meta = itemStack.getItemMeta();
+ if (meta == null) {
+ return;
+ }
+
+ // Disable italic decoration for all lore lines
+ List processedLore = new ArrayList<>();
+ for (Component line : lore) {
+ processedLore.add(line.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE));
+ }
+
+ if (PlatformType.isPaper()) {
+ // Use Paper's native Adventure API
+ meta.lore(processedLore);
+ } else {
+ // Convert Components to legacy format for Spigot
+ List legacyLore = new ArrayList<>();
+ for (Component line : processedLore) {
+ String legacy = LEGACY_SERIALIZER.serialize(line);
+ legacyLore.add(legacy);
+ }
+ meta.setLore(legacyLore);
+ }
+
+ itemStack.setItemMeta(meta);
+ }
+
+ /**
+ * Creates a new ItemStack with the specified material, amount, display name, and lore.
+ *
+ * @param material The material of the item
+ * @param amount The number of items in the stack
+ * @param displayName The display name as a Component
+ * @param lore The lore lines as Components
+ * @param itemName The item name as a Component
+ * @return The created ItemStack
+ */
+ public static ItemStack createItem(Material material, int amount, Component displayName, List lore, Component itemName) {
+ ItemStack itemStack = new ItemStack(material, amount);
+
+ if (displayName != null) {
+ setDisplayName(itemStack, displayName);
+ }
+
+ if (lore != null && !lore.isEmpty()) {
+ setLore(itemStack, lore);
+ }
+
+ if (itemName != null) {
+ setItemName(itemStack, itemName);
+ }
+
+ return itemStack;
+ }
+
+ /**
+ * Sets the item name of an ItemStack using a Component.
+ * Handles Paper (native) vs Spigot (legacy conversion) automatically.
+ *
+ * @param itemStack The ItemStack to modify
+ * @param itemName The item name as a Component
+ */
+ public static void setItemName(ItemStack itemStack, Component itemName) {
+ if (itemStack == null || itemName == null) {
+ return;
+ }
+
+ ItemMeta meta = itemStack.getItemMeta();
+ if (meta == null) {
+ return;
+ }
+
+ Component processedItemName = itemName.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE);
+
+
+ if (PlatformType.isPaper()) {
+ // Use Paper's native Adventure API
+ meta.itemName(processedItemName);
+ } else {
+ // Convert Component to legacy format for Spigot
+ String legacy = LEGACY_SERIALIZER.serialize(processedItemName);
+ meta.setItemName(legacy);
+ }
+
+ itemStack.setItemMeta(meta);
+ }
+
+ /**
+ * Adds a line to the lore of an ItemStack using a Component.
+ * Handles Paper (native) vs Spigot (legacy conversion) automatically.
+ * Automatically disables italic decoration for the added lore line.
+ *
+ * @param itemStack The ItemStack to modify
+ * @param line The lore line as a Component
+ */
+ public static void addLoreLine(ItemStack itemStack, Component line) {
+ if (itemStack == null || line == null) {
+ return;
+ }
+
+ ItemMeta meta = itemStack.getItemMeta();
+ if (meta == null) {
+ return;
+ }
+
+ List currentLore;
+ if (PlatformType.isPaper()) {
+ currentLore = meta.lore();
+ } else {
+ List legacyLore = meta.getLore();
+ currentLore = new ArrayList<>();
+ if (legacyLore != null) {
+ for (String legacyLine : legacyLore) {
+ currentLore.add(LEGACY_SERIALIZER.deserialize(legacyLine));
+ }
+ }
+ }
+
+ if (currentLore == null) {
+ currentLore = new ArrayList<>();
+ }
+
+ currentLore.add(line.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE));
+
+ setLore(itemStack, currentLore);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java b/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java
index 16d3eb1..6d72fdc 100644
--- a/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java
+++ b/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java
@@ -2,16 +2,18 @@
import fr.traqueur.crates.api.Logger;
import fr.traqueur.crates.api.PlatformType;
-import fr.traqueur.crates.api.placeholders.PlaceholderParser;
+import fr.traqueur.crates.api.providers.PlaceholderProvider;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
+import net.kyori.adventure.title.Title;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
+import java.time.Duration;
import java.util.Map;
/**
@@ -98,7 +100,7 @@ public static void close() {
* @param placeholders the placeholder resolvers
*/
public static void sendMessage(CommandSender sender, String message, TagResolver... placeholders) {
- String parsedString = sender instanceof Player player ? PlaceholderParser.parsePlaceholders(player, message) : message;
+ String parsedString = sender instanceof Player player ? PlaceholderProvider.parsePlaceholders(player, message) : message;
Component parsedComponent = parseMessage(parsedString, placeholders);
if (PlatformType.isPaper()) {
sender.sendMessage(parsedComponent);
@@ -108,6 +110,50 @@ public static void sendMessage(CommandSender sender, String message, TagResolver
}
}
+ /**
+ * Sends a title to a player.
+ * Handles Paper (native) vs Spigot (wrapper) automatically.
+ **
+ * @param player The player to send the title to
+ * @param title The main title text
+ * @param subtitle The subtitle text
+ * @param fadeIn Fade in duration in ticks
+ * @param stay Stay duration in ticks
+ * @param fadeOut Fade out duration in ticks
+ * @param placeholders Optional placeholder resolvers
+ */
+ public static void sendTitle(Player player, String title, String subtitle, int fadeIn, int stay, int fadeOut, TagResolver... placeholders) {
+ // Parse title and subtitle with PlaceholderAPI
+ String parsedTitle = PlaceholderProvider.parsePlaceholders(player, title);
+ String parsedSubtitle = PlaceholderProvider.parsePlaceholders(player, subtitle);
+
+ // Parse MiniMessage and custom placeholders
+ Component titleComponent = parseMessage(parsedTitle, placeholders);
+ Component subtitleComponent = parseMessage(parsedSubtitle, placeholders);
+
+ // Create title times
+ Title.Times times = Title.Times.times(
+ Duration.ofMillis(fadeIn * 50L),
+ Duration.ofMillis(stay * 50L),
+ Duration.ofMillis(fadeOut * 50L)
+ );
+
+ Title adventureTitle = Title.title(
+ titleComponent,
+ subtitleComponent,
+ times
+ );
+
+ // Send using appropriate backend
+ if (PlatformType.isPaper()) {
+ player.showTitle(adventureTitle);
+ } else {
+ Audience audience = bukkitAudiences.player(player);
+ audience.showTitle(adventureTitle);
+ }
+ }
+
+
/**
* Parses a message with MiniMessage tags, legacy color codes, and custom placeholders.
* Converts legacy codes to MiniMessage format first, then parses with placeholder resolution.
diff --git a/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java b/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java
index 13b38c4..bc8a549 100644
--- a/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java
+++ b/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java
@@ -4,14 +4,33 @@
import com.google.common.collect.MutableClassToInstanceMap;
import fr.traqueur.structura.api.Loadable;
+/**
+ * A generic interface for application settings that can be loaded.
+ * Provides methods to register and retrieve settings instances.
+ */
public interface Settings extends Loadable {
+ /** A map to hold instances of settings classes. */
ClassToInstanceMap INSTANCES = MutableClassToInstanceMap.create();
+ /**
+ * Retrieves the settings instance of the specified class.
+ *
+ * @param clazz the class of the settings to retrieve
+ * @param the type of the settings
+ * @return the settings instance
+ */
static T get(Class clazz) {
return INSTANCES.getInstance(clazz);
}
+ /**
+ * Registers a settings instance for the specified class.
+ *
+ * @param clazz the class of the settings to register
+ * @param instance the settings instance to register
+ * @param the type of the settings
+ */
static void register(Class clazz, T instance) {
INSTANCES.putInstance(clazz, instance);
}
diff --git a/api/src/main/java/fr/traqueur/crates/api/settings/models/DatabaseSettings.java b/api/src/main/java/fr/traqueur/crates/api/settings/models/DatabaseSettings.java
new file mode 100644
index 0000000..9aa3874
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/settings/models/DatabaseSettings.java
@@ -0,0 +1,35 @@
+package fr.traqueur.crates.api.settings.models;
+
+import fr.maxlego08.sarah.DatabaseConnection;
+import fr.traqueur.structura.annotations.Polymorphic;
+import fr.traqueur.structura.api.Loadable;
+
+/**
+ * Interface representing database settings for the application.
+ *
+ *
This interface extends {@link Loadable}, allowing implementations
+ * to load database configuration details such as table prefixes and
+ * connection parameters.
+ *
+ * @see Loadable
+ * @see DatabaseConnection
+ */
+@Polymorphic
+public interface DatabaseSettings extends Loadable {
+
+ /**
+ * Retrieves the table prefix used in the database.
+ *
+ * @return the table prefix as a {@code String}
+ */
+ String tablePrefix();
+
+ /**
+ * Creates a new database connection based on the settings.
+ *
+ * @param debug {@code true} to enable debug mode, {@code false} otherwise
+ * @return a new {@link DatabaseConnection} instance
+ */
+ DatabaseConnection connection(boolean debug);
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/settings/models/ItemStackWrapper.java b/api/src/main/java/fr/traqueur/crates/api/settings/models/ItemStackWrapper.java
new file mode 100644
index 0000000..4a2a14c
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/settings/models/ItemStackWrapper.java
@@ -0,0 +1,139 @@
+package fr.traqueur.crates.api.settings.models;
+
+
+import fr.traqueur.crates.api.providers.ItemsProvider;
+import fr.traqueur.crates.api.providers.PlaceholderProvider;
+import fr.traqueur.crates.api.registries.ItemsProvidersRegistry;
+import fr.traqueur.crates.api.registries.Registry;
+import fr.traqueur.crates.api.services.ItemsService;
+import fr.traqueur.crates.api.services.MessagesService;
+import fr.traqueur.structura.annotations.Options;
+import fr.traqueur.structura.annotations.defaults.DefaultInt;
+import fr.traqueur.structura.api.Loadable;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Wrapper class for configuring and building ItemStack instances.
+ * Supports direct material specification or delegation to an ItemsProvider.
+ * Allows customization of display name, lore, and item name with placeholder support.
+ * @param material the material of the item (optional if using copyFrom)
+ * @param amount the quantity of the item (default is 1)
+ * @param copyFrom delegate to an ItemsProvider to create the item (optional)
+ * @param displayName the display name of the item (optional)
+ * @param itemName the custom item name (optional)
+ * @param lore the lore of the item as a list of strings (optional)
+ */
+public record ItemStackWrapper(
+ @Options(optional = true) Material material,
+
+ @Options(optional = true) @DefaultInt(1) int amount,
+
+ @Options(optional = true) Delegate copyFrom,
+
+ @Options(optional = true) String displayName,
+
+ @Options(optional = true) String itemName,
+
+ @Options(optional = true) List lore
+) implements Loadable {
+
+ /**
+ * Delegate class for specifying an item via an ItemsProvider.
+ *
+ * @param pluginName the name of the plugin providing the ItemsProvider
+ * @param itemId the identifier of the item within the provider
+ */
+ public record Delegate(String pluginName, String itemId) implements Loadable {
+
+ /**
+ * Retrieves the ItemStack from the specified ItemsProvider.
+ *
+ * @param player the player for context (used when building custom items)
+ * @param itemId the identifier of the item to retrieve
+ * @return the ItemStack provided by the ItemsProvider
+ * @throws IllegalArgumentException if no provider is found for the given plugin name
+ */
+ public ItemStack item(Player player, String itemId) {
+ ItemsProvider provider = Registry.get(ItemsProvidersRegistry.class).getById(pluginName);
+ if (provider == null) {
+ throw new IllegalArgumentException("No item provider found for plugin: " + pluginName);
+ }
+ return provider.item(player, itemId);
+ }
+
+ }
+
+ /**
+ * Validates the configuration after loading.
+ *
+ * @throws IllegalArgumentException if amount is less than 1 or if neither itemId nor material is specified
+ */
+ public ItemStackWrapper {
+ if (amount < 1) {
+ throw new IllegalArgumentException("Amount must be at least 1");
+ }
+ if(copyFrom == null && material == null) {
+ throw new IllegalArgumentException("Either 'copy-from' or 'material' must be specified");
+ }
+ }
+
+ /**
+ * Builds an ItemStack from these settings.
+ *
+ * @param player the player for context (used when building custom items)
+ * @return the created ItemStack
+ * @throws IllegalStateException if neither itemId nor material is specified
+ */
+ public @NotNull ItemStack build(@Nullable Player player) {
+ Component parsedDisplayName = null;
+ if (displayName != null && !displayName.isEmpty()) {
+ String parsedDisplayNameStr = PlaceholderProvider.parsePlaceholders(player, displayName);
+ parsedDisplayName = MessagesService.parseMessage(parsedDisplayNameStr);
+ }
+
+ List lore = new ArrayList<>();
+ if (this.lore != null) {
+ for (String loreLine : this.lore) {
+ String parsedLoreLineStr = PlaceholderProvider.parsePlaceholders(player, loreLine);
+ Component parsedLoreLine = MessagesService.parseMessage(parsedLoreLineStr);
+ lore.add(parsedLoreLine);
+ }
+ }
+
+ Component parsedItemName = null;
+ if (itemName != null && !itemName.isEmpty()) {
+ String parsedItemNameStr = PlaceholderProvider.parsePlaceholders(player, itemName);
+ parsedItemName = MessagesService.parseMessage(parsedItemNameStr);
+ }
+
+ if (copyFrom != null) {
+ ItemStack item = copyFrom.item(player, itemName);
+ if(item == null) {
+ throw new IllegalStateException("Item provider returned null for itemId: " + copyFrom.itemId);
+ }
+ if(parsedDisplayName != null) {
+ ItemsService.setDisplayName(item, parsedDisplayName);
+ }
+ if(!lore.isEmpty()) {
+ ItemsService.setLore(item, lore);
+ }
+ if (parsedItemName != null) {
+ ItemsService.setItemName(item, parsedItemName);
+ }
+ if (item.getAmount() != amount) {
+ item.setAmount(amount);
+ }
+ return item;
+ }
+
+ return ItemsService.createItem(material, amount, parsedDisplayName, lore, parsedItemName);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/DTO.java b/api/src/main/java/fr/traqueur/crates/api/storage/DTO.java
new file mode 100644
index 0000000..4ae0dfd
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/storage/DTO.java
@@ -0,0 +1,17 @@
+package fr.traqueur.crates.api.storage;
+
+/**
+ * Generic Data Transfer Object (DTO) interface for converting to model objects.
+ *
+ * @param the type of the model object
+ */
+public interface DTO {
+
+ /**
+ * Converts this DTO to its corresponding model object.
+ *
+ * @return the model object of type T
+ */
+ T toModel();
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/Tables.java b/api/src/main/java/fr/traqueur/crates/api/storage/Tables.java
new file mode 100644
index 0000000..099fc44
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/storage/Tables.java
@@ -0,0 +1,15 @@
+package fr.traqueur.crates.api.storage;
+
+/**
+ * Defines constants for database table names used in the application.
+ */
+public interface Tables {
+
+ /** the users table */
+ String USERS_TABLE = "users";
+ /** the user keys table */
+ String USER_KEYS_TABLE = "user_keys";
+ /** the crate openings table */
+ String CRATE_OPENINGS_TABLE = "crate_openings";
+
+}
diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/repositories/Repository.java b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/Repository.java
new file mode 100644
index 0000000..49c09ec
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/Repository.java
@@ -0,0 +1,56 @@
+package fr.traqueur.crates.api.storage.repositories;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * A generic repository interface for managing entities of type T with an identifier of type ID.
+ * Provides methods for saving, deleting, and retrieving entities asynchronously.
+ *
+ * @param the type of the entity
+ * @param the type of the entity's identifier
+ */
+public interface Repository {
+
+ /**
+ * Initializes the repository asynchronously.
+ *
+ * @return a CompletableFuture that completes with true if initialization was successful, false otherwise
+ */
+ CompletableFuture init();
+
+ /**
+ * Retrieves all items asynchronously.
+ *
+ * @return a CompletableFuture that completes with a list of all items
+ */
+ CompletableFuture> findAll();
+
+ /**
+ * Saves the given item asynchronously.
+ *
+ * @param item the item to save
+ * @return a CompletableFuture that completes when the save operation is done
+ */
+ CompletableFuture save(@NotNull T item);
+
+
+ /**
+ * Deletes the item with the given identifier asynchronously.
+ *
+ * @param id the identifier of the item to delete
+ * @return a CompletableFuture that completes when the delete operation is done
+ */
+ CompletableFuture delete(@NotNull ID id);
+
+ /**
+ * Retrieves the item with the given identifier asynchronously.
+ *
+ * @param id the identifier of the item to retrieve
+ * @return a CompletableFuture that completes with the retrieved item
+ */
+ CompletableFuture get(@NotNull ID id);
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/repositories/SQLRepository.java b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/SQLRepository.java
new file mode 100644
index 0000000..00623a5
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/SQLRepository.java
@@ -0,0 +1,85 @@
+package fr.traqueur.crates.api.storage.repositories;
+
+import fr.maxlego08.sarah.Column;
+import fr.maxlego08.sarah.RequestHelper;
+import fr.traqueur.crates.api.storage.Tables;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.RecordComponent;
+import java.lang.reflect.Type;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Base class for SQL repositories.
+ * Provides common functionality for repositories that use SQL as a storage source.
+ *
+ * @param the type of the entity
+ * @param the type of the data representation of the entity
+ * @param the type of the entity's identifier
+ */
+public abstract class SQLRepository implements Repository {
+
+ /** The request helper for executing SQL queries. */
+ protected final RequestHelper requestHelper;
+ /** The data class type. */
+ protected Class dataClass = getDataClass();
+
+ public CompletableFuture init() {
+ return createTable();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Class getDataClass() {
+ // Check generic superclass first (for classes that extend SQLRepository)
+ Type genericSuperclass = getClass().getGenericSuperclass();
+ if (genericSuperclass instanceof ParameterizedType parameterizedType) {
+ Type rawType = parameterizedType.getRawType();
+ if (rawType instanceof Class> rawClass &&
+ SQLRepository.class.isAssignableFrom(rawClass)) {
+ Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
+ if (actualTypeArguments.length > 1) {
+ return (Class) actualTypeArguments[1];
+ }
+ }
+ }
+ throw new IllegalStateException("Could not determine data class for repository: " + getClass().getName());
+ }
+
+ /**
+ * Constructor for SQLRepository.
+ *
+ * @param requestHelper the request helper to be used by this repository
+ */
+ public SQLRepository(RequestHelper requestHelper) {
+ this.requestHelper = requestHelper;
+ }
+
+ /**
+ * Retrieves the name of the primary key column from the data class.
+ *
+ * @return the name of the primary key column
+ * @throws IllegalStateException if no primary key column is found
+ */
+ protected String getPrimaryKeyColumn() {
+ for (RecordComponent recordComponent : dataClass.getRecordComponents()) {
+ if(recordComponent.isAnnotationPresent(Column.class)) {
+ Column column = recordComponent.getAnnotation(Column.class);
+ if(column.primary()) {
+ return column.value();
+ }
+ }
+ }
+ throw new IllegalStateException("No primary key column found in data class: " + dataClass.getName());
+ }
+
+ /**
+ * Creates the table for the entity in the database.
+ * This method should be implemented by subclasses to define the table structure.
+ *
+ * @return a CompletableFuture that completes when the table is created
+ */
+ public abstract CompletableFuture createTable();
+
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index f02ffc4..1679d75 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -26,8 +26,7 @@ allprojects {
name = "papermc"
url = uri("https://repo.papermc.io/repository/maven-public/")
}
- maven(url = "https://repo.extendedclip.com/content/repositories/placeholderapi/")
- maven(url = "https://jitpack.io")
+ maven("https://jitpack.io")
}
tasks.compileJava {
@@ -38,16 +37,28 @@ allprojects {
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT")
- /* Depends */
- compileOnly("me.clip:placeholderapi:2.11.6")
- compileOnly("fr.maxlego08.menu:zmenu-api:1.1.0.4")
-
- /* Adventure for Spigot compatibility */
compileOnly("net.kyori:adventure-platform-bukkit:4.3.4")
+ compileOnly("org.mozilla:rhino:1.7.14")
+ compileOnly("org.reflections:reflections:0.10.2")
+
+ compileOnly(files(rootProject.files("libs/zMenu-1.1.0.4.jar")))
/* Libraries */
- implementation("com.github.Traqueur-dev:Structura:1.5.0")
+ implementation("fr.traqueur:structura:1.6.0")
implementation("com.github.Traqueur-dev.CommandsAPI:platform-spigot:4.2.3")
+ implementation("fr.maxlego08.sarah:sarah:1.21")
+ implementation("org.bstats:bstats-bukkit:3.1.0")
+
+ /* Test dependencies */
+ testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
+ testImplementation("org.mozilla:rhino:1.7.14")
+ testImplementation("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT")
+ testImplementation("org.slf4j:slf4j-simple:2.0.9")
+ testRuntimeOnly("org.junit.platform:junit-platform-launcher")
+ }
+
+ tasks.test {
+ useJUnitPlatform()
}
tasks.shadowJar {
@@ -55,14 +66,20 @@ allprojects {
archiveAppendix.set(if (project.path == ":") "" else project.name)
archiveClassifier.set("")
- relocate("fr.traqueur.structura", "fr.traqueur.items.libs.structura")
- relocate("fr.traqueur.commands", "fr.traqueur.items.libs.commands")
+ relocate("fr.traqueur.structura", "fr.traqueur.crates.libs.structura")
+ relocate("fr.traqueur.commands", "fr.traqueur.crates.libs.commands")
+ relocate("fr.maxlego08.sarah", "fr.traqueur.crates.libs.sarah")
+ relocate("org.bstats", "fr.traqueur.crates.libs.bstats")
}
}
dependencies {
api(project(":api"))
+ api(project(":common"))
+ rootProject.subprojects.filter { it.path.startsWith(":hooks:") }.forEach { subproject ->
+ implementation(project(subproject.path))
+ }
}
tasks {
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
new file mode 100644
index 0000000..6e5faf4
--- /dev/null
+++ b/common/build.gradle.kts
@@ -0,0 +1,3 @@
+dependencies {
+ api(project(":api"))
+}
\ No newline at end of file
diff --git a/common/src/main/java/fr/traqueur/crates/models/placedcrates/BlockCrateDisplay.java b/common/src/main/java/fr/traqueur/crates/models/placedcrates/BlockCrateDisplay.java
new file mode 100644
index 0000000..5366f56
--- /dev/null
+++ b/common/src/main/java/fr/traqueur/crates/models/placedcrates/BlockCrateDisplay.java
@@ -0,0 +1,38 @@
+package fr.traqueur.crates.models.placedcrates;
+
+import fr.traqueur.crates.api.models.placedcrates.CrateDisplay;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+
+public class BlockCrateDisplay implements CrateDisplay {
+
+ private final Location location;
+ private final Material material;
+
+ public BlockCrateDisplay(Location location, String value, float yaw) {
+ this.location = location;
+ this.material = Material.valueOf(value.toUpperCase());
+ }
+
+ @Override
+ public void spawn() {
+ Block block = location.getBlock();
+ block.setType(material);
+ }
+
+ @Override
+ public void remove() {
+ location.getBlock().setType(Material.AIR);
+ }
+
+ @Override
+ public boolean matches(Block block) {
+ return block.getLocation().equals(location);
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/fr/traqueur/crates/models/placedcrates/EntityCrateDisplay.java b/common/src/main/java/fr/traqueur/crates/models/placedcrates/EntityCrateDisplay.java
new file mode 100644
index 0000000..c1687c2
--- /dev/null
+++ b/common/src/main/java/fr/traqueur/crates/models/placedcrates/EntityCrateDisplay.java
@@ -0,0 +1,113 @@
+package fr.traqueur.crates.models.placedcrates;
+
+import fr.traqueur.crates.api.models.placedcrates.CrateDisplay;
+import fr.traqueur.crates.api.serialization.Keys;
+import org.bukkit.Location;
+import org.bukkit.entity.ArmorStand;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.LivingEntity;
+
+public class EntityCrateDisplay implements CrateDisplay {
+
+ private final Location location;
+ protected final String entityType;
+ private final float yaw;
+ protected Entity entity;
+
+ public EntityCrateDisplay(Location location, String value, float yaw) {
+ this.location = location;
+ this.entityType = value;
+ this.yaw = yaw;
+ }
+
+ @Override
+ public void spawn() {
+ // First, check if an entity already exists at this location (from previous server session)
+ Location centeredLocation = location.clone().add(0.5, 0, 0.5);
+ Entity existingEntity = findExistingEntity(centeredLocation);
+
+ if (existingEntity != null) {
+ this.entity = existingEntity;
+ // Ensure properties are still correct
+ this.entity.setRotation(yaw, 0);
+ this.entity.setInvulnerable(true);
+ this.entity.setPersistent(true);
+ this.entity.setGravity(false);
+
+ if (entity instanceof LivingEntity living) {
+ living.setAI(false);
+ living.setSilent(true);
+ living.setCollidable(false);
+ }
+
+ if (entity instanceof ArmorStand stand) {
+ stand.setCanMove(false);
+ stand.setCanTick(false);
+ }
+ return;
+ }
+
+ // Spawn new entity centered on the block
+ EntityType type = EntityType.valueOf(entityType);
+ this.entity = location.getWorld().spawnEntity(centeredLocation, type);
+ this.entity.setRotation(yaw, 0);
+ this.entity.setInvulnerable(true);
+ this.entity.setPersistent(true);
+ this.entity.setGravity(false);
+
+ if (entity instanceof LivingEntity living) {
+ living.setAI(false);
+ living.setSilent(true);
+ living.setCollidable(false);
+ }
+
+ if (entity instanceof ArmorStand stand) {
+ stand.setCanMove(false);
+ stand.setCanTick(false);
+ }
+
+ Keys.PLACED_CRATE_ENTITY.set(entity.getPersistentDataContainer(), true);
+ }
+
+ private Entity findExistingEntity(Location centeredLocation) {
+ // Search for entities with the PDC marker in the vicinity
+ return centeredLocation.getWorld().getNearbyEntities(centeredLocation, 1, 1, 1).stream()
+ .filter(e -> {
+ // Check if entity has our marker
+ if (!Keys.PLACED_CRATE_ENTITY.get(e.getPersistentDataContainer(), false)) {
+ return false;
+ }
+ // Check if entity type matches
+ try {
+ EntityType expectedType = EntityType.valueOf(entityType);
+ return e.getType() == expectedType;
+ } catch (IllegalArgumentException ex) {
+ return false;
+ }
+ })
+ .findFirst()
+ .orElse(null);
+ }
+
+ @Override
+ public void remove() {
+ if (entity != null && !entity.isDead()) {
+ entity.remove();
+ }
+ }
+
+ @Override
+ public boolean matches(Entity entity) {
+ return this.entity != null && entity.getUniqueId().equals(this.entity.getUniqueId());
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+
+ public Entity getEntity() {
+ return entity;
+ }
+}
\ No newline at end of file
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..0bd511f
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,936 @@
+# zCrates - Architecture Documentation
+
+High-level architecture overview for maintainers and contributors.
+
+## Table of Contents
+
+- [System Overview](#system-overview)
+- [Module Structure](#module-structure)
+- [Core Subsystems](#core-subsystems)
+- [Data Flow](#data-flow)
+- [Plugin Lifecycle](#plugin-lifecycle)
+- [Design Decisions](#design-decisions)
+- [Performance Considerations](#performance-considerations)
+- [Security Architecture](#security-architecture)
+
+---
+
+## System Overview
+
+zCrates is a modular Minecraft crate plugin built on Paper 1.21+ with Java 21. The architecture emphasizes:
+
+- **Separation of Concerns** - API, implementation, and extensions are distinct
+- **Extensibility** - Hook system for seamless plugin integration
+- **Security** - Sandboxed JavaScript execution with restricted API surface
+- **Performance** - Async database operations, in-memory caching, efficient registries
+- **Type Safety** - Polymorphic YAML deserialization, strong typing throughout
+
+### High-Level Architecture
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ USER INTERFACE │
+│ (Commands, GUIs, Chat Messages, Placed Crates) │
+└─────────────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────────────┐
+│ PRESENTATION LAYER │
+│ (Commands, Listeners, zMenu Integration) │
+└─────────────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────────────┐
+│ BUSINESS LOGIC LAYER │
+│ (CratesManager, UsersManager, AnimationExecutor) │
+└─────────────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────────────┐
+│ DOMAIN MODEL LAYER │
+│ (Crate, Reward, Key, Animation, Algorithm, User) │
+└─────────────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────────────┐
+│ INFRASTRUCTURE LAYER │
+│ (Registries, Script Engine, Storage, Serialization) │
+└─────────────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────────────┐
+│ EXTERNAL SERVICES │
+│ (Database, zMenu, Bukkit API, Custom Item Plugins) │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Module Structure
+
+### 1. API Module (`api/`)
+
+**Purpose:** Public contract for external plugins
+
+**Responsibilities:**
+- Define interfaces for all core models
+- Expose event system
+- Provide registry access
+- Define extension points (Hook, ItemsProvider, etc.)
+
+**Key Characteristics:**
+- No implementation details
+- Minimal dependencies (only Bukkit API, annotations)
+- Semantic versioning
+- Backward compatibility guarantees
+
+**Exported Packages:**
+- `fr.traqueur.crates.api` - Core interfaces
+- `fr.traqueur.crates.api.events` - Event system
+- `fr.traqueur.crates.api.models` - Model interfaces
+- `fr.traqueur.crates.api.registries` - Registry system
+- `fr.traqueur.crates.api.managers` - Manager interfaces
+- `fr.traqueur.crates.api.hooks` - Hook system
+- `fr.traqueur.crates.api.providers` - Provider interfaces
+
+### 2. Common Module (`common/`)
+
+**Purpose:** Shared implementations used across main plugin and hooks
+
+**Responsibilities:**
+- Block/Entity display implementations
+- Shared wrapper classes
+- Common utilities
+
+**Dependencies:**
+- API module
+- Bukkit API
+
+### 3. Main Plugin Module (`src/`)
+
+**Purpose:** Core plugin implementation
+
+**Responsibilities:**
+- Implement all API interfaces
+- Manage plugin lifecycle
+- Orchestrate business logic
+- Persist data
+- Execute JavaScript
+
+**Dependencies:**
+- API module
+- Common module
+- Bukkit/Paper API
+- Rhino (JavaScript engine)
+- Structura (YAML serialization)
+- CommandsAPI (command framework)
+- Sarah (ORM)
+- zMenu (GUI system)
+- Reflections (classpath scanning)
+
+### 4. Hooks Module (`hooks/`)
+
+**Purpose:** Optional plugin integrations
+
+**Structure:**
+```
+hooks/
+├── ItemsAdder/ - ItemsAdder custom items
+├── MythicMobs/ - MythicMobs entity displays
+├── Nexo/ - Nexo custom items
+├── Oraxen/ - Oraxen custom items
+├── PlaceholderAPI/ - Placeholder support
+└── zItems/ - zItems integration
+```
+
+**Characteristics:**
+- Each hook is a separate Gradle subproject
+- Isolated dependencies (only required hook targets)
+- Auto-discovered via `@AutoHook` annotation
+- Gracefully disabled if target plugin missing
+
+---
+
+## Core Subsystems
+
+### 1. Registry System
+
+**Architecture:**
+
+```
+┌────────────────────────────────────────┐
+│ Static Registry Accessor │
+│ ClassToInstanceMap │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ Registry Interface │
+│ - register(ID, T) │
+│ - getById(ID): T │
+│ - getAll(): Collection │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ FileBasedRegistry Abstract │
+│ - loadFromFolder() │
+│ - reload() │
+│ - folder hierarchy support │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ Concrete Implementations │
+│ - ZAnimationRegistry (.js) │
+│ - ZRandomAlgorithmRegistry (.js) │
+│ - ZCratesRegistry (.yml) │
+└────────────────────────────────────────┘
+```
+
+**Design Decisions:**
+
+1. **Static Access via ClassToInstanceMap**
+ - Avoids dependency injection complexity
+ - Type-safe registry retrieval
+ - Single source of truth for all registries
+
+2. **FileBasedRegistry Pattern**
+ - Automatic resource copying from JAR
+ - Folder hierarchy with metadata
+ - Hot-reload support
+ - Consistent loading behavior
+
+3. **Generic ID Type**
+ - String IDs for file-based registries
+ - Enum IDs for runtime registries (DisplayType)
+ - Type safety at compile time
+
+### 2. Manager System
+
+**Architecture:**
+
+```
+┌────────────────────────────────────────┐
+│ Bukkit ServicesManager │
+│ (Plugin service registration) │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ Manager Interface │
+│ - init() │
+│ - reload() │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────┬───────────────────────┐
+│ CratesManager │ UsersManager │
+│ - Opening │ - User cache │
+│ - Animation │ - Key management │
+│ - Reroll │ - Persistence │
+│ - Preview │ - History │
+│ - Placed │ │
+└────────────────┴───────────────────────┘
+```
+
+**Design Decisions:**
+
+1. **ServicesManager Integration**
+ - Standard Bukkit pattern
+ - Automatic lifecycle management
+ - Accessible from any plugin
+
+2. **Single Responsibility**
+ - CratesManager: crate operations
+ - UsersManager: user data
+ - Clear separation of concerns
+
+3. **In-Memory State**
+ - Active openings in CratesManager
+ - User cache in UsersManager
+ - Placed crates loaded per chunk
+
+### 3. Script Engine System
+
+**Architecture:**
+
+```
+┌────────────────────────────────────────┐
+│ ZScriptEngine (Singleton) │
+│ - Rhino Context │
+│ - Custom WrapFactory │
+│ - Security constraints │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ ScriptableObject Scopes │
+│ (Isolated per execution) │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────┬───────────────────────┐
+│ animations │ algorithms │
+│ global │ global │
+│ object │ object │
+└────────────────┴───────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ Context Objects (Wrappers) │
+│ - PlayerWrapper │
+│ - InventoryWrapper │
+│ - CrateWrapper │
+│ - RewardsWrapper │
+│ - HistoryWrapper │
+└────────────────────────────────────────┘
+```
+
+**Design Decisions:**
+
+1. **Single Engine Instance**
+ - Shared Rhino Context across all scripts
+ - Reduced memory footprint
+ - Consistent security settings
+
+2. **Isolated Scopes**
+ - Each script execution uses new scope
+ - Null parent (no prototype chain access)
+ - Prevents cross-script interference
+
+3. **Wrapper Pattern**
+ - Controlled API surface
+ - Type-safe method exposure
+ - No direct Java object access
+
+4. **Security First**
+ - Interpreter mode (no bytecode)
+ - Method blacklist via WrapFactory
+ - No Java package access
+ - No reflection
+
+### 4. Animation System
+
+**Architecture:**
+
+```
+┌────────────────────────────────────────┐
+│ AnimationsRegistry │
+│ - Load .js files │
+│ - Register via AnimationsRegistrar │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ Animation Model │
+│ - List │
+│ - onComplete callback │
+│ - onCancel callback │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ AnimationExecutor │
+│ - Phase timing (BukkitRunnable) │
+│ - SpeedCurve application │
+│ - Context passing │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ JavaScript Callbacks │
+│ - onStart(context) │
+│ - onTick(context, tickData) │
+│ - onComplete(context) │
+└────────────────────────────────────────┘
+```
+
+**Design Decisions:**
+
+1. **Phase-Based Execution**
+ - Sequential phase execution
+ - Independent timing per phase
+ - Clean state transitions
+
+2. **SpeedCurve Integration**
+ - Mathematical easing functions
+ - Applied to progress value
+ - Smooth visual transitions
+
+3. **BukkitRunnable Scheduling**
+ - Sync with server tick rate
+ - Cancel-safe execution
+ - Automatic cleanup
+
+### 5. Storage System
+
+**Architecture:**
+
+```
+┌────────────────────────────────────────┐
+│ DatabaseConnection │
+│ - SQLite / MySQL / MariaDB │
+│ - Connection pooling (HikariCP) │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ Sarah ORM Layer │
+│ - RequestHelper │
+│ - Repository pattern │
+│ - Migration system │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ UserRepository │
+│ - CRUD operations │
+│ - CompletableFuture async │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ DTO ↔ Domain Mapping │
+│ - UserDTO → ZUser │
+│ - Lightweight transfer objects │
+└────────────────────────────────────────┘
+```
+
+**Design Decisions:**
+
+1. **Sarah ORM**
+ - Lightweight abstraction
+ - Multiple database support
+ - Migration management
+ - Connection pooling
+
+2. **Repository Pattern**
+ - Separation of persistence logic
+ - Testable data access
+ - Async by default
+
+3. **DTO Pattern**
+ - Database schema independence
+ - Clear domain/persistence boundary
+ - Easy schema evolution
+
+4. **Async Operations**
+ - Non-blocking database access
+ - CompletableFuture API
+ - Main thread safety
+
+### 6. Display System
+
+**Architecture:**
+
+```
+┌────────────────────────────────────────┐
+│ CrateDisplayFactoriesRegistry │
+│ Map │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ CrateDisplayFactory │
+│ - create(Location, value, yaw) │
+│ - isValidValue(value) │
+│ - getSuggestions() │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ CrateDisplay │
+│ - spawn() │
+│ - remove() │
+│ - matches(T) │
+└────────────────────────────────────────┘
+ ↓
+┌────────────────────────────────────────┐
+│ PlacedCrate (Immutable Record) │
+│ - Stored in Chunk PDC │
+│ - Load/unload with chunks │
+└────────────────────────────────────────┘
+```
+
+**Design Decisions:**
+
+1. **Generic Type Parameter**
+ - Type-safe value matching
+ - Compile-time validation
+ - Flexible display types
+
+2. **Factory Pattern**
+ - Encapsulates creation logic
+ - Validation before creation
+ - Tab completion support
+
+3. **Chunk PDC Storage**
+ - Persistent across restarts
+ - Automatic chunk association
+ - No separate database table
+
+4. **Lazy Loading**
+ - Displays created on chunk load
+ - Removed on chunk unload
+ - Memory efficient
+
+---
+
+## Data Flow
+
+### Crate Opening Flow
+
+```
+1. Player interacts with placed crate
+ ↓
+2. CratesListener handles interaction
+ ↓
+3. CratesManager.tryOpenCrate(player, crate)
+ ├─ Check if player has key
+ ├─ Check all OpenConditions
+ ├─ Fire CratePreOpenEvent (cancellable)
+ └─ If all pass, continue
+ ↓
+4. Fire CrateOpenEvent
+ ↓
+5. Consume key (virtual or physical)
+ ↓
+6. CratesManager.openCrate(player, crate, animation)
+ ├─ Generate reward via algorithm
+ ├─ Fire RewardGeneratedEvent
+ ├─ Store OpenedCrate state
+ └─ Start animation
+ ↓
+7. AnimationExecutor.start(player, animation, context)
+ ├─ Execute phases sequentially
+ ├─ Call JavaScript callbacks
+ └─ Update inventory
+ ↓
+8. Animation completes
+ ↓
+9. If rerolls available, show reroll button
+ OR
+ Give reward and close inventory
+ ↓
+10. Fire RewardGivenEvent
+ ↓
+11. Persist CrateOpening to database (async)
+ ↓
+12. Cleanup OpenedCrate state
+```
+
+### User Data Flow
+
+```
+1. Player joins server
+ ↓
+2. PlayerJoinEvent listener
+ ↓
+3. UsersManager.loadUser(uuid) (async)
+ ├─ Query database via UserRepository
+ ├─ Convert UserDTO → ZUser
+ └─ Store in cache
+ ↓
+4. Player operations (synchronous via cache)
+ ├─ getKeyAmount()
+ ├─ addKey()
+ ├─ removeKey()
+ └─ All modifications in-memory
+ ↓
+5. Player quits server
+ ↓
+6. PlayerQuitEvent listener
+ ↓
+7. UsersManager.saveUser(user) (async)
+ ├─ Convert ZUser → UserDTO
+ ├─ Update database via UserRepository
+ └─ Remove from cache
+```
+
+### Configuration Loading Flow
+
+```
+1. Plugin enable or /zcrates reload
+ ↓
+2. For each FileBasedRegistry:
+ ├─ Clear existing entries
+ ├─ Scan folder recursively
+ ├─ Load each file via loadFile(Path)
+ │ ├─ For .js: evaluate via ZScriptEngine
+ │ └─ For .yml: deserialize via Structura
+ ├─ Capture registrations
+ └─ Store in internal map
+ ↓
+3. Validate cross-references
+ ├─ Crate → Animation exists
+ ├─ Crate → Algorithm exists
+ └─ Reward display items valid
+ ↓
+4. Log statistics
+```
+
+---
+
+## Plugin Lifecycle
+
+### Initialization Sequence
+
+```
+onEnable()
+├─1. Load config files (config.yml, messages.yml)
+│
+├─2. Register polymorphic type adapters
+│ ├─ Reward types (ITEM, ITEMS, COMMAND, COMMANDS)
+│ ├─ Key types (VIRTUAL, PHYSIC)
+│ ├─ DatabaseSettings types (SQLITE, MYSQL, MARIADB)
+│ └─ OpenCondition types (PERMISSION, COOLDOWN)
+│
+├─3. Initialize core services
+│ ├─ Logger (debug mode)
+│ ├─ Keys (PDC key registry)
+│ ├─ MessagesService (MiniMessage parsing)
+│ └─ PlacedCrateDataType (PDC serialization)
+│
+├─4. Create ZScriptEngine instance
+│
+├─5. Integrate with zMenu
+│ ├─ Get InventoryManager
+│ ├─ Get ButtonManager
+│ └─ Register custom buttons (ZCRATES_ANIMATION, etc.)
+│
+├─6. Create and register all registries
+│ ├─ AnimationsRegistry
+│ ├─ RandomAlgorithmsRegistry
+│ ├─ CratesRegistry
+│ ├─ ItemsProvidersRegistry
+│ ├─ HooksRegistry
+│ └─ CrateDisplayFactoriesRegistry
+│
+├─7. Discover and enable hooks
+│ ├─ Scan classpath for @AutoHook
+│ ├─ Check if target plugin loaded
+│ └─ Call hook.onEnable()
+│
+├─8. Register built-in display factories
+│ ├─ BLOCK (BlockCrateDisplayFactory)
+│ └─ ENTITY (EntityCrateDisplayFactory)
+│
+├─9. Load configurations
+│ ├─ loadFromFolder(animations/)
+│ ├─ loadFromFolder(algorithms/)
+│ └─ loadFromFolder(crates/)
+│
+├─10. Initialize database
+│ ├─ Create DatabaseConnection
+│ ├─ Validate connection
+│ ├─ Create UserRepository
+│ └─ Execute migrations
+│
+├─11. Create and register managers
+│ ├─ UsersManager (with repository)
+│ └─ CratesManager
+│
+├─12. Initialize managers
+│ ├─ usersManager.init()
+│ │ ├─ Register listeners
+│ │ └─ Load online players
+│ └─ cratesManager.init()
+│ └─ Verify menu files exist
+│
+└─13. Register commands
+ └─ ZCratesCommand with subcommands
+```
+
+### Shutdown Sequence
+
+```
+onDisable()
+├─1. Stop all active animations
+│ └─ cratesManager.stopAllOpening()
+│
+├─2. Unload all placed crate displays
+│ └─ cratesManager.unloadAllPlacedCrates()
+│
+├─3. Save all cached users
+│ └─ For each cached user: saveUser(user)
+│
+├─4. Close database connection
+│ └─ databaseConnection.disconnect()
+│
+├─5. Close script engine
+│ └─ scriptEngine.close()
+│
+└─6. Shutdown services
+ └─ MessagesService.close()
+```
+
+### Reload Sequence
+
+```
+/zcrates reload
+├─1. Stop all active animations
+│
+├─2. Clear all registries
+│ ├─ animationsRegistry.clear()
+│ ├─ algorithmsRegistry.clear()
+│ └─ cratesRegistry.clear()
+│
+├─3. Reload configuration files
+│ ├─ config.yml
+│ └─ messages.yml
+│
+├─4. Reload file-based registries
+│ ├─ loadFromFolder(animations/)
+│ ├─ loadFromFolder(algorithms/)
+│ └─ loadFromFolder(crates/)
+│
+└─5. Notify managers
+ └─ cratesManager.reload()
+```
+
+---
+
+## Design Decisions
+
+### 1. Why Rhino Instead of Nashorn/GraalVM?
+
+**Decision:** Use Rhino JavaScript engine
+
+**Rationale:**
+- **Security:** Fine-grained control over Java access
+- **Compatibility:** Works on all Java versions (11+)
+- **Sandbox:** Built-in sandboxing with `initSafeStandardObjects()`
+- **Performance:** Interpreter mode sufficient for animation/algorithm scripts
+
+**Trade-offs:**
+- Not ES2015+ compliant (limited ES6 support)
+- Slower than GraalVM for compute-intensive tasks
+- No native Promise support
+
+### 2. Why Static Registry Access?
+
+**Decision:** Use `ClassToInstanceMap` for static registry access
+
+**Rationale:**
+- **Simplicity:** No dependency injection framework needed
+- **Type Safety:** Compile-time verification of registry types
+- **Accessibility:** Accessible from any plugin code
+- **Singleton:** Single source of truth
+
+**Trade-offs:**
+- Global state (harder to test in isolation)
+- Cannot swap implementations at runtime
+- Requires manual registration
+
+### 3. Why Chunk PDC for Placed Crates?
+
+**Decision:** Store placed crates in Chunk PDC
+
+**Rationale:**
+- **Automatic Association:** Chunks own their crates
+- **Persistence:** Survives server restarts
+- **Performance:** No database queries for crate lookup
+- **Simplicity:** No separate placed_crates table
+
+**Trade-offs:**
+- Limited to chunk-sized queries (can't query all crates in world easily)
+- PDC size limits (unlikely to hit in practice)
+- Chunk load required for data access
+
+### 4. Why ServicesManager for Managers?
+
+**Decision:** Register managers via Bukkit ServicesManager
+
+**Rationale:**
+- **Standard Pattern:** Follows Bukkit conventions
+- **Discovery:** Other plugins can find managers
+- **Lifecycle:** Bukkit manages service lifecycle
+- **Type Safety:** Retrieval by class
+
+**Trade-offs:**
+- Couples to Bukkit API
+- Limited lifecycle control
+- No priority/ordering guarantees
+
+### 5. Why Polymorphic YAML with Structura?
+
+**Decision:** Use Structura for YAML deserialization
+
+**Rationale:**
+- **Type Safety:** Compile-time validation of YAML structure
+- **Polymorphism:** `@Polymorphic` annotation for type field
+- **Validation:** Built-in validation (@Options, @DefaultInt, etc.)
+- **Maintainability:** YAML changes reflected in code
+
+**Trade-offs:**
+- Additional dependency
+- Learning curve for contributors
+- Less flexible than manual parsing
+
+---
+
+## Performance Considerations
+
+### 1. Memory Management
+
+**User Cache:**
+- Cleared on player quit (prevents memory leaks)
+- ConcurrentHashMap for thread-safe access
+- No unbounded growth
+
+**Registry Storage:**
+- HashMap-based (O(1) lookup)
+- Immutable after loading (no synchronization needed)
+- Cleared on reload (prevents memory bloat)
+
+**Animation State:**
+- Removed immediately after completion
+- No persistent animation history
+- Bounded by concurrent player count
+
+### 2. Database Optimization
+
+**Connection Pooling:**
+- HikariCP for connection management
+- Configurable pool size
+- Connection validation
+
+**Async Operations:**
+- All DB operations use CompletableFuture
+- No main thread blocking
+- Batch operations where possible
+
+**Caching:**
+- User data cached in memory
+- Keys stored locally (no query per check)
+- Opening history loaded on-demand
+
+### 3. JavaScript Performance
+
+**Interpreter Mode:**
+- No bytecode compilation overhead
+- Lower memory usage
+- Acceptable performance for animations/algorithms
+
+**Scope Reuse:**
+- Scopes created per execution (not per phase)
+- Shared Rhino Context across all scripts
+- Minimal GC pressure
+
+**Wrapper Objects:**
+- Lightweight wrappers (no heavy computation)
+- Direct method calls (no reflection)
+- Cached references where possible
+
+### 4. Event System
+
+**Listener Registration:**
+- Single listener instances (not per crate)
+- Priority-based execution
+- Ignore if not handling event
+
+**Event Frequency:**
+- CratePreOpenEvent: Once per open attempt
+- CrateOpenEvent: Once per successful open
+- RewardGeneratedEvent: Once per opening + rerolls
+- RewardGivenEvent: Once per completion
+
+---
+
+## Security Architecture
+
+### 1. JavaScript Sandbox
+
+**Threat Model:**
+- Malicious server admin uploading dangerous scripts
+- Exploiting Java reflection to bypass security
+- Accessing file system or network
+- Escaping sandbox via prototype pollution
+
+**Mitigations:**
+- **No Java Package Access:** `initSafeStandardObjects()` blocks java.*
+- **Method Blacklist:** Custom WrapFactory blocks dangerous methods
+- **Null Parent Scopes:** Prevents prototype chain manipulation
+- **Interpreter Mode:** No bytecode execution (no JIT exploits)
+- **Wrapper Objects:** Controlled API surface
+
+**Residual Risks:**
+- Infinite loops (no timeout mechanism)
+- Memory exhaustion (no heap limits)
+- Malicious animation logic (e.g., inventory spam)
+
+### 2. Inventory Security
+
+**Threat Model:**
+- Script manipulating unauthorized slots
+- Duplication exploits via inventory manipulation
+- Item theft via inventory swaps
+
+**Mitigations:**
+- **Slot Authorization:** Only authorized slots can be modified
+- **Wrapper Methods:** Controlled inventory access
+- **State Validation:** Animation state tracked by manager
+
+### 3. Data Security
+
+**Threat Model:**
+- SQL injection via user input
+- Race conditions in user data
+- Data loss on crashes
+
+**Mitigations:**
+- **Prepared Statements:** Sarah ORM uses prepared statements
+- **Concurrent Collections:** Thread-safe user cache
+- **Transactions:** Database operations in transactions
+- **Async Saves:** Non-blocking persistence
+
+### 4. Permission System
+
+**Threat Model:**
+- Unauthorized crate access
+- Permission bypass via exploits
+
+**Mitigations:**
+- **PermissionCondition:** Checks before opening
+- **Event Cancellation:** External plugins can prevent opens
+- **Key Validation:** Keys verified before consumption
+
+---
+
+## Future Considerations
+
+### Scalability
+
+**Potential Bottlenecks:**
+1. User cache growth (many concurrent players)
+2. Database connection pool exhaustion
+3. Animation executor (many concurrent animations)
+4. Chunk PDC size limits
+
+**Mitigation Strategies:**
+1. TTL-based cache eviction
+2. Configurable pool size
+3. Queue system for concurrent animations
+4. Migration to separate placed_crates table if needed
+
+### Extensibility
+
+**Extension Points:**
+1. Custom OpenConditions via polymorphic registry
+2. Custom Rewards via polymorphic registry
+3. Custom DisplayTypes via factory registry
+4. Custom Hooks via @AutoHook annotation
+5. Custom ItemsProviders via provider registry
+
+**API Stability:**
+- Semantic versioning for API module
+- Deprecation warnings before removal
+- Backward compatibility for configuration
+
+### Monitoring
+
+**Observability:**
+1. Debug logging for troubleshooting
+2. Event system for external tracking
+3. Metrics (opening counts, reward distribution)
+
+**Health Checks:**
+1. Database connection validation
+2. Registry population verification
+3. Script engine health checks
+
+---
+
+## Conclusion
+
+The zCrates architecture prioritizes:
+
+1. **Modularity** - Clear separation between API, implementation, and extensions
+2. **Security** - Sandboxed JavaScript, controlled API surface
+3. **Performance** - Async operations, efficient caching, optimized data structures
+4. **Extensibility** - Hook system, polymorphic types, provider pattern
+5. **Maintainability** - Type safety, clear abstractions, comprehensive documentation
+
+For detailed implementation guidance, see:
+- `DEVELOPER_GUIDE.md` - Extension and integration guide
+- `USER_GUIDE.md` - Configuration and usage guide
\ No newline at end of file
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
new file mode 100644
index 0000000..ce366d4
--- /dev/null
+++ b/docs/DEVELOPER_GUIDE.md
@@ -0,0 +1,1170 @@
+# zCrates - Developer Guide
+
+Comprehensive guide for developers extending or integrating with the zCrates plugin.
+
+## Table of Contents
+
+- [Architecture Overview](#architecture-overview)
+- [Project Structure](#project-structure)
+- [Core Systems](#core-systems)
+- [Extension Points](#extension-points)
+- [JavaScript API](#javascript-api)
+- [Database Schema](#database-schema)
+- [Security Model](#security-model)
+- [Best Practices](#best-practices)
+
+---
+
+## Architecture Overview
+
+zCrates follows a layered architecture with clear separation between API, implementation, and extensions:
+
+```
+┌─────────────────────────────────────────────────────┐
+│ Extensions Layer │
+│ (Hooks: ItemsAdder, MythicMobs, PlaceholderAPI) │
+└─────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────┐
+│ Business Logic Layer │
+│ (Managers, Commands, Listeners) │
+└─────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────┐
+│ Registry Layer │
+│ (Crates, Animations, Algorithms, Providers) │
+└─────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────┐
+│ Data Layer │
+│ (Storage, Repositories, DTOs) │
+└─────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────┐
+│ API Layer │
+│ (Interfaces, Events, Models, Services) │
+└─────────────────────────────────────────────────────┘
+```
+
+### Key Design Patterns
+
+1. **Registry Pattern** - Centralized component registration and retrieval
+2. **Manager Pattern** - Business logic orchestration via Bukkit ServicesManager
+3. **Wrapper Pattern** - Safe object exposure to JavaScript sandboxes
+4. **Factory Pattern** - Display creation via CrateDisplayFactory
+5. **Polymorphic Serialization** - Type-safe YAML deserialization with Structura
+6. **Repository Pattern** - Database access abstraction via Sarah ORM
+
+---
+
+## Project Structure
+
+```
+zCrates/
+├── api/ # Public API module (interfaces, models, registries)
+│ └── src/main/java/fr/traqueur/crates/api/
+│ ├── annotations/ # @AutoHook for hook auto-discovery
+│ ├── events/ # Event system (6 event types)
+│ ├── hooks/ # Hook interface
+│ ├── managers/ # Manager interfaces
+│ ├── models/ # Core models (Crate, Key, Reward, etc.)
+│ ├── providers/ # Provider interfaces
+│ ├── registries/ # Registry system
+│ ├── services/ # Service interfaces
+│ ├── serialization/ # PDC and serialization
+│ ├── settings/ # Configuration models
+│ └── storage/ # Storage layer interfaces
+│
+├── common/ # Shared implementations (displays, wrappers)
+│
+├── src/main/java/fr/traqueur/crates/
+│ ├── zCrates.java # Main plugin entry point
+│ ├── algorithms/ # Algorithm system
+│ ├── animations/ # Animation system
+│ ├── commands/ # Command implementations
+│ ├── engine/ # ZScriptEngine (secure Rhino)
+│ ├── listeners/ # Event listeners
+│ ├── managers/ # Manager implementations
+│ ├── models/ # Data models and wrappers
+│ ├── registries/ # Registry implementations
+│ ├── settings/ # Settings implementation
+│ ├── storage/ # Storage implementation
+│ └── views/ # zMenu integration
+│
+└── hooks/ # Optional plugin integrations
+ ├── ItemsAdder/ # ItemsAdder custom items
+ ├── MythicMobs/ # MythicMobs entity displays
+ ├── Nexo/ # Nexo custom items
+ ├── Oraxen/ # Oraxen custom items
+ ├── PlaceholderAPI/ # Placeholder support
+ └── zItems/ # zItems integration
+```
+
+---
+
+## Core Systems
+
+### 1. Registry System
+
+The registry system provides centralized component registration and retrieval using Guava's `ClassToInstanceMap`.
+
+#### Registry Interface
+
+```java
+public interface Registry {
+ void register(ID id, T item);
+ T getById(ID id);
+ Collection getAll();
+ void clear();
+
+ // Static registry access
+ static > T get(Class clazz);
+ static void register(Class extends Registry, ?>> clazz, Registry, ?> registry);
+}
+```
+
+#### FileBasedRegistry
+
+Abstract base class for file-loaded registries with folder hierarchy support:
+
+```java
+public abstract class FileBasedRegistry implements Registry {
+ // Auto-loads from folder
+ // Supports folder.properties metadata
+ // Hot-reload via reload()
+
+ protected abstract T loadFile(Path path) throws IOException;
+ protected abstract ID getId(T item);
+}
+```
+
+#### Available Registries
+
+| Registry | ID Type | Item Type | Source |
+|---------------------------------|-------------|---------------------|----------------------|
+| `AnimationsRegistry` | String | Animation | `animations/*.js` |
+| `RandomAlgorithmsRegistry` | String | RandomAlgorithm | `algorithms/*.js` |
+| `CratesRegistry` | String | Crate | `crates/*.yml` |
+| `ItemsProvidersRegistry` | String | ItemsProvider | Runtime registration |
+| `HooksRegistry` | String | Hook | Classpath scanning |
+| `CrateDisplayFactoriesRegistry` | DisplayType | CrateDisplayFactory | Runtime registration |
+
+**Usage Example:**
+
+```java
+// Get a registry
+CratesRegistry registry = Registry.get(CratesRegistry.class);
+
+// Retrieve items
+Crate crate = registry.getById("legendary");
+Collection allCrates = registry.getAll();
+
+// Register custom item
+registry.register("custom-crate", new MyCrate());
+```
+
+---
+
+### 2. Manager Layer
+
+Managers orchestrate business logic and are registered via Bukkit's ServicesManager.
+
+#### CratesManager
+
+Handles crate opening, animations, and placed crates.
+
+**Key Methods:**
+
+```java
+// Opening system
+OpenResult tryOpenCrate(Player player, Crate crate);
+void openCrate(Player player, Crate crate, Animation animation);
+void openPreview(Player player, Crate crate);
+
+// Reroll system
+boolean canReroll(Player player);
+int getRerollsRemaining(Player player);
+boolean reroll(Player player);
+
+// Animation state
+Optional getCurrentReward(Player player);
+boolean isAnimationCompleted(Player player);
+
+// Placed crates
+PlacedCrate placeCrate(String crateId, Location location, DisplayType type, String value, float yaw);
+void removePlacedCrate(PlacedCrate placedCrate);
+Optional findPlacedCrateByBlock(Block block);
+Optional findPlacedCrateByEntity(Entity entity);
+```
+
+**Internal State Management:**
+
+```java
+// Active openings stored in memory
+Map openedCrates;
+
+record OpenedCrate(
+ Crate crate,
+ Animation animation,
+ Reward reward,
+ int rerollsRemaining,
+ AnimationProgress progress
+);
+```
+
+#### UsersManager
+
+Handles player data and key management with async database operations.
+
+**Key Methods:**
+
+```java
+// User lifecycle
+User getUser(UUID uuid); // Synchronous (cache)
+CompletableFuture loadUser(UUID uuid); // Asynchronous (DB)
+CompletableFuture saveUser(User user); // Asynchronous (DB)
+
+// Opening persistence
+void persistCrateOpening(CrateOpening opening);
+```
+
+**Caching Strategy:**
+
+```java
+// In-memory cache
+ConcurrentHashMap cache;
+
+// Auto-load on join, auto-save on quit
+// Accessed synchronously via getUser()
+```
+
+---
+
+### 3. Script Engine System
+
+ZScriptEngine provides secure, sandboxed JavaScript execution using Mozilla Rhino.
+
+#### Security Features
+
+```java
+public class ZScriptEngine implements AutoCloseable {
+ // Rhino context with security restrictions
+ - initSafeStandardObjects() // No java.* access
+ - Optimization level -1 // Interpreter mode
+ - Null parent scopes // Isolation
+ - Custom WrapFactory // Method blocking
+}
+```
+
+**Blocked Methods:**
+- `getClass()`, `class`
+- `notify()`, `notifyAll()`, `wait()`
+- `clone()`, `finalize()`, `hashCode()`
+
+**Allowed JavaScript:**
+- ES6 features (arrow functions, let/const, template literals)
+- Standard objects (Array, Object, Math, JSON, Date, RegExp)
+- Context objects (player, inventory, crate, rewards, history)
+
+#### Script Execution Methods
+
+```java
+// Fire-and-forget
+void executeFunction(ScriptableObject scope, String functionName, Object... args);
+
+// With result
+ T executeFunctionWithResult(ScriptableObject scope, String functionName, Class returnType, Object... args);
+
+// Load script
+void evaluateFile(Path scriptPath, ScriptableObject scope);
+
+// Create isolated scope
+ScriptableObject createSecureScope();
+```
+
+**Usage Example:**
+
+```java
+ZScriptEngine engine = new ZScriptEngine();
+ScriptableObject scope = engine.createSecureScope();
+
+// Load script
+engine.evaluateFile(Paths.get("animations/my_animation.js"), scope);
+
+// Execute function
+AnimationContext context = new AnimationContext(player, crate, inventory);
+engine.executeFunction(scope, "onStart", context);
+```
+
+---
+
+### 4. Animation System
+
+Animations provide frame-by-frame visual effects during crate opening.
+
+#### Animation Model
+
+```java
+public record Animation(
+ String id,
+ List phases,
+ JavaScriptFunction onComplete,
+ JavaScriptFunction onCancel
+) {}
+
+public record AnimationPhase(
+ String name,
+ long duration, // milliseconds
+ int interval, // ticks between onTick calls
+ SpeedCurve speedCurve, // Easing function
+ JavaScriptFunction onStart,
+ JavaScriptFunction onTick,
+ JavaScriptFunction onComplete
+) {}
+
+public enum SpeedCurve {
+ LINEAR, // Constant speed
+ EASE_IN, // Starts slow, accelerates
+ EASE_OUT, // Starts fast, decelerates
+ EASE_IN_OUT // Slow → Fast → Slow
+}
+```
+
+#### AnimationExecutor
+
+Manages animation timing and phase transitions:
+
+```java
+public class AnimationExecutor {
+ void start(Player player, Animation animation, AnimationContext context);
+ void stop(Player player);
+ boolean isRunning(Player player);
+}
+```
+
+**Execution Flow:**
+1. Initialize phase 0
+2. Call `onStart(context)`
+3. Every `interval` ticks: call `onTick(context, tickData)`
+4. After `duration` ms: call `onComplete(context)` and move to next phase
+5. After all phases: call animation's `onComplete(context)`
+
+#### AnimationContext
+
+Provides safe access to runtime objects:
+
+```java
+public record AnimationContext(
+ PlayerWrapper player,
+ InventoryWrapper inventory,
+ CrateWrapper crate
+) {}
+```
+
+#### TickData
+
+Per-tick information passed to `onTick()`:
+
+```java
+public record TickData(
+ int tickNumber, // Current tick in phase
+ double progress, // 0.0 to 1.0 (with speed curve applied)
+ long elapsedTime // Milliseconds since phase start
+) {}
+```
+
+**JavaScript API:**
+
+```javascript
+animations.register("my_animation", {
+ phases: [
+ {
+ name: "spin",
+ duration: 3000,
+ interval: 2,
+ speedCurve: "EASE_OUT",
+ onStart: function(context) {
+ context.player().playSound("BLOCK_NOTE_BLOCK_PLING", 1.0, 1.0);
+ },
+ onTick: function(context, tickData) {
+ var progress = tickData.progress();
+ context.inventory().randomizeSlots();
+ },
+ onComplete: function(context) {
+ context.player().playSound("ENTITY_PLAYER_LEVELUP", 1.0, 1.0);
+ }
+ }
+ ],
+ onComplete: function(context) {
+ if (!context.crate().hasRerolls()) {
+ context.inventory().close(60);
+ }
+ },
+ onCancel: function(context) {
+ context.player().sendMessage("Animation cancelled");
+ }
+});
+```
+
+---
+
+### 5. Algorithm System
+
+Algorithms provide customizable reward selection logic.
+
+#### RandomAlgorithm Model
+
+```java
+public record RandomAlgorithm(
+ String id,
+ JavaScriptFunction selector
+) {}
+```
+
+#### AlgorithmContext
+
+Runtime context passed to selector function:
+
+```java
+public record AlgorithmContext(
+ RewardsWrapper rewards,
+ HistoryWrapper history,
+ String crateId,
+ UUID playerUuid
+) {}
+```
+
+**JavaScript API:**
+
+```javascript
+algorithms.register("pity_system", function(context) {
+ var rewards = context.rewards();
+ var history = context.history();
+
+ // Get last 50 openings
+ var recentOpenings = history.getRecent(50);
+
+ // Count openings without rare reward
+ var openingsWithoutRare = 0;
+ for (var i = 0; i < recentOpenings.size(); i++) {
+ var opening = recentOpenings.get(i);
+ if (opening.crateId() === context.crateId()) {
+ var reward = rewards.getById(opening.rewardId());
+ if (reward && reward.weight() > 5.0) {
+ openingsWithoutRare++;
+ } else {
+ break;
+ }
+ }
+ }
+
+ // If 50 openings without rare, guarantee one
+ if (openingsWithoutRare >= 50) {
+ return rewards.filterByMaxWeight(5.0).weightedRandom();
+ }
+
+ return rewards.weightedRandom();
+});
+```
+
+---
+
+### 6. Reward System
+
+Rewards define what players receive from crates.
+
+#### Reward Interface
+
+```java
+@Polymorphic
+public interface Reward extends Loadable {
+ String id();
+ double weight();
+ ItemStackWrapper displayItem();
+ void give(Player player);
+}
+```
+
+#### Implementations
+
+| Type | Class | Fields | Behavior |
+|----------|--------------------|--------------------------------|---------------------------|
+| ITEM | ItemReward | `ItemStackWrapper item` | Give single item |
+| ITEMS | ItemsListReward | `List items` | Give multiple items |
+| COMMAND | CommandReward | `String command` | Execute command |
+| COMMANDS | CommandsListReward | `List commands` | Execute multiple commands |
+
+**YAML Configuration:**
+
+```yaml
+rewards:
+ - type: ITEM
+ id: diamond_reward
+ weight: 10.0
+ display-item:
+ material: DIAMOND
+ item:
+ material: DIAMOND
+ amount: 5
+ display-name: "Diamonds"
+
+ - type: COMMAND
+ id: money_reward
+ weight: 20.0
+ display-item:
+ material: GOLD_INGOT
+ command: "eco give %player% 1000"
+```
+
+#### ItemStackWrapper System
+
+Supports both vanilla materials and custom item plugins:
+
+```yaml
+# Vanilla material
+item:
+ material: DIAMOND_SWORD
+ amount: 1
+ display-name: "Legendary Sword"
+ lore:
+ - "A powerful weapon"
+
+# Delegate to custom item plugin
+item:
+ copy-from:
+ plugin-name: "Oraxen"
+ item-id: "ruby_sword"
+ amount: 1
+ display-name: "Ruby Sword" # Override display name
+```
+
+**Available Delegates:**
+- `ItemsAdder` - ItemsAdder custom items
+- `Oraxen` - Oraxen custom items
+- `Nexo` - Nexo custom items
+- `zItems` - zItems custom items (via ItemsAdder provider)
+
+---
+
+### 7. Key System
+
+Keys control crate access and come in two types.
+
+#### Key Interface
+
+```java
+@Polymorphic
+public interface Key extends Loadable {
+ String name();
+ boolean has(Player player);
+ void give(Player player, int amount);
+ void remove(Player player);
+}
+```
+
+#### VirtualKey
+
+Database-stored key balance:
+
+```yaml
+key:
+ type: VIRTUAL
+ name: "legendary-key"
+```
+
+**Storage:**
+- Table: `user_keys`
+- Columns: `user_id`, `key_name`, `amount`
+- Managed by `UsersManager`
+
+#### PhysicKey
+
+Physical item in inventory:
+
+```yaml
+key:
+ type: PHYSIC
+ name: "legendary-physical-key"
+ item:
+ material: TRIPWIRE_HOOK
+ display-name: "Legendary Key"
+ lore:
+ - "Right-click on a crate"
+ glow: true
+ custom-model-data: 1001
+```
+
+**Matching:**
+- Uses PDC (PersistentDataContainer) for identification
+- Key: `zcrates:key_name`
+- Value: key name string
+
+---
+
+### 8. Condition System
+
+Conditions gate crate opening with arbitrary requirements.
+
+#### OpenCondition Interface
+
+```java
+@Polymorphic
+public interface OpenCondition extends Loadable {
+ boolean check(Player player, Crate crate);
+ void onOpen(Player player, Crate crate); // Side effects
+ String errorMessageKey();
+}
+```
+
+#### Built-in Conditions
+
+| Type | Class | Configuration | Behavior |
+|-------------|----------------------|-----------------------------------|----------------------|
+| PERMISSION | PermissionCondition | `permission: node` | Check permission |
+| COOLDOWN | CooldownCondition | `cooldown: 60000` | Rate-limit (ms) |
+| PLACEHOLDER | PlaceholderCondition | `placeholder, comparison, result` | PlaceholderAPI check |
+
+**YAML Configuration:**
+
+```yaml
+conditions:
+ - type: PERMISSION
+ permission: "zcrates.open.legendary"
+
+ - type: COOLDOWN
+ cooldown: 3600000 # 1 hour
+
+ - type: PLACEHOLDER
+ placeholder: "%player_level%"
+ comparison: GREATER_THAN_OR_EQUALS
+ result: "50"
+```
+
+---
+
+### 9. Display System
+
+Displays represent crates in the game world.
+
+#### CrateDisplay Interface
+
+```java
+public interface CrateDisplay {
+ void spawn();
+ void remove();
+ boolean matches(T value);
+ Location getLocation();
+}
+```
+
+#### CrateDisplayFactory Interface
+
+```java
+public interface CrateDisplayFactory {
+ CrateDisplay create(Location location, String value, float yaw);
+ boolean isValidValue(String value);
+ List getSuggestions();
+}
+```
+
+#### Built-in Display Types
+
+| DisplayType | Factory | Display | Value Type |
+|-------------|------------------------------|-----------------------|-------------------------|
+| BLOCK | BlockCrateDisplayFactory | BlockCrateDisplay | Material name |
+| ENTITY | EntityCrateDisplayFactory | EntityCrateDisplay | EntityType name |
+| MYTHIC_MOB | MythicMobCrateDisplayFactory | MythicMobCrateDisplay | MythicMob ID |
+| ITEMS_ADDER | IACrateDisplayFactory | IACrateDisplay | ItemsAdder furniture ID |
+| NEXO | NexoCrateDisplayFactory | NexoCrateDisplay | Nexo furniture ID |
+| ORAXEN | OraxenCrateDisplayFactory | OraxenCrateDisplay | Oraxen furniture ID |
+
+#### PlacedCrate Record
+
+```java
+public record PlacedCrate(
+ UUID id,
+ String crateId,
+ String worldName,
+ int x, int y, int z,
+ DisplayType displayType,
+ String displayValue,
+ float yaw
+) {}
+```
+
+**Persistence:**
+- Stored in Chunk PDC via `PlacedCrateDataType`
+- Loads on chunk load, unloads on chunk unload
+- Managed by `CratesManager`
+
+---
+
+### 10. Event System
+
+All events extend `CrateEvent` which provides player and crate context.
+
+#### Event Types
+
+| Event | Cancellable | When Fired | Purpose |
+|----------------------|-------------|--------------------|--------------------|
+| CratePreOpenEvent | Yes | Before opening | Prevent opening |
+| CrateOpenEvent | No | After key consumed | Track opening |
+| CrateRerollEvent | Yes | Before reroll | Prevent reroll |
+| RewardGeneratedEvent | No | Reward selected | Track selection |
+| RewardGivenEvent | No | Reward given | Track distribution |
+
+**Usage Example:**
+
+```java
+@EventHandler
+public void onRewardGiven(RewardGivenEvent event) {
+ Player player = event.getPlayer();
+ Crate crate = event.getCrate();
+ Reward reward = event.getReward();
+
+ // Log to external system
+ myLogger.log(player.getName() + " won " + reward.id() + " from " + crate.id());
+
+ // Broadcast rare rewards
+ if (reward.weight() < 5.0) {
+ Bukkit.broadcastMessage(
+ player.getName() + " won a rare reward from " + crate.displayName()
+ );
+ }
+}
+```
+
+---
+
+## Extension Points
+
+### 1. Creating Custom Hooks
+
+Hooks integrate with external plugins using the `@AutoHook` annotation.
+
+**Step 1: Create Hook Class**
+
+```java
+package fr.traqueur.crates.hooks.myplugin;
+
+import fr.traqueur.crates.api.annotations.AutoHook;
+import fr.traqueur.crates.api.hooks.Hook;
+import fr.traqueur.crates.api.registries.*;
+
+@AutoHook("MyPlugin") // Name of target plugin
+public class MyPluginHook implements Hook {
+
+ @Override
+ public void onEnable() {
+ // Register providers, factories, etc.
+ ItemsProvidersRegistry itemsRegistry = Registry.get(ItemsProvidersRegistry.class);
+ itemsRegistry.register("MyPlugin", (player, itemId) -> {
+ // Return ItemStack from your plugin
+ return MyPlugin.getAPI().getItem(itemId);
+ });
+
+ CrateDisplayFactoriesRegistry displayRegistry = Registry.get(CrateDisplayFactoriesRegistry.class);
+ displayRegistry.register(DisplayType.MY_TYPE, new MyDisplayFactory());
+ }
+}
+```
+
+**Step 2: Create build.gradle.kts (if in hooks/ directory)**
+
+```kotlin
+dependencies {
+ compileOnly("my-plugin:api:version")
+}
+```
+
+**Step 3: Hook Auto-Discovery**
+
+Hooks are automatically discovered if:
+- Located in package `fr.traqueur.crates` or subpackages
+- Annotated with `@AutoHook("PluginName")`
+- Target plugin is loaded
+
+### 2. Creating Custom Conditions
+
+**Step 1: Implement OpenCondition**
+
+```java
+public record MyCustomCondition(String myParam) implements OpenCondition {
+
+ @Override
+ public boolean check(Player player, Crate crate) {
+ // Your validation logic
+ return player.hasPermission("custom.permission");
+ }
+
+ @Override
+ public void onOpen(Player player, Crate crate) {
+ // Side effects after successful open
+ player.sendMessage("Custom condition passed!");
+ }
+
+ @Override
+ public String errorMessageKey() {
+ return "custom-condition-failed";
+ }
+}
+```
+
+**Step 2: Register Polymorphic Type**
+
+```java
+@Override
+public void onEnable() {
+ PolymorphicRegistry.get(OpenCondition.class, registry -> {
+ registry.register("MY_CUSTOM", MyCustomCondition.class);
+ });
+}
+```
+
+**Step 3: Use in YAML**
+
+```yaml
+conditions:
+ - type: MY_CUSTOM
+ myParam: "value"
+```
+
+### 3. Creating Custom ItemsProvider
+
+```java
+ItemsProvidersRegistry registry = Registry.get(ItemsProvidersRegistry.class);
+
+registry.register("MyPlugin", (player, itemId) -> {
+ // Resolve item from your plugin
+ MyCustomItem item = MyPlugin.getItemRegistry().get(itemId);
+ if (item == null) return null;
+
+ return item.build(player);
+});
+```
+
+**Usage in YAML:**
+
+```yaml
+item:
+ copy-from:
+ plugin-name: "MyPlugin"
+ item-id: "custom_sword"
+ display-name: "Custom Sword"
+```
+
+### 4. Creating Custom CrateDisplayFactory
+
+```java
+public class MyDisplayFactory implements CrateDisplayFactory {
+
+ @Override
+ public CrateDisplay create(Location location, String value, float yaw) {
+ return new MyDisplay(location, value, yaw);
+ }
+
+ @Override
+ public boolean isValidValue(String value) {
+ return MyPlugin.getAPI().exists(value);
+ }
+
+ @Override
+ public List getSuggestions() {
+ return MyPlugin.getAPI().getAllIds();
+ }
+}
+
+public class MyDisplay implements CrateDisplay {
+ private final Location location;
+ private final String value;
+ private MyObject object;
+
+ @Override
+ public void spawn() {
+ this.object = MyPlugin.getAPI().spawn(location, value);
+ }
+
+ @Override
+ public void remove() {
+ if (object != null) {
+ object.remove();
+ }
+ }
+
+ @Override
+ public boolean matches(String value) {
+ return this.value.equals(value);
+ }
+
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+}
+```
+
+**Registration:**
+
+```java
+CrateDisplayFactoriesRegistry registry = Registry.get(CrateDisplayFactoriesRegistry.class);
+registry.register(DisplayType.MY_TYPE, new MyDisplayFactory());
+```
+
+---
+
+## JavaScript API
+
+### Animation Context API
+
+```javascript
+// PlayerWrapper methods
+context.player().sendMessage("Message");
+context.player().sendTitle("Title", "Subtitle", fadeIn, stay, fadeOut);
+context.player().playSound("SOUND_NAME", volume, pitch);
+
+// InventoryWrapper methods
+context.inventory().setItem(slot, itemStack);
+context.inventory().setRandomItem(slot);
+context.inventory().setWinningItem(slot, itemStack);
+context.inventory().rotateItems([13, 14, 15, 16]); // Array of slots
+context.inventory().clear();
+context.inventory().clear(slot);
+context.inventory().highlightSlot(slot, "GLOWING");
+context.inventory().close(delayTicks);
+context.inventory().closeImmediately();
+
+// CrateWrapper methods
+context.crate().displayName();
+context.crate().id();
+context.crate().getReward(); // Current reward
+context.crate().rerollsRemaining();
+context.crate().hasRerolls();
+
+// TickData (in onTick callback)
+tickData.tickNumber(); // Current tick
+tickData.progress(); // 0.0 to 1.0 (with speed curve)
+tickData.elapsedTime(); // Milliseconds since phase start
+```
+
+### Algorithm Context API
+
+```javascript
+// RewardsWrapper methods
+var rewards = context.rewards();
+
+rewards.weightedRandom(); // Standard weighted random
+rewards.weightedRandom([reward1, reward2]); // From specific list
+rewards.getAll(); // All rewards as list
+rewards.size(); // Total count
+rewards.getById("reward_id"); // Find by ID
+rewards.filterByMinWeight(5.0); // Filter by min weight
+rewards.filterByMaxWeight(10.0); // Filter by max weight
+
+// HistoryWrapper methods
+var history = context.history();
+
+history.getRecent(50); // Last N openings
+history.getRecentForCrate("crate_id", 50); // Last N for specific crate
+history.getAll(); // All openings
+```
+
+### ArrayHelper Utility
+
+```javascript
+// Convert JavaScript arrays to Java int[] arrays
+var javaArray = ArrayHelper.toIntArray([1, 2, 3, 4, 5]);
+context.inventory().rotateItems(javaArray);
+```
+
+---
+
+## Database Schema
+
+### Tables
+
+**users**
+```sql
+CREATE TABLE users (
+ id UUID PRIMARY KEY
+);
+```
+
+**user_keys**
+```sql
+CREATE TABLE user_keys (
+ user_id UUID NOT NULL,
+ key_name VARCHAR(255) NOT NULL,
+ amount INT NOT NULL,
+ PRIMARY KEY (user_id, key_name),
+ FOREIGN KEY (user_id) REFERENCES users(id)
+);
+```
+
+**crate_openings**
+```sql
+CREATE TABLE crate_openings (
+ id UUID PRIMARY KEY,
+ player_id UUID NOT NULL,
+ crate_id VARCHAR(255) NOT NULL,
+ reward_id VARCHAR(255) NOT NULL,
+ timestamp BIGINT NOT NULL
+);
+```
+
+---
+
+## Security Model
+
+### JavaScript Sandbox
+
+**Blocked Java Access:**
+- No `java.*` package access
+- No `Packages` global
+- No reflection APIs
+
+**Blocked Methods:**
+- `getClass()`, `class`
+- `notify()`, `notifyAll()`, `wait()`
+- `clone()`, `finalize()`, `hashCode()`
+
+**Allowed:**
+- Standard JavaScript objects (Array, Object, Math, JSON, Date, RegExp)
+- ES6 features (arrow functions, let/const, template literals)
+- Context objects (player, inventory, crate, rewards, history)
+- Utility: `ArrayHelper`
+
+**Isolation:**
+- Each script execution uses isolated scope
+- Null parent scopes prevent prototype pollution
+- Interpreter mode (no bytecode generation)
+
+### Inventory Security
+
+**Slot Authorization:**
+- Only authorized slots can be manipulated during animation
+- Prevents inventory manipulation exploits
+
+### Data Security
+
+**PDC Storage:**
+- Chunk data persisted in PersistentDataContainer
+- Type-safe serialization via `PlacedCrateDataType`
+
+**User Cache:**
+- Proper lifecycle (load on join, save on quit)
+- Concurrent-safe operations
+
+**Database:**
+- Async operations via `CompletableFuture`
+- Connection pooling
+- Prepared statements (SQL injection protection)
+
+---
+
+## Best Practices
+
+### 1. Event Handling
+
+```java
+// Always check if event is cancelled
+@EventHandler(priority = EventPriority.HIGHEST)
+public void onRewardGiven(RewardGivenEvent event) {
+ if (event.isCancelled()) return; // If event becomes cancellable in future
+
+ // Your logic
+}
+
+// Listen at appropriate priority
+// LOWEST - Execute first
+// NORMAL - Default
+// HIGHEST - Execute last
+// MONITOR - Observe only (don't modify)
+```
+
+### 2. Registry Access
+
+```java
+// Cache registry references
+private final CratesRegistry cratesRegistry = Registry.get(CratesRegistry.class);
+
+// Don't call Registry.get() in loops
+for (Player player : players) {
+ Crate crate = cratesRegistry.getById("legendary"); // Good
+}
+```
+
+### 3. Async Operations
+
+```java
+// Use async for database operations
+usersManager.loadUser(uuid).thenAccept(user -> {
+ // Process user data
+}).exceptionally(throwable -> {
+ Logger.error("Failed to load user", throwable);
+ return null;
+});
+
+// Return to main thread for Bukkit API
+usersManager.loadUser(uuid).thenAcceptAsync(user -> {
+ player.sendMessage("Loaded!");
+}, Bukkit.getScheduler().getMainThreadExecutor(plugin));
+```
+
+### 4. Custom Animations
+
+```javascript
+// Use speedCurve for smooth animations
+{
+ speedCurve: "EASE_OUT", // Decelerates naturally
+ onTick: function(context, tickData) {
+ var progress = tickData.progress(); // 0.0 to 1.0
+ // Use progress for smooth transitions
+ }
+}
+
+// Clean up in onCancel
+onCancel: function(context) {
+ context.inventory().clear();
+ context.player().sendMessage("Cancelled");
+}
+```
+
+### 5. Custom Algorithms
+
+```javascript
+// Cache rewards list
+algorithms.register("my_algo", function(context) {
+ var rewards = context.rewards();
+ var allRewards = rewards.getAll(); // Cache
+
+ // Don't call getAll() in loops
+ for (var i = 0; i < 100; i++) {
+ // Use allRewards
+ }
+
+ return rewards.weightedRandom();
+});
+```
+
+### 6. Error Handling
+
+```java
+// Always handle Optional properly
+Optional optCrate = registry.getById("legendary");
+optCrate.ifPresent(crate -> {
+ // Process crate
+});
+
+// Or with fallback
+Crate crate = registry.getById("legendary")
+ .orElseThrow(() -> new IllegalStateException("Crate not found"));
+```
+
+---
+
+## Support
+
+For questions, issues, or contributions:
+- **Discord**: [https://discord.gg/PTSYTC53d3]
+- **GitHub**: [https://github.com/GroupeZ-dev/zCrates]
+- **Documentation**: See `USER_GUIDE.md`
\ No newline at end of file
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
new file mode 100644
index 0000000..700739a
--- /dev/null
+++ b/docs/USER_GUIDE.md
@@ -0,0 +1,834 @@
+# zCrates - User Guide
+
+Complete guide for Minecraft server administrators to configure and use the zCrates plugin.
+
+## Table of Contents
+
+- [Installation](#installation)
+- [Basic Configuration](#basic-configuration)
+- [Creating a Crate](#creating-a-crate)
+- [Key Types](#key-types)
+- [Reward Types](#reward-types)
+- [ItemStack Configuration with Delegates](#itemstack-configuration-with-delegates)
+- [Opening Conditions](#opening-conditions)
+- [Animations](#animations)
+- [Selection Algorithms](#selection-algorithms)
+- [Placing Crates](#placing-crates)
+- [Commands](#commands)
+- [Permissions](#permissions)
+- [Hooks & Integrations](#hooks--integrations)
+- [FAQ](#faq)
+
+---
+
+## Installation
+
+1. Download the `zCrates.jar` file
+2. Place it in your server's `plugins/` folder
+3. Restart the server
+4. Configuration files will be generated automatically
+
+### Requirements
+
+- **Minecraft**: 1.21+
+- **Server**: Paper/Purpur
+- **Java**: 21+
+- **Dependencies**: zMenu (bundled)
+
+---
+
+## Basic Configuration
+
+### config.yml
+
+```yaml
+# Enable debug messages
+debug: false
+
+# Database configuration
+database:
+ type: SQLITE # SQLITE, MYSQL, or MARIADB
+ table-prefix: "zcrates_"
+
+ # For MySQL/MariaDB only
+ host: "localhost"
+ port: 3306
+ database: "minecraft"
+ username: "root"
+ password: ""
+```
+
+### messages.yml
+
+Customize all plugin messages:
+
+```yaml
+no-key: "You don't have a key for this crate!"
+no-permission: "You don't have permission."
+condition-no-permission: "You don't have permission to open this crate!"
+condition-cooldown: "You must wait