diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c9183e5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: Build Action + +on: + push: + branches: [ main, develop ] + pull_request: + types: [ opened, synchronize, reopened ] + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and Publish zCrates + uses: GroupeZ-dev/actions/.github/workflows/build.yml@main + with: + project-name: "zCrates" + publish: true + project-to-publish: "api:publish" + discord-avatar-url: "https://groupez.dev/storage/images/337.png" + secrets: + WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 7bc07ec..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Environment-dependent path to Maven home directory -/mavenHomeManager.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml deleted file mode 100644 index 4ea72a9..0000000 --- a/.idea/copilot.data.migration.agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml deleted file mode 100644 index 7ef04e2..0000000 --- a/.idea/copilot.data.migration.ask.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml deleted file mode 100644 index 1f2ea11..0000000 --- a/.idea/copilot.data.migration.ask2agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml deleted file mode 100644 index 8648f94..0000000 --- a/.idea/copilot.data.migration.edit.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index 104c42f..0000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 8627761..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml deleted file mode 100644 index a8bdd19..0000000 --- a/.idea/material_theme_project_new.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index f16dea7..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/MARKET.md b/MARKET.md new file mode 100644 index 0000000..9e9ab63 --- /dev/null +++ b/MARKET.md @@ -0,0 +1,288 @@ +# 🎁 zCrates + +[![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)](https://github.com/GroupeZ-dev/zCrates) +[![Minecraft](https://img.shields.io/badge/minecraft-1.21+-green.svg)](https://www.spigotmc.org/) +[![Java](https://img.shields.io/badge/java-21-orange.svg)](https://www.oracle.com/java/) +[![License](https://img.shields.io/badge/license-All%20Rights%20Reserved-red.svg)](https://groupez.dev) + +A modern, JavaScript-powered Minecraft crate plugin with stunning animations and smart reward systems. + +## 📋 Description + +**zCrates** brings a new level of customization to your Minecraft server's crate system. Built with cutting-edge technology and powered by **JavaScript**, create unique opening experiences that keep your players engaged and coming back for more. + +## ✨ Key Features + +### 🎯 What Makes zCrates Special + +- **🎬 JavaScript-Powered** - Create custom animations and reward algorithms without touching Java code +- **🎨 Beautiful Animations** - 4 built-in animations (instant, roulette, cascade, simple) with full customization +- **🎲 Smart Algorithms** - Pity system, progressive luck, and weighted random distribution +- **💎 Flexible Rewards** - Items, commands, or both - with weight-based probability +- **🔐 Advanced Conditions** - Permission, cooldown, and PlaceholderAPI support +- **🗄️ Database Support** - SQLite, MySQL, or MariaDB for persistent data +- **🎮 Beautiful GUI** - Powered by zMenu for stunning inventory interfaces +- **⚡ Hot Reload** - Update configurations without restarting your server + +### 🎁 Reward Types + +Create exciting rewards for your players: + +- **ITEM** - Give single items with custom enchantments and names +- **ITEMS** - Award multiple items at once (perfect for armor sets!) +- **COMMAND** - Execute any console command +- **COMMANDS** - Run multiple commands together + +All rewards support **weight-based probability** - a reward with weight 10 in a pool of 100 total weight = 10% chance! + +### 🔐 Opening Conditions + +Control who can open your crates: + +- **PERMISSION** - Require specific permissions +- **COOLDOWN** - Set time restrictions (hourly, daily, etc.) +- **PLACEHOLDER** - Advanced PlaceholderAPI comparisons (level, balance, world, etc.) + +Mix multiple conditions together - all must pass for the crate to open! + +### 🎬 Animation System + +**4 Built-in Animations:** + +- **instant** - Quick reward reveal (500ms) +- **roulette** - Classic spinning wheel (11100ms) +- **cascade** - Progressive fill effect (3900ms) +- **simple** - Basic display (600ms) + +Want more? **Create your own** with JavaScript - full control over timing, effects, and display! + +### 🎲 Smart Reward Algorithms + +**3 Intelligent Systems:** + +| Algorithm | How It Works | Best For | +|----------------------|--------------------------------------|---------------------| +| **weighted** | Standard probability-based selection | General crates | +| **pity_system** | Guarantees legendary after 10 tries | Premium/paid crates | +| **progressive_luck** | Increases rare chances over time | Event crates | + +**Create custom algorithms** with JavaScript to implement your own reward logic! + +### 🔑 Key Management + +Choose your key type: + +- **VIRTUAL** - Stored in database (no inventory clutter) +- **PHYSIC** - Real items players can trade + +### 🖼️ Display Options + +**6 Ways to Display Your Crates:** + +- BLOCK - Classic block display +- ENTITY - Animated mob display +- MYTHIC_MOB - MythicMobs integration +- ITEMS_ADDER - ItemsAdder custom items +- ORAXEN - Oraxen custom items +- NEXO - Nexo custom items + +## 🎬 Showcase + +> **Note:** GIFs are sped up for demonstration purposes - actual animations run smoother in-game! + +### Virtual Crate Opening +Experience seamless crate openings with virtual keys - no inventory clutter, just pure excitement! Watch as the roulette animation reveals your reward. + +![Virtual Crate Opening](https://img.groupez.dev/zcrates/virtual-crate.gif) + +### Physical Crate Opening +Prefer tangible keys? Physical keys give players tradeable items they can hold, share, or collect before opening their crates. + +![Physical Crate Opening](https://img.groupez.dev/zcrates/physic-opening.gif) + +### Reroll System +Don't like your reward? The reroll feature lets players try their luck again for a chance at something better! + +![Reroll Feature](https://img.groupez.dev/zcrates/reroll.gif) + +### Block Display Placement +Place crates as interactive blocks anywhere in your world - perfect for spawn areas, shops, or event locations. + +![Block Crate Placement](https://img.groupez.dev/zcrates/place-block-crate.gif) + +### Entity Display Placement +Make your crates stand out with animated entity displays - floating, rotating, and eye-catching! + +![Entity Crate Placement](https://img.groupez.dev/zcrates/place-entity-crate.gif) + +### MythicMobs Display Placement +Integrate with MythicMobs for custom creature displays - turn your crates into unique, custom mob presentations! + +![MythicMobs Crate Placement](https://img.groupez.dev/zcrates/place-mm-crate.gif) + +## 📦 Requirements + +- **Server**: Spigot or Paper 1.21+ +- **Java**: Version 21 or higher +- **Required**: [zMenu](https://www.spigotmc.org/resources/zmenu.109103/) + +**Optional Integrations:** +- [PlaceholderAPI](https://www.spigotmc.org/resources/placeholderapi.6245/) - For PLACEHOLDER conditions +- [MythicMobs](https://www.spigotmc.org/resources/mythicmobs.5702/) - For MYTHIC_MOB display +- [ItemsAdder](https://www.spigotmc.org/resources/itemsadder.73355/) - For custom item displays +- [Oraxen](https://www.spigotmc.org/resources/oraxen.72448/) - For custom item displays +- [Nexo](https://www.spigotmc.org/resources/nexo.115448/) - For custom item displays + +## 🚀 Quick Start + +1. Download zCrates and install **zMenu** (required) +2. Place both JARs in your `plugins/` folder +3. Restart your server +4. Edit configurations in `plugins/zCrates/` +5. Create your first crate and start rewarding players! + +## ⚙️ Configuration Example + +Simple crate configuration: + +```yaml +id: my_crate +animation: roulette +algorithm: weighted +display-name: "Epic Crate" + +key: + type: VIRTUAL + name: epic_key + +conditions: + - type: PERMISSION + permission: "crates.epic" + - type: COOLDOWN + cooldown: 3600000 # 1 hour + - type: PLACEHOLDER + placeholder: "%player_level%" + comparison: GREATER_THAN_OR_EQUALS + result: "10" + +rewards: + - type: ITEM + id: diamond_sword + weight: 20.0 + display-item: + material: DIAMOND_SWORD + name: "Legendary Sword" + item: + material: DIAMOND_SWORD + enchantments: + - SHARPNESS:5 + - UNBREAKING:3 + + - type: COMMAND + id: vip_rank + weight: 5.0 + display-item: + material: GOLD_INGOT + name: "VIP Rank" + command: "lp user %player% parent set vip" + + - type: ITEMS + id: armor_set + weight: 10.0 + display-item: + material: DIAMOND_CHESTPLATE + name: "Full Armor Set" + items: + - material: DIAMOND_HELMET + - material: DIAMOND_CHESTPLATE + - material: DIAMOND_LEGGINGS + - material: DIAMOND_BOOTS +``` + +## 🎮 Commands + +| Command | Description | +|-----------------------------------------------|----------------------------| +| `/zcrates` | Show plugin info | +| `/zcrates reload` | Reload configurations | +| `/zcrates place ` | Place crate at location | +| `/zcrates remove` | Remove crate at target | +| `/zcrates purge` | Remove all crates in chunk | +| `/zcrates givekeys ` | Give keys to player | +| `/zcrates open [force]` | Force crate opening | + +**Aliases:** `/zc`, `/crates` + +## 🎨 Customization + +### Create Custom Animations + +Write your own animations in JavaScript: + +```javascript +animations.register("my-animation", { + phases: [ + { + name: "spinning", + duration: 3000, + interval: 50, + speedCurve: "EASE_OUT", + onTick: (context, tickData) => { + // Your animation logic + } + } + ] +}); +``` + +### Create Custom Algorithms + +Implement your own reward logic: + +```javascript +algorithms.register("my-algorithm", (context) => { + // Your reward selection logic + return context.rewards().weightedRandom(); +}); +``` + +## 💡 Why Choose zCrates? + +✅ **Easy to Use** - YAML configuration with clear examples +✅ **Highly Customizable** - JavaScript support for unlimited possibilities +✅ **Performance** - Built with Java 21 and optimized for large servers +✅ **Modern** - MiniMessage support for beautiful text formatting +✅ **Reliable** - Database persistence ensures data safety +✅ **Supported** - Active development and updates + +## 🤝 Support & Links + +- **Website**: [groupez.dev](https://groupez.dev) +- **Author**: Traqueur_ +- **Report Issues**: [GitHub Issues](https://github.com/GroupeZ-dev/zCrates/issues) + +--- + +## 🔄 What's Included + +✅ 4 reward types (ITEM, ITEMS, COMMAND, COMMANDS) +✅ 3 condition types (PERMISSION, COOLDOWN, PLACEHOLDER) +✅ JavaScript animation system with 4 built-in animations +✅ 3 smart reward algorithms +✅ Virtual and physical key support +✅ 6 display type integrations +✅ Full database persistence +✅ Beautiful GUI with zMenu +✅ Hot reload system +✅ MiniMessage formatting +✅ PlaceholderAPI integration + +--- + +**Ready to elevate your server's crate experience?** + +Download now and start creating unforgettable moments for your players! 🎉 + +--- + +Developed with ❤️ by [Traqueur_](https://groupez.dev) diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb36af4 --- /dev/null +++ b/README.md @@ -0,0 +1,277 @@ +# zCrates + +A modern, feature-rich crate/loot box plugin for Minecraft Paper servers with JavaScript-powered animations and algorithms. + +## Features + +- **JavaScript Animations** - Create custom animations using ES6 JavaScript +- **Custom Algorithms** - Define reward selection logic with JavaScript +- **Multiple Key Types** - Virtual (database) and physical (item) keys +- **Flexible Rewards** - Items, commands, or multiple of each +- **Opening Conditions** - Permissions, cooldowns, and custom conditions +- **Reroll System** - Allow players to reroll for a different reward +- **Multi-Display Support** - Blocks, entities, MythicMobs, ItemsAdder, Nexo, Oraxen +- **PlaceholderAPI** - Built-in placeholders for keys and statistics +- **Database Support** - SQLite, MySQL, MariaDB + +## Requirements + +- **Minecraft**: 1.21+ +- **Server**: Paper/Purpur +- **Java**: 21+ +- **Dependencies**: zMenu (bundled) + +## Installation + +1. Download `zCrates.jar` +2. Place in `plugins/` folder +3. Restart server +4. Configure crates in `plugins/zCrates/crates/` + +## Quick Start + +### Creating a Crate + +Create `plugins/zCrates/crates/example.yml`: + +```yaml +id: example +animation: roulette +algorithm: weighted +display-name: "Example Crate" +max-rerolls: 1 + +key: + type: VIRTUAL + name: "example-key" + +related-menu: crate-menu-example + +rewards: + - type: ITEM + id: diamond-reward + weight: 10.0 + display-item: + material: DIAMOND + name: "Diamonds" + item: + material: DIAMOND + amount: 5 + + - type: COMMAND + id: money-reward + weight: 20.0 + display-item: + material: GOLD_INGOT + name: "$1000" + command: "eco give %player% 1000" +``` + +### Placing a Crate + +```bash +/zcrates place example BLOCK CHEST +``` + +### Giving Keys + +```bash +/zcrates give example 5 +``` + +## Commands + +| Command | Description | Permission | +|-------------------------------------------|--------------------------|-------------------------| +| `/zcrates reload` | Reload configurations | `crates.command.reload` | +| `/zcrates open [force]` | Open a crate | `crates.command.open` | +| `/zcrates give ` | Give keys | `crates.command.give` | +| `/zcrates place [value]` | Place a crate | `crates.command.place` | +| `/zcrates remove` | Remove placed crate | `crates.command.remove` | +| `/zcrates purge` | Remove all placed crates | `crates.command.purge` | + +## Key Types + +### Virtual Key +Stored in database, no physical item. +```yaml +key: + type: VIRTUAL + name: "my-key" +``` + +### Physical Key +Item in player inventory. +```yaml +key: + type: PHYSIC + name: "my-key" + item: + material: TRIPWIRE_HOOK + name: "Legendary Key" + glow: true +``` + +## Reward Types + +| Type | Description | +|------------|-----------------------| +| `ITEM` | Single item reward | +| `ITEMS` | Multiple items reward | +| `COMMAND` | Single command | +| `COMMANDS` | Multiple commands | + +## Display Types + +| Type | Description | +|---------------|-----------------------------------| +| `BLOCK` | Placed block (CHEST, ENDER_CHEST) | +| `ENTITY` | Spawned entity (ARMOR_STAND) | +| `MYTHIC_MOB` | MythicMobs entity | +| `ITEMS_ADDER` | ItemsAdder furniture | +| `NEXO` | Nexo furniture | +| `ORAXEN` | Oraxen furniture | + +## JavaScript API + +### Custom Animation + +Create `plugins/zCrates/animations/my-animation.js`: + +```javascript +animations.register("my-animation", { + phases: [ + { + name: "spin", + duration: 3000, + interval: 2, + speedCurve: "EASE_OUT", + onTick: function(context, tickData) { + context.inventory().randomizeSlots(); + } + } + ], + onComplete: function(context) { + context.inventory().close(60); + } +}); +``` + +### Custom Algorithm + +Create `plugins/zCrates/algorithms/my-algorithm.js`: + +```javascript +algorithms.register("my-algorithm", function(context) { + var rewards = context.rewards(); + var history = context.history(); + + // Custom selection logic + return rewards.weightedRandom(); +}); +``` + +## Opening Conditions + +```yaml +conditions: + - type: PERMISSION + permission: "zcrates.open.vip" + - type: COOLDOWN + cooldown: 3600000 # 1 hour in ms + - type: PLACEHOLDER # Requires PlaceholderAPI hook + placeholder: "%player_level%" + comparison: GREATER_THAN_OR_EQUALS # EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS + result: "10" +``` + +**Comparison Types for PLACEHOLDER:** +- `EQUALS` - String/numeric equality (default) +- `NOT_EQUALS` - String/numeric inequality +- `GREATER_THAN` - Numeric comparison (>) +- `LESS_THAN` - Numeric comparison (<) +- `GREATER_THAN_OR_EQUALS` - Numeric comparison (>=) +- `LESS_THAN_OR_EQUALS` - Numeric comparison (<=) + +## PlaceholderAPI + +Numbers are formatted in compact notation (1.2K, 3.5M, etc.). Use `-raw` suffix for raw numbers. + +| Placeholder | Description | Example | +|--------------------------------|---------------------------------------|-----------| +| `%zcrates__keys%` | Key count (formatted) | `1.2K` | +| `%zcrates__keys-raw%` | Key count (raw) | `1234` | +| `%zcrates__opened%` | Crate openings (formatted) | `3.5M` | +| `%zcrates__opened-raw%` | Crate openings (raw) | `3500000` | +| `%zcrates_crates_opened%` | Total openings all crates (formatted) | `15K` | +| `%zcrates_crates_opened_raw%` | Total openings all crates (raw) | `15000` | + +**Examples:** +``` +%zcrates_legendary_keys% → 5 +%zcrates_legendary_opened% → 1.2K +%zcrates_crates_opened% → 3.5M +``` + +## API Usage + +### Maven Dependency + +```xml + + fr.traqueur + zcrates-api + 1.0.0 + provided + +``` + +### Accessing the API + +```java +CratesPlugin plugin = (CratesPlugin) Bukkit.getPluginManager().getPlugin("zCrates"); +CratesManager cratesManager = plugin.getManager(CratesManager.class); + +// Open a crate +Crate crate = Registry.get(CratesRegistry.class).getById("example"); +OpenResult result = cratesManager.tryOpenCrate(player, crate); + +// Give keys +UsersManager usersManager = plugin.getManager(UsersManager.class); +User user = usersManager.getUser(player.getUniqueId()); +user.addKeys("example-key", 5); +``` + +### Listening to Events + +```java +@EventHandler +public void onRewardGiven(RewardGivenEvent event) { + Player player = event.getPlayer(); + Reward reward = event.getReward(); + // Log, broadcast, etc. +} +``` + +## Documentation + +- **[User Guide](docs/USER_GUIDE.md)** - Complete configuration guide for server administrators +- **[Developer Guide](docs/DEVELOPER_GUIDE.md)** - Extension and integration guide +- **[Architecture](docs/ARCHITECTURE.md)** - High-level architecture overview + +## Building + +```bash +./gradlew build +``` + +Output: `target/zCrates.jar` + +## License + +Proprietary - All rights reserved. + +## Support + +- **Discord**: [https://discord.gg/PTSYTC53d3] +- **GitHub**: [https://github.com/GroupeZ-dev/zCrates] \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 8650400..1e68b52 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,3 +1,7 @@ +plugins { + id("re.alwyn974.groupez.publish") version "1.0.0" +} + rootProject.extra.properties["sha"]?.let { sha -> version = sha } diff --git a/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java b/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java index 3eeb2b7..a61ef84 100644 --- a/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java +++ b/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java @@ -1,9 +1,25 @@ package fr.traqueur.crates.api; +import fr.maxlego08.menu.api.InventoryManager; import fr.traqueur.crates.api.managers.Manager; +import org.bukkit.event.Listener; import org.bukkit.plugin.ServicePriority; import org.bukkit.plugin.java.JavaPlugin; +/** + * Base class for zItems plugins, providing manager registration and retrieval. + * + *

This class extends {@link JavaPlugin} and offers methods to register + * and retrieve various manager implementations used within the zItems ecosystem. + * Managers are registered as services with Bukkit's {@code ServicesManager}, + * allowing for easy access by other plugins.

+ * + *

Plugins extending this class can leverage the provided methods to + * manage effects, items, and other functionalities through their respective managers.

+ * + * @see Manager + * @see org.bukkit.plugin.ServicesManager + */ public abstract class CratesPlugin extends JavaPlugin { /** @@ -67,4 +83,19 @@ public I getManager(Class clazz) { return rsp.getProvider(); } + /** + * Registers an event listener with the plugin's event system. + * + * @param listener the listener to register + */ + public void registerListener(Listener listener) { + this.getServer().getPluginManager().registerEvents(listener, this); + } + + /** + * Retrieves the InventoryManager associated with this plugin. + * + * @return the InventoryManager instance + */ + public abstract InventoryManager getInventoryManager(); } diff --git a/api/src/main/java/fr/traqueur/crates/api/annotations/AutoHook.java b/api/src/main/java/fr/traqueur/crates/api/annotations/AutoHook.java new file mode 100644 index 0000000..5214702 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/annotations/AutoHook.java @@ -0,0 +1,24 @@ +package fr.traqueur.crates.api.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark an {@link fr.traqueur.crates.api.hooks.Hook} for automatic registration. + * When the specified plugin is present, the hook will be enabled automatically. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface AutoHook { + + /** + * The name of the plugin that this hook integrates with. + * This should match the exact plugin name as it appears in the plugin.yml. + * The hook will only be enabled if a plugin with this name is present. + * + * @return the plugin name + */ + String value(); +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java new file mode 100644 index 0000000..12d0ec0 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java @@ -0,0 +1,34 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerEvent; + +/** + * Base class for all crate-related events. + */ +public abstract class CrateEvent extends PlayerEvent { + + /** The crate involved in this event. */ + protected final Crate crate; + + /** + * Constructs a CrateEvent with the specified player and crate. + * + * @param player the player involved in the event + * @param crate the crate involved in the event + */ + public CrateEvent(Player player, Crate crate) { + super(player); + this.crate = crate; + } + + /** + * Gets the crate involved in this event. + * + * @return the crate + */ + public Crate getCrate() { + return crate; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CrateOpenEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CrateOpenEvent.java new file mode 100644 index 0000000..032a782 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateOpenEvent.java @@ -0,0 +1,59 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.animations.Animation; +import fr.traqueur.crates.api.models.crates.Crate; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a player opens a crate (after key is consumed, menu is about to open). + * This event is not cancellable. + */ +public class CrateOpenEvent extends CrateEvent { + + /** The handler list for this event. */ + private static final HandlerList HANDLERS = new HandlerList(); + /** The animation that will be played when the crate is opened. */ + private final Animation animation; + + /** + * Constructs a CrateOpenEvent with the specified player, crate, and animation. + * + * @param player the player opening the crate + * @param crate the crate being opened + * @param animation the animation that will be played + */ + public CrateOpenEvent(Player player, Crate crate, Animation animation) { + super(player, crate); + this.animation = animation; + } + + /** + * Gets the animation that will be played. + * + * @return the animation + */ + public Animation getAnimation() { + return animation; + } + + /** + * Gets the handler list for this event. + * + * @return the handler list + */ + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the static handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CratePreOpenEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CratePreOpenEvent.java new file mode 100644 index 0000000..975672b --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/CratePreOpenEvent.java @@ -0,0 +1,54 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called before a player opens a crate. + * This event is cancellable - if cancelled, the crate will not be opened + * and the key will not be consumed. + */ +public class CratePreOpenEvent extends CrateEvent implements Cancellable { + + /** The handler list for this event. */ + private static final HandlerList HANDLERS = new HandlerList(); + /** Indicates whether the event has been cancelled. */ + private boolean cancelled = false; + + /** + * Constructs a CratePreOpenEvent with the specified player and crate. + * + * @param player the player attempting to open the crate + * @param crate the crate being opened + */ + public CratePreOpenEvent(Player player, Crate crate) { + super(player, crate); + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the static handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CrateRerollEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CrateRerollEvent.java new file mode 100644 index 0000000..e5aa85b --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateRerollEvent.java @@ -0,0 +1,76 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import fr.traqueur.crates.api.models.crates.Reward; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a player attempts to reroll their reward. + * This event is cancellable - if cancelled, the reroll will not occur. + */ +public class CrateRerollEvent extends CrateEvent implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + private boolean cancelled = false; + private final Reward currentReward; + private final int rerollsRemaining; + + /** + * Constructs a CrateRerollEvent with the specified player, crate, current reward, and rerolls remaining. + * + * @param player the player attempting the reroll + * @param crate the crate being rerolled + * @param currentReward the current reward that will be replaced if reroll succeeds + * @param rerollsRemaining the number of rerolls remaining after this one + */ + public CrateRerollEvent(Player player, Crate crate, Reward currentReward, int rerollsRemaining) { + super(player, crate); + this.currentReward = currentReward; + this.rerollsRemaining = rerollsRemaining; + } + + /** + * Gets the current reward that will be replaced if reroll succeeds. + * + * @return the current reward + */ + public Reward getCurrentReward() { + return currentReward; + } + + /** + * Gets the number of rerolls remaining after this reroll (if it succeeds). + * + * @return rerolls remaining after this one + */ + public int getRerollsRemaining() { + return rerollsRemaining; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the static handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/RewardGeneratedEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/RewardGeneratedEvent.java new file mode 100644 index 0000000..95e860c --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/RewardGeneratedEvent.java @@ -0,0 +1,64 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import fr.traqueur.crates.api.models.crates.Reward; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a reward is generated for a player (after animation starts). + * This can be used to track reward distribution or modify logging. + */ +public class RewardGeneratedEvent extends CrateEvent { + + private static final HandlerList HANDLERS = new HandlerList(); + private final Reward reward; + private final boolean isReroll; + + /** + * Constructs a new RewardGeneratedEvent. + * + * @param player the player receiving the reward + * @param crate the crate from which the reward is generated + * @param reward the reward that was generated + * @param isReroll whether this reward was generated as a result of a reroll + */ + public RewardGeneratedEvent(Player player, Crate crate, Reward reward, boolean isReroll) { + super(player, crate); + this.reward = reward; + this.isReroll = isReroll; + } + + /** + * Gets the reward that was generated. + * + * @return the reward + */ + public Reward getReward() { + return reward; + } + + /** + * Whether this reward was generated as a result of a reroll. + * + * @return true if this is a reroll + */ + public boolean isReroll() { + return isReroll; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/RewardGivenEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/RewardGivenEvent.java new file mode 100644 index 0000000..7c94b59 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/RewardGivenEvent.java @@ -0,0 +1,52 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import fr.traqueur.crates.api.models.crates.Reward; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a reward is given to a player (when they close the inventory). + * This event fires after the reward has been given. + */ +public class RewardGivenEvent extends CrateEvent { + + private static final HandlerList HANDLERS = new HandlerList(); + private final Reward reward; + + /** + * Constructs a new RewardGivenEvent. + * + * @param player the player receiving the reward + * @param crate the crate from which the reward is given + * @param reward the reward that was given + */ + public RewardGivenEvent(Player player, Crate crate, Reward reward) { + super(player, crate); + this.reward = reward; + } + + /** + * Gets the reward that was given to the player. + * + * @return the reward + */ + public Reward getReward() { + return reward; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/hooks/Hook.java b/api/src/main/java/fr/traqueur/crates/api/hooks/Hook.java new file mode 100644 index 0000000..8f37532 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/hooks/Hook.java @@ -0,0 +1,13 @@ +package fr.traqueur.crates.api.hooks; + +/** + * Interface representing a hook that can be enabled. + */ +public interface Hook { + + /** + * Method to be called when the hook is enabled. + */ + void onEnable(); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/managers/CratesManager.java b/api/src/main/java/fr/traqueur/crates/api/managers/CratesManager.java new file mode 100644 index 0000000..b71cddd --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/managers/CratesManager.java @@ -0,0 +1,263 @@ +package fr.traqueur.crates.api.managers; + +import fr.traqueur.crates.api.models.crates.Crate; +import fr.traqueur.crates.api.models.crates.OpenResult; +import fr.traqueur.crates.api.models.animations.Animation; +import fr.traqueur.crates.api.models.crates.Reward; +import fr.traqueur.crates.api.models.placedcrates.DisplayType; +import fr.traqueur.crates.api.models.placedcrates.PlacedCrate; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +import java.util.List; +import java.util.Optional; + +/** + * Manager responsible for all crate-related operations including opening crates, + * managing animations, handling rerolls, and managing placed crates in the world. + * + *

This is the main entry point for interacting with the crate system programmatically. + * Obtain an instance via {@code plugin.getManager(CratesManager.class)}.

+ * + * @see OpenResult + * @see Crate + * @see PlacedCrate + */ +public non-sealed interface CratesManager extends Manager { + + // ==================== Crate Opening ==================== + + /** + * Attempts to open a crate for a player with all validation checks. + * + *

This method performs the following checks in order:

+ *
    + *
  1. Verifies the player has the required key
  2. + *
  3. Checks all configured conditions (permissions, cooldowns, etc.)
  4. + *
  5. Fires {@link fr.traqueur.crates.api.events.CratePreOpenEvent} (cancellable)
  6. + *
  7. Consumes the key
  8. + *
  9. Calls {@code onOpen()} on all conditions
  10. + *
  11. Opens the crate menu
  12. + *
+ * + * @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. + * + *

Called during plugin initialization.

+ */ + void loadAllPlacedCrates(); + + /** + * Unloads all placed crates, removing all displays. + * + *

Called during plugin shutdown.

+ */ + void unloadAllPlacedCrates(); + + /** + * Gets all placed crates in a specific world. + * + * @param world the world to search in + * @return a list of placed crates in the world + */ + List getPlacedCratesInWorld(World world); +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java b/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java index 7ec79e2..3c33fcf 100644 --- a/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java +++ b/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java @@ -3,8 +3,19 @@ import fr.traqueur.crates.api.CratesPlugin; import org.bukkit.plugin.java.JavaPlugin; -public interface Manager { +/** + * Base interface for all manager classes in the zCrates plugin. + * Managers are responsible for handling specific aspects of the plugin's functionality. + */ +public sealed interface Manager permits CratesManager, UsersManager { + /** Initializes the manager. This method is called during the plugin's startup sequence. */ + void init(); + + /** + * Gets the main CratesPlugin instance. + * @return the CratesPlugin instance + */ default CratesPlugin getPlugin() { return JavaPlugin.getPlugin(CratesPlugin.class); } diff --git a/api/src/main/java/fr/traqueur/crates/api/managers/UsersManager.java b/api/src/main/java/fr/traqueur/crates/api/managers/UsersManager.java new file mode 100644 index 0000000..aa85b9f --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/managers/UsersManager.java @@ -0,0 +1,74 @@ +package fr.traqueur.crates.api.managers; + +import fr.traqueur.crates.api.models.CrateOpening; +import fr.traqueur.crates.api.models.User; + +import java.util.UUID; + +/** + * Manager responsible for player data including virtual keys and opening history. + * + *

This manager handles the lifecycle of user data:

+ *
    + *
  • Loading user data when a player joins
  • + *
  • Caching user data in memory for performance
  • + *
  • Persisting changes to the database
  • + *
  • Unloading user data when a player leaves
  • + *
+ * + *

Usage example:

+ *
{@code
+ * UsersManager usersManager = plugin.getManager(UsersManager.class);
+ * User user = usersManager.getUser(player.getUniqueId());
+ *
+ * // Modify user data
+ * user.addKeys("legendary-key", 5);
+ *
+ * // Changes are automatically persisted
+ * }
+ * + * @see User + * @see CrateOpening + */ +public non-sealed interface UsersManager extends Manager { + + /** + * Loads a user's data from the database into cache. + * + *

This is called automatically when a player joins the server. + * The operation is asynchronous and loads both keys and opening history.

+ * + * @param uuid the player's UUID + */ + void loadUser(UUID uuid); + + /** + * Unloads a user's data from cache and saves any pending changes. + * + *

This is called automatically when a player leaves the server.

+ * + * @param uuid the player's UUID + */ + void unloadUser(UUID uuid); + + /** + * Gets a user from the cache. + * + *

The user must have been loaded first via {@link #loadUser(UUID)}. + * Returns null if the user is not in cache.

+ * + * @param uuid the player's UUID + * @return the cached User object, or null if not loaded + */ + User getUser(UUID uuid); + + /** + * Persists a crate opening record to the database. + * + *

This is called internally when a reward is given to a player. + * The operation is asynchronous.

+ * + * @param opening the crate opening record to persist + */ + void persistCrateOpening(CrateOpening opening); +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/CrateOpening.java b/api/src/main/java/fr/traqueur/crates/api/models/CrateOpening.java new file mode 100644 index 0000000..d1a0909 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/CrateOpening.java @@ -0,0 +1,21 @@ +package fr.traqueur.crates.api.models; + +import java.util.UUID; + +/** + * Represents a record of a crate opening by a player. + * + * @param id The unique identifier of the crate opening. + * @param playerUuid The UUID of the player who opened the crate. + * @param crateId The identifier of the crate that was opened. + * @param rewardId The identifier of the reward obtained from the crate. + * @param timestamp The timestamp when the crate was opened. + */ +public record CrateOpening( + UUID id, + UUID playerUuid, + String crateId, + String rewardId, + long timestamp +) { +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/User.java b/api/src/main/java/fr/traqueur/crates/api/models/User.java new file mode 100644 index 0000000..8ea73ad --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/User.java @@ -0,0 +1,101 @@ +package fr.traqueur.crates.api.models; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Represents a player's crate-related data including virtual keys and opening history. + * + *

User data is persisted in the database and cached in memory for performance. + * The {@link fr.traqueur.crates.api.managers.UsersManager} handles loading and saving.

+ * + *

Usage example:

+ *
{@code
+ * UsersManager usersManager = plugin.getManager(UsersManager.class);
+ * User user = usersManager.getUser(player.getUniqueId());
+ *
+ * // Check and use keys
+ * if (user.hasKey("legendary-key")) {
+ *     user.removeKeys("legendary-key", 1);
+ * }
+ *
+ * // Give keys
+ * user.addKeys("common-key", 5);
+ * }
+ * + * @see fr.traqueur.crates.api.managers.UsersManager + * @see CrateOpening + */ +public interface User { + + /** + * Gets the UUID of this user. + * + * @return the player's unique identifier + */ + UUID uuid(); + + /** + * Gets the number of keys a user has for a specific key type. + * + * @param keyName the key name + * @return the number of keys (0 if none) + */ + int getKeyCount(String keyName); + + /** + * Adds keys to this user's balance. + * + * @param keyName the key name + * @param amount the amount to add (must be positive) + */ + void addKeys(String keyName, int amount); + + /** + * Removes keys from this user's balance. + * + *

The balance will not go below 0.

+ * + * @param keyName the key name + * @param amount the amount to remove (must be positive) + */ + void removeKeys(String keyName, int amount); + + /** + * Checks if this user has at least one key of the specified type. + * + * @param keyName the key name + * @return true if the user has at least one key + */ + boolean hasKey(String keyName); + + /** + * Gets all keys and their counts for this user. + * + * @return an unmodifiable map of key names to counts + */ + Map getAllKeys(); + + /** + * Gets all crate openings for this user. + * + *

Used by algorithms that need player history (e.g., pity systems).

+ * + * @return the list of crate openings, most recent first + */ + List crateOpenings(); + + /** + * Adds a crate opening to this user's history. + * + *

Called automatically when a reward is given. The opening is persisted + * to the database asynchronously.

+ * + * @param crateId the crate ID + * @param rewardId the reward ID + * @return the created CrateOpening record + */ + CrateOpening addCrateOpening(String crateId, String rewardId); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/Wrapper.java b/api/src/main/java/fr/traqueur/crates/api/models/Wrapper.java new file mode 100644 index 0000000..c4bd2ba --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/Wrapper.java @@ -0,0 +1,22 @@ +package fr.traqueur.crates.api.models; + +/** + * A generic wrapper class that holds a delegate object of type T. + * + * @param the type of the delegate object + */ +public abstract class Wrapper { + + /** The delegate object being wrapped */ + protected final T delegate; + + /** + * Constructs a Wrapper with the specified delegate. + * + * @param delegate the object to be wrapped + */ + public Wrapper(T delegate) { + this.delegate = delegate; + } + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/algorithms/AlgorithmContext.java b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/AlgorithmContext.java new file mode 100644 index 0000000..0fa384f --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/AlgorithmContext.java @@ -0,0 +1,26 @@ +package fr.traqueur.crates.api.models.algorithms; + +import fr.traqueur.crates.api.models.CrateOpening; +import fr.traqueur.crates.api.models.Wrapper; +import fr.traqueur.crates.api.models.crates.Reward; + +import java.util.List; + +/** + * Context provided to random algorithms when selecting a reward. + * Contains wrapped objects with helper methods for algorithm development. + * + * This follows the same pattern as AnimationContext, using Wrapper objects + * to expose safe and convenient APIs to JavaScript. + * @param rewards Wrapped list of possible rewards. + * @param history Wrapped list of history of crate openings for the player. + * @param crateId Identifier of the crate being opened. + * @param playerUuid UUID of the player opening the crate. + */ +public record AlgorithmContext( + Wrapper> rewards, + Wrapper> history, + String crateId, + String playerUuid +) { +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/algorithms/RandomAlgorithm.java b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/RandomAlgorithm.java new file mode 100644 index 0000000..a316267 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/RandomAlgorithm.java @@ -0,0 +1,34 @@ +package fr.traqueur.crates.api.models.algorithms; + +import fr.traqueur.crates.api.models.crates.Reward; + +import java.util.List; +import java.util.function.Function; + +/** + * Interface for reward selection algorithms. + * Algorithms can be implemented in JavaScript to provide custom logic + * for selecting rewards from crates (e.g., pity systems, guaranteed rewards). + */ +public interface RandomAlgorithm { + + /** + * Unique identifier for this algorithm + * @return Algorithm ID + */ + String id(); + + /** + * Source file where this algorithm was defined + * @return Source file path + */ + String sourceFile(); + + /** + * Function that selects a reward based on the context + * The function receives an AlgorithmContext and returns the selected Reward + * @return Function that selects a reward + */ + Function selector(); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/animations/Animation.java b/api/src/main/java/fr/traqueur/crates/api/models/animations/Animation.java new file mode 100644 index 0000000..2b6cd59 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/animations/Animation.java @@ -0,0 +1,117 @@ +package fr.traqueur.crates.api.models.animations; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Represents an animation that plays when a crate is opened. + * + *

Animations are defined in JavaScript files in {@code plugins/zCrates/animations/} + * and consist of multiple phases with timing and visual effects.

+ * + *

Example JavaScript animation:

+ *
{@code
+ * animations.register("roulette", {
+ *     phases: [
+ *         {
+ *             name: "spin",
+ *             duration: 3000,
+ *             interval: 2,
+ *             speedCurve: "EASE_OUT",
+ *             onStart: function(context) { },
+ *             onTick: function(context, tickData) { },
+ *             onEnd: function(context) { }
+ *         }
+ *     ],
+ *     onComplete: function(context) { },
+ *     onCancel: function(context) { }
+ * });
+ * }
+ * + * @see AnimationPhase + * @see AnimationContext + */ +public interface Animation { + + /** + * Gets the unique identifier for this animation. + * + *

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.

+ * + *

Example YAML configuration:

+ *
{@code
+ * id: legendary
+ * animation: roulette
+ * algorithm: weighted
+ * display-name: "Legendary Crate"
+ * max-rerolls: 3
+ * key:
+ *   type: VIRTUAL
+ *   name: "legendary-key"
+ * rewards:
+ *   - type: ITEM
+ *     id: diamond-reward
+ *     weight: 10.0
+ *     display-item:
+ *       material: DIAMOND
+ *     item:
+ *       material: DIAMOND
+ *       amount: 5
+ * }
+ * + * @see Reward + * @see Key + * @see Animation + * @see OpenCondition + */ +public interface Crate { + + /** + * Gets the unique identifier for this crate. + * + *

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
  • + *
+ * + *

Example YAML configurations:

+ *
{@code
+ * # Virtual key (database-stored)
+ * key:
+ *   type: VIRTUAL
+ *   name: "legendary-key"
+ *
+ * # Physical key (item)
+ * key:
+ *   type: PHYSIC
+ *   name: "legendary-key"
+ *   item:
+ *     material: TRIPWIRE_HOOK
+ *     name: "Legendary Key"
+ *     lore:
+ *       - "Right-click on a crate"
+ *     glow: true
+ * }
+ * + * @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:

+ *
    + *
  • {@code ITEM} - A single item
  • + *
  • {@code ITEMS} - Multiple items
  • + *
  • {@code COMMAND} - A single console command
  • + *
  • {@code COMMANDS} - Multiple console commands
  • + *
+ * + *

Example YAML configurations:

+ *
{@code
+ * # Single item reward
+ * - type: ITEM
+ *   id: diamond-sword
+ *   weight: 5.0
+ *   display-item:
+ *     material: DIAMOND_SWORD
+ *     name: "Legendary Sword"
+ *   item:
+ *     material: DIAMOND_SWORD
+ *     enchantments:
+ *       SHARPNESS: 5
+ *
+ * # Command reward
+ * - type: COMMAND
+ *   id: money-reward
+ *   weight: 20.0
+ *   display-item:
+ *     material: GOLD_INGOT
+ *     name: "$1000"
+ *   command: "eco give %player% 1000"
+ * }
+ * + * @see Crate + */ +@Polymorphic() +public interface Reward extends Loadable { + + /** + * Gets the unique identifier for this reward within the crate. + * + *

Used for logging, history tracking, and algorithm references.

+ * + * @return the reward ID + */ + String id(); + + /** + * Gets the weight of this reward for random selection. + * + *

Higher weight means higher probability of being selected. + * The probability is calculated as: {@code weight / totalWeights}.

+ * + *

Example: With rewards weighted 10, 20, 70:

+ *
    + *
  • 10 weight = 10% chance
  • + *
  • 20 weight = 20% chance
  • + *
  • 70 weight = 70% chance
  • + *
+ * + * @return the weight value (higher = more common) + */ + double weight(); + + /** + * Gets the item to display in preview menus and animations. + * + *

This is what players see before receiving the reward. + * It may differ from the actual reward item.

+ * + * @return the display item configuration + */ + ItemStackWrapper displayItem(); + + /** + * Gives this reward to a player. + * + *

The implementation depends on the reward type:

+ *
    + *
  • ITEM/ITEMS: Adds items to inventory (drops if full)
  • + *
  • COMMAND/COMMANDS: Executes console commands with %player% placeholder
  • + *
+ * + * @param player the player to give the reward to + */ + void give(Player player); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplay.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplay.java new file mode 100644 index 0000000..7e14106 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplay.java @@ -0,0 +1,32 @@ +package fr.traqueur.crates.api.models.placedcrates; + +import org.bukkit.Location; + +/** + * Represents a display for a crate in the game world. + * + * @param The type of element that the crate display represents. + */ +public interface CrateDisplay { + + /** Spawns the crate display in the game world. */ + void spawn(); + + /** Removes the crate display from the game world. */ + void remove(); + + /** + * Checks if the given element matches the crate display. + * + * @param element The element to check. + * @return true if the element matches, false otherwise. + */ + boolean matches(T element); + + /** + * Gets the location of the crate display in the game world. + * + * @return The location of the crate display. + */ + Location getLocation(); +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplayFactory.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplayFactory.java new file mode 100644 index 0000000..f3853fe --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplayFactory.java @@ -0,0 +1,38 @@ +package fr.traqueur.crates.api.models.placedcrates; + +import org.bukkit.Location; + +import java.util.List; + +/** + * Factory interface for creating crate displays. + * + * @param The type of element that the crate display represents. + */ +public interface CrateDisplayFactory { + + /** + * Creates a crate display at the specified location with the given value and yaw. + * + * @param location the location to create the display + * @param value the value representing the display content + * @param yaw the yaw orientation of the display + * @return the created crate display + */ + CrateDisplay create(Location location, String value, float yaw); + + /** + * Validates if the given value is valid for this display type. + * + * @param value the value to validate + * @return true if valid, false otherwise + */ + boolean isValidValue(String value); + + /** + * Returns a list of suggested values for tab completion. + * + * @return list of suggested values + */ + List getSuggestions(); +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/DisplayType.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/DisplayType.java new file mode 100644 index 0000000..7f49577 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/DisplayType.java @@ -0,0 +1,19 @@ +package fr.traqueur.crates.api.models.placedcrates; + +/** + * Enum representing different types of displays for placed crates. + */ +public enum DisplayType { + /** Display type for blocks. */ + BLOCK, + /** Display type for entities. */ + ENTITY, + /** Display type for holograms. */ + MYTHIC_MOB, + /** Display type for items adder. */ + ITEMS_ADDER, + /** Display type for oraxen. */ + ORAXEN, + /** Display type for nexo. */ + NEXO; +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/PlacedCrate.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/PlacedCrate.java new file mode 100644 index 0000000..601a72b --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/PlacedCrate.java @@ -0,0 +1,46 @@ +package fr.traqueur.crates.api.models.placedcrates; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.UUID; + +/** + * Record representing a placed crate in the game world. + * + * @param id Unique identifier for the placed crate. + * @param crateId Identifier of the crate type. + * @param worldName Name of the world where the crate is placed. + * @param x X coordinate of the crate's location. + * @param y Y coordinate of the crate's location. + * @param z Z coordinate of the crate's location. + * @param displayType Type of display for the crate. + * @param displayValue Value associated with the display type. + * @param yaw Yaw orientation of the crate. + */ +public record PlacedCrate( + UUID id, + String crateId, + String worldName, + int x, + int y, + int z, + DisplayType displayType, + String displayValue, + float yaw +) { + + /** + * Converts the PlacedCrate to a Bukkit Location object. + * + * @return Location object representing the crate's location, or null if the world does not exist. + */ + public Location getLocation() { + World world = Bukkit.getWorld(worldName); + if (world == null) { + return null; + } + return new Location(world, x, y, z, yaw, 0); + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/providers/ItemsProvider.java b/api/src/main/java/fr/traqueur/crates/api/providers/ItemsProvider.java new file mode 100644 index 0000000..ab5b1bf --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/providers/ItemsProvider.java @@ -0,0 +1,20 @@ +package fr.traqueur.crates.api.providers; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +/** + * Functional interface for providing ItemStack instances based on a player and item ID. + */ +@FunctionalInterface +public interface ItemsProvider { + + /** + * Retrieves an ItemStack for the given player and item ID. + * + * @param player the player for whom the item is being retrieved + * @param itemId the identifier of the item + * @return the corresponding ItemStack + */ + ItemStack item(Player player, String itemId); +} diff --git a/api/src/main/java/fr/traqueur/crates/api/placeholders/PlaceholderParser.java b/api/src/main/java/fr/traqueur/crates/api/providers/PlaceholderProvider.java similarity index 86% rename from api/src/main/java/fr/traqueur/crates/api/placeholders/PlaceholderParser.java rename to api/src/main/java/fr/traqueur/crates/api/providers/PlaceholderProvider.java index c76972b..b17ce6a 100644 --- a/api/src/main/java/fr/traqueur/crates/api/placeholders/PlaceholderParser.java +++ b/api/src/main/java/fr/traqueur/crates/api/providers/PlaceholderProvider.java @@ -1,4 +1,4 @@ -package fr.traqueur.crates.api.placeholders; +package fr.traqueur.crates.api.providers; import org.bukkit.entity.Player; @@ -21,7 +21,7 @@ *
  • PAPIHook: Integrates with PlaceholderAPI for full placeholder support
  • * */ -public interface PlaceholderParser { +public interface PlaceholderProvider { /** * The singleton instance holder. @@ -33,14 +33,14 @@ class Holder { */ private Holder() {} - private static PlaceholderParser instance = new EmptyParser(); + private static PlaceholderProvider instance = new EmptyProvider(); /** * Sets the global placeholder parser instance. * * @param parser The parser implementation to use */ - public static void setInstance(PlaceholderParser parser) { + public static void setInstance(PlaceholderProvider parser) { instance = parser; } @@ -49,7 +49,7 @@ public static void setInstance(PlaceholderParser parser) { * * @return The active parser (never null, defaults to EmptyParser) */ - public static PlaceholderParser getInstance() { + public static PlaceholderProvider getInstance() { return instance; } } @@ -81,12 +81,12 @@ static String parsePlaceholders(Player player, String text) { *

    This implementation is used as the default when no placeholder system * (like PlaceholderAPI) is available.

    */ - class EmptyParser implements PlaceholderParser { + class EmptyProvider implements PlaceholderProvider { /** * Constructs an EmptyParser instance. */ - private EmptyParser() {} + private EmptyProvider() {} @Override public String parse(Player player, String text) { diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/AnimationsRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/AnimationsRegistry.java new file mode 100644 index 0000000..a8e9bf3 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/AnimationsRegistry.java @@ -0,0 +1,20 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.CratesPlugin; +import fr.traqueur.crates.api.models.animations.Animation; + +/** + * Registry for managing animations loaded from files. + */ +public abstract class AnimationsRegistry extends FileBasedRegistry { + + /** + * Constructs an AnimationsRegistry with the specified plugin and resource folder. + * + * @param plugin The CratesPlugin instance. + * @param resourceFolder The folder where animation files are located. + */ + protected AnimationsRegistry(CratesPlugin plugin, String resourceFolder) { + super(plugin, resourceFolder, "animations", ".js"); + } +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/CrateDisplayFactoriesRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/CrateDisplayFactoriesRegistry.java new file mode 100644 index 0000000..ffd88ae --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/CrateDisplayFactoriesRegistry.java @@ -0,0 +1,22 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.models.placedcrates.CrateDisplayFactory; +import fr.traqueur.crates.api.models.placedcrates.DisplayType; + +/** + * Registry for managing crate display factories. + */ +public interface CrateDisplayFactoriesRegistry extends Registry> { + + /** + * Registers a generic crate display factory for the specified display type. + * + * @param type The display type. + * @param factory The crate display factory. + * @param The type of element that the crate display represents. + */ + default void registerGeneric(DisplayType type, CrateDisplayFactory factory) { + this.register(type, factory); + } + +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/CratesRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/CratesRegistry.java new file mode 100644 index 0000000..db5acf5 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/CratesRegistry.java @@ -0,0 +1,30 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.CratesPlugin; +import fr.traqueur.crates.api.models.crates.Crate; + +/** + * Abstract registry for managing crate configurations stored in files. + * + *

    This class extends {@link FileBasedRegistry} to provide functionality + * for loading and managing crate definitions from YAML files located in a + * specified resource folder.

    + * + *

    Concrete implementations of this class should specify the resource folder + * where crate configuration files are stored.

    + * + * @see FileBasedRegistry + * @see Crate + */ +public abstract class CratesRegistry extends FileBasedRegistry { + + /** + * Constructs a new {@code CratesRegistry} with the specified plugin and resource folder. + * + * @param plugin the main plugin instance + * @param resourceFolder the folder where crate configuration files are located + */ + protected CratesRegistry(CratesPlugin plugin, String resourceFolder) { + super(plugin, resourceFolder, "crates", ".yml", ".yaml"); + } +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java index c7fbff8..1edb0dd 100644 --- a/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java +++ b/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java @@ -11,7 +11,12 @@ import java.util.*; import java.util.stream.Stream; - +/** + * A registry that loads items from files in a specified folder structure. + * + * @param the type of the identifier for registered items + * @param the type of items being registered + */ public abstract class FileBasedRegistry implements Registry { /** The plugin instance */ @@ -23,6 +28,9 @@ public abstract class FileBasedRegistry implements Registry { /** The name used in logging messages */ private final String logName; + /** The supported file types for loading items */ + private final Set supportedFileTypes; + /** The root folder of the loaded folder structure */ private Folder rootFolder; @@ -32,19 +40,21 @@ public abstract class FileBasedRegistry implements Registry { * @param plugin the ItemsPlugin instance * @param resourceFolder the folder in resources containing example files * @param logName the name used in logging messages + * @param supportedFileTypes the supported file extensions (e.g., ".yml", ".json") */ - protected FileBasedRegistry(CratesPlugin plugin, String resourceFolder, String logName) { + protected FileBasedRegistry(CratesPlugin plugin, String resourceFolder, String logName, String... supportedFileTypes) { this.plugin = plugin; this.storage = new HashMap<>(); this.resourceFolder = resourceFolder; this.logName = logName; + this.supportedFileTypes = Set.of(supportedFileTypes); } /** * Loads items from the specified folder, building the folder structure. */ public void loadFromFolder() { - Path folder = this.plugin.getDataPath().resolve(this.resourceFolder); + Path folder = this.plugin.getDataFolder().toPath().resolve(this.resourceFolder); if (!ensureFolderExists(folder)) { return; } @@ -78,7 +88,7 @@ private Folder buildFolderStructure(Path currentPath, Path rootPath) { if (!sub.isEmpty()) { subFolders.add(sub); } - } else if (isYamlFile(path)) { + } else if (isFile(path.toString().toLowerCase())) { T element = loadFile(path); if (element != null) { elements.add(element); @@ -140,16 +150,6 @@ protected boolean ensureFolderExists(Path folder) { return true; } - /** Checks if the given path points to a YAML file. - * - * @param path the path to check - * @return true if the file is a YAML file, false otherwise - */ - private boolean isYamlFile(Path path) { - String name = path.toString().toLowerCase(); - return name.endsWith(".yml") || name.endsWith(".yaml"); - } - /** Copies example files from the resource folder to the plugin's data folder. */ private void copyExampleFiles() { Logger.info("Copying example " + logName + " files..."); @@ -237,7 +237,12 @@ private List listResourceFiles(String folder) throws IOException { */ private boolean isFile(String fileName) { String lower = fileName.toLowerCase(); - return lower.endsWith(".yml") || lower.endsWith(".yaml") || lower.endsWith(".properties"); + for (String ext : supportedFileTypes) { + if (lower.endsWith(ext)) { + return true; + } + } + return fileName.endsWith(".properties"); } /** Loads an item from the specified file. @@ -258,8 +263,8 @@ public T getById(ID id) { } @Override - public Collection getAll() { - return storage.values(); + public List getAll() { + return new ArrayList<>(storage.values()); } @Override diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java new file mode 100644 index 0000000..636bc94 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java @@ -0,0 +1,25 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.CratesPlugin; +import fr.traqueur.crates.api.hooks.Hook; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Registry interface for managing hooks within the application. + */ +public interface HooksRegistry extends Registry { + + /** + * Enables all registered hooks. + */ + void enableAll(); + + /** + * Scans the specified package for hook implementations and registers them. + * + * @param plugin the JavaPlugin instance + * @param packageName the package name to scan for hooks + */ + void scanPackage(CratesPlugin plugin, String packageName); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/ItemsProvidersRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/ItemsProvidersRegistry.java new file mode 100644 index 0000000..e252097 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/ItemsProvidersRegistry.java @@ -0,0 +1,9 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.providers.ItemsProvider; + +/** + * Registry interface for managing ItemsProvider instances within the application. + */ +public interface ItemsProvidersRegistry extends Registry { +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/RandomAlgorithmsRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/RandomAlgorithmsRegistry.java new file mode 100644 index 0000000..c1c412f --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/RandomAlgorithmsRegistry.java @@ -0,0 +1,20 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.CratesPlugin; +import fr.traqueur.crates.api.models.algorithms.RandomAlgorithm; + +/** + * Registry for random algorithms used in crates. + */ +public abstract class RandomAlgorithmsRegistry extends FileBasedRegistry { + + /** + * Constructor for RandomAlgorithmsRegistry. + * + * @param plugin the CratesPlugin instance + * @param resourceFolder the resource folder path + */ + protected RandomAlgorithmsRegistry(CratesPlugin plugin, String resourceFolder) { + super(plugin, resourceFolder, "algorithms", ".js"); + } +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java b/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java index d413096..e65fe33 100644 --- a/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java +++ b/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java @@ -4,6 +4,7 @@ import com.google.common.collect.MutableClassToInstanceMap; import java.util.Collection; +import java.util.List; /** * A generic registry interface for managing items identified by unique IDs. @@ -60,7 +61,7 @@ public interface Registry { * * @return A collection of all registered items. */ - Collection getAll(); + List getAll(); /** * Clear all registered items in this registry. diff --git a/api/src/main/java/fr/traqueur/crates/api/serialization/Keys.java b/api/src/main/java/fr/traqueur/crates/api/serialization/Keys.java new file mode 100644 index 0000000..fc73dcc --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/serialization/Keys.java @@ -0,0 +1,168 @@ +package fr.traqueur.crates.api.serialization; + +import fr.traqueur.crates.api.models.placedcrates.PlacedCrate; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Registry of all data keys used in the zCrates plugin. + * Each key automatically uses its field name as the key identifier. + */ +public class Keys { + + /** Key for the name of the crate */ + public static final DataKey KEY_NAME = new DataKey<>(PersistentDataType.STRING); + /** Key indicating whether the crate is a placed crate entity */ + public static final DataKey PLACED_CRATE_ENTITY = new DataKey<>(PersistentDataType.BOOLEAN); + /** Key for the list of placed crates */ + public static final DataKey> PLACED_CRATES = new DataKey<>(PersistentDataType.LIST.listTypeFrom(PlacedCrateDataType.INSTANCE)); + + // Internal keys for PlacedCrate PDC serialization + + /** Key for the unique ID of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_ID = new DataKey<>(PersistentDataType.STRING); + /** Key for the crate ID associated with the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_CRATE_ID = new DataKey<>(PersistentDataType.STRING); + /** Key for the world name where the placed crate is located */ + public static final DataKey INTERNAL_PLACED_CRATE_WORLD_NAME = new DataKey<>(PersistentDataType.STRING); + /** Key for the X coordinate of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_X = new DataKey<>(PersistentDataType.INTEGER); + /** Key for the Y coordinate of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_Y = new DataKey<>(PersistentDataType.INTEGER); + /** Key for the Z coordinate of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_Z = new DataKey<>(PersistentDataType.INTEGER); + /** Key indicating whether the placed crate is currently active */ + public static final DataKey INTERNAL_PLACED_CRATE_DISPLAY_TYPE = new DataKey<>(PersistentDataType.STRING); + /** Key for the display value of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_DISPLAY_VALUE = new DataKey<>(PersistentDataType.STRING); + /** Key for the yaw rotation of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_YAW = new DataKey<>(PersistentDataType.FLOAT); + + private static Plugin PLUGIN; + + private Keys() { + } + + /** + * Initializes the Keys registry with the given plugin instance. + * This must be called before using any DataKey. + * Initializes dependent data types as well. + * + * @param plugin the plugin instance + */ + public static void initialize(Plugin plugin) { + PLUGIN = plugin; + } + + /** + * Generic typed persistent data key that automatically resolves its name from the static field name. + * + * @param the type of data this key stores + */ + public static class DataKey { + + /** Cache of resolved key names to avoid repeated reflection lookups */ + private static final Map, String> KEY_NAMES = new HashMap<>(); + + /** The PersistentDataType associated with this key */ + private final PersistentDataType 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 type) { + this.type = type; + } + + /** + * Gets the NamespacedKey for this DataKey, resolving the field name if needed. + * @return the {@link NamespacedKey} for this DataKey + */ + public NamespacedKey getNamespacedKey() { + if (namespacedKey == null) { + String keyName = resolveFieldName(); + namespacedKey = new NamespacedKey(PLUGIN, keyName.toLowerCase()); + } + return namespacedKey; + } + + /** + * Resolves the field name by scanning the Keys class for this instance. + */ + private String resolveFieldName() { + String cachedName = KEY_NAMES.get(this); + if (cachedName != null) { + return cachedName; + } + + try { + Field[] fields = Keys.class.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers()) && + Modifier.isFinal(field.getModifiers()) && + DataKey.class.isAssignableFrom(field.getType())) { + + field.setAccessible(true); + Object fieldValue = field.get(null); + + if (fieldValue == this) { + String fieldName = field.getName(); + KEY_NAMES.put(this, fieldName); + return fieldName; + } + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to resolve field name for DataKey", e); + } + + throw new RuntimeException("Could not resolve field name for DataKey instance"); + } + + /** + * Retrieves the value associated with this key from the given {@link PersistentDataContainer}. + * + * @param container the {@link PersistentDataContainer} from which to retrieve the value + * @return an {@link Optional} containing the value if it exists, or empty if it does not + */ + public Optional get(PersistentDataContainer container) { + return Optional.ofNullable(container.get(getNamespacedKey(), type)); + } + + /** + * Retrieves the value associated with this key from the given {@link PersistentDataContainer}. + * If the value does not exist, the provided default value is returned. + * + * @param container the {@link PersistentDataContainer} from which to retrieve the value + * @param defaultValue the default value to return if the key does not exist + * @return the value associated with this key, or the default value if it does not exist + */ + public T get(PersistentDataContainer container, T defaultValue) { + return container.getOrDefault(getNamespacedKey(), type, defaultValue); + } + + /** + * Sets the value associated with this key in the given {@link PersistentDataContainer}. + * + * @param container the {@link PersistentDataContainer} in which to store the value + * @param value the value to store + */ + public void set(PersistentDataContainer container, T value) { + container.set(getNamespacedKey(), type, value); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/serialization/PlacedCrateDataType.java b/api/src/main/java/fr/traqueur/crates/api/serialization/PlacedCrateDataType.java new file mode 100644 index 0000000..c66454e --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/serialization/PlacedCrateDataType.java @@ -0,0 +1,35 @@ +package fr.traqueur.crates.api.serialization; + +import fr.traqueur.crates.api.models.placedcrates.DisplayType; +import fr.traqueur.crates.api.models.placedcrates.PlacedCrate; +import org.bukkit.persistence.PersistentDataAdapterContext; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Abstract class for PlacedCrate data type serialization. + */ +public abstract class PlacedCrateDataType implements PersistentDataType { + + /** Singleton instance of PlacedCrateDataType */ + public static PlacedCrateDataType INSTANCE; + + /** + * Constructor for PlacedCrateDataType. + */ + protected PlacedCrateDataType() { + } + + @Override + public @NotNull Class getPrimitiveType() { + return PersistentDataContainer.class; + } + + @Override + public @NotNull Class getComplexType() { + return PlacedCrate.class; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/services/ItemsService.java b/api/src/main/java/fr/traqueur/crates/api/services/ItemsService.java new file mode 100644 index 0000000..295e8b6 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/services/ItemsService.java @@ -0,0 +1,201 @@ +package fr.traqueur.crates.api.services; + +import fr.traqueur.crates.api.PlatformType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextDecoration; + +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for creating and modifying ItemStacks with Paper/Spigot compatibility. + * Works with Adventure Components throughout the code and handles conversion only at the final step. + */ +public class ItemsService { + + private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacySection(); + + /* + * Private constructor to prevent instantiation + */ + private ItemsService() { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Sets the display name of an ItemStack using a Component. + * Handles Paper (native) vs Spigot (legacy conversion) automatically. + * + * @param itemStack The ItemStack to modify + * @param displayName The display name as a Component + */ + public static void setDisplayName(ItemStack itemStack, Component displayName) { + if (itemStack == null || displayName == null) { + return; + } + + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + return; + } + + Component processedDisplayName = displayName.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE); + + + if (PlatformType.isPaper()) { + // Use Paper's native Adventure API + meta.displayName(processedDisplayName); + } else { + // Convert Component to legacy format for Spigot + String legacy = LEGACY_SERIALIZER.serialize(processedDisplayName); + meta.setDisplayName(legacy); + } + + itemStack.setItemMeta(meta); + } + + /** + * Sets the lore of an ItemStack using a list of Components. + * Handles Paper (native) vs Spigot (legacy conversion) automatically. + * Automatically disables italic decoration for all lore lines. + * + * @param itemStack The ItemStack to modify + * @param lore The lore lines as Components + */ + public static void setLore(ItemStack itemStack, List lore) { + if (itemStack == null || lore == null) { + return; + } + + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + return; + } + + // Disable italic decoration for all lore lines + List processedLore = new ArrayList<>(); + for (Component line : lore) { + processedLore.add(line.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE)); + } + + if (PlatformType.isPaper()) { + // Use Paper's native Adventure API + meta.lore(processedLore); + } else { + // Convert Components to legacy format for Spigot + List legacyLore = new ArrayList<>(); + for (Component line : processedLore) { + String legacy = LEGACY_SERIALIZER.serialize(line); + legacyLore.add(legacy); + } + meta.setLore(legacyLore); + } + + itemStack.setItemMeta(meta); + } + + /** + * Creates a new ItemStack with the specified material, amount, display name, and lore. + * + * @param material The material of the item + * @param amount The number of items in the stack + * @param displayName The display name as a Component + * @param lore The lore lines as Components + * @param itemName The item name as a Component + * @return The created ItemStack + */ + public static ItemStack createItem(Material material, int amount, Component displayName, List lore, Component itemName) { + ItemStack itemStack = new ItemStack(material, amount); + + if (displayName != null) { + setDisplayName(itemStack, displayName); + } + + if (lore != null && !lore.isEmpty()) { + setLore(itemStack, lore); + } + + if (itemName != null) { + setItemName(itemStack, itemName); + } + + return itemStack; + } + + /** + * Sets the item name of an ItemStack using a Component. + * Handles Paper (native) vs Spigot (legacy conversion) automatically. + * + * @param itemStack The ItemStack to modify + * @param itemName The item name as a Component + */ + public static void setItemName(ItemStack itemStack, Component itemName) { + if (itemStack == null || itemName == null) { + return; + } + + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + return; + } + + Component processedItemName = itemName.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE); + + + if (PlatformType.isPaper()) { + // Use Paper's native Adventure API + meta.itemName(processedItemName); + } else { + // Convert Component to legacy format for Spigot + String legacy = LEGACY_SERIALIZER.serialize(processedItemName); + meta.setItemName(legacy); + } + + itemStack.setItemMeta(meta); + } + + /** + * Adds a line to the lore of an ItemStack using a Component. + * Handles Paper (native) vs Spigot (legacy conversion) automatically. + * Automatically disables italic decoration for the added lore line. + * + * @param itemStack The ItemStack to modify + * @param line The lore line as a Component + */ + public static void addLoreLine(ItemStack itemStack, Component line) { + if (itemStack == null || line == null) { + return; + } + + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + return; + } + + List currentLore; + if (PlatformType.isPaper()) { + currentLore = meta.lore(); + } else { + List legacyLore = meta.getLore(); + currentLore = new ArrayList<>(); + if (legacyLore != null) { + for (String legacyLine : legacyLore) { + currentLore.add(LEGACY_SERIALIZER.deserialize(legacyLine)); + } + } + } + + if (currentLore == null) { + currentLore = new ArrayList<>(); + } + + currentLore.add(line.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE)); + + setLore(itemStack, currentLore); + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java b/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java index 16d3eb1..6d72fdc 100644 --- a/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java +++ b/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java @@ -2,16 +2,18 @@ import fr.traqueur.crates.api.Logger; import fr.traqueur.crates.api.PlatformType; -import fr.traqueur.crates.api.placeholders.PlaceholderParser; +import fr.traqueur.crates.api.providers.PlaceholderProvider; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.title.Title; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; +import java.time.Duration; import java.util.Map; /** @@ -98,7 +100,7 @@ public static void close() { * @param placeholders the placeholder resolvers */ public static void sendMessage(CommandSender sender, String message, TagResolver... placeholders) { - String parsedString = sender instanceof Player player ? PlaceholderParser.parsePlaceholders(player, message) : message; + String parsedString = sender instanceof Player player ? PlaceholderProvider.parsePlaceholders(player, message) : message; Component parsedComponent = parseMessage(parsedString, placeholders); if (PlatformType.isPaper()) { sender.sendMessage(parsedComponent); @@ -108,6 +110,50 @@ public static void sendMessage(CommandSender sender, String message, TagResolver } } + /** + * Sends a title to a player. + * Handles Paper (native) vs Spigot (wrapper) automatically. + ** + * @param player The player to send the title to + * @param title The main title text + * @param subtitle The subtitle text + * @param fadeIn Fade in duration in ticks + * @param stay Stay duration in ticks + * @param fadeOut Fade out duration in ticks + * @param placeholders Optional placeholder resolvers + */ + public static void sendTitle(Player player, String title, String subtitle, int fadeIn, int stay, int fadeOut, TagResolver... placeholders) { + // Parse title and subtitle with PlaceholderAPI + String parsedTitle = PlaceholderProvider.parsePlaceholders(player, title); + String parsedSubtitle = PlaceholderProvider.parsePlaceholders(player, subtitle); + + // Parse MiniMessage and custom placeholders + Component titleComponent = parseMessage(parsedTitle, placeholders); + Component subtitleComponent = parseMessage(parsedSubtitle, placeholders); + + // Create title times + Title.Times times = Title.Times.times( + Duration.ofMillis(fadeIn * 50L), + Duration.ofMillis(stay * 50L), + Duration.ofMillis(fadeOut * 50L) + ); + + Title adventureTitle = Title.title( + titleComponent, + subtitleComponent, + times + ); + + // Send using appropriate backend + if (PlatformType.isPaper()) { + player.showTitle(adventureTitle); + } else { + Audience audience = bukkitAudiences.player(player); + audience.showTitle(adventureTitle); + } + } + + /** * Parses a message with MiniMessage tags, legacy color codes, and custom placeholders. * Converts legacy codes to MiniMessage format first, then parses with placeholder resolution. diff --git a/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java b/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java index 13b38c4..bc8a549 100644 --- a/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java +++ b/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java @@ -4,14 +4,33 @@ import com.google.common.collect.MutableClassToInstanceMap; import fr.traqueur.structura.api.Loadable; +/** + * A generic interface for application settings that can be loaded. + * Provides methods to register and retrieve settings instances. + */ public interface Settings extends Loadable { + /** A map to hold instances of settings classes. */ ClassToInstanceMap INSTANCES = MutableClassToInstanceMap.create(); + /** + * Retrieves the settings instance of the specified class. + * + * @param clazz the class of the settings to retrieve + * @param the type of the settings + * @return the settings instance + */ static T get(Class clazz) { return INSTANCES.getInstance(clazz); } + /** + * Registers a settings instance for the specified class. + * + * @param clazz the class of the settings to register + * @param instance the settings instance to register + * @param the type of the settings + */ static void register(Class clazz, T instance) { INSTANCES.putInstance(clazz, instance); } diff --git a/api/src/main/java/fr/traqueur/crates/api/settings/models/DatabaseSettings.java b/api/src/main/java/fr/traqueur/crates/api/settings/models/DatabaseSettings.java new file mode 100644 index 0000000..9aa3874 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/settings/models/DatabaseSettings.java @@ -0,0 +1,35 @@ +package fr.traqueur.crates.api.settings.models; + +import fr.maxlego08.sarah.DatabaseConnection; +import fr.traqueur.structura.annotations.Polymorphic; +import fr.traqueur.structura.api.Loadable; + +/** + * Interface representing database settings for the application. + * + *

    This interface extends {@link Loadable}, allowing implementations + * to load database configuration details such as table prefixes and + * connection parameters.

    + * + * @see Loadable + * @see DatabaseConnection + */ +@Polymorphic +public interface DatabaseSettings extends Loadable { + + /** + * Retrieves the table prefix used in the database. + * + * @return the table prefix as a {@code String} + */ + String tablePrefix(); + + /** + * Creates a new database connection based on the settings. + * + * @param debug {@code true} to enable debug mode, {@code false} otherwise + * @return a new {@link DatabaseConnection} instance + */ + DatabaseConnection connection(boolean debug); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/settings/models/ItemStackWrapper.java b/api/src/main/java/fr/traqueur/crates/api/settings/models/ItemStackWrapper.java new file mode 100644 index 0000000..4a2a14c --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/settings/models/ItemStackWrapper.java @@ -0,0 +1,139 @@ +package fr.traqueur.crates.api.settings.models; + + +import fr.traqueur.crates.api.providers.ItemsProvider; +import fr.traqueur.crates.api.providers.PlaceholderProvider; +import fr.traqueur.crates.api.registries.ItemsProvidersRegistry; +import fr.traqueur.crates.api.registries.Registry; +import fr.traqueur.crates.api.services.ItemsService; +import fr.traqueur.crates.api.services.MessagesService; +import fr.traqueur.structura.annotations.Options; +import fr.traqueur.structura.annotations.defaults.DefaultInt; +import fr.traqueur.structura.api.Loadable; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Wrapper class for configuring and building ItemStack instances. + * Supports direct material specification or delegation to an ItemsProvider. + * Allows customization of display name, lore, and item name with placeholder support. + * @param material the material of the item (optional if using copyFrom) + * @param amount the quantity of the item (default is 1) + * @param copyFrom delegate to an ItemsProvider to create the item (optional) + * @param displayName the display name of the item (optional) + * @param itemName the custom item name (optional) + * @param lore the lore of the item as a list of strings (optional) + */ +public record ItemStackWrapper( + @Options(optional = true) Material material, + + @Options(optional = true) @DefaultInt(1) int amount, + + @Options(optional = true) Delegate copyFrom, + + @Options(optional = true) String displayName, + + @Options(optional = true) String itemName, + + @Options(optional = true) List lore +) implements Loadable { + + /** + * Delegate class for specifying an item via an ItemsProvider. + * + * @param pluginName the name of the plugin providing the ItemsProvider + * @param itemId the identifier of the item within the provider + */ + public record Delegate(String pluginName, String itemId) implements Loadable { + + /** + * Retrieves the ItemStack from the specified ItemsProvider. + * + * @param player the player for context (used when building custom items) + * @param itemId the identifier of the item to retrieve + * @return the ItemStack provided by the ItemsProvider + * @throws IllegalArgumentException if no provider is found for the given plugin name + */ + public ItemStack item(Player player, String itemId) { + ItemsProvider provider = Registry.get(ItemsProvidersRegistry.class).getById(pluginName); + if (provider == null) { + throw new IllegalArgumentException("No item provider found for plugin: " + pluginName); + } + return provider.item(player, itemId); + } + + } + + /** + * Validates the configuration after loading. + * + * @throws IllegalArgumentException if amount is less than 1 or if neither itemId nor material is specified + */ + public ItemStackWrapper { + if (amount < 1) { + throw new IllegalArgumentException("Amount must be at least 1"); + } + if(copyFrom == null && material == null) { + throw new IllegalArgumentException("Either 'copy-from' or 'material' must be specified"); + } + } + + /** + * Builds an ItemStack from these settings. + * + * @param player the player for context (used when building custom items) + * @return the created ItemStack + * @throws IllegalStateException if neither itemId nor material is specified + */ + public @NotNull ItemStack build(@Nullable Player player) { + Component parsedDisplayName = null; + if (displayName != null && !displayName.isEmpty()) { + String parsedDisplayNameStr = PlaceholderProvider.parsePlaceholders(player, displayName); + parsedDisplayName = MessagesService.parseMessage(parsedDisplayNameStr); + } + + List lore = new ArrayList<>(); + if (this.lore != null) { + for (String loreLine : this.lore) { + String parsedLoreLineStr = PlaceholderProvider.parsePlaceholders(player, loreLine); + Component parsedLoreLine = MessagesService.parseMessage(parsedLoreLineStr); + lore.add(parsedLoreLine); + } + } + + Component parsedItemName = null; + if (itemName != null && !itemName.isEmpty()) { + String parsedItemNameStr = PlaceholderProvider.parsePlaceholders(player, itemName); + parsedItemName = MessagesService.parseMessage(parsedItemNameStr); + } + + if (copyFrom != null) { + ItemStack item = copyFrom.item(player, itemName); + if(item == null) { + throw new IllegalStateException("Item provider returned null for itemId: " + copyFrom.itemId); + } + if(parsedDisplayName != null) { + ItemsService.setDisplayName(item, parsedDisplayName); + } + if(!lore.isEmpty()) { + ItemsService.setLore(item, lore); + } + if (parsedItemName != null) { + ItemsService.setItemName(item, parsedItemName); + } + if (item.getAmount() != amount) { + item.setAmount(amount); + } + return item; + } + + return ItemsService.createItem(material, amount, parsedDisplayName, lore, parsedItemName); + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/DTO.java b/api/src/main/java/fr/traqueur/crates/api/storage/DTO.java new file mode 100644 index 0000000..4ae0dfd --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/storage/DTO.java @@ -0,0 +1,17 @@ +package fr.traqueur.crates.api.storage; + +/** + * Generic Data Transfer Object (DTO) interface for converting to model objects. + * + * @param the type of the model object + */ +public interface DTO { + + /** + * Converts this DTO to its corresponding model object. + * + * @return the model object of type T + */ + T toModel(); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/Tables.java b/api/src/main/java/fr/traqueur/crates/api/storage/Tables.java new file mode 100644 index 0000000..099fc44 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/storage/Tables.java @@ -0,0 +1,15 @@ +package fr.traqueur.crates.api.storage; + +/** + * Defines constants for database table names used in the application. + */ +public interface Tables { + + /** the users table */ + String USERS_TABLE = "users"; + /** the user keys table */ + String USER_KEYS_TABLE = "user_keys"; + /** the crate openings table */ + String CRATE_OPENINGS_TABLE = "crate_openings"; + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/repositories/Repository.java b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/Repository.java new file mode 100644 index 0000000..49c09ec --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/Repository.java @@ -0,0 +1,56 @@ +package fr.traqueur.crates.api.storage.repositories; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * A generic repository interface for managing entities of type T with an identifier of type ID. + * Provides methods for saving, deleting, and retrieving entities asynchronously. + * + * @param the type of the entity + * @param the type of the entity's identifier + */ +public interface Repository { + + /** + * Initializes the repository asynchronously. + * + * @return a CompletableFuture that completes with true if initialization was successful, false otherwise + */ + CompletableFuture init(); + + /** + * Retrieves all items asynchronously. + * + * @return a CompletableFuture that completes with a list of all items + */ + CompletableFuture> findAll(); + + /** + * Saves the given item asynchronously. + * + * @param item the item to save + * @return a CompletableFuture that completes when the save operation is done + */ + CompletableFuture save(@NotNull T item); + + + /** + * Deletes the item with the given identifier asynchronously. + * + * @param id the identifier of the item to delete + * @return a CompletableFuture that completes when the delete operation is done + */ + CompletableFuture delete(@NotNull ID id); + + /** + * Retrieves the item with the given identifier asynchronously. + * + * @param id the identifier of the item to retrieve + * @return a CompletableFuture that completes with the retrieved item + */ + CompletableFuture get(@NotNull ID id); + +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/repositories/SQLRepository.java b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/SQLRepository.java new file mode 100644 index 0000000..00623a5 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/SQLRepository.java @@ -0,0 +1,85 @@ +package fr.traqueur.crates.api.storage.repositories; + +import fr.maxlego08.sarah.Column; +import fr.maxlego08.sarah.RequestHelper; +import fr.traqueur.crates.api.storage.Tables; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.RecordComponent; +import java.lang.reflect.Type; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Base class for SQL repositories. + * Provides common functionality for repositories that use SQL as a storage source. + * + * @param the type of the entity + * @param the type of the data representation of the entity + * @param the type of the entity's identifier + */ +public abstract class SQLRepository implements Repository { + + /** The request helper for executing SQL queries. */ + protected final RequestHelper requestHelper; + /** The data class type. */ + protected Class dataClass = getDataClass(); + + public CompletableFuture init() { + return createTable(); + } + + @SuppressWarnings("unchecked") + private Class getDataClass() { + // Check generic superclass first (for classes that extend SQLRepository) + Type genericSuperclass = getClass().getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType parameterizedType) { + Type rawType = parameterizedType.getRawType(); + if (rawType instanceof Class rawClass && + SQLRepository.class.isAssignableFrom(rawClass)) { + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length > 1) { + return (Class) actualTypeArguments[1]; + } + } + } + throw new IllegalStateException("Could not determine data class for repository: " + getClass().getName()); + } + + /** + * Constructor for SQLRepository. + * + * @param requestHelper the request helper to be used by this repository + */ + public SQLRepository(RequestHelper requestHelper) { + this.requestHelper = requestHelper; + } + + /** + * Retrieves the name of the primary key column from the data class. + * + * @return the name of the primary key column + * @throws IllegalStateException if no primary key column is found + */ + protected String getPrimaryKeyColumn() { + for (RecordComponent recordComponent : dataClass.getRecordComponents()) { + if(recordComponent.isAnnotationPresent(Column.class)) { + Column column = recordComponent.getAnnotation(Column.class); + if(column.primary()) { + return column.value(); + } + } + } + throw new IllegalStateException("No primary key column found in data class: " + dataClass.getName()); + } + + /** + * Creates the table for the entity in the database. + * This method should be implemented by subclasses to define the table structure. + * + * @return a CompletableFuture that completes when the table is created + */ + public abstract CompletableFuture createTable(); + +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f02ffc4..1679d75 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,8 +26,7 @@ allprojects { name = "papermc" url = uri("https://repo.papermc.io/repository/maven-public/") } - maven(url = "https://repo.extendedclip.com/content/repositories/placeholderapi/") - maven(url = "https://jitpack.io") + maven("https://jitpack.io") } tasks.compileJava { @@ -38,16 +37,28 @@ allprojects { dependencies { compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT") - /* Depends */ - compileOnly("me.clip:placeholderapi:2.11.6") - compileOnly("fr.maxlego08.menu:zmenu-api:1.1.0.4") - - /* Adventure for Spigot compatibility */ compileOnly("net.kyori:adventure-platform-bukkit:4.3.4") + compileOnly("org.mozilla:rhino:1.7.14") + compileOnly("org.reflections:reflections:0.10.2") + + compileOnly(files(rootProject.files("libs/zMenu-1.1.0.4.jar"))) /* Libraries */ - implementation("com.github.Traqueur-dev:Structura:1.5.0") + implementation("fr.traqueur:structura:1.6.0") implementation("com.github.Traqueur-dev.CommandsAPI:platform-spigot:4.2.3") + implementation("fr.maxlego08.sarah:sarah:1.21") + implementation("org.bstats:bstats-bukkit:3.1.0") + + /* Test dependencies */ + testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") + testImplementation("org.mozilla:rhino:1.7.14") + testImplementation("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT") + testImplementation("org.slf4j:slf4j-simple:2.0.9") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + } + + tasks.test { + useJUnitPlatform() } tasks.shadowJar { @@ -55,14 +66,20 @@ allprojects { archiveAppendix.set(if (project.path == ":") "" else project.name) archiveClassifier.set("") - relocate("fr.traqueur.structura", "fr.traqueur.items.libs.structura") - relocate("fr.traqueur.commands", "fr.traqueur.items.libs.commands") + relocate("fr.traqueur.structura", "fr.traqueur.crates.libs.structura") + relocate("fr.traqueur.commands", "fr.traqueur.crates.libs.commands") + relocate("fr.maxlego08.sarah", "fr.traqueur.crates.libs.sarah") + relocate("org.bstats", "fr.traqueur.crates.libs.bstats") } } dependencies { api(project(":api")) + api(project(":common")) + rootProject.subprojects.filter { it.path.startsWith(":hooks:") }.forEach { subproject -> + implementation(project(subproject.path)) + } } tasks { diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..6e5faf4 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + api(project(":api")) +} \ No newline at end of file diff --git a/common/src/main/java/fr/traqueur/crates/models/placedcrates/BlockCrateDisplay.java b/common/src/main/java/fr/traqueur/crates/models/placedcrates/BlockCrateDisplay.java new file mode 100644 index 0000000..5366f56 --- /dev/null +++ b/common/src/main/java/fr/traqueur/crates/models/placedcrates/BlockCrateDisplay.java @@ -0,0 +1,38 @@ +package fr.traqueur.crates.models.placedcrates; + +import fr.traqueur.crates.api.models.placedcrates.CrateDisplay; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; + +public class BlockCrateDisplay implements CrateDisplay { + + private final Location location; + private final Material material; + + public BlockCrateDisplay(Location location, String value, float yaw) { + this.location = location; + this.material = Material.valueOf(value.toUpperCase()); + } + + @Override + public void spawn() { + Block block = location.getBlock(); + block.setType(material); + } + + @Override + public void remove() { + location.getBlock().setType(Material.AIR); + } + + @Override + public boolean matches(Block block) { + return block.getLocation().equals(location); + } + + @Override + public Location getLocation() { + return location; + } +} \ No newline at end of file diff --git a/common/src/main/java/fr/traqueur/crates/models/placedcrates/EntityCrateDisplay.java b/common/src/main/java/fr/traqueur/crates/models/placedcrates/EntityCrateDisplay.java new file mode 100644 index 0000000..c1687c2 --- /dev/null +++ b/common/src/main/java/fr/traqueur/crates/models/placedcrates/EntityCrateDisplay.java @@ -0,0 +1,113 @@ +package fr.traqueur.crates.models.placedcrates; + +import fr.traqueur.crates.api.models.placedcrates.CrateDisplay; +import fr.traqueur.crates.api.serialization.Keys; +import org.bukkit.Location; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; + +public class EntityCrateDisplay implements CrateDisplay { + + private final Location location; + protected final String entityType; + private final float yaw; + protected Entity entity; + + public EntityCrateDisplay(Location location, String value, float yaw) { + this.location = location; + this.entityType = value; + this.yaw = yaw; + } + + @Override + public void spawn() { + // First, check if an entity already exists at this location (from previous server session) + Location centeredLocation = location.clone().add(0.5, 0, 0.5); + Entity existingEntity = findExistingEntity(centeredLocation); + + if (existingEntity != null) { + this.entity = existingEntity; + // Ensure properties are still correct + this.entity.setRotation(yaw, 0); + this.entity.setInvulnerable(true); + this.entity.setPersistent(true); + this.entity.setGravity(false); + + if (entity instanceof LivingEntity living) { + living.setAI(false); + living.setSilent(true); + living.setCollidable(false); + } + + if (entity instanceof ArmorStand stand) { + stand.setCanMove(false); + stand.setCanTick(false); + } + return; + } + + // Spawn new entity centered on the block + EntityType type = EntityType.valueOf(entityType); + this.entity = location.getWorld().spawnEntity(centeredLocation, type); + this.entity.setRotation(yaw, 0); + this.entity.setInvulnerable(true); + this.entity.setPersistent(true); + this.entity.setGravity(false); + + if (entity instanceof LivingEntity living) { + living.setAI(false); + living.setSilent(true); + living.setCollidable(false); + } + + if (entity instanceof ArmorStand stand) { + stand.setCanMove(false); + stand.setCanTick(false); + } + + Keys.PLACED_CRATE_ENTITY.set(entity.getPersistentDataContainer(), true); + } + + private Entity findExistingEntity(Location centeredLocation) { + // Search for entities with the PDC marker in the vicinity + return centeredLocation.getWorld().getNearbyEntities(centeredLocation, 1, 1, 1).stream() + .filter(e -> { + // Check if entity has our marker + if (!Keys.PLACED_CRATE_ENTITY.get(e.getPersistentDataContainer(), false)) { + return false; + } + // Check if entity type matches + try { + EntityType expectedType = EntityType.valueOf(entityType); + return e.getType() == expectedType; + } catch (IllegalArgumentException ex) { + return false; + } + }) + .findFirst() + .orElse(null); + } + + @Override + public void remove() { + if (entity != null && !entity.isDead()) { + entity.remove(); + } + } + + @Override + public boolean matches(Entity entity) { + return this.entity != null && entity.getUniqueId().equals(this.entity.getUniqueId()); + } + + @Override + public Location getLocation() { + return location; + } + + public Entity getEntity() { + return entity; + } +} \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..0bd511f --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,936 @@ +# zCrates - Architecture Documentation + +High-level architecture overview for maintainers and contributors. + +## Table of Contents + +- [System Overview](#system-overview) +- [Module Structure](#module-structure) +- [Core Subsystems](#core-subsystems) +- [Data Flow](#data-flow) +- [Plugin Lifecycle](#plugin-lifecycle) +- [Design Decisions](#design-decisions) +- [Performance Considerations](#performance-considerations) +- [Security Architecture](#security-architecture) + +--- + +## System Overview + +zCrates is a modular Minecraft crate plugin built on Paper 1.21+ with Java 21. The architecture emphasizes: + +- **Separation of Concerns** - API, implementation, and extensions are distinct +- **Extensibility** - Hook system for seamless plugin integration +- **Security** - Sandboxed JavaScript execution with restricted API surface +- **Performance** - Async database operations, in-memory caching, efficient registries +- **Type Safety** - Polymorphic YAML deserialization, strong typing throughout + +### High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ USER INTERFACE │ +│ (Commands, GUIs, Chat Messages, Placed Crates) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ PRESENTATION LAYER │ +│ (Commands, Listeners, zMenu Integration) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ BUSINESS LOGIC LAYER │ +│ (CratesManager, UsersManager, AnimationExecutor) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ DOMAIN MODEL LAYER │ +│ (Crate, Reward, Key, Animation, Algorithm, User) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ INFRASTRUCTURE LAYER │ +│ (Registries, Script Engine, Storage, Serialization) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ EXTERNAL SERVICES │ +│ (Database, zMenu, Bukkit API, Custom Item Plugins) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Module Structure + +### 1. API Module (`api/`) + +**Purpose:** Public contract for external plugins + +**Responsibilities:** +- Define interfaces for all core models +- Expose event system +- Provide registry access +- Define extension points (Hook, ItemsProvider, etc.) + +**Key Characteristics:** +- No implementation details +- Minimal dependencies (only Bukkit API, annotations) +- Semantic versioning +- Backward compatibility guarantees + +**Exported Packages:** +- `fr.traqueur.crates.api` - Core interfaces +- `fr.traqueur.crates.api.events` - Event system +- `fr.traqueur.crates.api.models` - Model interfaces +- `fr.traqueur.crates.api.registries` - Registry system +- `fr.traqueur.crates.api.managers` - Manager interfaces +- `fr.traqueur.crates.api.hooks` - Hook system +- `fr.traqueur.crates.api.providers` - Provider interfaces + +### 2. Common Module (`common/`) + +**Purpose:** Shared implementations used across main plugin and hooks + +**Responsibilities:** +- Block/Entity display implementations +- Shared wrapper classes +- Common utilities + +**Dependencies:** +- API module +- Bukkit API + +### 3. Main Plugin Module (`src/`) + +**Purpose:** Core plugin implementation + +**Responsibilities:** +- Implement all API interfaces +- Manage plugin lifecycle +- Orchestrate business logic +- Persist data +- Execute JavaScript + +**Dependencies:** +- API module +- Common module +- Bukkit/Paper API +- Rhino (JavaScript engine) +- Structura (YAML serialization) +- CommandsAPI (command framework) +- Sarah (ORM) +- zMenu (GUI system) +- Reflections (classpath scanning) + +### 4. Hooks Module (`hooks/`) + +**Purpose:** Optional plugin integrations + +**Structure:** +``` +hooks/ +├── ItemsAdder/ - ItemsAdder custom items +├── MythicMobs/ - MythicMobs entity displays +├── Nexo/ - Nexo custom items +├── Oraxen/ - Oraxen custom items +├── PlaceholderAPI/ - Placeholder support +└── zItems/ - zItems integration +``` + +**Characteristics:** +- Each hook is a separate Gradle subproject +- Isolated dependencies (only required hook targets) +- Auto-discovered via `@AutoHook` annotation +- Gracefully disabled if target plugin missing + +--- + +## Core Subsystems + +### 1. Registry System + +**Architecture:** + +``` +┌────────────────────────────────────────┐ +│ Static Registry Accessor │ +│ ClassToInstanceMap │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ Registry Interface │ +│ - register(ID, T) │ +│ - getById(ID): T │ +│ - getAll(): Collection │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ FileBasedRegistry Abstract │ +│ - loadFromFolder() │ +│ - reload() │ +│ - folder hierarchy support │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ Concrete Implementations │ +│ - ZAnimationRegistry (.js) │ +│ - ZRandomAlgorithmRegistry (.js) │ +│ - ZCratesRegistry (.yml) │ +└────────────────────────────────────────┘ +``` + +**Design Decisions:** + +1. **Static Access via ClassToInstanceMap** + - Avoids dependency injection complexity + - Type-safe registry retrieval + - Single source of truth for all registries + +2. **FileBasedRegistry Pattern** + - Automatic resource copying from JAR + - Folder hierarchy with metadata + - Hot-reload support + - Consistent loading behavior + +3. **Generic ID Type** + - String IDs for file-based registries + - Enum IDs for runtime registries (DisplayType) + - Type safety at compile time + +### 2. Manager System + +**Architecture:** + +``` +┌────────────────────────────────────────┐ +│ Bukkit ServicesManager │ +│ (Plugin service registration) │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ Manager Interface │ +│ - init() │ +│ - reload() │ +└────────────────────────────────────────┘ + ↓ +┌────────────────┬───────────────────────┐ +│ CratesManager │ UsersManager │ +│ - Opening │ - User cache │ +│ - Animation │ - Key management │ +│ - Reroll │ - Persistence │ +│ - Preview │ - History │ +│ - Placed │ │ +└────────────────┴───────────────────────┘ +``` + +**Design Decisions:** + +1. **ServicesManager Integration** + - Standard Bukkit pattern + - Automatic lifecycle management + - Accessible from any plugin + +2. **Single Responsibility** + - CratesManager: crate operations + - UsersManager: user data + - Clear separation of concerns + +3. **In-Memory State** + - Active openings in CratesManager + - User cache in UsersManager + - Placed crates loaded per chunk + +### 3. Script Engine System + +**Architecture:** + +``` +┌────────────────────────────────────────┐ +│ ZScriptEngine (Singleton) │ +│ - Rhino Context │ +│ - Custom WrapFactory │ +│ - Security constraints │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ ScriptableObject Scopes │ +│ (Isolated per execution) │ +└────────────────────────────────────────┘ + ↓ +┌────────────────┬───────────────────────┐ +│ animations │ algorithms │ +│ global │ global │ +│ object │ object │ +└────────────────┴───────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ Context Objects (Wrappers) │ +│ - PlayerWrapper │ +│ - InventoryWrapper │ +│ - CrateWrapper │ +│ - RewardsWrapper │ +│ - HistoryWrapper │ +└────────────────────────────────────────┘ +``` + +**Design Decisions:** + +1. **Single Engine Instance** + - Shared Rhino Context across all scripts + - Reduced memory footprint + - Consistent security settings + +2. **Isolated Scopes** + - Each script execution uses new scope + - Null parent (no prototype chain access) + - Prevents cross-script interference + +3. **Wrapper Pattern** + - Controlled API surface + - Type-safe method exposure + - No direct Java object access + +4. **Security First** + - Interpreter mode (no bytecode) + - Method blacklist via WrapFactory + - No Java package access + - No reflection + +### 4. Animation System + +**Architecture:** + +``` +┌────────────────────────────────────────┐ +│ AnimationsRegistry │ +│ - Load .js files │ +│ - Register via AnimationsRegistrar │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ Animation Model │ +│ - List │ +│ - onComplete callback │ +│ - onCancel callback │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ AnimationExecutor │ +│ - Phase timing (BukkitRunnable) │ +│ - SpeedCurve application │ +│ - Context passing │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ JavaScript Callbacks │ +│ - onStart(context) │ +│ - onTick(context, tickData) │ +│ - onComplete(context) │ +└────────────────────────────────────────┘ +``` + +**Design Decisions:** + +1. **Phase-Based Execution** + - Sequential phase execution + - Independent timing per phase + - Clean state transitions + +2. **SpeedCurve Integration** + - Mathematical easing functions + - Applied to progress value + - Smooth visual transitions + +3. **BukkitRunnable Scheduling** + - Sync with server tick rate + - Cancel-safe execution + - Automatic cleanup + +### 5. Storage System + +**Architecture:** + +``` +┌────────────────────────────────────────┐ +│ DatabaseConnection │ +│ - SQLite / MySQL / MariaDB │ +│ - Connection pooling (HikariCP) │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ Sarah ORM Layer │ +│ - RequestHelper │ +│ - Repository pattern │ +│ - Migration system │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ UserRepository │ +│ - CRUD operations │ +│ - CompletableFuture async │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ DTO ↔ Domain Mapping │ +│ - UserDTO → ZUser │ +│ - Lightweight transfer objects │ +└────────────────────────────────────────┘ +``` + +**Design Decisions:** + +1. **Sarah ORM** + - Lightweight abstraction + - Multiple database support + - Migration management + - Connection pooling + +2. **Repository Pattern** + - Separation of persistence logic + - Testable data access + - Async by default + +3. **DTO Pattern** + - Database schema independence + - Clear domain/persistence boundary + - Easy schema evolution + +4. **Async Operations** + - Non-blocking database access + - CompletableFuture API + - Main thread safety + +### 6. Display System + +**Architecture:** + +``` +┌────────────────────────────────────────┐ +│ CrateDisplayFactoriesRegistry │ +│ Map │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ CrateDisplayFactory │ +│ - create(Location, value, yaw) │ +│ - isValidValue(value) │ +│ - getSuggestions() │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ CrateDisplay │ +│ - spawn() │ +│ - remove() │ +│ - matches(T) │ +└────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────┐ +│ PlacedCrate (Immutable Record) │ +│ - Stored in Chunk PDC │ +│ - Load/unload with chunks │ +└────────────────────────────────────────┘ +``` + +**Design Decisions:** + +1. **Generic Type Parameter** + - Type-safe value matching + - Compile-time validation + - Flexible display types + +2. **Factory Pattern** + - Encapsulates creation logic + - Validation before creation + - Tab completion support + +3. **Chunk PDC Storage** + - Persistent across restarts + - Automatic chunk association + - No separate database table + +4. **Lazy Loading** + - Displays created on chunk load + - Removed on chunk unload + - Memory efficient + +--- + +## Data Flow + +### Crate Opening Flow + +``` +1. Player interacts with placed crate + ↓ +2. CratesListener handles interaction + ↓ +3. CratesManager.tryOpenCrate(player, crate) + ├─ Check if player has key + ├─ Check all OpenConditions + ├─ Fire CratePreOpenEvent (cancellable) + └─ If all pass, continue + ↓ +4. Fire CrateOpenEvent + ↓ +5. Consume key (virtual or physical) + ↓ +6. CratesManager.openCrate(player, crate, animation) + ├─ Generate reward via algorithm + ├─ Fire RewardGeneratedEvent + ├─ Store OpenedCrate state + └─ Start animation + ↓ +7. AnimationExecutor.start(player, animation, context) + ├─ Execute phases sequentially + ├─ Call JavaScript callbacks + └─ Update inventory + ↓ +8. Animation completes + ↓ +9. If rerolls available, show reroll button + OR + Give reward and close inventory + ↓ +10. Fire RewardGivenEvent + ↓ +11. Persist CrateOpening to database (async) + ↓ +12. Cleanup OpenedCrate state +``` + +### User Data Flow + +``` +1. Player joins server + ↓ +2. PlayerJoinEvent listener + ↓ +3. UsersManager.loadUser(uuid) (async) + ├─ Query database via UserRepository + ├─ Convert UserDTO → ZUser + └─ Store in cache + ↓ +4. Player operations (synchronous via cache) + ├─ getKeyAmount() + ├─ addKey() + ├─ removeKey() + └─ All modifications in-memory + ↓ +5. Player quits server + ↓ +6. PlayerQuitEvent listener + ↓ +7. UsersManager.saveUser(user) (async) + ├─ Convert ZUser → UserDTO + ├─ Update database via UserRepository + └─ Remove from cache +``` + +### Configuration Loading Flow + +``` +1. Plugin enable or /zcrates reload + ↓ +2. For each FileBasedRegistry: + ├─ Clear existing entries + ├─ Scan folder recursively + ├─ Load each file via loadFile(Path) + │ ├─ For .js: evaluate via ZScriptEngine + │ └─ For .yml: deserialize via Structura + ├─ Capture registrations + └─ Store in internal map + ↓ +3. Validate cross-references + ├─ Crate → Animation exists + ├─ Crate → Algorithm exists + └─ Reward display items valid + ↓ +4. Log statistics +``` + +--- + +## Plugin Lifecycle + +### Initialization Sequence + +``` +onEnable() +├─1. Load config files (config.yml, messages.yml) +│ +├─2. Register polymorphic type adapters +│ ├─ Reward types (ITEM, ITEMS, COMMAND, COMMANDS) +│ ├─ Key types (VIRTUAL, PHYSIC) +│ ├─ DatabaseSettings types (SQLITE, MYSQL, MARIADB) +│ └─ OpenCondition types (PERMISSION, COOLDOWN) +│ +├─3. Initialize core services +│ ├─ Logger (debug mode) +│ ├─ Keys (PDC key registry) +│ ├─ MessagesService (MiniMessage parsing) +│ └─ PlacedCrateDataType (PDC serialization) +│ +├─4. Create ZScriptEngine instance +│ +├─5. Integrate with zMenu +│ ├─ Get InventoryManager +│ ├─ Get ButtonManager +│ └─ Register custom buttons (ZCRATES_ANIMATION, etc.) +│ +├─6. Create and register all registries +│ ├─ AnimationsRegistry +│ ├─ RandomAlgorithmsRegistry +│ ├─ CratesRegistry +│ ├─ ItemsProvidersRegistry +│ ├─ HooksRegistry +│ └─ CrateDisplayFactoriesRegistry +│ +├─7. Discover and enable hooks +│ ├─ Scan classpath for @AutoHook +│ ├─ Check if target plugin loaded +│ └─ Call hook.onEnable() +│ +├─8. Register built-in display factories +│ ├─ BLOCK (BlockCrateDisplayFactory) +│ └─ ENTITY (EntityCrateDisplayFactory) +│ +├─9. Load configurations +│ ├─ loadFromFolder(animations/) +│ ├─ loadFromFolder(algorithms/) +│ └─ loadFromFolder(crates/) +│ +├─10. Initialize database +│ ├─ Create DatabaseConnection +│ ├─ Validate connection +│ ├─ Create UserRepository +│ └─ Execute migrations +│ +├─11. Create and register managers +│ ├─ UsersManager (with repository) +│ └─ CratesManager +│ +├─12. Initialize managers +│ ├─ usersManager.init() +│ │ ├─ Register listeners +│ │ └─ Load online players +│ └─ cratesManager.init() +│ └─ Verify menu files exist +│ +└─13. Register commands + └─ ZCratesCommand with subcommands +``` + +### Shutdown Sequence + +``` +onDisable() +├─1. Stop all active animations +│ └─ cratesManager.stopAllOpening() +│ +├─2. Unload all placed crate displays +│ └─ cratesManager.unloadAllPlacedCrates() +│ +├─3. Save all cached users +│ └─ For each cached user: saveUser(user) +│ +├─4. Close database connection +│ └─ databaseConnection.disconnect() +│ +├─5. Close script engine +│ └─ scriptEngine.close() +│ +└─6. Shutdown services + └─ MessagesService.close() +``` + +### Reload Sequence + +``` +/zcrates reload +├─1. Stop all active animations +│ +├─2. Clear all registries +│ ├─ animationsRegistry.clear() +│ ├─ algorithmsRegistry.clear() +│ └─ cratesRegistry.clear() +│ +├─3. Reload configuration files +│ ├─ config.yml +│ └─ messages.yml +│ +├─4. Reload file-based registries +│ ├─ loadFromFolder(animations/) +│ ├─ loadFromFolder(algorithms/) +│ └─ loadFromFolder(crates/) +│ +└─5. Notify managers + └─ cratesManager.reload() +``` + +--- + +## Design Decisions + +### 1. Why Rhino Instead of Nashorn/GraalVM? + +**Decision:** Use Rhino JavaScript engine + +**Rationale:** +- **Security:** Fine-grained control over Java access +- **Compatibility:** Works on all Java versions (11+) +- **Sandbox:** Built-in sandboxing with `initSafeStandardObjects()` +- **Performance:** Interpreter mode sufficient for animation/algorithm scripts + +**Trade-offs:** +- Not ES2015+ compliant (limited ES6 support) +- Slower than GraalVM for compute-intensive tasks +- No native Promise support + +### 2. Why Static Registry Access? + +**Decision:** Use `ClassToInstanceMap` for static registry access + +**Rationale:** +- **Simplicity:** No dependency injection framework needed +- **Type Safety:** Compile-time verification of registry types +- **Accessibility:** Accessible from any plugin code +- **Singleton:** Single source of truth + +**Trade-offs:** +- Global state (harder to test in isolation) +- Cannot swap implementations at runtime +- Requires manual registration + +### 3. Why Chunk PDC for Placed Crates? + +**Decision:** Store placed crates in Chunk PDC + +**Rationale:** +- **Automatic Association:** Chunks own their crates +- **Persistence:** Survives server restarts +- **Performance:** No database queries for crate lookup +- **Simplicity:** No separate placed_crates table + +**Trade-offs:** +- Limited to chunk-sized queries (can't query all crates in world easily) +- PDC size limits (unlikely to hit in practice) +- Chunk load required for data access + +### 4. Why ServicesManager for Managers? + +**Decision:** Register managers via Bukkit ServicesManager + +**Rationale:** +- **Standard Pattern:** Follows Bukkit conventions +- **Discovery:** Other plugins can find managers +- **Lifecycle:** Bukkit manages service lifecycle +- **Type Safety:** Retrieval by class + +**Trade-offs:** +- Couples to Bukkit API +- Limited lifecycle control +- No priority/ordering guarantees + +### 5. Why Polymorphic YAML with Structura? + +**Decision:** Use Structura for YAML deserialization + +**Rationale:** +- **Type Safety:** Compile-time validation of YAML structure +- **Polymorphism:** `@Polymorphic` annotation for type field +- **Validation:** Built-in validation (@Options, @DefaultInt, etc.) +- **Maintainability:** YAML changes reflected in code + +**Trade-offs:** +- Additional dependency +- Learning curve for contributors +- Less flexible than manual parsing + +--- + +## Performance Considerations + +### 1. Memory Management + +**User Cache:** +- Cleared on player quit (prevents memory leaks) +- ConcurrentHashMap for thread-safe access +- No unbounded growth + +**Registry Storage:** +- HashMap-based (O(1) lookup) +- Immutable after loading (no synchronization needed) +- Cleared on reload (prevents memory bloat) + +**Animation State:** +- Removed immediately after completion +- No persistent animation history +- Bounded by concurrent player count + +### 2. Database Optimization + +**Connection Pooling:** +- HikariCP for connection management +- Configurable pool size +- Connection validation + +**Async Operations:** +- All DB operations use CompletableFuture +- No main thread blocking +- Batch operations where possible + +**Caching:** +- User data cached in memory +- Keys stored locally (no query per check) +- Opening history loaded on-demand + +### 3. JavaScript Performance + +**Interpreter Mode:** +- No bytecode compilation overhead +- Lower memory usage +- Acceptable performance for animations/algorithms + +**Scope Reuse:** +- Scopes created per execution (not per phase) +- Shared Rhino Context across all scripts +- Minimal GC pressure + +**Wrapper Objects:** +- Lightweight wrappers (no heavy computation) +- Direct method calls (no reflection) +- Cached references where possible + +### 4. Event System + +**Listener Registration:** +- Single listener instances (not per crate) +- Priority-based execution +- Ignore if not handling event + +**Event Frequency:** +- CratePreOpenEvent: Once per open attempt +- CrateOpenEvent: Once per successful open +- RewardGeneratedEvent: Once per opening + rerolls +- RewardGivenEvent: Once per completion + +--- + +## Security Architecture + +### 1. JavaScript Sandbox + +**Threat Model:** +- Malicious server admin uploading dangerous scripts +- Exploiting Java reflection to bypass security +- Accessing file system or network +- Escaping sandbox via prototype pollution + +**Mitigations:** +- **No Java Package Access:** `initSafeStandardObjects()` blocks java.* +- **Method Blacklist:** Custom WrapFactory blocks dangerous methods +- **Null Parent Scopes:** Prevents prototype chain manipulation +- **Interpreter Mode:** No bytecode execution (no JIT exploits) +- **Wrapper Objects:** Controlled API surface + +**Residual Risks:** +- Infinite loops (no timeout mechanism) +- Memory exhaustion (no heap limits) +- Malicious animation logic (e.g., inventory spam) + +### 2. Inventory Security + +**Threat Model:** +- Script manipulating unauthorized slots +- Duplication exploits via inventory manipulation +- Item theft via inventory swaps + +**Mitigations:** +- **Slot Authorization:** Only authorized slots can be modified +- **Wrapper Methods:** Controlled inventory access +- **State Validation:** Animation state tracked by manager + +### 3. Data Security + +**Threat Model:** +- SQL injection via user input +- Race conditions in user data +- Data loss on crashes + +**Mitigations:** +- **Prepared Statements:** Sarah ORM uses prepared statements +- **Concurrent Collections:** Thread-safe user cache +- **Transactions:** Database operations in transactions +- **Async Saves:** Non-blocking persistence + +### 4. Permission System + +**Threat Model:** +- Unauthorized crate access +- Permission bypass via exploits + +**Mitigations:** +- **PermissionCondition:** Checks before opening +- **Event Cancellation:** External plugins can prevent opens +- **Key Validation:** Keys verified before consumption + +--- + +## Future Considerations + +### Scalability + +**Potential Bottlenecks:** +1. User cache growth (many concurrent players) +2. Database connection pool exhaustion +3. Animation executor (many concurrent animations) +4. Chunk PDC size limits + +**Mitigation Strategies:** +1. TTL-based cache eviction +2. Configurable pool size +3. Queue system for concurrent animations +4. Migration to separate placed_crates table if needed + +### Extensibility + +**Extension Points:** +1. Custom OpenConditions via polymorphic registry +2. Custom Rewards via polymorphic registry +3. Custom DisplayTypes via factory registry +4. Custom Hooks via @AutoHook annotation +5. Custom ItemsProviders via provider registry + +**API Stability:** +- Semantic versioning for API module +- Deprecation warnings before removal +- Backward compatibility for configuration + +### Monitoring + +**Observability:** +1. Debug logging for troubleshooting +2. Event system for external tracking +3. Metrics (opening counts, reward distribution) + +**Health Checks:** +1. Database connection validation +2. Registry population verification +3. Script engine health checks + +--- + +## Conclusion + +The zCrates architecture prioritizes: + +1. **Modularity** - Clear separation between API, implementation, and extensions +2. **Security** - Sandboxed JavaScript, controlled API surface +3. **Performance** - Async operations, efficient caching, optimized data structures +4. **Extensibility** - Hook system, polymorphic types, provider pattern +5. **Maintainability** - Type safety, clear abstractions, comprehensive documentation + +For detailed implementation guidance, see: +- `DEVELOPER_GUIDE.md` - Extension and integration guide +- `USER_GUIDE.md` - Configuration and usage guide \ No newline at end of file diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..ce366d4 --- /dev/null +++ b/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,1170 @@ +# zCrates - Developer Guide + +Comprehensive guide for developers extending or integrating with the zCrates plugin. + +## Table of Contents + +- [Architecture Overview](#architecture-overview) +- [Project Structure](#project-structure) +- [Core Systems](#core-systems) +- [Extension Points](#extension-points) +- [JavaScript API](#javascript-api) +- [Database Schema](#database-schema) +- [Security Model](#security-model) +- [Best Practices](#best-practices) + +--- + +## Architecture Overview + +zCrates follows a layered architecture with clear separation between API, implementation, and extensions: + +``` +┌─────────────────────────────────────────────────────┐ +│ Extensions Layer │ +│ (Hooks: ItemsAdder, MythicMobs, PlaceholderAPI) │ +└─────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────┐ +│ Business Logic Layer │ +│ (Managers, Commands, Listeners) │ +└─────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────┐ +│ Registry Layer │ +│ (Crates, Animations, Algorithms, Providers) │ +└─────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────┐ +│ Data Layer │ +│ (Storage, Repositories, DTOs) │ +└─────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────┐ +│ API Layer │ +│ (Interfaces, Events, Models, Services) │ +└─────────────────────────────────────────────────────┘ +``` + +### Key Design Patterns + +1. **Registry Pattern** - Centralized component registration and retrieval +2. **Manager Pattern** - Business logic orchestration via Bukkit ServicesManager +3. **Wrapper Pattern** - Safe object exposure to JavaScript sandboxes +4. **Factory Pattern** - Display creation via CrateDisplayFactory +5. **Polymorphic Serialization** - Type-safe YAML deserialization with Structura +6. **Repository Pattern** - Database access abstraction via Sarah ORM + +--- + +## Project Structure + +``` +zCrates/ +├── api/ # Public API module (interfaces, models, registries) +│ └── src/main/java/fr/traqueur/crates/api/ +│ ├── annotations/ # @AutoHook for hook auto-discovery +│ ├── events/ # Event system (6 event types) +│ ├── hooks/ # Hook interface +│ ├── managers/ # Manager interfaces +│ ├── models/ # Core models (Crate, Key, Reward, etc.) +│ ├── providers/ # Provider interfaces +│ ├── registries/ # Registry system +│ ├── services/ # Service interfaces +│ ├── serialization/ # PDC and serialization +│ ├── settings/ # Configuration models +│ └── storage/ # Storage layer interfaces +│ +├── common/ # Shared implementations (displays, wrappers) +│ +├── src/main/java/fr/traqueur/crates/ +│ ├── zCrates.java # Main plugin entry point +│ ├── algorithms/ # Algorithm system +│ ├── animations/ # Animation system +│ ├── commands/ # Command implementations +│ ├── engine/ # ZScriptEngine (secure Rhino) +│ ├── listeners/ # Event listeners +│ ├── managers/ # Manager implementations +│ ├── models/ # Data models and wrappers +│ ├── registries/ # Registry implementations +│ ├── settings/ # Settings implementation +│ ├── storage/ # Storage implementation +│ └── views/ # zMenu integration +│ +└── hooks/ # Optional plugin integrations + ├── ItemsAdder/ # ItemsAdder custom items + ├── MythicMobs/ # MythicMobs entity displays + ├── Nexo/ # Nexo custom items + ├── Oraxen/ # Oraxen custom items + ├── PlaceholderAPI/ # Placeholder support + └── zItems/ # zItems integration +``` + +--- + +## Core Systems + +### 1. Registry System + +The registry system provides centralized component registration and retrieval using Guava's `ClassToInstanceMap`. + +#### Registry Interface + +```java +public interface Registry { + void register(ID id, T item); + T getById(ID id); + Collection getAll(); + void clear(); + + // Static registry access + static > T get(Class clazz); + static void register(Class> clazz, Registry registry); +} +``` + +#### FileBasedRegistry + +Abstract base class for file-loaded registries with folder hierarchy support: + +```java +public abstract class FileBasedRegistry implements Registry { + // Auto-loads from folder + // Supports folder.properties metadata + // Hot-reload via reload() + + protected abstract T loadFile(Path path) throws IOException; + protected abstract ID getId(T item); +} +``` + +#### Available Registries + +| Registry | ID Type | Item Type | Source | +|---------------------------------|-------------|---------------------|----------------------| +| `AnimationsRegistry` | String | Animation | `animations/*.js` | +| `RandomAlgorithmsRegistry` | String | RandomAlgorithm | `algorithms/*.js` | +| `CratesRegistry` | String | Crate | `crates/*.yml` | +| `ItemsProvidersRegistry` | String | ItemsProvider | Runtime registration | +| `HooksRegistry` | String | Hook | Classpath scanning | +| `CrateDisplayFactoriesRegistry` | DisplayType | CrateDisplayFactory | Runtime registration | + +**Usage Example:** + +```java +// Get a registry +CratesRegistry registry = Registry.get(CratesRegistry.class); + +// Retrieve items +Crate crate = registry.getById("legendary"); +Collection allCrates = registry.getAll(); + +// Register custom item +registry.register("custom-crate", new MyCrate()); +``` + +--- + +### 2. Manager Layer + +Managers orchestrate business logic and are registered via Bukkit's ServicesManager. + +#### CratesManager + +Handles crate opening, animations, and placed crates. + +**Key Methods:** + +```java +// Opening system +OpenResult tryOpenCrate(Player player, Crate crate); +void openCrate(Player player, Crate crate, Animation animation); +void openPreview(Player player, Crate crate); + +// Reroll system +boolean canReroll(Player player); +int getRerollsRemaining(Player player); +boolean reroll(Player player); + +// Animation state +Optional getCurrentReward(Player player); +boolean isAnimationCompleted(Player player); + +// Placed crates +PlacedCrate placeCrate(String crateId, Location location, DisplayType type, String value, float yaw); +void removePlacedCrate(PlacedCrate placedCrate); +Optional findPlacedCrateByBlock(Block block); +Optional findPlacedCrateByEntity(Entity entity); +``` + +**Internal State Management:** + +```java +// Active openings stored in memory +Map openedCrates; + +record OpenedCrate( + Crate crate, + Animation animation, + Reward reward, + int rerollsRemaining, + AnimationProgress progress +); +``` + +#### UsersManager + +Handles player data and key management with async database operations. + +**Key Methods:** + +```java +// User lifecycle +User getUser(UUID uuid); // Synchronous (cache) +CompletableFuture loadUser(UUID uuid); // Asynchronous (DB) +CompletableFuture saveUser(User user); // Asynchronous (DB) + +// Opening persistence +void persistCrateOpening(CrateOpening opening); +``` + +**Caching Strategy:** + +```java +// In-memory cache +ConcurrentHashMap cache; + +// Auto-load on join, auto-save on quit +// Accessed synchronously via getUser() +``` + +--- + +### 3. Script Engine System + +ZScriptEngine provides secure, sandboxed JavaScript execution using Mozilla Rhino. + +#### Security Features + +```java +public class ZScriptEngine implements AutoCloseable { + // Rhino context with security restrictions + - initSafeStandardObjects() // No java.* access + - Optimization level -1 // Interpreter mode + - Null parent scopes // Isolation + - Custom WrapFactory // Method blocking +} +``` + +**Blocked Methods:** +- `getClass()`, `class` +- `notify()`, `notifyAll()`, `wait()` +- `clone()`, `finalize()`, `hashCode()` + +**Allowed JavaScript:** +- ES6 features (arrow functions, let/const, template literals) +- Standard objects (Array, Object, Math, JSON, Date, RegExp) +- Context objects (player, inventory, crate, rewards, history) + +#### Script Execution Methods + +```java +// Fire-and-forget +void executeFunction(ScriptableObject scope, String functionName, Object... args); + +// With result + T executeFunctionWithResult(ScriptableObject scope, String functionName, Class returnType, Object... args); + +// Load script +void evaluateFile(Path scriptPath, ScriptableObject scope); + +// Create isolated scope +ScriptableObject createSecureScope(); +``` + +**Usage Example:** + +```java +ZScriptEngine engine = new ZScriptEngine(); +ScriptableObject scope = engine.createSecureScope(); + +// Load script +engine.evaluateFile(Paths.get("animations/my_animation.js"), scope); + +// Execute function +AnimationContext context = new AnimationContext(player, crate, inventory); +engine.executeFunction(scope, "onStart", context); +``` + +--- + +### 4. Animation System + +Animations provide frame-by-frame visual effects during crate opening. + +#### Animation Model + +```java +public record Animation( + String id, + List phases, + JavaScriptFunction onComplete, + JavaScriptFunction onCancel +) {} + +public record AnimationPhase( + String name, + long duration, // milliseconds + int interval, // ticks between onTick calls + SpeedCurve speedCurve, // Easing function + JavaScriptFunction onStart, + JavaScriptFunction onTick, + JavaScriptFunction onComplete +) {} + +public enum SpeedCurve { + LINEAR, // Constant speed + EASE_IN, // Starts slow, accelerates + EASE_OUT, // Starts fast, decelerates + EASE_IN_OUT // Slow → Fast → Slow +} +``` + +#### AnimationExecutor + +Manages animation timing and phase transitions: + +```java +public class AnimationExecutor { + void start(Player player, Animation animation, AnimationContext context); + void stop(Player player); + boolean isRunning(Player player); +} +``` + +**Execution Flow:** +1. Initialize phase 0 +2. Call `onStart(context)` +3. Every `interval` ticks: call `onTick(context, tickData)` +4. After `duration` ms: call `onComplete(context)` and move to next phase +5. After all phases: call animation's `onComplete(context)` + +#### AnimationContext + +Provides safe access to runtime objects: + +```java +public record AnimationContext( + PlayerWrapper player, + InventoryWrapper inventory, + CrateWrapper crate +) {} +``` + +#### TickData + +Per-tick information passed to `onTick()`: + +```java +public record TickData( + int tickNumber, // Current tick in phase + double progress, // 0.0 to 1.0 (with speed curve applied) + long elapsedTime // Milliseconds since phase start +) {} +``` + +**JavaScript API:** + +```javascript +animations.register("my_animation", { + phases: [ + { + name: "spin", + duration: 3000, + interval: 2, + speedCurve: "EASE_OUT", + onStart: function(context) { + context.player().playSound("BLOCK_NOTE_BLOCK_PLING", 1.0, 1.0); + }, + onTick: function(context, tickData) { + var progress = tickData.progress(); + context.inventory().randomizeSlots(); + }, + onComplete: function(context) { + context.player().playSound("ENTITY_PLAYER_LEVELUP", 1.0, 1.0); + } + } + ], + onComplete: function(context) { + if (!context.crate().hasRerolls()) { + context.inventory().close(60); + } + }, + onCancel: function(context) { + context.player().sendMessage("Animation cancelled"); + } +}); +``` + +--- + +### 5. Algorithm System + +Algorithms provide customizable reward selection logic. + +#### RandomAlgorithm Model + +```java +public record RandomAlgorithm( + String id, + JavaScriptFunction selector +) {} +``` + +#### AlgorithmContext + +Runtime context passed to selector function: + +```java +public record AlgorithmContext( + RewardsWrapper rewards, + HistoryWrapper history, + String crateId, + UUID playerUuid +) {} +``` + +**JavaScript API:** + +```javascript +algorithms.register("pity_system", function(context) { + var rewards = context.rewards(); + var history = context.history(); + + // Get last 50 openings + var recentOpenings = history.getRecent(50); + + // Count openings without rare reward + var openingsWithoutRare = 0; + for (var i = 0; i < recentOpenings.size(); i++) { + var opening = recentOpenings.get(i); + if (opening.crateId() === context.crateId()) { + var reward = rewards.getById(opening.rewardId()); + if (reward && reward.weight() > 5.0) { + openingsWithoutRare++; + } else { + break; + } + } + } + + // If 50 openings without rare, guarantee one + if (openingsWithoutRare >= 50) { + return rewards.filterByMaxWeight(5.0).weightedRandom(); + } + + return rewards.weightedRandom(); +}); +``` + +--- + +### 6. Reward System + +Rewards define what players receive from crates. + +#### Reward Interface + +```java +@Polymorphic +public interface Reward extends Loadable { + String id(); + double weight(); + ItemStackWrapper displayItem(); + void give(Player player); +} +``` + +#### Implementations + +| Type | Class | Fields | Behavior | +|----------|--------------------|--------------------------------|---------------------------| +| ITEM | ItemReward | `ItemStackWrapper item` | Give single item | +| ITEMS | ItemsListReward | `List items` | Give multiple items | +| COMMAND | CommandReward | `String command` | Execute command | +| COMMANDS | CommandsListReward | `List commands` | Execute multiple commands | + +**YAML Configuration:** + +```yaml +rewards: + - type: ITEM + id: diamond_reward + weight: 10.0 + display-item: + material: DIAMOND + item: + material: DIAMOND + amount: 5 + display-name: "Diamonds" + + - type: COMMAND + id: money_reward + weight: 20.0 + display-item: + material: GOLD_INGOT + command: "eco give %player% 1000" +``` + +#### ItemStackWrapper System + +Supports both vanilla materials and custom item plugins: + +```yaml +# Vanilla material +item: + material: DIAMOND_SWORD + amount: 1 + display-name: "Legendary Sword" + lore: + - "A powerful weapon" + +# Delegate to custom item plugin +item: + copy-from: + plugin-name: "Oraxen" + item-id: "ruby_sword" + amount: 1 + display-name: "Ruby Sword" # Override display name +``` + +**Available Delegates:** +- `ItemsAdder` - ItemsAdder custom items +- `Oraxen` - Oraxen custom items +- `Nexo` - Nexo custom items +- `zItems` - zItems custom items (via ItemsAdder provider) + +--- + +### 7. Key System + +Keys control crate access and come in two types. + +#### Key Interface + +```java +@Polymorphic +public interface Key extends Loadable { + String name(); + boolean has(Player player); + void give(Player player, int amount); + void remove(Player player); +} +``` + +#### VirtualKey + +Database-stored key balance: + +```yaml +key: + type: VIRTUAL + name: "legendary-key" +``` + +**Storage:** +- Table: `user_keys` +- Columns: `user_id`, `key_name`, `amount` +- Managed by `UsersManager` + +#### PhysicKey + +Physical item in inventory: + +```yaml +key: + type: PHYSIC + name: "legendary-physical-key" + item: + material: TRIPWIRE_HOOK + display-name: "Legendary Key" + lore: + - "Right-click on a crate" + glow: true + custom-model-data: 1001 +``` + +**Matching:** +- Uses PDC (PersistentDataContainer) for identification +- Key: `zcrates:key_name` +- Value: key name string + +--- + +### 8. Condition System + +Conditions gate crate opening with arbitrary requirements. + +#### OpenCondition Interface + +```java +@Polymorphic +public interface OpenCondition extends Loadable { + boolean check(Player player, Crate crate); + void onOpen(Player player, Crate crate); // Side effects + String errorMessageKey(); +} +``` + +#### Built-in Conditions + +| Type | Class | Configuration | Behavior | +|-------------|----------------------|-----------------------------------|----------------------| +| PERMISSION | PermissionCondition | `permission: node` | Check permission | +| COOLDOWN | CooldownCondition | `cooldown: 60000` | Rate-limit (ms) | +| PLACEHOLDER | PlaceholderCondition | `placeholder, comparison, result` | PlaceholderAPI check | + +**YAML Configuration:** + +```yaml +conditions: + - type: PERMISSION + permission: "zcrates.open.legendary" + + - type: COOLDOWN + cooldown: 3600000 # 1 hour + + - type: PLACEHOLDER + placeholder: "%player_level%" + comparison: GREATER_THAN_OR_EQUALS + result: "50" +``` + +--- + +### 9. Display System + +Displays represent crates in the game world. + +#### CrateDisplay Interface + +```java +public interface CrateDisplay { + void spawn(); + void remove(); + boolean matches(T value); + Location getLocation(); +} +``` + +#### CrateDisplayFactory Interface + +```java +public interface CrateDisplayFactory { + CrateDisplay create(Location location, String value, float yaw); + boolean isValidValue(String value); + List getSuggestions(); +} +``` + +#### Built-in Display Types + +| DisplayType | Factory | Display | Value Type | +|-------------|------------------------------|-----------------------|-------------------------| +| BLOCK | BlockCrateDisplayFactory | BlockCrateDisplay | Material name | +| ENTITY | EntityCrateDisplayFactory | EntityCrateDisplay | EntityType name | +| MYTHIC_MOB | MythicMobCrateDisplayFactory | MythicMobCrateDisplay | MythicMob ID | +| ITEMS_ADDER | IACrateDisplayFactory | IACrateDisplay | ItemsAdder furniture ID | +| NEXO | NexoCrateDisplayFactory | NexoCrateDisplay | Nexo furniture ID | +| ORAXEN | OraxenCrateDisplayFactory | OraxenCrateDisplay | Oraxen furniture ID | + +#### PlacedCrate Record + +```java +public record PlacedCrate( + UUID id, + String crateId, + String worldName, + int x, int y, int z, + DisplayType displayType, + String displayValue, + float yaw +) {} +``` + +**Persistence:** +- Stored in Chunk PDC via `PlacedCrateDataType` +- Loads on chunk load, unloads on chunk unload +- Managed by `CratesManager` + +--- + +### 10. Event System + +All events extend `CrateEvent` which provides player and crate context. + +#### Event Types + +| Event | Cancellable | When Fired | Purpose | +|----------------------|-------------|--------------------|--------------------| +| CratePreOpenEvent | Yes | Before opening | Prevent opening | +| CrateOpenEvent | No | After key consumed | Track opening | +| CrateRerollEvent | Yes | Before reroll | Prevent reroll | +| RewardGeneratedEvent | No | Reward selected | Track selection | +| RewardGivenEvent | No | Reward given | Track distribution | + +**Usage Example:** + +```java +@EventHandler +public void onRewardGiven(RewardGivenEvent event) { + Player player = event.getPlayer(); + Crate crate = event.getCrate(); + Reward reward = event.getReward(); + + // Log to external system + myLogger.log(player.getName() + " won " + reward.id() + " from " + crate.id()); + + // Broadcast rare rewards + if (reward.weight() < 5.0) { + Bukkit.broadcastMessage( + player.getName() + " won a rare reward from " + crate.displayName() + ); + } +} +``` + +--- + +## Extension Points + +### 1. Creating Custom Hooks + +Hooks integrate with external plugins using the `@AutoHook` annotation. + +**Step 1: Create Hook Class** + +```java +package fr.traqueur.crates.hooks.myplugin; + +import fr.traqueur.crates.api.annotations.AutoHook; +import fr.traqueur.crates.api.hooks.Hook; +import fr.traqueur.crates.api.registries.*; + +@AutoHook("MyPlugin") // Name of target plugin +public class MyPluginHook implements Hook { + + @Override + public void onEnable() { + // Register providers, factories, etc. + ItemsProvidersRegistry itemsRegistry = Registry.get(ItemsProvidersRegistry.class); + itemsRegistry.register("MyPlugin", (player, itemId) -> { + // Return ItemStack from your plugin + return MyPlugin.getAPI().getItem(itemId); + }); + + CrateDisplayFactoriesRegistry displayRegistry = Registry.get(CrateDisplayFactoriesRegistry.class); + displayRegistry.register(DisplayType.MY_TYPE, new MyDisplayFactory()); + } +} +``` + +**Step 2: Create build.gradle.kts (if in hooks/ directory)** + +```kotlin +dependencies { + compileOnly("my-plugin:api:version") +} +``` + +**Step 3: Hook Auto-Discovery** + +Hooks are automatically discovered if: +- Located in package `fr.traqueur.crates` or subpackages +- Annotated with `@AutoHook("PluginName")` +- Target plugin is loaded + +### 2. Creating Custom Conditions + +**Step 1: Implement OpenCondition** + +```java +public record MyCustomCondition(String myParam) implements OpenCondition { + + @Override + public boolean check(Player player, Crate crate) { + // Your validation logic + return player.hasPermission("custom.permission"); + } + + @Override + public void onOpen(Player player, Crate crate) { + // Side effects after successful open + player.sendMessage("Custom condition passed!"); + } + + @Override + public String errorMessageKey() { + return "custom-condition-failed"; + } +} +``` + +**Step 2: Register Polymorphic Type** + +```java +@Override +public void onEnable() { + PolymorphicRegistry.get(OpenCondition.class, registry -> { + registry.register("MY_CUSTOM", MyCustomCondition.class); + }); +} +``` + +**Step 3: Use in YAML** + +```yaml +conditions: + - type: MY_CUSTOM + myParam: "value" +``` + +### 3. Creating Custom ItemsProvider + +```java +ItemsProvidersRegistry registry = Registry.get(ItemsProvidersRegistry.class); + +registry.register("MyPlugin", (player, itemId) -> { + // Resolve item from your plugin + MyCustomItem item = MyPlugin.getItemRegistry().get(itemId); + if (item == null) return null; + + return item.build(player); +}); +``` + +**Usage in YAML:** + +```yaml +item: + copy-from: + plugin-name: "MyPlugin" + item-id: "custom_sword" + display-name: "Custom Sword" +``` + +### 4. Creating Custom CrateDisplayFactory + +```java +public class MyDisplayFactory implements CrateDisplayFactory { + + @Override + public CrateDisplay create(Location location, String value, float yaw) { + return new MyDisplay(location, value, yaw); + } + + @Override + public boolean isValidValue(String value) { + return MyPlugin.getAPI().exists(value); + } + + @Override + public List getSuggestions() { + return MyPlugin.getAPI().getAllIds(); + } +} + +public class MyDisplay implements CrateDisplay { + private final Location location; + private final String value; + private MyObject object; + + @Override + public void spawn() { + this.object = MyPlugin.getAPI().spawn(location, value); + } + + @Override + public void remove() { + if (object != null) { + object.remove(); + } + } + + @Override + public boolean matches(String value) { + return this.value.equals(value); + } + + @Override + public Location getLocation() { + return location; + } +} +``` + +**Registration:** + +```java +CrateDisplayFactoriesRegistry registry = Registry.get(CrateDisplayFactoriesRegistry.class); +registry.register(DisplayType.MY_TYPE, new MyDisplayFactory()); +``` + +--- + +## JavaScript API + +### Animation Context API + +```javascript +// PlayerWrapper methods +context.player().sendMessage("Message"); +context.player().sendTitle("Title", "Subtitle", fadeIn, stay, fadeOut); +context.player().playSound("SOUND_NAME", volume, pitch); + +// InventoryWrapper methods +context.inventory().setItem(slot, itemStack); +context.inventory().setRandomItem(slot); +context.inventory().setWinningItem(slot, itemStack); +context.inventory().rotateItems([13, 14, 15, 16]); // Array of slots +context.inventory().clear(); +context.inventory().clear(slot); +context.inventory().highlightSlot(slot, "GLOWING"); +context.inventory().close(delayTicks); +context.inventory().closeImmediately(); + +// CrateWrapper methods +context.crate().displayName(); +context.crate().id(); +context.crate().getReward(); // Current reward +context.crate().rerollsRemaining(); +context.crate().hasRerolls(); + +// TickData (in onTick callback) +tickData.tickNumber(); // Current tick +tickData.progress(); // 0.0 to 1.0 (with speed curve) +tickData.elapsedTime(); // Milliseconds since phase start +``` + +### Algorithm Context API + +```javascript +// RewardsWrapper methods +var rewards = context.rewards(); + +rewards.weightedRandom(); // Standard weighted random +rewards.weightedRandom([reward1, reward2]); // From specific list +rewards.getAll(); // All rewards as list +rewards.size(); // Total count +rewards.getById("reward_id"); // Find by ID +rewards.filterByMinWeight(5.0); // Filter by min weight +rewards.filterByMaxWeight(10.0); // Filter by max weight + +// HistoryWrapper methods +var history = context.history(); + +history.getRecent(50); // Last N openings +history.getRecentForCrate("crate_id", 50); // Last N for specific crate +history.getAll(); // All openings +``` + +### ArrayHelper Utility + +```javascript +// Convert JavaScript arrays to Java int[] arrays +var javaArray = ArrayHelper.toIntArray([1, 2, 3, 4, 5]); +context.inventory().rotateItems(javaArray); +``` + +--- + +## Database Schema + +### Tables + +**users** +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY +); +``` + +**user_keys** +```sql +CREATE TABLE user_keys ( + user_id UUID NOT NULL, + key_name VARCHAR(255) NOT NULL, + amount INT NOT NULL, + PRIMARY KEY (user_id, key_name), + FOREIGN KEY (user_id) REFERENCES users(id) +); +``` + +**crate_openings** +```sql +CREATE TABLE crate_openings ( + id UUID PRIMARY KEY, + player_id UUID NOT NULL, + crate_id VARCHAR(255) NOT NULL, + reward_id VARCHAR(255) NOT NULL, + timestamp BIGINT NOT NULL +); +``` + +--- + +## Security Model + +### JavaScript Sandbox + +**Blocked Java Access:** +- No `java.*` package access +- No `Packages` global +- No reflection APIs + +**Blocked Methods:** +- `getClass()`, `class` +- `notify()`, `notifyAll()`, `wait()` +- `clone()`, `finalize()`, `hashCode()` + +**Allowed:** +- Standard JavaScript objects (Array, Object, Math, JSON, Date, RegExp) +- ES6 features (arrow functions, let/const, template literals) +- Context objects (player, inventory, crate, rewards, history) +- Utility: `ArrayHelper` + +**Isolation:** +- Each script execution uses isolated scope +- Null parent scopes prevent prototype pollution +- Interpreter mode (no bytecode generation) + +### Inventory Security + +**Slot Authorization:** +- Only authorized slots can be manipulated during animation +- Prevents inventory manipulation exploits + +### Data Security + +**PDC Storage:** +- Chunk data persisted in PersistentDataContainer +- Type-safe serialization via `PlacedCrateDataType` + +**User Cache:** +- Proper lifecycle (load on join, save on quit) +- Concurrent-safe operations + +**Database:** +- Async operations via `CompletableFuture` +- Connection pooling +- Prepared statements (SQL injection protection) + +--- + +## Best Practices + +### 1. Event Handling + +```java +// Always check if event is cancelled +@EventHandler(priority = EventPriority.HIGHEST) +public void onRewardGiven(RewardGivenEvent event) { + if (event.isCancelled()) return; // If event becomes cancellable in future + + // Your logic +} + +// Listen at appropriate priority +// LOWEST - Execute first +// NORMAL - Default +// HIGHEST - Execute last +// MONITOR - Observe only (don't modify) +``` + +### 2. Registry Access + +```java +// Cache registry references +private final CratesRegistry cratesRegistry = Registry.get(CratesRegistry.class); + +// Don't call Registry.get() in loops +for (Player player : players) { + Crate crate = cratesRegistry.getById("legendary"); // Good +} +``` + +### 3. Async Operations + +```java +// Use async for database operations +usersManager.loadUser(uuid).thenAccept(user -> { + // Process user data +}).exceptionally(throwable -> { + Logger.error("Failed to load user", throwable); + return null; +}); + +// Return to main thread for Bukkit API +usersManager.loadUser(uuid).thenAcceptAsync(user -> { + player.sendMessage("Loaded!"); +}, Bukkit.getScheduler().getMainThreadExecutor(plugin)); +``` + +### 4. Custom Animations + +```javascript +// Use speedCurve for smooth animations +{ + speedCurve: "EASE_OUT", // Decelerates naturally + onTick: function(context, tickData) { + var progress = tickData.progress(); // 0.0 to 1.0 + // Use progress for smooth transitions + } +} + +// Clean up in onCancel +onCancel: function(context) { + context.inventory().clear(); + context.player().sendMessage("Cancelled"); +} +``` + +### 5. Custom Algorithms + +```javascript +// Cache rewards list +algorithms.register("my_algo", function(context) { + var rewards = context.rewards(); + var allRewards = rewards.getAll(); // Cache + + // Don't call getAll() in loops + for (var i = 0; i < 100; i++) { + // Use allRewards + } + + return rewards.weightedRandom(); +}); +``` + +### 6. Error Handling + +```java +// Always handle Optional properly +Optional optCrate = registry.getById("legendary"); +optCrate.ifPresent(crate -> { + // Process crate +}); + +// Or with fallback +Crate crate = registry.getById("legendary") + .orElseThrow(() -> new IllegalStateException("Crate not found")); +``` + +--- + +## Support + +For questions, issues, or contributions: +- **Discord**: [https://discord.gg/PTSYTC53d3] +- **GitHub**: [https://github.com/GroupeZ-dev/zCrates] +- **Documentation**: See `USER_GUIDE.md` \ No newline at end of file diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..700739a --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,834 @@ +# zCrates - User Guide + +Complete guide for Minecraft server administrators to configure and use the zCrates plugin. + +## Table of Contents + +- [Installation](#installation) +- [Basic Configuration](#basic-configuration) +- [Creating a Crate](#creating-a-crate) +- [Key Types](#key-types) +- [Reward Types](#reward-types) +- [ItemStack Configuration with Delegates](#itemstack-configuration-with-delegates) +- [Opening Conditions](#opening-conditions) +- [Animations](#animations) +- [Selection Algorithms](#selection-algorithms) +- [Placing Crates](#placing-crates) +- [Commands](#commands) +- [Permissions](#permissions) +- [Hooks & Integrations](#hooks--integrations) +- [FAQ](#faq) + +--- + +## Installation + +1. Download the `zCrates.jar` file +2. Place it in your server's `plugins/` folder +3. Restart the server +4. Configuration files will be generated automatically + +### Requirements + +- **Minecraft**: 1.21+ +- **Server**: Paper/Purpur +- **Java**: 21+ +- **Dependencies**: zMenu (bundled) + +--- + +## Basic Configuration + +### config.yml + +```yaml +# Enable debug messages +debug: false + +# Database configuration +database: + type: SQLITE # SQLITE, MYSQL, or MARIADB + table-prefix: "zcrates_" + + # For MySQL/MariaDB only + host: "localhost" + port: 3306 + database: "minecraft" + username: "root" + password: "" +``` + +### messages.yml + +Customize all plugin messages: + +```yaml +no-key: "You don't have a key for this crate!" +no-permission: "You don't have permission." +condition-no-permission: "You don't have permission to open this crate!" +condition-cooldown: "You must wait