diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..1fc0385
--- /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/325.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/README.md b/README.md
new file mode 100644
index 0000000..9e3403f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,264 @@
+# 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
+```
+
+## 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
+- **[API Reference](docs/API_REFERENCE.md)** - Developer documentation
+
+## Building
+
+```bash
+./gradlew build
+```
+
+Output: `target/zCrates.jar`
+
+## License
+
+Proprietary - All rights reserved.
+
+## Support
+
+- Discord: [Server Link]
+- GitHub Issues: [Report bugs]
\ 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..6d8a97f
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java
@@ -0,0 +1,46 @@
+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;
+ }
+
+ /**
+ * Constructs a CrateEvent with the specified player, crate, and async flag.
+ *
+ * @param player the player involved in the event
+ * @param crate the crate involved in the event
+ * @param async whether the event is asynchronous
+ */
+ public CrateEvent(Player player, Crate crate, boolean async) {
+ super(player, async);
+ 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..2be292d
--- /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 getCrateOpenings();
+
+ /**
+ * 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..af77274 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,12 +40,14 @@ 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);
}
/**
@@ -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..e950f20
--- /dev/null
+++ b/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java
@@ -0,0 +1,24 @@
+package fr.traqueur.crates.api.registries;
+
+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(JavaPlugin 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..0514b9e
--- /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 = ItemStack.of(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..a5eef42 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -26,7 +26,6 @@ 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")
}
@@ -38,16 +37,27 @@ 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")
+
+ /* 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 +65,19 @@ 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")
}
}
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..aaaff92
--- /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.toUpperCase();
+ 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/API_REFERENCE.md b/docs/API_REFERENCE.md
new file mode 100644
index 0000000..ca9f6de
--- /dev/null
+++ b/docs/API_REFERENCE.md
@@ -0,0 +1,635 @@
+# zCrates API Reference
+
+This document provides comprehensive API documentation for developers who want to integrate with or extend the zCrates plugin.
+
+## Table of Contents
+
+- [Getting Started](#getting-started)
+- [Core Interfaces](#core-interfaces)
+- [Managers](#managers)
+- [Models](#models)
+- [Events](#events)
+- [Registries](#registries)
+- [Conditions System](#conditions-system)
+- [Examples](#examples)
+
+---
+
+## Getting Started
+
+### Maven/Gradle Dependency
+
+```xml
+
+ fr.traqueur
+ zcrates-api
+ 1.0.0
+ provided
+
+```
+
+### Accessing the API
+
+```java
+// Get the plugin instance
+CratesPlugin plugin = (CratesPlugin) JavaPlugin.getPlugin(CratesPlugin.class);
+
+// Get managers
+CratesManager cratesManager = plugin.getManager(CratesManager.class);
+UsersManager usersManager = plugin.getManager(UsersManager.class);
+
+// Get registries
+CratesRegistry cratesRegistry = Registry.get(CratesRegistry.class);
+AnimationsRegistry animationsRegistry = Registry.get(AnimationsRegistry.class);
+```
+
+---
+
+## Core Interfaces
+
+### CratesPlugin
+
+The main plugin class providing access to all plugin services.
+
+```java
+public abstract class CratesPlugin extends JavaPlugin {
+
+ // Get a registered manager
+ public T getManager(Class clazz);
+
+ // Register an event listener
+ public void registerListener(Listener listener);
+
+ // Get the zMenu inventory manager
+ public abstract InventoryManager getInventoryManager();
+}
+```
+
+---
+
+## Managers
+
+### CratesManager
+
+Handles crate opening, animations, and placed crates.
+
+```java
+public interface CratesManager extends Manager {
+
+ /**
+ * Attempts to open a crate for a player.
+ * Checks key ownership, conditions, and fires events.
+ *
+ * @param player the player opening the crate
+ * @param crate the crate to open
+ * @return OpenResult indicating success or failure reason
+ */
+ OpenResult tryOpenCrate(Player player, Crate crate);
+
+ /**
+ * Force opens a crate (bypasses key check and conditions).
+ *
+ * @param player the player
+ * @param crate the crate
+ * @param animation the animation to play
+ */
+ void openCrate(Player player, Crate crate, Animation animation);
+
+ /**
+ * Opens the preview menu for a crate.
+ *
+ * @param player the player
+ * @param crate the crate to preview
+ */
+ void openPreview(Player player, Crate crate);
+
+ /**
+ * Gets the crate a player is currently previewing.
+ *
+ * @param player the player
+ * @return Optional containing the crate, or empty
+ */
+ Optional getPreviewingCrate(Player player);
+
+ /**
+ * Checks if a player can reroll their reward.
+ *
+ * @param player the player
+ * @return true if reroll is available
+ */
+ boolean canReroll(Player player);
+
+ /**
+ * Gets remaining rerolls for a player.
+ *
+ * @param player the player
+ * @return number of rerolls remaining
+ */
+ int getRerollsRemaining(Player player);
+
+ /**
+ * Performs a reroll for a player.
+ *
+ * @param player the player
+ * @return true if reroll succeeded
+ */
+ boolean reroll(Player player);
+
+ /**
+ * Gets the current reward for a player (during animation).
+ *
+ * @param player the player
+ * @return Optional containing the reward, or empty
+ */
+ Optional getCurrentReward(Player player);
+
+ /**
+ * Checks if animation has completed for a player.
+ *
+ * @param player the player
+ * @return true if animation is complete
+ */
+ boolean isAnimationCompleted(Player player);
+
+ // Placed Crates Management
+
+ /**
+ * Places a crate at a location.
+ *
+ * @param crateId the crate ID
+ * @param location the location
+ * @param displayType the display type (BLOCK, ENTITY, etc.)
+ * @param displayValue the display value (material/entity type)
+ * @param yaw the rotation
+ * @return the created PlacedCrate
+ */
+ PlacedCrate placeCrate(String crateId, Location location,
+ DisplayType displayType, String displayValue, float yaw);
+
+ /**
+ * Removes a placed crate.
+ *
+ * @param placedCrate the placed crate to remove
+ */
+ void removePlacedCrate(PlacedCrate placedCrate);
+
+ /**
+ * Finds a placed crate by block.
+ *
+ * @param block the block
+ * @return Optional containing the placed crate, or empty
+ */
+ Optional findPlacedCrateByBlock(Block block);
+
+ /**
+ * Finds a placed crate by entity.
+ *
+ * @param entity the entity
+ * @return Optional containing the placed crate, or empty
+ */
+ Optional findPlacedCrateByEntity(Entity entity);
+}
+```
+
+### UsersManager
+
+Handles player data and key management.
+
+```java
+public interface UsersManager extends Manager {
+
+ /**
+ * Gets a user from cache (synchronous).
+ *
+ * @param uuid the player UUID
+ * @return the User object
+ */
+ User getUser(UUID uuid);
+
+ /**
+ * Loads a user from database (asynchronous).
+ *
+ * @param uuid the player UUID
+ * @return CompletableFuture with the User
+ */
+ CompletableFuture loadUser(UUID uuid);
+
+ /**
+ * Saves a user to database (asynchronous).
+ *
+ * @param user the user to save
+ * @return CompletableFuture
+ */
+ CompletableFuture saveUser(User user);
+
+ /**
+ * Persists a crate opening to database.
+ *
+ * @param opening the opening to persist
+ */
+ void persistCrateOpening(CrateOpening opening);
+}
+```
+
+---
+
+## Models
+
+### Crate
+
+Represents a crate configuration.
+
+```java
+public interface Crate {
+ String id();
+ String displayName();
+ Key key();
+ Animation animation();
+ RandomAlgorithm algorithm();
+ String relatedMenu();
+ List rewards();
+ int maxRerolls();
+ List conditions();
+ ItemStackWrapper randomDisplay();
+ Reward generateReward(User user);
+}
+```
+
+### Key
+
+Represents a key type (virtual or physical).
+
+```java
+@Polymorphic
+public interface Key extends Loadable {
+ String name();
+ boolean has(Player player);
+ void give(Player player, int amount);
+ void remove(Player player);
+}
+```
+
+**Implementations:**
+- `VIRTUAL` - Stored in database, no physical item
+- `PHYSIC` - Physical item in player's inventory
+
+### Reward
+
+Represents a reward that can be won from a crate.
+
+```java
+@Polymorphic
+public interface Reward extends Loadable {
+ String id();
+ double weight();
+ ItemStackWrapper displayItem();
+ void give(Player player);
+}
+```
+
+**Implementations:**
+- `ITEM` - Single item reward
+- `ITEMS` - Multiple items reward
+- `COMMAND` - Single command reward
+- `COMMANDS` - Multiple commands reward
+
+### OpenCondition
+
+Represents a condition to open a crate.
+
+```java
+@Polymorphic
+public interface OpenCondition extends Loadable {
+
+ /**
+ * Checks if the player meets this condition.
+ *
+ * @param player the player
+ * @param crate the crate
+ * @return true if condition is met
+ */
+ boolean check(Player player, Crate crate);
+
+ /**
+ * Called when player successfully opens the crate.
+ * Used for side effects like setting cooldowns.
+ *
+ * @param player the player
+ * @param crate the crate
+ */
+ default void onOpen(Player player, Crate crate) {}
+
+ /**
+ * Gets the error message key for failed condition.
+ *
+ * @return the message key
+ */
+ String errorMessageKey();
+}
+```
+
+**Implementations:**
+- `PERMISSION` - Requires a permission node
+- `COOLDOWN` - Requires time since last opening
+
+### OpenResult
+
+Result of attempting to open a crate.
+
+```java
+public record OpenResult(Status status, @Nullable OpenCondition failedCondition) {
+
+ public enum Status {
+ SUCCESS, // Crate opened successfully
+ NO_KEY, // Player doesn't have the key
+ CONDITION_FAILED, // A condition was not met
+ EVENT_CANCELLED // CratePreOpenEvent was cancelled
+ }
+
+ public static OpenResult success();
+ public static OpenResult noKey();
+ public static OpenResult conditionFailed(OpenCondition condition);
+ public static OpenResult eventCancelled();
+
+ public boolean isSuccess();
+}
+```
+
+### User
+
+Represents a player's data.
+
+```java
+public interface User {
+ UUID uuid();
+ Map getKeys();
+ int getKeyAmount(String keyName);
+ void addKey(String keyName, int amount);
+ void removeKey(String keyName, int amount);
+ List getCrateOpenings(String crateId);
+ CrateOpening addCrateOpening(String crateId, String rewardId);
+}
+```
+
+### CrateOpening
+
+Represents a recorded crate opening.
+
+```java
+public record CrateOpening(
+ UUID userUuid,
+ String crateId,
+ String rewardId,
+ LocalDateTime openedAt
+) {}
+```
+
+---
+
+## Events
+
+All events extend `CrateEvent` which provides access to player and crate.
+
+### CratePreOpenEvent (Cancellable)
+
+Fired before a crate opens. Cancel to prevent opening.
+
+```java
+@EventHandler
+public void onPreOpen(CratePreOpenEvent event) {
+ Player player = event.getPlayer();
+ Crate crate = event.getCrate();
+
+ if (/* some condition */) {
+ event.setCancelled(true);
+ }
+}
+```
+
+### CrateOpenEvent
+
+Fired when a crate opens (after key consumed, menu opening).
+
+```java
+@EventHandler
+public void onOpen(CrateOpenEvent event) {
+ Player player = event.getPlayer();
+ Crate crate = event.getCrate();
+ Animation animation = event.getAnimation();
+}
+```
+
+### RewardGeneratedEvent
+
+Fired when a reward is generated (during animation).
+
+```java
+@EventHandler
+public void onRewardGenerated(RewardGeneratedEvent event) {
+ Player player = event.getPlayer();
+ Crate crate = event.getCrate();
+ Reward reward = event.getReward();
+ boolean isReroll = event.isReroll();
+}
+```
+
+### CrateRerollEvent (Cancellable)
+
+Fired before a reroll. Cancel to prevent.
+
+```java
+@EventHandler
+public void onReroll(CrateRerollEvent event) {
+ Player player = event.getPlayer();
+ Reward currentReward = event.getCurrentReward();
+ int rerollsRemaining = event.getRerollsRemaining();
+
+ if (/* some condition */) {
+ event.setCancelled(true);
+ }
+}
+```
+
+### RewardGivenEvent
+
+Fired when a reward is given to a player.
+
+```java
+@EventHandler
+public void onRewardGiven(RewardGivenEvent event) {
+ Player player = event.getPlayer();
+ Crate crate = event.getCrate();
+ Reward reward = event.getReward();
+
+ // Log, notify, etc.
+}
+```
+
+---
+
+## Registries
+
+### Accessing Registries
+
+```java
+// Get a registry
+CratesRegistry cratesRegistry = Registry.get(CratesRegistry.class);
+AnimationsRegistry animationsRegistry = Registry.get(AnimationsRegistry.class);
+RandomAlgorithmRegistry algorithmRegistry = Registry.get(RandomAlgorithmRegistry.class);
+
+// Get items from registry
+Crate crate = cratesRegistry.getById("example");
+Animation animation = animationsRegistry.getById("roulette");
+Collection allCrates = cratesRegistry.getAll();
+```
+
+### Creating Custom Hooks
+
+```java
+@AutoHook("YourPluginName")
+public class YourPluginHook implements Hook {
+
+ @Override
+ public void onEnable() {
+ // Register custom display factories, item providers, etc.
+ CrateDisplayFactoriesRegistry registry =
+ Registry.get(CrateDisplayFactoriesRegistry.class);
+ registry.registerGeneric(DisplayType.YOUR_TYPE, new YourDisplayFactory());
+ }
+}
+```
+
+---
+
+## Conditions System
+
+### Creating Custom Conditions
+
+1. Create the condition class:
+
+```java
+public record MyCustomCondition(String myParam) implements OpenCondition {
+
+ @Override
+ public boolean check(Player player, Crate crate) {
+ // Your logic here
+ return true;
+ }
+
+ @Override
+ public void onOpen(Player player, Crate crate) {
+ // Called after successful open
+ }
+
+ @Override
+ public String errorMessageKey() {
+ return "my-custom-error";
+ }
+}
+```
+
+2. Register in your plugin:
+
+```java
+PolymorphicRegistry.create(OpenCondition.class, registry -> {
+ registry.register("MY_CUSTOM", MyCustomCondition.class);
+});
+```
+
+3. Use in crate YAML:
+
+```yaml
+conditions:
+ - type: MY_CUSTOM
+ myParam: "value"
+```
+
+---
+
+## Examples
+
+### Opening a Crate Programmatically
+
+```java
+CratesManager manager = plugin.getManager(CratesManager.class);
+CratesRegistry registry = Registry.get(CratesRegistry.class);
+
+Crate crate = registry.getById("example");
+if (crate != null) {
+ OpenResult result = manager.tryOpenCrate(player, crate);
+
+ if (!result.isSuccess()) {
+ switch (result.status()) {
+ case NO_KEY -> player.sendMessage("You need a key!");
+ case CONDITION_FAILED -> {
+ OpenCondition condition = result.failedCondition();
+ // Handle specific condition failure
+ }
+ case EVENT_CANCELLED -> player.sendMessage("Opening was cancelled.");
+ }
+ }
+}
+```
+
+### Giving Keys to a Player
+
+```java
+UsersManager usersManager = plugin.getManager(UsersManager.class);
+User user = usersManager.getUser(player.getUniqueId());
+
+// For virtual keys
+user.addKey("example-key", 5);
+
+// For physical keys
+Crate crate = registry.getById("example");
+crate.key().give(player, 5);
+```
+
+### Listening for Rewards
+
+```java
+public class MyRewardListener implements Listener {
+
+ @EventHandler
+ public void onRewardGiven(RewardGivenEvent event) {
+ Player player = event.getPlayer();
+ Reward reward = event.getReward();
+
+ // Log to external system
+ myLogger.log(player.getName() + " won " + reward.id());
+
+ // Broadcast rare rewards
+ if (reward.weight() < 5.0) {
+ Bukkit.broadcastMessage(player.getName() + " won a rare reward!");
+ }
+ }
+}
+```
+
+### Creating a Custom Algorithm
+
+Create `plugins/zCrates/algorithms/my-algorithm.js`:
+
+```javascript
+algorithms.register("my-algorithm", function(context) {
+ var rewards = context.rewards();
+ var history = context.history();
+
+ // Get player's recent openings
+ var recentOpenings = history.getRecent(10);
+
+ // Custom logic
+ if (recentOpenings.length < 3) {
+ // New players get better odds
+ return rewards.weightedRandom(2.0); // 2x weight boost
+ }
+
+ return rewards.weightedRandom();
+});
+```
+
+---
+
+## Support
+
+For issues and feature requests, please use the GitHub issue tracker.
\ No newline at end of file
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
new file mode 100644
index 0000000..4f631ef
--- /dev/null
+++ b/docs/USER_GUIDE.md
@@ -0,0 +1,541 @@
+# 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)
+- [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