diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f5654d4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,186 @@ +name: Stable Release + +# Fires when a vX.Y.Z tag is pushed, or can be triggered manually with a tag. +on: + push: + tags: + - 'v[0-9]*.[0-9]*.[0-9]*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g. v2.0.1)' + required: true + +jobs: + release: + name: Stable Release + runs-on: ubuntu-latest + permissions: + contents: write # Required for creating the GitHub Release + + steps: + # ────────────────────────────────────────────────────────────────────── + # 1. CHECKOUT + # ────────────────────────────────────────────────────────────────────── + - name: Checkout + uses: actions/checkout@v6 + with: + # For push:tags: github.ref is the tag ref (e.g. refs/tags/v2.0.1). + # For workflow_dispatch: github.ref is the branch (e.g. refs/heads/main). + # We never use the tag input here because the tag may not exist yet — + # it is created in a later step. + ref: ${{ github.ref }} + fetch-depth: 0 + + # ────────────────────────────────────────────────────────────────────── + # 2. RESOLVE VERSION + # ────────────────────────────────────────────────────────────────────── + - name: Resolve version from tag + id: meta + run: | + REF="${{ github.event.inputs.tag || github.ref_name }}" + VERSION="${REF#v}" + echo "ref=${REF}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Releasing version: ${VERSION}" + + # ────────────────────────────────────────────────────────────────────── + # 3. CREATE TAG (if it doesn't exist yet) + # + # When triggered via workflow_dispatch the tag may not exist yet. + # This step creates and pushes it so the GitHub Release can reference it. + # ────────────────────────────────────────────────────────────────────── + - name: Create and push tag if missing + run: | + REF="${{ steps.meta.outputs.ref }}" + if git rev-parse --verify --quiet "refs/tags/${REF}" > /dev/null; then + echo "Tag ${REF} already exists; skipping creation." + else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${REF}" + git push origin "${REF}" + echo "Created and pushed tag ${REF}." + fi + + # ────────────────────────────────────────────────────────────────────── + # 4. BUILD & VERSION + # ────────────────────────────────────────────────────────────────────── + - name: Set up JDK 25 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '25' + cache: maven + + - name: Set release version in POM + run: | + mvn -B -ntp versions:set \ + -DnewVersion="${{ steps.meta.outputs.version }}" \ + -DgenerateBackupPoms=false + + - name: Build + run: mvn -B -ntp -DskipTests package + + # ────────────────────────────────────────────────────────────────────── + # 5. EXTRACT CHANGELOG + # + # Reads the section for this version from CHANGELOG.md. + # Falls back to a generic message when the section is missing. + # ────────────────────────────────────────────────────────────────────── + - name: Extract changelog for this version + id: changelog + run: | + VERSION="${{ steps.meta.outputs.version }}" + HEADER="## [${VERSION}]" + + # Fixed-string match via index() avoids regex issues with dots in versions. + NOTES=$(awk \ + -v hdr="$HEADER" \ + '/^## \[/ && p { exit } + index($0, hdr) == 1 { p=1; next } + p { print }' \ + CHANGELOG.md 2>/dev/null || true) + + if [[ -z "$NOTES" ]]; then + NOTES="No changelog entry found for ${VERSION}." + fi + + printf '%s\n' "$NOTES" > release-notes.md + echo "=== Release notes preview ===" && cat release-notes.md + + # ────────────────────────────────────────────────────────────────────── + # 6. GITHUB RELEASE + # ────────────────────────────────────────────────────────────────────── + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ steps.meta.outputs.ref }}" \ + --title "EzBoost ${{ steps.meta.outputs.version }}" \ + --notes-file release-notes.md \ + "target/EzBoost-${{ steps.meta.outputs.version }}.jar" + + # ────────────────────────────────────────────────────────────────────── + # 7. MODRINTH RELEASE + # ────────────────────────────────────────────────────────────────────── + - name: Release to Modrinth (stable) + uses: Kir-Antipov/mc-publish@v3.3 + with: + modrinth-id: hxlosrDv + modrinth-token: ${{ secrets.MODRINTH_TOKEN }} + modrinth-featured: true + name: "EzBoost ${{ steps.meta.outputs.version }}" + version: "${{ steps.meta.outputs.version }}" + version-type: release + files: | + target/EzBoost-${{ steps.meta.outputs.version }}.jar + game-versions: ">=1.13" + game-versions-filter: release + loaders: | + bukkit + spigot + paper + folia + changelog-file: release-notes.md + dependencies: | + placeholderapi(optional) + worldguard(optional) + + # ────────────────────────────────────────────────────────────────────── + # 8. DISCORD NOTIFICATION + # ────────────────────────────────────────────────────────────────────── + - name: Post Discord notification + env: + DISCORD_WEBHOOK: ${{ secrets.EZBOOST_RELEASE_DISCORD_WEBHOOK }} + run: | + REPO="${{ github.repository }}" + VERSION="${{ steps.meta.outputs.version }}" + GH_URL="https://github.com/${REPO}/releases/tag/${{ steps.meta.outputs.ref }}" + MODRINTH_URL="https://modrinth.com/plugin/ezplugins-ezboost/version/${VERSION}" + + # Truncate changelog to fit Discord's 4096-char embed description limit. + CHANGELOG=$(head -c 3800 release-notes.md || true) + + PAYLOAD=$(jq -n \ + --arg version "$VERSION" \ + --arg gh_url "$GH_URL" \ + --arg modrinth_url "$MODRINTH_URL" \ + --arg changelog "$CHANGELOG" \ + '{ + embeds: [{ + title: ("🚀 EzBoost " + $version + " — Stable Release"), + url: $gh_url, + color: 3447003, + description: $changelog, + fields: [ + { name: "GitHub Release", value: ("[View on GitHub](" + $gh_url + ")"), inline: true }, + { name: "Modrinth", value: ("[Download on Modrinth](" + $modrinth_url + ")"), inline: true } + ], + footer: { text: "Stable release" } + }] + }') + + curl --fail -s -X POST "$DISCORD_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 7c0d000..cac0d1b 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -12,6 +12,7 @@ on: env: PAPER_261_VERSION: "26.1.2" # New MC versioning scheme; uses fill.papermc.io API FOLIA_VERSION: "26.1.2" # Folia tracks the same MC version as Paper + PAPER_121_VERSION: "1.21.11" # Legacy MC versioning; uses api.papermc.io API SPIGOT_VERSION: "1.21.11" BUKKIT_VERSION: "1.21.11" @@ -52,7 +53,7 @@ jobs: strategy: fail-fast: false matrix: - server: [paper, folia, spigot, bukkit] + server: [paper, folia, paper-1.21, spigot, bukkit] steps: - name: Set up Java 25 @@ -84,6 +85,17 @@ jobs: curl -sfLo server.jar "$URL" echo "Paper 26.1.2 URL: $URL" + # ── Paper 1.21.11 (legacy MC versioning; uses api.papermc.io) ──────── + - name: Download Paper ${{ env.PAPER_121_VERSION }} + if: matrix.server == 'paper-1.21' + run: | + LATEST=$(curl -sf \ + "https://api.papermc.io/v2/projects/paper/versions/${PAPER_121_VERSION}" \ + | jq -r '.builds[-1]') + curl -sfLo server.jar \ + "https://api.papermc.io/v2/projects/paper/versions/${PAPER_121_VERSION}/builds/${LATEST}/downloads/paper-${PAPER_121_VERSION}-${LATEST}.jar" + echo "Paper ${PAPER_121_VERSION} build ${LATEST} downloaded" + # ── Folia 26.1.2 (same fill.papermc.io API, project=folia) ─────────── - name: Download Folia ${{ env.FOLIA_VERSION }} if: matrix.server == 'folia' @@ -202,13 +214,15 @@ jobs: RESULT=fail break fi - # Server fully started → all plugins have been enabled without errors. - # "Done (" is more reliable than checking "Enabling EzBoost" because - # some server variants (e.g. Paper 26.1) load worlds before enabling - # plugins, so "Enabling" can appear late. - if grep -qE "Done \([0-9.]" server.log 2>/dev/null; then + # Server fully started — verify EzBoost was actually enabled. + # "Done (" is more reliable than checking "Enabling EzBoost" because + # some server variants (e.g. Paper 26.1) load worlds before enabling + # plugins, so "Enabling" can appear late. + if grep -qE "Done \([0-9.]" server.log 2>/dev/null; then if grep -qE "$FAIL_PAT" server.log 2>/dev/null; then RESULT=fail + elif ! grep -q "Enabling EzBoost" server.log 2>/dev/null; then + RESULT=fail # Server started but plugin was never enabled (e.g. incompatible api-version) else RESULT=pass fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..49ce386 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,156 @@ +# Changelog + +All notable changes to EzBoost are documented here. + +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Release tags use the `v` prefix (e.g. `v2.0.1`). + +--- + +## [Unreleased] + +--- + +## [2.1.0] - 2026-05-18 + +### Added + +- **Folia compatibility**: EzBoost now supports Folia servers via a `FoliaScheduler` abstraction that routes task scheduling through Folia's `GlobalRegionScheduler` / entity schedulers when the Folia runtime is detected. +- **Boost top leaderboard** (`/boosttop`): tracks and displays the top boost buyers using a Jaloquent-backed persistent storage layer. +- **Jaloquent storage backend**: replaced the previous `BoostStorage` abstraction with a fully Jaloquent-backed `EzBoostRepository` for consistent flat-file persistence across all storage operations. +- **`storage.debug-logging` option** (`storage.yml`, default `false`): suppresses verbose Jaloquent console output (`Queried X rows`, `Saved model…`). Set to `true` to re-enable for debugging. +- **Paper 1.21 smoke-test**: CI matrix now includes Paper 1.21.11 alongside Paper 26.1.2, Folia, Spigot, and Bukkit. + +### Changed + +- `api-version` lowered from `26.1.2` to `1.13` so the plugin loads on any Spigot/Paper 1.13+ server without an api-version warning or rejection. +- Java compiler target lowered from 25 to 17 for wider JDK compatibility. +- `AsyncChatEvent` (Paper-only) replaced with `AsyncPlayerChatEvent` for Spigot compatibility. +- Plugin version lookup changed from `getPluginMeta().getVersion()` to `getDescription().getVersion()` for broader server compatibility. +- CI smoke-test pass condition now also verifies `Enabling EzBoost` appears in the server log, preventing a false pass when the server starts but rejects the plugin due to an incompatible api-version. + +### Fixed + +- `YamlDataStore.query()` corrected to handle Jaloquent's flat key format. +- `storage.yml` added to `pom.xml` resource includes so it is correctly packaged. +- Vault is now fully optional: economy class access and listener registration are guarded so the plugin loads cleanly without Vault present. + +--- + +## [2.0.0] - 2026-04-15 + +### Added + +- **Minecraft 26.1.2 support**: updated `api-version` and build toolchain to target Paper MC 26.1.2 / Java 25. +- **9 new PlaceholderAPI placeholders** via an internal PAPI expansion (`EzBoostPlaceholder`): expose active boost info, remaining duration, cooldowns, and more. +- **MiniMessage tag resolvers** (`BoostTagResolvers`): native MiniMessage tags for boost context (name, duration, etc.) wired into all `BoostManager` messages for rich formatting without external placeholders. + +### Changed + +- CI updated to Java 25. + +--- + +## [1.6.0] - 2026-04-14 + +### Added + +- **11 preset boosts**: waterbreathing, saturation, luck, absorption, slow-falling, miner, warrior, farmer, explorer, xpboost, diver — all available out of the box with sensible defaults in `boosts.yml`. +- **XP boost custom effect** (`XpBoostEffect`): configurable experience multiplier applied for the duration of the boost. +- GUI slots pre-configured for the new preset boosts. + +--- + +## [1.5.6] - 2026-04-01 + +### Fixed + +- PlaceholderAPI registration timing corrected so placeholders are reliably available after server startup. +- `/ezboost about` subcommand output and tab-completion fixed. + +--- + +## [1.5.5] - 2026-03-28 + +### Added + +- **PlaceholderAPI integration**: `EzBoostPlaceholder` expansion exposes boost data to any PAPI-compatible plugin. +- **`/ezboost about` subcommand**: displays plugin version, authors, and resource links. +- **Formatted price placeholder** (`%ezboost_price_formatted%`): presents boost costs in a human-readable currency format. + +--- + +## [1.5.4] - 2026-03-13 + +### Added + +- **`show-effects` GUI option**: configurable toggle to show or hide active potion effects in the boost GUI. +- **Vault economy boost effects**: boosts can now charge players via Vault when activated (`XpBoostEffect` and economy hooks). +- **Cooldown improvements**: per-boost cooldown configuration expanded with additional options. +- CI pipeline to validate configuration and run tests. + +--- + +## [1.5.3] - 2026-01-22 + +### Fixed + +- Removed debug console messages that were printed when no effects were applied to a custom boost. + +--- + +## [1.5.2] - 2026-01-21 + +### Fixed + +- Custom boost definitions now load correctly from `boosts.yml`; a regression in 1.5.1 prevented custom boosts from being registered. + +--- + +## [1.5.1] - 2026-01-21 + +### Changed + +- Tab-completion for `/boost` and `/ezboost` improved: suggestions are now context-aware and include boost names. +- `BoostEffect` records now include a `name` field used in completions and display. + +--- + +## [1.5.0] - 2026-01-20 + +### Added + +- **Admin GUI** (`/ezboost create`): in-game inventory interface for creating and configuring boosts without editing config files directly. + +--- + +## [1.4.0] - 2026-01-13 + +### Added + +- **EzBoost API** (`EzBoostAPI`): public API surface for third-party plugins to query and trigger boosts programmatically. +- **Custom boost events**: `BoostStartEvent` and `BoostEndEvent` fired on the Bukkit event bus so other plugins can react to boost lifecycle changes. + +--- + +## [1.3.0] - 2026-01-12 + +### Added + +- **WorldGuard regional overrides**: define per-region boost behaviour (allow, deny, or override settings) using the built-in override system. WorldGuard is detected automatically as a soft dependency. + +### Fixed + +- Build version warning in `pom.xml` resolved. + +--- + +## [1.2.0] - 2026-01-02 + +### Added + +- Initial public release of EzBoost. +- Configurable potion boosts with durations, cooldowns, world restrictions, and GUI activation. +- Multi-file configuration (`settings.yml`, `boosts.yml`, `gui.yml`, `messages.yml`, `limits.yml`). +- Vault economy integration for charging players on boost activation. diff --git a/README.md b/README.md index d9bb941..5e3ca2a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![CI](https://github.com/ez-plugins/ezboost/actions/workflows/smoke-test.yml/badge.svg)](https://github.com/ez-plugins/ezboost/actions/workflows/smoke-test.yml) -[![GitHub Packages](https://img.shields.io/badge/GitHub_Packages-2.0.0-blue?logo=github)](https://github.com/ez-plugins/ezboost/packages) +[![GitHub Packages](https://img.shields.io/badge/GitHub_Packages-2.1.0-blue?logo=github)](https://github.com/ez-plugins/ezboost/packages) [![Coverage](https://img.shields.io/codecov/c/github/ez-plugins/ezboost)](https://codecov.io/github/ez-plugins/ezboost) [![Docs](https://img.shields.io/badge/Docs-GitHub_Pages-blue?logo=github)](https://ez-plugins.github.io/ezboost) [![Platform](https://img.shields.io/badge/Platform-Spigot%20%7C%20Paper%20%7C%20Bukkit-blue)](#) @@ -9,127 +9,171 @@ # EzBoost -![EzBoost GUI](https://i.ibb.co/1GgSfvWs/image.png) +> Configurable potion boosts for Spigot / Paper / Bukkit 1.7–1.21.* – GUI activation, cooldowns, economy costs, boost tokens, and WorldGuard region overrides. -![EzBoost Admin GUI](https://i.ibb.co/cXTcS3LT/image.png) +![EzBoost GUI](https://i.ibb.co/1GgSfvWs/image.png) -EzBoost is a modern, production-ready Minecraft plugin for Spigot, Paper, and Bukkit servers (Minecraft 1.7–1.21.*). It empowers server owners to offer configurable, time-limited potion boosts to players. Designed for flexibility, maintainability, and performance, EzBoost features a clean multi-file configuration system, a customizable GUI, per-boost cooldowns, world restrictions, optional Vault economy integration, boost token items, and advanced region-based overrides with WorldGuard support. +EzBoost lets server owners offer time-limited potion boosts through a fully customisable chest GUI or +direct commands. Every boost is independently configurable – potion effects, amplifier, duration, cooldown, +permission, economy cost, and behaviour on death or reconnect. Boosts can be scoped to specific worlds or +WorldGuard regions, and players can receive tradeable **boost tokens** as crate prizes or vote rewards. --- ## Features -- **Admin GUI**: Create and manage boosts through an intuitive admin interface. -- **Highly configurable**: Define custom boosts with effects, durations, cooldowns, permissions, and costs. -- **Multi-file configuration**: Clean separation of settings, GUI, boosts, and more for easy management. -- **Interactive GUI**: Customizable inventory interface for boost activation. -- **Per-boost cooldowns**: Prevents abuse and enables balanced gameplay. -- **World restrictions**: Allow or deny boosts in specific worlds. -- **Region-based overrides (WorldGuard)**: Apply different boost settings or disable boosts in specific WorldGuard regions using the built-in override system. No hard dependency—WorldGuard is detected automatically if present. -- **Vault economy support**: Optionally charge players for activating boosts. -- **Boost token items**: Give, trade, or reward boost tokens. Players redeem tokens by right-clicking them to activate the corresponding boost. -- **Live reload**: Reload all configuration and messages at runtime. -- **MiniMessage support**: Rich formatting for all messages and GUI text. -- **Internal message tags**: Boost-specific tags (``, ``, ``, etc.) are available in `messages.yml` without PlaceholderAPI. -- **PlaceholderAPI expansion**: Exposes 18+ placeholders covering boost status, active boost, cooldowns, time remaining, XP multiplier, and economy formatting for use in other plugins. +### Player experience +- **Chest GUI** – browse boosts with live cooldown timers, cost display, and active-boost indicator +- **Direct activation** – `/boost ` for players who prefer commands over the GUI +- **Boost tokens** – physical inventory items redeemed by right-click; tradeable and giftable +- **Rich feedback** – MiniMessage-formatted actionbar and chat messages, fully customisable + +### Server management +- **Fully configurable boosts** – any potion effect, any amplifier, per-boost cooldown, permission, and cost +- **In-game admin GUI** – create and edit boosts with `/ezboost create`, no YAML editing required +- **World allow / deny lists** – restrict boosts to specific worlds for gameplay balance +- **Region overrides** – change any boost property (effect, cost, enabled state) per WorldGuard region; no hard dependency +- **Live reload** – `/ezboost reload` applies all config changes without a server restart +- **Persistent storage** – boost states and cooldowns survive restarts; choice of YAML, SQLite, MySQL, MariaDB, or PostgreSQL backend +- **Boost top leaderboard** – `/boosttop` shows the top boost buyers, backed by persistent storage + +### Integrations +- **Folia** – fully compatible; task scheduling routes through Folia's region schedulers when detected +- **Vault** – optional economy cost per boost; gracefully disabled if Vault is absent +- **PlaceholderAPI** – 18+ placeholders for scoreboards, holograms, and GUI plugins +- **Internal message tags** – ``, ``, ``, and more available directly in `messages.yml` --- +## Installation -## Documentation +1. Download `EzBoost-.jar` from the [releases page](https://github.com/ez-plugins/ezboost/releases). +2. Drop the JAR into your server's `plugins/` folder. +3. Start (or restart) your server – EzBoost generates all config files in `plugins/EzBoost/`. +4. Edit `boosts.yml` to define your boosts, then run `/ezboost reload` to apply. -Full documentation is available at ****. +**Optional extras:** +- Enable economy costs in `economy.yml` (requires Vault). +- Switch the storage backend in `storage.yml` (default: YAML; supports SQLite, MySQL, MariaDB, PostgreSQL). -| Page | What it covers | -|------|----------------| -| [Commands](https://ez-plugins.github.io/ezboost/commands) | All `/boost` and `/ezboost` commands | -| [Permissions](https://ez-plugins.github.io/ezboost/permissions) | Permissions reference and defaults | -| [Configuration](https://ez-plugins.github.io/ezboost/config) | `settings.yml`, `limits.yml`, `worlds.yml`, `economy.yml` | -| [Boosts](https://ez-plugins.github.io/ezboost/boosts) | `boosts.yml` schema — effects, durations, costs | -| [GUI](https://ez-plugins.github.io/ezboost/gui) | `gui.yml` — chest-based boost menu | -| [Overrides](https://ez-plugins.github.io/ezboost/overrides) | World, group, and region multiplier overrides | -| [Events](https://ez-plugins.github.io/ezboost/events) | Plugin lifecycle events | -| [API](https://ez-plugins.github.io/ezboost/api) | Developer API reference | -| [PlaceholderAPI](https://ez-plugins.github.io/ezboost/integration/PlaceholderAPI) | Available placeholders | +--- + +## Commands + +| Command | Description | Permission | +|---------|-------------|------------| +| `/boost` | Open the boost GUI | `ezboost.use` | +| `/boost ` | Activate a boost directly | `ezboost.use` + boost node | +| `/ezboost create` | Open the admin GUI | `ezboost.admin` | +| `/ezboost reload` | Reload all configuration | `ezboost.reload` | +| `/ezboost give [amount]` | Give boost tokens to a player | `ezboost.give` | + +→ Full reference: [Commands](https://ez-plugins.github.io/ezboost/commands) --- -## Installation +## Permissions + +| Permission | Description | Default | +|-----------|-------------|---------| +| `ezboost.use` | Use `/boost` and the GUI | `true` | +| `ezboost.admin` | Admin commands and GUI | `op` | +| `ezboost.reload` | Reload configuration | `op` | +| `ezboost.give` | Give boost tokens | `op` | +| `ezboost.cooldown.bypass` | Skip cooldown checks | `op` | +| `ezboost.boost.` | Activate a specific boost | `true` | -1. Build the plugin JAR from this repository or download a release from the [releases page](https://github.com/ez-plugins/ezboost/releases). -2. Place `EzBoost-.jar` in your server's `plugins/` directory. -3. Start your Spigot, Paper, or Bukkit server (Minecraft 1.7–1.21.*). EzBoost will generate all required configuration files in the plugin data folder. -4. Use `/ezboost create` to open the admin GUI and create boosts. +→ Full reference: [Permissions](https://ez-plugins.github.io/ezboost/permissions) --- -## Usage +## Configuration Files -For detailed command documentation, see [docs/commands.md](docs/commands.md). +| File | Purpose | +|------|---------| +| `boosts.yml` | Define boosts – effects, duration, cooldown, cost, permissions, command hooks | +| `settings.yml` | General toggles: replace-active-boost, keep-on-death, reapply-on-join | +| `limits.yml` | Clamp amplifier and duration ranges across all boosts | +| `worlds.yml` | World allow / deny lists | +| `economy.yml` | Vault economy enable / disable and cost settings | +| `gui.yml` | GUI title, size, filler items, and slot assignments | +| `messages.yml` | All MiniMessage-formatted feedback strings and actionbar text | +| `storage.yml` | Storage backend selection and connection settings | -### Quick Commands Reference +→ Full reference: [Configuration](https://ez-plugins.github.io/ezboost/config) -- `/boost` — Open the boosts GUI (if enabled) or display usage. -- `/boost ` — Activate a boost directly. -- `/ezboost create` — Open the admin GUI to create boosts. -- `/ezboost reload` — Reload all configuration and messages. -- `/ezboost give [amount]` — Give boost token items to a player. Tokens can be redeemed by right-clicking them. +--- -For detailed permissions documentation, see [docs/permissions.md](docs/permissions.md). +## Storage Backends -### Quick Permissions Reference +Player boost states, cooldowns, and leaderboard data are persisted by [Jaloquent](https://github.com/EzFramework/Jaloquent). +Configure the backend in `plugins/EzBoost/storage.yml`: -- `ezboost.use` — Use boosts (`/boost`). -- `ezboost.admin` — Access admin commands. -- `ezboost.reload` — Reload configuration. -- `ezboost.give` — Give boost tokens. -- `ezboost.cooldown.bypass` — Bypass boost cooldowns. -- `ezboost.boost.` — Per-boost permissions (example: `ezboost.boost.speed`). +| Backend | Notes | +|---------|-------| +| `yaml` | **Default.** Zero setup; data stored in flat files inside the plugin folder | +| `sqlite` | Single-file database; good for small-to-medium servers | +| `mysql` | Recommended for high-traffic or multi-server setups | +| `mariadb` | Drop-in MySQL-compatible alternative | +| `postgresql` | Full support; bring your own JDBC driver | --- ## Boost Tokens -Boost tokens are special items that can be given to players as rewards, crate prizes, or shop items. -- **Giving tokens:** Use `/ezboost give [amount]` to give boost tokens. -- **Redeeming tokens:** Players right-click a boost token in their main hand to instantly activate the corresponding boost. The token is consumed on use. +Boost tokens are inventory items that activate a specific boost when right-clicked in the main hand. + +- **Give tokens:** `/ezboost give [amount]` +- **Redeem:** The player right-clicks the token – it is consumed and the boost activates immediately. +- Tokens work as crate prizes, vote rewards, auction house listings, or shop items. --- -## Configuration +## WorldGuard Integration + +EzBoost detects WorldGuard automatically. Use region IDs in `boosts.yml` to change any boost property +on a per-region basis – useful for PvP arenas, spawn zones, or event worlds. +If WorldGuard is not installed, region overrides are silently ignored. -EzBoost uses a multi-file configuration system for clarity and maintainability. All configuration files are located in the plugin's data folder: +→ Full reference: [Overrides](https://ez-plugins.github.io/ezboost/overrides) -- `settings.yml` — General plugin toggles (e.g., replace-active-boost, keep-boost-on-death). -- `limits.yml` — Minimum/maximum duration and amplifier values for boosts. -- `worlds.yml` — World allow/deny lists for boost usage. -- `economy.yml` — Economy integration settings (Vault, enable/disable). -- `gui.yml` — GUI layout, appearance, and slot mapping. -- `boosts.yml` — All boost definitions (effects, duration, cooldown, cost, permissions). -- `messages.yml` — MiniMessage-formatted strings for feedback and actionbar text. -- `data.yml` — Persisted player boost states and cooldowns (auto-managed). +--- -### Region & World Overrides +## Documentation -EzBoost supports advanced overrides for boosts based on world or WorldGuard region. You can: -- Change boost effects, duration, cost, or permissions for a specific world or region. -- Disable certain boosts in specific regions (e.g., PvP arenas, spawn zones). -- Use `boosts.yml` to define per-world or per-region settings. If WorldGuard is installed, region overrides are applied automatically using reflection (no hard dependency). +Full documentation is at ****. -See [docs/overrides.md](docs/overrides.md) for syntax and examples. +| Page | What it covers | +|------|----------------| +| [Commands](https://ez-plugins.github.io/ezboost/commands) | All `/boost` and `/ezboost` commands | +| [Permissions](https://ez-plugins.github.io/ezboost/permissions) | Permissions reference and defaults | +| [Configuration](https://ez-plugins.github.io/ezboost/config) | All config files explained | +| [Boosts](https://ez-plugins.github.io/ezboost/boosts) | `boosts.yml` schema – effects, duration, costs | +| [GUI](https://ez-plugins.github.io/ezboost/gui) | `gui.yml` layout and slot configuration | +| [Overrides](https://ez-plugins.github.io/ezboost/overrides) | World and region override syntax | +| [Events](https://ez-plugins.github.io/ezboost/events) | Plugin lifecycle events | +| [Developer API](https://ez-plugins.github.io/ezboost/api) | Java API reference | +| [PlaceholderAPI](https://ez-plugins.github.io/ezboost/integration/PlaceholderAPI) | Available placeholders | --- -## WorldGuard Integration +## Developer API -- EzBoost automatically detects WorldGuard if present and applies region-based overrides for boosts. -- No hard dependency: If WorldGuard is not installed, region overrides are ignored. -- Use region IDs from WorldGuard in your `overrides.yml` to customize boost behavior per region. +EzBoost exposes a Java API for starting/stopping boosts, querying active boost state, registering custom +effect types, and listening to lifecycle events (`BoostStartEvent`, `BoostEndEvent`). ---- +```xml + + com.github.ez-plugins + EzBoost + 2.1.0 + +``` -## Support & License +→ Full reference: [Developer API](https://ez-plugins.github.io/ezboost/api) + +--- -For help, open an issue or discussion on the repository. +## License -EzBoost is licensed under the MIT License. See [LICENSE](LICENSE). +EzBoost is licensed under the [MIT License](LICENSE). diff --git a/docs/EzBoostAPI.md b/docs/EzBoostAPI.md index f58a278..fd193ff 100644 --- a/docs/EzBoostAPI.md +++ b/docs/EzBoostAPI.md @@ -1,3 +1,8 @@ +--- +title: EzBoost API +nav_exclude: true +--- + # EzBoost API Overview EzBoost exposes a professional, extensible API for plugin developers and advanced users. The API allows you to register custom boost effects, manage player boosts, and integrate deeply with the boost system. diff --git a/docs/_config.yml b/docs/_config.yml index d61ead0..1b12486 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -remote_theme: just-the-docs/just-the-docs +remote_theme: just-the-docs/just-the-docs@v0.10.0 title: EzBoost description: >- diff --git a/docs/api.md b/docs/api.md index 02b1405..1de1868 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,7 +2,7 @@ title: API nav_order: 9 has_children: true -description: "EzBoost developer API — integrating boost management into your own plugins" +description: "EzBoost developer API – integrating boost management into your own plugins" --- # EzBoost API Overview @@ -55,7 +55,7 @@ You can use JitPack to include the latest version directly from GitHub: com.github.ez-plugins EzBoost - 1.4.0 + 2.1.0 ``` diff --git a/docs/api/CustomBoostEffect.md b/docs/api/CustomBoostEffect.md index 964f614..2bb6a6b 100644 --- a/docs/api/CustomBoostEffect.md +++ b/docs/api/CustomBoostEffect.md @@ -2,7 +2,7 @@ title: CustomBoostEffect parent: API nav_order: 2 -description: "CustomBoostEffect interface — implementing custom plugin-driven boost effects" +description: "CustomBoostEffect interface – implementing custom plugin-driven boost effects" --- # CustomBoostEffect Interface Reference diff --git a/docs/api/EzBoostAPI.md b/docs/api/EzBoostAPI.md index 673dd1e..b1adb67 100644 --- a/docs/api/EzBoostAPI.md +++ b/docs/api/EzBoostAPI.md @@ -2,7 +2,7 @@ title: EzBoostAPI parent: API nav_order: 1 -description: "EzBoostAPI class reference — full public method tables" +description: "EzBoostAPI class reference – full public method tables" --- # EzBoostAPI Class Reference diff --git a/docs/boosts.md b/docs/boosts.md index 665730a..5fcc7d9 100644 --- a/docs/boosts.md +++ b/docs/boosts.md @@ -1,7 +1,7 @@ --- title: Boosts nav_order: 5 -description: "boosts.yml schema — defining effects, durations, cooldowns, costs, and particles" +description: "boosts.yml schema – defining effects, durations, cooldowns, costs, and particles" --- # EzBoost – Default Boosts Reference diff --git a/docs/commands.md b/docs/commands.md index 2784a05..131d606 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -45,9 +45,18 @@ This document details all commands provided by EzBoost, including syntax, argume Activates the "speed" boost if available and permitted. -## Admin Commands +### `/boosttop` -### `/ezboost create` +- **Description**: Displays the top boost buyers leaderboard, ranked by total boosts purchased. +- **Permission**: `ezboost.top` +- **Usage**: `/boosttop` +- **Example**: + + ```text + /boosttop + ``` + + Shows the all-time top boost buyers. - **Description**: Opens the admin GUI for creating and managing boosts. Allows administrators to define new boosts with effects, durations, cooldowns, and more. - **Permission**: `ezboost.admin` @@ -94,11 +103,6 @@ For detailed permissions documentation, see [docs/permissions.md](permissions.md ## Notes -- Boost keys are case-insensitive. -- Players must have both `ezboost.use` and the specific `ezboost.boost.` permission to activate a boost. -- The admin GUI (`/ezboost create`) provides an intuitive interface for creating boosts without editing config files directly. -- Boost tokens are consumable items that players can right-click to activate boosts instantly. - - Boost keys are case-insensitive. - Players must have both `ezboost.use` and the specific `ezboost.boost.` permission to activate a boost. - The admin GUI (`/ezboost create`) provides an intuitive interface for creating boosts without editing config files directly. diff --git a/docs/events.md b/docs/events.md index d2912fe..c04829d 100644 --- a/docs/events.md +++ b/docs/events.md @@ -2,7 +2,7 @@ title: Events nav_order: 8 has_children: true -description: "Plugin events emitted by EzBoost — overview and usage guide" +description: "Plugin events emitted by EzBoost – overview and usage guide" --- # EzBoost Events Overview diff --git a/docs/events/BoostEndEvent.md b/docs/events/BoostEndEvent.md index 263635f..f91f937 100644 --- a/docs/events/BoostEndEvent.md +++ b/docs/events/BoostEndEvent.md @@ -2,7 +2,7 @@ title: BoostEndEvent parent: Events nav_order: 2 -description: "Event fired when a boost expires or is cancelled — fields and usage" +description: "Event fired when a boost expires or is cancelled – fields and usage" --- # BoostEndEvent Class Reference @@ -33,11 +33,11 @@ public class BoostEndEvent extends Event implements Cancellable { ## Key Methods & Fields -- `Player getPlayer()` — The player whose boost is ending. -- `String getBoostKey()` — The unique key of the boost. -- `BoostDefinition getBoostDefinition()` — The full boost definition. -- `boolean isCancelled()` — Whether the event is cancelled. -- `void setCancelled(boolean cancel)` — Cancel or allow the boost end. +- `Player getPlayer()` – The player whose boost is ending. +- `String getBoostKey()` – The unique key of the boost. +- `BoostDefinition getBoostDefinition()` – The full boost definition. +- `boolean isCancelled()` – Whether the event is cancelled. +- `void setCancelled(boolean cancel)` – Cancel or allow the boost end. ## Usage Example diff --git a/docs/events/BoostStartEvent.md b/docs/events/BoostStartEvent.md index e8fea4b..6b755b4 100644 --- a/docs/events/BoostStartEvent.md +++ b/docs/events/BoostStartEvent.md @@ -2,7 +2,7 @@ title: BoostStartEvent parent: Events nav_order: 1 -description: "Event fired when a boost is activated — fields, cancellation, and usage" +description: "Event fired when a boost is activated – fields, cancellation, and usage" --- # BoostStartEvent Class Reference @@ -33,11 +33,11 @@ public class BoostStartEvent extends Event implements Cancellable { ## Key Methods & Fields -- `Player getPlayer()` — The player receiving the boost. -- `String getBoostKey()` — The unique key of the boost. -- `BoostDefinition getBoostDefinition()` — The full boost definition. -- `boolean isCancelled()` — Whether the event is cancelled. -- `void setCancelled(boolean cancel)` — Cancel or allow the boost start. +- `Player getPlayer()` – The player receiving the boost. +- `String getBoostKey()` – The unique key of the boost. +- `BoostDefinition getBoostDefinition()` – The full boost definition. +- `boolean isCancelled()` – Whether the event is cancelled. +- `void setCancelled(boolean cancel)` – Cancel or allow the boost start. ## Usage Example diff --git a/docs/gui.md b/docs/gui.md index ff270d4..b0b15bd 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -1,7 +1,7 @@ --- title: GUI nav_order: 6 -description: "gui.yml schema — configuring the interactive chest-based boost menu" +description: "gui.yml schema – configuring the interactive chest-based boost menu" --- # EzBoost – GUI Configuration Reference @@ -56,19 +56,19 @@ gui: ## Option Reference -- **enabled**: `true`/`false` — Enable or disable the GUI entirely. -- **title**: String — The GUI title (MiniMessage formatting supported). -- **size**: Integer — Number of slots (must be a multiple of 9, max 54). -- **close-on-click**: `true`/`false` — Whether the GUI closes after a boost is selected. +- **enabled**: `true`/`false` – Enable or disable the GUI entirely. +- **title**: String – The GUI title (MiniMessage formatting supported). +- **size**: Integer – Number of slots (must be a multiple of 9, max 54). +- **close-on-click**: `true`/`false` – Whether the GUI closes after a boost is selected. - **filler**: Section for the background/filler item. - **material**: Item material (e.g., `BLACK_STAINED_GLASS_PANE`). - **name**: Display name for the filler item. - **lore**: List of lore lines for the filler item. -- **lore**: List — Template for each boost's lore. Supports placeholders: +- **lore**: List – Template for each boost's lore. Supports placeholders: - ``, ``, ``, `` -- **status**: Section — Custom text for each status type: +- **status**: Section – Custom text for each status type: - **available**, **locked**, **cooldown**, **active** -- **slots**: Section — Maps each boost key to a slot index (0-based). +- **slots**: Section – Maps each boost key to a slot index (0-based). --- diff --git a/docs/index.md b/docs/index.md index 4a6c808..50fca3b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,26 +9,28 @@ permalink: / # EzBoost [![CI](https://github.com/ez-plugins/ezboost/actions/workflows/smoke-test.yml/badge.svg)](https://github.com/ez-plugins/ezboost/actions/workflows/smoke-test.yml) -[![GitHub Packages](https://img.shields.io/badge/GitHub_Packages-2.0.0-blue?logo=github)](https://github.com/ez-plugins/ezboost/packages) +[![GitHub Packages](https://img.shields.io/badge/GitHub_Packages-2.1.0-blue?logo=github)](https://github.com/ez-plugins/ezboost/packages) [![Coverage](https://img.shields.io/codecov/c/github/ez-plugins/ezboost)](https://codecov.io/github/ez-plugins/ezboost) **EzBoost** is a modern, production-ready Minecraft plugin for Spigot, Paper, and Bukkit servers (1.7–1.21.*). -It gives server owners full control over configurable, time-limited potion boosts — complete with GUI, +It gives server owners full control over configurable, time-limited potion boosts – complete with GUI, cooldowns, economy integration, WorldGuard region support, and a developer API. --- ## Features -- **Flexible boost definitions** — configure any potion effect with duration, amplifier, cooldowns, limits, and particle effects via `boosts.yml` -- **Interactive GUI** — fully-customisable chest-based boost menu driven by `gui.yml`; fully disableable -- **Economy integration** — optional Vault economy support with per-boost pricing; gracefully skipped when Vault is absent -- **WorldGuard support** — restrict boost activation to specific WorldGuard regions -- **Per-player and global limits** — cap how many active boosts a player or the server can run simultaneously -- **Overrides** — server-wide event-driven multipliers layered on top of individual boosts -- **PlaceholderAPI** — exposes boost state and duration as placeholders for scoreboards, holograms, and more -- **MiniMessage formatting** — all messages use the Adventure MiniMessage format for rich, hex-colour text -- **Developer API** — clean Java API to start/stop boosts and listen to lifecycle events from other plugins +- **Folia support** – compatible with Folia; task scheduling automatically routes through Folia's region schedulers when detected +- **Flexible boost definitions** – configure any potion effect with duration, amplifier, cooldowns, limits, and particle effects via `boosts.yml` +- **Interactive GUI** – fully-customisable chest-based boost menu driven by `gui.yml`; fully disableable +- **Economy integration** – optional Vault economy support with per-boost pricing; gracefully skipped when Vault is absent +- **WorldGuard support** – restrict boost activation to specific WorldGuard regions +- **Per-player and global limits** – cap how many active boosts a player or the server can run simultaneously +- **Overrides** – server-wide event-driven multipliers layered on top of individual boosts +- **PlaceholderAPI** – exposes boost state and duration as placeholders for scoreboards, holograms, and more +- **Boost top leaderboard** – `/boosttop` tracks and displays the all-time top boost buyers +- **MiniMessage formatting** – all messages use the Adventure MiniMessage format for rich, hex-colour text +- **Developer API** – clean Java API to start/stop boosts and listen to lifecycle events from other plugins --- @@ -54,8 +56,8 @@ and drop it into your server's `plugins/` directory. | [Commands](commands) | All `/boost` and `/ezboost` commands with syntax and permissions | | [Permissions](permissions) | Full permissions reference and default values | | [Configuration](config) | `settings.yml`, `limits.yml`, `worlds.yml`, `economy.yml` | -| [Boosts](boosts) | `boosts.yml` schema — effects, duration, cooldowns, costs | -| [GUI](gui) | `gui.yml` schema — slots, items, actions | +| [Boosts](boosts) | `boosts.yml` schema – effects, duration, cooldowns, costs | +| [GUI](gui) | `gui.yml` schema – slots, items, actions | | [Overrides](overrides) | Server-wide boost multiplier overrides | | [Events](events) | Plugin events overview | |   [BoostStartEvent](events/BoostStartEvent) | Fired when a boost activates | @@ -64,3 +66,19 @@ and drop it into your server's `plugins/` directory. |   [EzBoostAPI](api/EzBoostAPI) | Full public-method reference | |   [CustomBoostEffect](api/CustomBoostEffect) | Implementing custom boost effects | | [PlaceholderAPI](integration/PlaceholderAPI) | Available placeholders and usage | + +--- + +## Developer API + +EzBoost exposes a Java API for starting/stopping boosts, querying active state, registering custom +effect types, and listening to lifecycle events. See the [API reference](api) for setup instructions, +method tables, and code examples. + +```xml + + com.github.ez-plugins + EzBoost + 2.1.0 + +``` diff --git a/docs/integration/PlaceholderAPI.md b/docs/integration/PlaceholderAPI.md index 7de91c1..3c3b0a2 100644 --- a/docs/integration/PlaceholderAPI.md +++ b/docs/integration/PlaceholderAPI.md @@ -1,7 +1,7 @@ --- title: PlaceholderAPI nav_order: 10 -description: "PlaceholderAPI expansion for EzBoost — available placeholders and examples" +description: "PlaceholderAPI expansion for EzBoost – available placeholders and examples" --- # PlaceholderAPI integration @@ -20,7 +20,7 @@ This document describes the PlaceholderAPI expansion bundled with EzBoost and th ## Installation - Ensure PlaceholderAPI is installed on the server (). -- The EzBoost expansion is registered automatically when both EzBoost and PlaceholderAPI are present — no extra files required. +- The EzBoost expansion is registered automatically when both EzBoost and PlaceholderAPI are present – no extra files required. ## Expansion identifier @@ -47,18 +47,18 @@ All placeholders use the `ezboost` expansion identifier. Wrap the identifier in | `%ezboost_boost_cost_%` | Formatted cost of `` (same as `price_formatted`) | | `%ezboost_boost_duration_%` | Duration in seconds | | `%ezboost_boost_status_%` | Player's status: `available`, `locked`, `active`, `insufficient`, or `cooldown` | -| `%ezboost_player_can_afford_%` | `true` / `false` — whether the requesting player can afford the boost | +| `%ezboost_player_can_afford_%` | `true` / `false` – whether the requesting player can afford the boost | ### Active boost state | Placeholder | Returns | |-------------|---------| -| `%ezboost_has_active_boost%` | `true` / `false` — whether the player has a running boost | +| `%ezboost_has_active_boost%` | `true` / `false` – whether the player has a running boost | | `%ezboost_active_boost%` | Config key of the active boost, or empty | | `%ezboost_active_boost_display%` | Display name of the active boost, or empty | | `%ezboost_active_boost_time_remaining%` | Seconds remaining as a plain integer | | `%ezboost_active_boost_time_remaining_formatted%` | `MM:SS` (under 1 hour) or `HH:MM:SS` (1 hour or more) | -| `%ezboost_is_active_%` | `true` / `false` — plain boolean, no permission or cost check | +| `%ezboost_is_active_%` | `true` / `false` – plain boolean, no permission or cost check | ### Cooldowns @@ -79,10 +79,10 @@ All placeholders use the `ezboost` expansion identifier. Wrap the identifier in Number formatting honours `economy.format.*` settings from `economy.yml`: -- `grouping` — `true`/`false`, enable thousands grouping -- `grouping-separator` — single character, default `,` -- `decimal-separator` — single character, default `.` -- `decimal-places` — integer, default `2` +- `grouping` – `true`/`false`, enable thousands grouping +- `grouping-separator` – single character, default `,` +- `decimal-separator` – single character, default `.` +- `decimal-places` – integer, default `2` Compact formatting (`price_compact`) uses K / M suffixes and ignores the decimal-places setting. @@ -96,7 +96,7 @@ Compact formatting (`price_compact`) uses K / M suffixes and ignores the decimal Boost: %ezboost_active_boost_display% (%ezboost_active_boost_time_remaining_formatted%) ``` -**Conditional button (e.g. in a GUI plugin) — only visible when no boost is active:** +**Conditional button (e.g. in a GUI plugin) – only visible when no boost is active:** ```text condition: %ezboost_has_active_boost% == false diff --git a/docs/permissions.md b/docs/permissions.md index ebaaf53..44c2917 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -26,6 +26,7 @@ EzBoost uses a hierarchical permission system to control access to different fea | Permission | Description | Default | Examples | | --- | --- | --- | --- | | `ezboost.use` | Allows basic use of boost commands and GUI | Players | `/boost`, `/boost speed` | +| `ezboost.top` | Allows viewing the boost top leaderboard | Players | `/boosttop` | | `ezboost.admin` | Grants access to admin commands | OPs | `/ezboost create` | | `ezboost.reload` | Allows reloading the plugin configuration | OPs | `/ezboost reload` | | `ezboost.give` | Allows giving boost tokens to players | OPs | `/ezboost give player speed` | diff --git a/listings/bbcode.txt b/listings/bbcode.txt index 13e7400..117f188 100644 --- a/listings/bbcode.txt +++ b/listings/bbcode.txt @@ -1,7 +1,7 @@ [CENTER][SIZE=6][B]EzBoost[/B][/SIZE] [SIZE=3]Configurable potion boosts with GUI activation, cooldowns, Vault costs, boost tokens, and region-based overrides (WorldGuard support)[/SIZE] [SIZE=2]Renewed take on [URL='https://dev.bukkit.org/projects/redbull']RedBull[/URL][/SIZE] -[SIZE=2]Spigot / Paper / Bukkit 1.7–1.21.* • Optional Vault economy • Fully configurable boosts • GUI + command activation • Boost tokens • Region overrides[/SIZE][/CENTER] +[SIZE=2]Spigot / Paper / Bukkit / Folia 1.7–1.21.* • Optional Vault economy • Fully configurable boosts • GUI + command activation • Boost tokens • Region overrides[/SIZE][/CENTER] [SIZE=4][B]Why EzBoost?[/B][/SIZE] [LIST] @@ -14,7 +14,7 @@ [IMG]https://i.ibb.co/nsKmgK0H/image.png[/IMG] [*][B]Economy-ready[/B] – Optional Vault integration to charge currency per boost activation. [*][B]World restrictions[/B] – Allow/deny specific worlds for tight gameplay balancing. -[*][B]Region-based overrides (WorldGuard)[/B] – Change boost effects, cost, or disable boosts in specific WorldGuard regions using `overrides.yml`. WorldGuard is detected automatically if present. +[*][B]Region-based overrides (WorldGuard)[/B] – Change boost effects, cost, or disable boosts in specific WorldGuard regions. WorldGuard is detected automatically if present. [*][B]Boost tokens[/B] – Give, trade, or reward boost tokens. Players redeem tokens by right-clicking them to activate the boost. [*][B]Player safety options[/B] – Keep boosts on death, reapply on join, and refund on failed activation. [/LIST] @@ -22,9 +22,11 @@ [SIZE=4][B]Feature Highlights[/B][/SIZE] [LIST] [*][B]GUI-first experience[/B] – Inventory menu with configurable filler, slot layout, and MiniMessage formatting. -[*][B]Admin GUI[/B] – Create and manage boosts through an intuitive admin interface. [*][B]Fully configurable boosts[/B] – Define custom potion effects, amplifiers, durations, and permissions per boost. [*][B]Multi-file configuration[/B] – Clean separation of settings, GUI, boosts, and more for easy management. +[*][B]Storage backends[/B] – Persist boost states to YAML (default), SQLite, MySQL, MariaDB, or PostgreSQL. Configured in [icode]storage.yml[/icode]. +[*][B]Boost top leaderboard[/B] – [icode]/boosttop[/icode] tracks and displays the all-time top boost buyers. +[*][B]Folia support[/B] – Fully compatible with Folia servers; task scheduling routes through Folia's region schedulers automatically. [*][B]Region & World Overrides[/B] – Use [icode]overrides.yml[/icode] to define per-world or per-region settings. If WorldGuard is installed, region overrides are applied automatically using region IDs. [*][B]Boost tokens[/B] – Grant boost token items with [icode]/ezboost give[/icode] for rewards, crates, or shops. Players redeem tokens by right-clicking them to instantly activate the boost. [*][B]Flexible limits[/B] – Clamp duration/amplifier ranges to keep boosts balanced. @@ -33,7 +35,7 @@ [*][B]Vault economy support[/B] – Optionally charge players for activating boosts. [*][B]Live reload[/B] – Reload all configuration and messages at runtime with [icode]/ezboost reload[/icode]. [*][B]Friendly messaging[/B] – Customizable MiniMessage strings for actionbar/status feedback. -[*][B]Internal message tags[/B] – Use [icode][/icode], [icode][/icode], [icode][/icode] and more directly in [icode]messages.yml[/icode] — no PlaceholderAPI needed. +[*][B]Internal message tags[/B] – Use [icode][/icode], [icode][/icode], [icode][/icode] and more directly in [icode]messages.yml[/icode] – no PlaceholderAPI needed. [*][B]PlaceholderAPI expansion[/B] – 18+ placeholders for boost status, active boost, cooldowns, time remaining, XP multiplier and economy formatting. Usable in scoreboards, GUI plugins and any PAPI-compatible plugin. [*][B]Command hooks[/B] – Run console commands on enable/disable/toggle per boost. [*][B]Player-friendly behavior[/B] – Reapply boosts on join, keep on death, and refund on failed activation. @@ -41,7 +43,7 @@ [SIZE=4][B]Quick Start[/B][/SIZE] [LIST] -[*]Drop [icode]EzBoost.jar[/icode] into [icode]plugins/[/icode], then start your Paper server (1.20+). +[*]Drop [icode]EzBoost.jar[/icode] into [icode]plugins/[/icode], then start your Spigot, Paper, or Bukkit server (1.7–1.21.*). [*]Use [icode]/ezboost create[/icode] to open the admin GUI and create boosts. [*]Edit [icode]plugins/EzBoost/boosts.yml[/icode], [icode]gui.yml[/icode], and related config files to configure boosts, cooldowns, costs, and GUI slots. [*]Use [icode]/boost[/icode] to open the GUI, or [icode]/boost [/icode] for direct activation. @@ -54,29 +56,31 @@ [tr][th]Command[/th][th]Description[/th][th]Permission[/th][/tr] [tr][td]/boost[/td][td]Open the boosts GUI or show usage.[/td][td]ezboost.use[/td][/tr] [tr][td]/boost [/td][td]Activate a specific boost directly.[/td][td]ezboost.use + boost permission[/td][/tr] +[tr][td]/boosttop[/td][td]View the top boost buyers leaderboard.[/td][td]ezboost.top[/td][/tr] [tr][td]/ezboost create[/td][td]Open the admin GUI to create boosts.[/td][td]ezboost.admin[/td][/tr] [tr][td]/ezboost reload[/td][td]Reload configuration and messages.[/td][td]ezboost.reload[/td][/tr] [tr][td]/ezboost give [amount][/td][td]Give boost token items. Players redeem by right-clicking.[/td][td]ezboost.give[/td][/tr] [/table] -For detailed command and permission documentation, see [docs/commands.md](https://github.com/ez-plugins/ezboost/blob/main/docs/commands.md) and [docs/permissions.md](https://github.com/ez-plugins/ezboost/blob/main/docs/permissions.md). +For detailed command and permission documentation, see [URL='https://ez-plugins.github.io/ezboost/commands']Commands[/URL] and [URL='https://ez-plugins.github.io/ezboost/permissions']Permissions[/URL]. [SIZE=4][B]Setup Guide[/B][/SIZE] [spoiler="Installation & Configuration"] [B]Requirements[/B] [table] [tr][th]Requirement[/th][th]Notes[/th][/tr] -[tr][td]Java 17+[/td][td]Recommended runtime for Paper 1.20–1.21 servers.[/td][/tr] -[tr][td]Paper/Purpur 1.20–1.21[/td][td]Built for modern server APIs.[/td][/tr] +[tr][td]Java 17+[/td][td]Required for Minecraft 1.17+ servers; earlier versions may use Java 11 or 8.[/td][/tr] +[tr][td]Spigot / Paper / Bukkit 1.7–1.21.*[/td][td]Broad server compatibility. WorldGuard and Vault are optional.[/td][/tr] [tr][td]Vault (optional)[/td][td]Required only if you enable economy costs in [icode]economy.enabled[/icode].[/td][/tr] [/table] [B]Configuration Overview[/B] [LIST] -[*][B]Boost definitions[/B] – Add or edit boosts in [icode]boosts.yml[/icode] with effects, duration, cooldown, cost, and permissions. See [docs/boosts.md](https://github.com/ez-plugins/ezboost/blob/main/docs/boosts.md) for a full reference. +[*][B]Boost definitions[/B] – Add or edit boosts in [icode]boosts.yml[/icode] with effects, duration, cooldown, cost, and permissions. See [URL='https://ez-plugins.github.io/ezboost/boosts']Boosts Reference[/URL] for a full reference. [*][B]GUI layout[/B] – Configure size, filler, lore templates, and per-boost slot placement in [icode]gui.yml[/icode]. [*][B]Limits[/B] – Clamp duration/amplifier ranges to keep effects balanced in [icode]limits.yml[/icode]. [*][B]World rules[/B] – Use [icode]worlds.allow-list[/icode] or [icode]worlds.deny-list[/icode] for restrictions in [icode]worlds.yml[/icode]. +[*][B]Storage backend[/B] – Choose YAML (default), SQLite, MySQL, MariaDB, or PostgreSQL in [icode]storage.yml[/icode]. [*][B]Region & World Overrides[/B] – Use [icode]boosts.yml[/icode] to define per-world or per-region settings. If WorldGuard is installed, region overrides are applied automatically using region IDs. [*][B]Behavior toggles[/B] – Replace active boosts, reapply on join, keep on death, and refund on fail in [icode]settings.yml[/icode]. [/LIST] @@ -86,7 +90,7 @@ For detailed command and permission documentation, see [docs/commands.md](https: [LIST] [*]Use region IDs from WorldGuard in [icode]boosts.yml[/icode] to customize boost behavior per region. [*]If WorldGuard is not installed, region overrides are ignored. -[*]See [docs/overrides.md](https://github.com/ez-plugins/EzBoost/blob/main/docs/overrides.md) for syntax and examples. +[*]See [URL='https://ez-plugins.github.io/ezboost/overrides']Overrides Documentation[/URL] for syntax and examples. [/LIST] [CENTER][URL='https://www.spigotmc.org/resources/authors/shadow48402.25936/'][IMG]https://i.ibb.co/PzfjNjh0/ezplugins-try-other-plugins.png[/IMG][/URL][/CENTER] @@ -95,10 +99,10 @@ For detailed command and permission documentation, see [docs/commands.md](https: [SIZE=4][B]Documentation & Support[/B][/SIZE] [LIST] [*][URL='https://github.com/ez-plugins/EzBoost'][B]EzBoost GitHub Repository[/B][/URL] – Main source for all documentation, guides, and updates. -[*][URL='https://github.com/ez-plugins/EzBoost/blob/main/docs/config.md']Configuration Guide[/URL] – Full details on all config options. -[*][URL='https://github.com/ez-plugins/EzBoost/blob/main/docs/boosts.md']Boosts Reference[/URL] – YAML format and boost customization. -[*][URL='https://github.com/ez-plugins/EzBoost/blob/main/docs/gui.md']GUI Customization[/URL] – How to configure the boost GUI. -[*][URL='https://github.com/ez-plugins/EzBoost/blob/main/docs/overrides.md']Overrides Documentation[/URL] – Region/world override syntax and examples. +[*][URL='https://ez-plugins.github.io/ezboost/config']Configuration Guide[/URL] – Full details on all config options. +[*][URL='https://ez-plugins.github.io/ezboost/boosts']Boosts Reference[/URL] – YAML format and boost customization. +[*][URL='https://ez-plugins.github.io/ezboost/gui']GUI Customization[/URL] – How to configure the boost GUI. +[*][URL='https://ez-plugins.github.io/ezboost/overrides']Overrides Documentation[/URL] – Region/world override syntax and examples. [*][URL='https://discord.gg/yWP95XfmBS'][B]EzBoost Discord Support[/B][/URL] – Need help or want to chat? Join our Discord! [/LIST] diff --git a/listings/markdown.md b/listings/markdown.md index 073497e..fad7db1 100644 --- a/listings/markdown.md +++ b/listings/markdown.md @@ -1,36 +1,18 @@ -# EzBoost +**EzBoost** is a feature-rich potion boost plugin for Spigot, Paper, Bukkit, and **Folia** (Minecraft 1.7–1.21.*). It provides an inventory GUI for boost selection, per-boost cooldowns, Vault economy integration, WorldGuard region overrides, boost token items, PlaceholderAPI support, persistent storage (YAML, SQLite, MySQL, MariaDB, PostgreSQL), and a leaderboard for top boost buyers. Inspired by [RedBull](https://dev.bukkit.org/projects/redbull). ![EzBoost GUI](https://i.ibb.co/1GgSfvWs/image.png) ![EzBoost Admin GUI](https://i.ibb.co/cXTcS3LT/image.png) -**EzBoost** is a modern, production-ready boosts plugin for Spigot / Paper / Bukkit 1.7–1.21.*. It delivers configurable potion effects with GUI activation, cooldown management, optional Vault costs, world-based restrictions, boost tokens, and advanced region-based overrides with WorldGuard support. It is a renewed take on [RedBull](https://dev.bukkit.org/projects/redbull). -**EzBoost** is a modern, production-ready boosts plugin for Spigot / Paper / Bukkit 1.7–1.21.*. It delivers configurable potion effects with GUI activation, cooldown management, optional Vault costs, world-based restrictions, boost tokens, and advanced region-based overrides with WorldGuard support. Inspired by [RedBull](https://dev.bukkit.org/projects/redbull). - ---- - - -## 📚 Documentation & Support - -**The GitHub repository is the main source for all documentation, guides, and updates:** -- [EzBoost GitHub Repository](https://github.com/ez-plugins/EzBoost) -- [Configuration Guide](https://github.com/ez-plugins/EzBoost/blob/main/docs/config.md) -- [Boosts Reference](https://github.com/ez-plugins/EzBoost/blob/main/docs/boosts.md) -- [GUI Customization](https://github.com/ez-plugins/EzBoost/blob/main/docs/gui.md) -- [Overrides Documentation](https://github.com/ez-plugins/EzBoost/blob/main/docs/overrides.md) - -**Need help or want to chat? Join our Discord:** -[https://discord.gg/yWP95XfmBS](https://discord.gg/yWP95XfmBS) - -For issues, feature requests, and the latest releases, always check GitHub first. - --- ## ✨ Key Features - **GUI-first activation**: Players can browse boosts with clear status, cooldown, and cost info. - **Admin GUI**: Create and manage boosts through an intuitive admin interface. +- **Folia support**: Fully compatible with Folia servers – task scheduling routes through Folia's region schedulers automatically. +- **Boost top leaderboard**: `/boosttop` displays the all-time top boost buyers, backed by persistent storage. - **Fully configurable boosts**: Define custom potion effects, amplifiers, durations, and permissions per boost. - **Multi-file configuration**: Clean separation of settings, GUI, boosts, and more for easy management. - **Interactive GUI**: Customizable inventory interface for boost activation. @@ -41,7 +23,7 @@ For issues, feature requests, and the latest releases, always check GitHub first - **Boost token items**: Give, trade, or reward boost tokens with `/ezboost give`. Players redeem tokens by right-clicking them to activate the boost. - **Live reload**: Reload all configuration and messages at runtime with `/ezboost reload`. - **MiniMessage support**: Rich formatting for all messages and GUI text. -- **Internal message tags**: Boost-specific tags (``, ``, ``, etc.) are available directly in `messages.yml` — no PlaceholderAPI required. +- **Internal message tags**: Boost-specific tags (``, ``, ``, etc.) are available directly in `messages.yml` – no PlaceholderAPI required. - **PlaceholderAPI expansion**: 18+ placeholders covering boost status, active boost, cooldowns, time remaining, XP multiplier, and economy formatting, usable in scoreboards, GUI plugins, and any PAPI-compatible plugin. See the [PlaceholderAPI integration guide](https://github.com/ez-plugins/EzBoost/blob/main/docs/integration/PlaceholderAPI.md). - **Command hooks**: Run console commands on enable/disable/toggle per boost. - **Player-friendly behavior**: Reapply boosts on join, keep on death, and refund on failed activation. @@ -56,6 +38,7 @@ For issues, feature requests, and the latest releases, always check GitHub first | --- | --- | --- | | `/boost` | Open the boosts GUI or show usage. | `ezboost.use` | | `/boost ` | Activate a boost directly. | `ezboost.use` + boost permission | +| `/boosttop` | View the top boost buyers leaderboard. | `ezboost.top` | | `/ezboost create` | Open the admin GUI to create boosts. | `ezboost.admin` | | `/ezboost reload` | Reload configuration and messages. | `ezboost.reload` | | `/ezboost give [amount]` | Give boost token items. Players redeem by right-clicking. | `ezboost.give` | @@ -66,12 +49,13 @@ For detailed command and permission documentation, see [docs/commands.md](https: ## 🛡️ Permissions -- `ezboost.use` — Use boosts (`/boost`). -- `ezboost.admin` — Access admin commands. -- `ezboost.reload` — Reload configuration. -- `ezboost.give` — Give boost tokens. -- `ezboost.cooldown.bypass` — Bypass boost cooldowns. -- `ezboost.boost.` — Per-boost permissions (example: `ezboost.boost.speed`). +- `ezboost.use` – Use boosts (`/boost`). +- `ezboost.top` – View the boost leaderboard (`/boosttop`). +- `ezboost.admin` – Access admin commands. +- `ezboost.reload` – Reload configuration. +- `ezboost.give` – Give boost tokens. +- `ezboost.cooldown.bypass` – Bypass boost cooldowns. +- `ezboost.boost.` – Per-boost permissions (example: `ezboost.boost.speed`). --- @@ -94,6 +78,7 @@ For detailed command and permission documentation, see [docs/commands.md](https: - **GUI layout**: Customize title, size, filler, lore templates, and per-boost slot positions in `gui.yml`. - **Limits**: Clamp amplifier and duration ranges for balance in `limits.yml`. - **World rules**: Use `worlds.allow-list` / `worlds.deny-list` to control where boosts apply in `worlds.yml`. +- **Storage backend**: Choose YAML (default), SQLite, MySQL, MariaDB, or PostgreSQL in `storage.yml`. - **Region & World Overrides**: Use `boosts.yml` to define per-world or per-region settings. If WorldGuard is installed, region overrides are applied automatically using region IDs. - **Behavior toggles**: Replace active boosts, reapply on join, keep on death, or refund failed activations in `settings.yml`. - **Economy**: Enable Vault costs with `economy.enabled` and `economy.vault` in `economy.yml`. @@ -118,14 +103,21 @@ For detailed command and permission documentation, see [docs/commands.md](https: --- +## 📚 Documentation & Support + +Full documentation is available at **[ez-plugins.github.io/ezboost](https://ez-plugins.github.io/ezboost)**. -- [EzBoost GitHub Repository](https://github.com/ez-plugins/EzBoost) — Source code, issues, and latest updates. -- [Configuration Guide](https://github.com/ez-plugins/EzBoost/blob/main/docs/config.md) — Full details on all config options. -- [Boosts Reference](https://github.com/ez-plugins/EzBoost/blob/main/docs/boosts.md) — YAML format and boost customization. -- [GUI Customization](https://github.com/ez-plugins/EzBoost/blob/main/docs/gui.md) — How to configure the boost GUI. -- [Overrides Documentation](https://github.com/ez-plugins/EzBoost/blob/main/docs/overrides.md) — Region/world override syntax and examples. +| Page | What it covers | +|------|----------------| +| [Commands](https://ez-plugins.github.io/ezboost/commands) | All `/boost` and `/ezboost` commands | +| [Permissions](https://ez-plugins.github.io/ezboost/permissions) | Permissions reference and defaults | +| [Configuration](https://ez-plugins.github.io/ezboost/config) | All config files explained | +| [Boosts](https://ez-plugins.github.io/ezboost/boosts) | `boosts.yml` schema – effects, duration, costs | +| [GUI](https://ez-plugins.github.io/ezboost/gui) | `gui.yml` layout and slot configuration | +| [Overrides](https://ez-plugins.github.io/ezboost/overrides) | World and region override syntax | +| [PlaceholderAPI](https://ez-plugins.github.io/ezboost/integration/PlaceholderAPI) | 18+ available placeholders | + +**Need help or want to chat? Join our Discord:** +[https://discord.gg/yWP95XfmBS](https://discord.gg/yWP95XfmBS) [![Try the other Minecraft plugins in the EzPlugins series](https://i.ibb.co/PzfjNjh0/ezplugins-try-other-plugins.png)](https://modrinth.com/collection/Q98Ov6dA) ---- -For the latest documentation, advanced configuration, and troubleshooting, visit: -[https://github.com/ez-plugins/EzBoost](https://github.com/ez-plugins/EzBoost) diff --git a/pom.xml b/pom.xml index 64cee67..1213182 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.skyblockexp ezboost - 2.0.0 + 2.1.0 EzBoost Standalone boost plugin with configurable potion boosts, cooldowns, and GUI. jar @@ -36,9 +36,9 @@ ez-plugins ezboost - 25 - 25 - 25 + 17 + 17 + 17 [26.1.2.build,) org.mockbukkit.mockbukkit mockbukkit-v1.21 @@ -145,6 +145,12 @@ jaloquent 1.3.0 + + + org.xerial + sqlite-jdbc + 3.46.1.3 + @@ -192,6 +198,7 @@ economy.yml gui.yml boosts.yml + storage.yml @@ -281,6 +288,7 @@ net.kyori:examination-api com.github.EzFramework:jaloquent com.github.EzFramework:JavaQueryBuilder + org.xerial:sqlite-jdbc diff --git a/src/main/java/com/skyblockexp/ezboost/EzBoostPlugin.java b/src/main/java/com/skyblockexp/ezboost/EzBoostPlugin.java index 221500e..bae444c 100644 --- a/src/main/java/com/skyblockexp/ezboost/EzBoostPlugin.java +++ b/src/main/java/com/skyblockexp/ezboost/EzBoostPlugin.java @@ -19,14 +19,16 @@ import com.skyblockexp.ezboost.listener.XpBoostListener; import com.skyblockexp.ezboost.storage.BoostLeaderboard; import com.skyblockexp.ezboost.storage.BoostPurchaseRecord; -import com.skyblockexp.ezboost.storage.BoostStorage; -import com.skyblockexp.ezboost.storage.YamlDataStore; +import com.skyblockexp.ezboost.storage.EzBoostRepository; +import com.skyblockexp.ezboost.storage.StorageFactory; +import com.skyblockexp.ezboost.storage.StorageSettings; import com.skyblockexp.ezboost.update.SpigotUpdateChecker; import com.github.ezframework.jaloquent.model.ModelRepository; import java.io.File; import java.util.Objects; import org.bstats.bukkit.Metrics; import org.bukkit.command.PluginCommand; +import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.event.HandlerList; import org.bukkit.plugin.java.JavaPlugin; @@ -37,7 +39,7 @@ public final class EzBoostPlugin extends JavaPlugin { private EzBoostConfig config; private Messages messages; private EconomyService economyService; - private BoostStorage storage; + private EzBoostRepository boostRepository; private BoostManager boostManager; private BoostGui boostGui; private AdminBoostCreationGui adminGui; @@ -48,15 +50,21 @@ public final class EzBoostPlugin extends JavaPlugin { @Override public void onEnable() { ensureResource("messages.yml"); - ensureDataFile(); + ensureResource("storage.yml"); // Initialize basic services first messages = new Messages(this); economyService = new EconomyService(); - storage = new BoostStorage(this); + + // Build storage layer from storage.yml + StorageSettings storageSettings = loadStorageSettings(); + getLogger().info("Storage backend: " + storageSettings.backend()); + StorageFactory.StorageBundle storageBundle = + StorageFactory.build(storageSettings, getDataFolder(), getLogger()); + boostRepository = storageBundle.boostRepository(); // Create boost manager first (without config initially) - boostManager = new BoostManager(this, null, messages, economyService, storage); + boostManager = new BoostManager(this, null, messages, economyService, boostRepository); // Initialize API so custom effects can be registered EzBoostAPI.init(boostManager); @@ -72,12 +80,8 @@ public void onEnable() { boostManager.loadStates(); - // Leaderboard (Jaloquent + YAML backing) - YamlDataStore leaderboardStore = new YamlDataStore( - new File(getDataFolder(), "leaderboard.yml"), getLogger()); - ModelRepository leaderboardRepo = - new ModelRepository<>(leaderboardStore, "leaderboard", BoostPurchaseRecord.FACTORY); - boostLeaderboard = new BoostLeaderboard(leaderboardRepo, getLogger()); + // Leaderboard (Jaloquent-backed — same backend as game state) + boostLeaderboard = new BoostLeaderboard(storageBundle.leaderboardRepo(), getLogger()); boostManager.setLeaderboard(boostLeaderboard); boostGui = new BoostGui(this, boostManager, config.guiSettings()); @@ -121,7 +125,7 @@ public void onEnable() { StartupLogger.logEnable( getLogger(), - getPluginMeta().getVersion(), + getDescription().getVersion(), boostManager.totalBoostCount(), boostManager.vaultHookAvailable(), papiHooked @@ -183,18 +187,23 @@ private void ensureResource(String name) { } } - private void ensureDataFile() { - File file = new File(getDataFolder(), "data.yml"); - if (!file.exists()) { - if (!getDataFolder().exists()) { - getDataFolder().mkdirs(); - } - try { - file.createNewFile(); - } catch (Exception ex) { - getLogger().warning("Failed to create data.yml: " + ex.getMessage()); - } - } + private StorageSettings loadStorageSettings() { + File file = new File(getDataFolder(), "storage.yml"); + YamlConfiguration cfg = YamlConfiguration.loadConfiguration(file); + String backend = cfg.getString("storage.backend", "yaml"); + return new StorageSettings( + backend, + cfg.getString("storage.sqlite.file", "ezboost.db"), + cfg.getString("storage." + (backend.equals("postgresql") ? "postgresql" : "mysql") + ".host", "localhost"), + cfg.getInt("storage." + (backend.equals("postgresql") ? "postgresql" : "mysql") + ".port", + backend.equals("postgresql") ? 5432 : 3306), + cfg.getString("storage." + (backend.equals("postgresql") ? "postgresql" : "mysql") + ".database", "ezboost"), + cfg.getString("storage." + (backend.equals("postgresql") ? "postgresql" : "mysql") + ".username", + backend.equals("postgresql") ? "postgres" : "root"), + cfg.getString("storage." + (backend.equals("postgresql") ? "postgresql" : "mysql") + ".password", ""), + cfg.getInt("storage." + (backend.equals("postgresql") ? "postgresql" : "mysql") + ".pool-size", 10), + cfg.getBoolean("storage.debug-logging", false) + ); } private void initializeMetrics() { diff --git a/src/main/java/com/skyblockexp/ezboost/boost/BoostManager.java b/src/main/java/com/skyblockexp/ezboost/boost/BoostManager.java index acc5a7d..c62676e 100644 --- a/src/main/java/com/skyblockexp/ezboost/boost/BoostManager.java +++ b/src/main/java/com/skyblockexp/ezboost/boost/BoostManager.java @@ -6,7 +6,7 @@ import com.skyblockexp.ezboost.event.BoostEndEvent; import com.skyblockexp.ezboost.event.BoostStartEvent; import com.skyblockexp.ezboost.storage.BoostLeaderboard; -import com.skyblockexp.ezboost.storage.BoostStorage; +import com.skyblockexp.ezboost.storage.EzBoostRepository; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -36,7 +36,7 @@ public final class BoostManager { private EzBoostConfig config; private Messages messages; private EconomyService economyService; - private final BoostStorage storage; + private final EzBoostRepository storage; private BoostLeaderboard leaderboard; private final Logger logger; private final Map states = new ConcurrentHashMap<>(); @@ -89,7 +89,7 @@ public BoostManager(JavaPlugin plugin, EzBoostConfig config, Messages messages, EconomyService economyService, - BoostStorage storage) { + EzBoostRepository storage) { this.plugin = Objects.requireNonNull(plugin, "plugin"); this.config = config; // Allow null initially this.messages = Objects.requireNonNull(messages, "messages"); diff --git a/src/main/java/com/skyblockexp/ezboost/command/BoostCommand.java b/src/main/java/com/skyblockexp/ezboost/command/BoostCommand.java index a78103d..d4ef127 100644 --- a/src/main/java/com/skyblockexp/ezboost/command/BoostCommand.java +++ b/src/main/java/com/skyblockexp/ezboost/command/BoostCommand.java @@ -43,7 +43,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } org.bukkit.plugin.Plugin plugin = org.bukkit.Bukkit.getPluginManager().getPlugin("EzBoost"); - String pluginVersion = plugin != null ? plugin.getPluginMeta().getVersion() : "unknown"; + String pluginVersion = plugin != null ? plugin.getDescription().getVersion() : "unknown"; String serverVersion = org.bukkit.Bukkit.getBukkitVersion(); String serverType = org.bukkit.Bukkit.getVersion(); String database = "data.yml (file)"; diff --git a/src/main/java/com/skyblockexp/ezboost/command/EzBoostCommand.java b/src/main/java/com/skyblockexp/ezboost/command/EzBoostCommand.java index 189f9bb..2531aec 100644 --- a/src/main/java/com/skyblockexp/ezboost/command/EzBoostCommand.java +++ b/src/main/java/com/skyblockexp/ezboost/command/EzBoostCommand.java @@ -137,7 +137,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } org.bukkit.plugin.Plugin plugin = org.bukkit.Bukkit.getPluginManager().getPlugin("EzBoost"); - String pluginVersion = plugin != null ? plugin.getPluginMeta().getVersion() : "unknown"; + String pluginVersion = plugin != null ? plugin.getDescription().getVersion() : "unknown"; String serverVersion = org.bukkit.Bukkit.getBukkitVersion(); String serverType = org.bukkit.Bukkit.getVersion(); String database = "data.yml (file)"; diff --git a/src/main/java/com/skyblockexp/ezboost/listener/AdminGuiChatListener.java b/src/main/java/com/skyblockexp/ezboost/listener/AdminGuiChatListener.java index 004e5cf..d5e0eee 100644 --- a/src/main/java/com/skyblockexp/ezboost/listener/AdminGuiChatListener.java +++ b/src/main/java/com/skyblockexp/ezboost/listener/AdminGuiChatListener.java @@ -2,8 +2,7 @@ import com.skyblockexp.ezboost.FoliaScheduler; import com.skyblockexp.ezboost.gui.AdminBoostCreationGui; -import io.papermc.paper.event.player.AsyncChatEvent; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -22,14 +21,14 @@ public AdminGuiChatListener(AdminBoostCreationGui adminGui, JavaPlugin plugin) { } @EventHandler - public void onPlayerChat(AsyncChatEvent event) { + public void onPlayerChat(AsyncPlayerChatEvent event) { Player player = event.getPlayer(); // Use thread-safe check before touching any entity state if (!adminGui.isPlayerPendingAnyInput(player.getUniqueId())) { return; } event.setCancelled(true); - final String message = PlainTextComponentSerializer.plainText().serialize(event.message()); + final String message = event.getMessage(); if (FoliaScheduler.FOLIA) { // On Folia, PDC must be accessed on the entity's region thread player.getScheduler().run(plugin, t -> processInput(player, message), null); diff --git a/src/main/java/com/skyblockexp/ezboost/storage/BoostStorage.java b/src/main/java/com/skyblockexp/ezboost/storage/BoostStorage.java deleted file mode 100644 index 32c9633..0000000 --- a/src/main/java/com/skyblockexp/ezboost/storage/BoostStorage.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.skyblockexp.ezboost.storage; - -import com.skyblockexp.ezboost.boost.BoostState; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.logging.Level; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.plugin.java.JavaPlugin; - -public final class BoostStorage { - private final JavaPlugin plugin; - private final File file; - - public BoostStorage(JavaPlugin plugin) { - this.plugin = plugin; - this.file = new File(plugin.getDataFolder(), "data.yml"); - } - - public Map load() { - Map states = new HashMap<>(); - if (!file.exists()) { - return states; - } - FileConfiguration configuration = YamlConfiguration.loadConfiguration(file); - ConfigurationSection playersSection = configuration.getConfigurationSection("players"); - if (playersSection == null) { - return states; - } - for (String key : playersSection.getKeys(false)) { - UUID uuid; - try { - uuid = UUID.fromString(key); - } catch (IllegalArgumentException ex) { - continue; - } - ConfigurationSection playerSection = playersSection.getConfigurationSection(key); - if (playerSection == null) { - continue; - } - BoostState state = new BoostState(); - String active = playerSection.getString("active", null); - long end = playerSection.getLong("end", 0L); - if (active != null && !active.isBlank()) { - state.setActiveBoost(active, end); - } - ConfigurationSection cooldownSection = playerSection.getConfigurationSection("cooldowns"); - if (cooldownSection != null) { - for (String boostKey : cooldownSection.getKeys(false)) { - long cooldownEnd = cooldownSection.getLong(boostKey, 0L); - state.setCooldownEnd(boostKey.toLowerCase(), cooldownEnd); - } - } - states.put(uuid, state); - } - return states; - } - - public void save(Map states) { - FileConfiguration configuration = new YamlConfiguration(); - ConfigurationSection playersSection = configuration.createSection("players"); - for (Map.Entry entry : states.entrySet()) { - ConfigurationSection playerSection = playersSection.createSection(entry.getKey().toString()); - BoostState state = entry.getValue(); - if (state.activeBoostKey() != null) { - playerSection.set("active", state.activeBoostKey()); - playerSection.set("end", state.endTimestamp()); - } - ConfigurationSection cooldownSection = playerSection.createSection("cooldowns"); - for (Map.Entry cooldown : state.cooldowns().entrySet()) { - cooldownSection.set(cooldown.getKey(), cooldown.getValue()); - } - } - try { - configuration.save(file); - } catch (IOException ex) { - plugin.getLogger().log(Level.SEVERE, "Failed to save EzBoost data.yml", ex); - } - } -} diff --git a/src/main/java/com/skyblockexp/ezboost/storage/EzBoostRepository.java b/src/main/java/com/skyblockexp/ezboost/storage/EzBoostRepository.java new file mode 100644 index 0000000..93208a9 --- /dev/null +++ b/src/main/java/com/skyblockexp/ezboost/storage/EzBoostRepository.java @@ -0,0 +1,197 @@ +package com.skyblockexp.ezboost.storage; + +import com.github.ezframework.jaloquent.exception.StorageException; +import com.github.ezframework.jaloquent.model.ModelRepository; +import com.github.ezframework.jaloquent.model.Model; +import com.skyblockexp.ezboost.boost.BoostState; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Jaloquent-backed repository for player boost state and cooldowns. + * + *

Replaces the legacy {@code BoostStorage} YAML implementation. + * Uses two internal {@link ModelRepository} instances: + *

    + *
  • {@code boost_states} prefix — one {@link PlayerBoostStateRecord} per player.
  • + *
  • {@code cooldowns} prefix — one {@link PlayerCooldownRecord} per (player, boostKey) pair.
  • + *
+ * The same {@link com.github.ezframework.jaloquent.store.DataStore} is shared by both + * repositories so that YAML and SQL backends are both supported. + */ +public final class EzBoostRepository { + + private static final String PREFIX_STATES = "boost_states"; + private static final String PREFIX_COOLDOWNS = "cooldowns"; + + private final ModelRepository stateRepo; + private final ModelRepository cooldownRepo; + private final Logger logger; + + public EzBoostRepository( + ModelRepository stateRepo, + ModelRepository cooldownRepo, + Logger logger) { + this.stateRepo = stateRepo; + this.cooldownRepo = cooldownRepo; + this.logger = logger; + } + + // ── Prefix constants used by callers ───────────────────────────────────── + + public static String prefixStates() { return PREFIX_STATES; } + public static String prefixCooldowns() { return PREFIX_COOLDOWNS; } + + // ── Load ───────────────────────────────────────────────────────────────── + + /** + * Load all persisted player states (active boost + cooldowns) into a map. + * Called once on plugin startup. + * + * @return map of player UUID → {@link BoostState}; never {@code null}. + */ + public Map load() { + Map result = new HashMap<>(); + + // 1. Load all active-boost records + try { + List all = + stateRepo.query(Model.queryBuilder().build()); + for (PlayerBoostStateRecord rec : all) { + UUID uuid = parseUUID(rec.getId()); + if (uuid == null) continue; + + BoostState state = result.computeIfAbsent(uuid, u -> new BoostState()); + String activeBoost = rec.getActiveBoost(); + long boostEnd = rec.getBoostEnd(); + if (activeBoost != null && !activeBoost.isEmpty() && boostEnd > System.currentTimeMillis()) { + state.setActiveBoost(activeBoost, boostEnd); + } + } + } catch (StorageException ex) { + logger.log(Level.SEVERE, "Failed to load boost states from storage", ex); + } + + // 2. Load all cooldown records + try { + List all = + cooldownRepo.query(Model.queryBuilder().build()); + for (PlayerCooldownRecord rec : all) { + String id = rec.getId(); + if (id == null || id.length() < 38) continue; // malformed + + UUID uuid = PlayerCooldownRecord.extractUUID(id); + String boostKey = PlayerCooldownRecord.extractBoostKey(id); + long end = rec.getCooldownEnd(); + + if (end > System.currentTimeMillis()) { + result.computeIfAbsent(uuid, u -> new BoostState()) + .setCooldownEnd(boostKey, end); + } + } + } catch (StorageException ex) { + logger.log(Level.SEVERE, "Failed to load cooldowns from storage", ex); + } + + return result; + } + + // ── Save ───────────────────────────────────────────────────────────────── + + /** + * Persist the full player state map. + * Called on plugin disable and periodically. + * + * @param states current in-memory player state map. + */ + public void save(Map states) { + for (Map.Entry entry : states.entrySet()) { + UUID uuid = entry.getKey(); + BoostState state = entry.getValue(); + saveState(uuid, state); + } + } + + // ── Per-player helpers ─────────────────────────────────────────────────── + + /** + * Persist a single player's boost state and cooldowns. + * This is safe to call any time the state changes. + */ + public void saveState(UUID uuid, BoostState state) { + saveBoostStateRecord(uuid, state); + saveCooldownRecords(uuid, state); + } + + private void saveBoostStateRecord(UUID uuid, BoostState state) { + try { + PlayerBoostStateRecord rec = new PlayerBoostStateRecord(uuid.toString()); + String key = state.activeBoostKey(); + rec.setActiveBoost(key == null ? "" : key); + rec.setBoostEnd(state.endTimestamp()); + stateRepo.save(rec); + } catch (StorageException ex) { + logger.log(Level.WARNING, "Failed to save boost state for " + uuid, ex); + } + } + + private void saveCooldownRecords(UUID uuid, BoostState state) { + Map cooldowns = state.cooldowns(); + long now = System.currentTimeMillis(); + + for (Map.Entry cd : cooldowns.entrySet()) { + String boostKey = cd.getKey(); + long end = cd.getValue(); + String compositeId = PlayerCooldownRecord.makeId(uuid, boostKey); + try { + if (end <= now) { + // Expired — remove from storage to avoid accumulation + cooldownRepo.delete(compositeId); + } else { + PlayerCooldownRecord rec = new PlayerCooldownRecord(compositeId); + rec.setCooldownEnd(end); + cooldownRepo.save(rec); + } + } catch (StorageException ex) { + logger.log(Level.WARNING, + "Failed to save cooldown for " + uuid + " / " + boostKey, ex); + } + } + } + + /** + * Remove all stored data for the given player. + * Not used during normal gameplay but available for admin commands. + */ + public void deletePlayer(UUID uuid) { + try { stateRepo.delete(uuid.toString()); } catch (StorageException ex) { + logger.log(Level.WARNING, "Failed to delete boost state for " + uuid, ex); + } + // Cooldowns: iterate and delete – we don't have a "deleteWhere by prefix" here, + // so load first then delete individually. + try { + List playerCds = + cooldownRepo.query(Model.queryBuilder().build()); + String prefix = uuid.toString() + ":"; + for (PlayerCooldownRecord rec : playerCds) { + if (rec.getId() != null && rec.getId().startsWith(prefix)) { + cooldownRepo.delete(rec.getId()); + } + } + } catch (StorageException ex) { + logger.log(Level.WARNING, "Failed to delete cooldowns for " + uuid, ex); + } + } + + // ── Internal utils ─────────────────────────────────────────────────────── + + private UUID parseUUID(String raw) { + if (raw == null) return null; + try { return UUID.fromString(raw); } catch (IllegalArgumentException e) { return null; } + } +} diff --git a/src/main/java/com/skyblockexp/ezboost/storage/PlayerBoostStateRecord.java b/src/main/java/com/skyblockexp/ezboost/storage/PlayerBoostStateRecord.java new file mode 100644 index 0000000..48683b1 --- /dev/null +++ b/src/main/java/com/skyblockexp/ezboost/storage/PlayerBoostStateRecord.java @@ -0,0 +1,43 @@ +package com.skyblockexp.ezboost.storage; + +import com.github.ezframework.jaloquent.model.Model; +import com.github.ezframework.jaloquent.model.ModelFactory; + +/** + * Jaloquent model representing a player's active boost state. + * The record ID is the player's UUID string. + * Stored under the {@code boost_states} repository prefix. + */ +public final class PlayerBoostStateRecord extends Model { + + public static final ModelFactory FACTORY = + (id, data) -> { + PlayerBoostStateRecord r = new PlayerBoostStateRecord(id); + r.fromMap(data); + return r; + }; + + public PlayerBoostStateRecord(String uuid) { + super(uuid); + } + + /** @return the active boost key, or {@code null} if no boost is active. */ + public String getActiveBoost() { + return getAs("active_boost", String.class, null); + } + + public void setActiveBoost(String key) { + set("active_boost", key); + } + + /** @return epoch-millis timestamp when the active boost expires, or 0 if none. */ + public long getBoostEnd() { + Object v = get("boost_end"); + if (v instanceof Number n) return n.longValue(); + return 0L; + } + + public void setBoostEnd(long end) { + set("boost_end", end); + } +} diff --git a/src/main/java/com/skyblockexp/ezboost/storage/PlayerCooldownRecord.java b/src/main/java/com/skyblockexp/ezboost/storage/PlayerCooldownRecord.java new file mode 100644 index 0000000..1787b71 --- /dev/null +++ b/src/main/java/com/skyblockexp/ezboost/storage/PlayerCooldownRecord.java @@ -0,0 +1,61 @@ +package com.skyblockexp.ezboost.storage; + +import com.github.ezframework.jaloquent.model.Model; +import com.github.ezframework.jaloquent.model.ModelFactory; +import java.util.UUID; + +/** + * Jaloquent model representing a single cooldown entry for one player + boost combination. + * + *

The record ID uses a composite key: {@code ":"}. + * Stored under the {@code cooldowns} repository prefix. + */ +public final class PlayerCooldownRecord extends Model { + + public static final ModelFactory FACTORY = + (id, data) -> { + PlayerCooldownRecord r = new PlayerCooldownRecord(id); + r.fromMap(data); + return r; + }; + + public PlayerCooldownRecord(String compositeId) { + super(compositeId); + } + + // ── ID helpers ─────────────────────────────────────────────────────────── + + /** Compose the record ID from a player UUID and boost key. */ + public static String makeId(UUID uuid, String boostKey) { + return uuid.toString() + ":" + boostKey; + } + + /** + * Extract the player UUID from a composite record ID. + * UUID occupies the first 36 characters of the composite string. + */ + public static UUID extractUUID(String compositeId) { + return UUID.fromString(compositeId.substring(0, 36)); + } + + /** + * Extract the boost key from a composite record ID. + * The boost key starts at index 37 (after {@code "uuid:"}). + */ + public static String extractBoostKey(String compositeId) { + return compositeId.substring(37); + } + + // ── Field accessors ────────────────────────────────────────────────────── + + /** @return epoch-millis timestamp when the cooldown ends. */ + public long getCooldownEnd() { + Object v = get("cooldown_end"); + if (v instanceof Number n) return n.longValue(); + return 0L; + } + + public void setCooldownEnd(long end) { + set("cooldown_end", end); + } +} diff --git a/src/main/java/com/skyblockexp/ezboost/storage/StorageFactory.java b/src/main/java/com/skyblockexp/ezboost/storage/StorageFactory.java new file mode 100644 index 0000000..8dd3fa3 --- /dev/null +++ b/src/main/java/com/skyblockexp/ezboost/storage/StorageFactory.java @@ -0,0 +1,266 @@ +package com.skyblockexp.ezboost.storage; + +import com.github.ezframework.jaloquent.config.DatabaseSettings; +import com.github.ezframework.jaloquent.config.JaloquentConfig; +import com.github.ezframework.jaloquent.config.JdbcScheme; +import com.github.ezframework.jaloquent.exception.MigrationException; +import com.github.ezframework.jaloquent.migration.MigrationBlueprint; +import com.github.ezframework.jaloquent.migration.MigrationRunner; +import com.github.ezframework.jaloquent.migration.Schema; +import com.github.ezframework.jaloquent.model.ModelRepository; +import com.github.ezframework.jaloquent.model.TableRegistry; +import com.github.ezframework.jaloquent.store.DataStore; +import com.github.ezframework.jaloquent.store.sql.DataSourceJdbcStore; +import com.github.ezframework.jaloquent.store.sql.DriverManagerDataSource; +import com.github.ezframework.javaquerybuilder.query.sql.SqlDialect; +import java.io.File; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Builds all {@link ModelRepository} instances needed by the plugin based on + * {@link StorageSettings}. + * + *

For SQL backends the factory also: + *

    + *
  • Registers table mappings with {@link TableRegistry}.
  • + *
  • Runs Jaloquent migrations to create tables if they do not exist.
  • + *
+ * + *

Use {@link #build(StorageSettings, File, Logger)} and consume the returned + * {@link StorageBundle}. + * + * @see Jaloquent on GitHub + */ +public final class StorageFactory { + + // Repository prefix constants — must match EzBoostRepository + static final String PREFIX_STATES = EzBoostRepository.prefixStates(); + static final String PREFIX_COOLDOWNS = EzBoostRepository.prefixCooldowns(); + static final String PREFIX_LEADERBOARD = "leaderboard"; + + // SQL table names + static final String TABLE_STATES = "ezboost_boost_states"; + static final String TABLE_COOLDOWNS = "ezboost_cooldowns"; + static final String TABLE_LEADERBOARD = "ezboost_leaderboard"; + + private StorageFactory() {} + + // ── Public API ─────────────────────────────────────────────────────────── + + /** + * Holds all repositories built by {@link StorageFactory#build}. + * + * @param boostRepository boost-state + cooldown persistence + * @param leaderboardRepo purchase-count leaderboard persistence + */ + public record StorageBundle( + EzBoostRepository boostRepository, + ModelRepository leaderboardRepo) {} + + /** + * Build the full storage layer from the given settings. + * + * @param settings storage settings loaded from {@code storage.yml} + * @param dataFolder plugin data folder (used for YAML/SQLite file paths) + * @param logger plugin logger + * @return fully initialised {@link StorageBundle} + */ + public static StorageBundle build( + StorageSettings settings, + File dataFolder, + Logger logger) { + + String backend = settings.backend().toLowerCase(Locale.ROOT); + + JaloquentConfig.enableLogging(settings.debugLogging()); + + if ("yaml".equals(backend)) { + return buildYaml(dataFolder, logger); + } + return buildSql(settings, backend, dataFolder, logger); + } + + // ── YAML backend ───────────────────────────────────────────────────────── + + private static StorageBundle buildYaml(File dataFolder, Logger logger) { + // Separate YAML files keep game state and leaderboard data distinct + YamlDataStore gameStore = new YamlDataStore( + new File(dataFolder, "data.yml"), logger); + YamlDataStore leaderboardStore = new YamlDataStore( + new File(dataFolder, "leaderboard.yml"), logger); + + ModelRepository stateRepo = + new ModelRepository<>(gameStore, PREFIX_STATES, PlayerBoostStateRecord.FACTORY); + ModelRepository cooldownRepo = + new ModelRepository<>(gameStore, PREFIX_COOLDOWNS, PlayerCooldownRecord.FACTORY); + ModelRepository leaderboardRepo = + new ModelRepository<>(leaderboardStore, PREFIX_LEADERBOARD, BoostPurchaseRecord.FACTORY); + + return new StorageBundle( + new EzBoostRepository(stateRepo, cooldownRepo, logger), + leaderboardRepo); + } + + // ── SQL backend ────────────────────────────────────────────────────────── + + private static StorageBundle buildSql( + StorageSettings settings, + String backend, + File dataFolder, + Logger logger) { + + DatabaseSettings dbSettings = buildDatabaseSettings(settings, backend, dataFolder); + SqlDialect dialect = mapDialect(backend); + + DriverManagerDataSource ds = new DriverManagerDataSource(dbSettings); + DataSourceJdbcStore store = new DataSourceJdbcStore(ds); + + // Register table mappings so Jaloquent knows which SQL table to use per prefix + registerTables(); + + // Run migrations (CREATE TABLE IF NOT EXISTS) + runMigrations(store, dialect, logger); + + ModelRepository stateRepo = + new ModelRepository<>(store, PREFIX_STATES, PlayerBoostStateRecord.FACTORY, dialect); + ModelRepository cooldownRepo = + new ModelRepository<>(store, PREFIX_COOLDOWNS, PlayerCooldownRecord.FACTORY, dialect); + ModelRepository leaderboardRepo = + new ModelRepository<>(store, PREFIX_LEADERBOARD, BoostPurchaseRecord.FACTORY, dialect); + + return new StorageBundle( + new EzBoostRepository(stateRepo, cooldownRepo, logger), + leaderboardRepo); + } + + // ── SQL helpers ────────────────────────────────────────────────────────── + + private static DatabaseSettings buildDatabaseSettings( + StorageSettings s, String backend, File dataFolder) { + + DatabaseSettings.Builder b = DatabaseSettings.builder(); + + switch (backend) { + case "sqlite" -> { + String filePath = new File(dataFolder, s.dbFile()).getAbsolutePath(); + b.jdbcScheme(JdbcScheme.SQLITE) + .driverClassName("org.sqlite.JDBC") + .url("jdbc:sqlite:" + filePath) + .username("") + .password(""); + } + case "h2" -> { + String filePath = new File(dataFolder, s.dbFile()).getAbsolutePath(); + b.jdbcScheme("h2") + .driverClassName("org.h2.Driver") + .url("jdbc:h2:file:" + filePath + ";AUTO_SERVER=TRUE") + .username("sa") + .password(""); + } + case "mysql" -> b + .jdbcScheme(JdbcScheme.MYSQL) + .driverClassName("com.mysql.cj.jdbc.Driver") + .host(s.host()).port(s.port()) + .databaseName(s.database()) + .username(s.username()).password(s.password()) + .maximumPoolSize(s.poolSize()); + case "mariadb" -> b + .jdbcScheme(JdbcScheme.MARIADB) + .driverClassName("org.mariadb.jdbc.Driver") + .host(s.host()).port(s.port()) + .databaseName(s.database()) + .username(s.username()).password(s.password()) + .maximumPoolSize(s.poolSize()); + case "postgresql" -> b + .jdbcScheme(JdbcScheme.POSTGRESQL) + .driverClassName("org.postgresql.Driver") + .host(s.host()).port(s.port()) + .databaseName(s.database()) + .username(s.username()).password(s.password()) + .maximumPoolSize(s.poolSize()); + default -> throw new IllegalArgumentException( + "Unknown storage backend: '" + backend + + "'. Valid values: yaml, sqlite, mysql, mariadb, postgresql, h2"); + } + + return b.build(); + } + + private static SqlDialect mapDialect(String backend) { + return switch (backend) { + case "mysql", "mariadb" -> SqlDialect.MYSQL; + case "sqlite" -> SqlDialect.SQLITE; + case "postgresql" -> SqlDialect.POSTGRESQL; + default -> SqlDialect.STANDARD; + }; + } + + private static void registerTables() { + TableRegistry.register(PREFIX_STATES, TABLE_STATES, + Map.of("active_boost", "active_boost", "boost_end", "boost_end")); + TableRegistry.register(PREFIX_COOLDOWNS, TABLE_COOLDOWNS, + Map.of("cooldown_end", "cooldown_end")); + TableRegistry.register(PREFIX_LEADERBOARD, TABLE_LEADERBOARD, + Map.of("player_name", "player_name", "total_purchases", "total_purchases")); + } + + private static void runMigrations( + DataSourceJdbcStore store, SqlDialect dialect, Logger logger) { + Schema schema = new Schema(store, dialect); + + List migrations = List.of( + new com.github.ezframework.jaloquent.migration.Migration() { + @Override public String getId() { return "001_create_boost_states"; } + @Override public void up(Schema s) throws MigrationException { + s.create(TABLE_STATES, (MigrationBlueprint t) -> t + .ifNotExists() + .string("id", 36) + .primaryKey("id") + .string("active_boost", 100) + .bigInteger("boost_end")); + } + @Override public void down(Schema s) throws MigrationException { + s.dropIfExists(TABLE_STATES); + } + }, + new com.github.ezframework.jaloquent.migration.Migration() { + @Override public String getId() { return "002_create_cooldowns"; } + @Override public void up(Schema s) throws MigrationException { + s.create(TABLE_COOLDOWNS, (MigrationBlueprint t) -> t + .ifNotExists() + .string("id", 200) + .primaryKey("id") + .bigInteger("cooldown_end")); + } + @Override public void down(Schema s) throws MigrationException { + s.dropIfExists(TABLE_COOLDOWNS); + } + }, + new com.github.ezframework.jaloquent.migration.Migration() { + @Override public String getId() { return "003_create_leaderboard"; } + @Override public void up(Schema s) throws MigrationException { + s.create(TABLE_LEADERBOARD, (MigrationBlueprint t) -> t + .ifNotExists() + .string("id", 36) + .primaryKey("id") + .string("player_name", 16) + .integer("total_purchases")); + } + @Override public void down(Schema s) throws MigrationException { + s.dropIfExists(TABLE_LEADERBOARD); + } + } + ); + + try { + new MigrationRunner(store, dialect, migrations).run(); + } catch (MigrationException ex) { + logger.log(Level.SEVERE, + "Failed to run storage migrations — plugin may not persist data correctly", ex); + } + } +} diff --git a/src/main/java/com/skyblockexp/ezboost/storage/StorageSettings.java b/src/main/java/com/skyblockexp/ezboost/storage/StorageSettings.java new file mode 100644 index 0000000..8a6d9b8 --- /dev/null +++ b/src/main/java/com/skyblockexp/ezboost/storage/StorageSettings.java @@ -0,0 +1,39 @@ +package com.skyblockexp.ezboost.storage; + +/** + * Immutable snapshot of the {@code storage:} section of {@code storage.yml}. + */ +public record StorageSettings( + /** Backend type: {@code yaml | sqlite | mysql | mariadb | postgresql | h2} */ + String backend, + + // SQLite / H2 + /** Database file name (relative to plugin data folder); used for sqlite and h2 backends. */ + String dbFile, + + // SQL server backends + String host, + int port, + String database, + String username, + String password, + int poolSize, + + /** When {@code true}, Jaloquent will emit query/save debug logs to the console. */ + boolean debugLogging +) { + /** Default settings (YAML backend, no connection details needed). */ + public static StorageSettings defaults() { + return new StorageSettings( + "yaml", + "ezboost.db", + "localhost", + 3306, + "ezboost", + "root", + "", + 10, + false + ); + } +} diff --git a/src/main/java/com/skyblockexp/ezboost/storage/YamlDataStore.java b/src/main/java/com/skyblockexp/ezboost/storage/YamlDataStore.java index 786688f..8a51786 100644 --- a/src/main/java/com/skyblockexp/ezboost/storage/YamlDataStore.java +++ b/src/main/java/com/skyblockexp/ezboost/storage/YamlDataStore.java @@ -1,10 +1,16 @@ package com.skyblockexp.ezboost.storage; import com.github.ezframework.jaloquent.store.DataStore; +import com.github.ezframework.javaquerybuilder.query.Query; +import com.github.ezframework.javaquerybuilder.query.QueryableStorage; +import com.github.ezframework.javaquerybuilder.query.condition.ConditionEntry; +import com.github.ezframework.javaquerybuilder.query.condition.Connector; import java.io.File; import java.io.IOException; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.logging.Level; @@ -14,9 +20,17 @@ /** * Jaloquent {@link DataStore} backed by a single YAML file. - * Each record is stored as a YAML section keyed by its path (e.g. "leaderboard.uuid"). + * Each record is stored as a YAML section keyed by its storage path (e.g. {@code "boost_states/uuid"}). + * Jaloquent's {@link com.github.ezframework.jaloquent.model.BaseModel#getStoragePath} produces + * paths in the form {@code prefix + "/" + id}, so the YAML file has a flat top-level structure + * where each key is the full storage path of a record. + * + *

Implements {@link QueryableStorage} so that {@link com.github.ezframework.jaloquent.model.ModelRepository#query} + * works without a SQL backend. The {@link #query(Query)} method scans all flat keys in the YAML + * file, evaluates the query's conditions in Java, applies ordering and limit, and returns + * the matching record IDs (the portion of the key after the last {@code /}). */ -public final class YamlDataStore implements DataStore { +public final class YamlDataStore implements DataStore, QueryableStorage { private final File file; private final Logger logger; @@ -68,6 +82,88 @@ public boolean exists(String path) { return yaml.isConfigurationSection(path) || yaml.contains(path); } + // ── QueryableStorage ────────────────────────────────────────────────────── + + /** + * Scan every record in the YAML file (top-level keys have the form {@code prefix/id}), + * apply the query's conditions in Java, sort and limit, then return the matching + * record IDs (the portion of the key after the last {@code /}). + * + *

The returned IDs are handed to {@link com.github.ezframework.jaloquent.model.ModelRepository} + * which calls {@code find(id)} for each, reconstructing the model from storage. + */ + @Override + public List query(Query query) { + // Each top-level key is a storage path of the form "prefix/id". + record Entry(String id, Map data) {} + List candidates = new ArrayList<>(); + + for (String storageKey : yaml.getKeys(false)) { + ConfigurationSection section = yaml.getConfigurationSection(storageKey); + if (section == null) continue; + // Extract the record ID (everything after the last '/') + int slashIdx = storageKey.lastIndexOf('/'); + String id = slashIdx >= 0 ? storageKey.substring(slashIdx + 1) : storageKey; + Map data = new LinkedHashMap<>(); + for (String k : section.getKeys(false)) { + data.put(k, section.get(k)); + } + candidates.add(new Entry(id, data)); + } + + // Apply WHERE conditions using Jaloquent's Condition.matches() + List whereEntries = query.getConditions(); + List filtered = new ArrayList<>(); + outer: + for (Entry e : candidates) { + if (whereEntries == null || whereEntries.isEmpty()) { + filtered.add(e); + continue; + } + boolean result = true; + for (ConditionEntry ce : whereEntries) { + boolean matches = ce.getCondition().matches(e.data(), ce.getColumn()); + if (ce.getConnector() == Connector.OR) { + result = result || matches; + } else { // AND + result = result && matches; + if (!result) continue outer; + } + } + if (result) filtered.add(e); + } + + // Apply ORDER BY (single column, first entry wins) + List orderCols = query.getOrderBy(); + List orderAsc = query.getOrderByAsc(); + if (orderCols != null && !orderCols.isEmpty()) { + String col = orderCols.get(0); + boolean asc = (orderAsc == null || orderAsc.isEmpty()) || Boolean.TRUE.equals(orderAsc.get(0)); + Comparator cmp = Comparator.comparing( + e -> toComparable(e.data().get(col)), + Comparator.nullsFirst(Comparator.naturalOrder())); + filtered.sort(asc ? cmp : cmp.reversed()); + } + + // Apply LIMIT + Integer limit = query.getLimit(); + List limited = (limit != null && limit > 0 && filtered.size() > limit) + ? filtered.subList(0, limit) + : filtered; + + // Return record IDs (not full storage paths) + List result = new ArrayList<>(limited.size()); + for (Entry e : limited) result.add(e.id()); + return result; + } + + @SuppressWarnings("unchecked") + private static Comparable toComparable(Object value) { + if (value == null) return null; + if (value instanceof Comparable) return (Comparable) value; + return (Comparable) (Object) value.toString(); + } + private void persist() { try { file.getParentFile().mkdirs(); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 6cd74a6..d0813ba 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: EzBoost version: ${project.version} main: com.skyblockexp.ezboost.EzBoostPlugin -api-version: '26.1.2' +api-version: '1.13' folia-supported: true softdepend: [Vault, EzEconomy, WorldGuard, EzShops] author: SkyblockExperience diff --git a/src/main/resources/storage.yml b/src/main/resources/storage.yml new file mode 100644 index 0000000..b506e0a --- /dev/null +++ b/src/main/resources/storage.yml @@ -0,0 +1,48 @@ +# EzBoost Storage Configuration +# Controls which database backend is used to persist player boost state and cooldowns. +# Storage is powered by Jaloquent: https://github.com/EzFramework/Jaloquent +# +# Available backends: +# yaml - Flat YAML files in the plugin data folder (default, no setup required) +# sqlite - SQLite database file (no server required, single-file DB) +# mysql - MySQL 5.7+ / MySQL 8 +# mariadb - MariaDB 10.3+ +# postgresql - PostgreSQL 12+ +# h2 - H2 embedded database (primarily for testing) +# +# For MySQL, MariaDB and PostgreSQL the JDBC driver JAR must be present on the server +# classpath (e.g. drop the driver JAR into the server root or /lib folder). + +storage: + backend: yaml + + # Set to true to enable verbose Jaloquent query/save logs in the console. + # Useful for debugging storage issues; keep false in production. + debug-logging: false + + # ── SQLite ────────────────────────────────────────────────────────────────── + # File path relative to the EzBoost plugin data folder. + sqlite: + file: ezboost.db + + # ── MySQL / MariaDB ───────────────────────────────────────────────────────── + mysql: + host: localhost + port: 3306 + database: ezboost + username: root + password: "" + pool-size: 10 + + # ── PostgreSQL ─────────────────────────────────────────────────────────────── + postgresql: + host: localhost + port: 5432 + database: ezboost + username: postgres + password: "" + pool-size: 10 + + # ── H2 (embedded, mainly for testing) ─────────────────────────────────────── + h2: + file: ezboost diff --git a/src/test/java/com/skyblockexp/ezboost/boost/BoostManagerStorageTest.java b/src/test/java/com/skyblockexp/ezboost/boost/BoostManagerStorageTest.java new file mode 100644 index 0000000..e475962 --- /dev/null +++ b/src/test/java/com/skyblockexp/ezboost/boost/BoostManagerStorageTest.java @@ -0,0 +1,57 @@ +package com.skyblockexp.ezboost.boost; + +import com.skyblockexp.ezboost.config.Messages; +import com.skyblockexp.ezboost.economy.EconomyService; +import com.skyblockexp.ezboost.storage.EzBoostRepository; +import com.skyblockexp.ezboost.storage.PlayerBoostStateRecord; +import com.skyblockexp.ezboost.storage.PlayerCooldownRecord; +import com.skyblockexp.ezboost.storage.YamlDataStore; +import com.github.ezframework.jaloquent.model.ModelRepository; +import org.bukkit.plugin.java.JavaPlugin; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class BoostManagerStorageTest { + + @TempDir + Path tempDir; + + private EzBoostRepository buildRepo() { + Logger logger = Logger.getLogger("test"); + YamlDataStore store = new YamlDataStore(tempDir.resolve("data.yml").toFile(), logger); + ModelRepository stateRepo = + new ModelRepository<>(store, "boost_states", PlayerBoostStateRecord.FACTORY); + ModelRepository cooldownRepo = + new ModelRepository<>(store, "cooldowns", PlayerCooldownRecord.FACTORY); + return new EzBoostRepository(stateRepo, cooldownRepo, logger); + } + + @Test + public void constructor_acceptsEzBoostRepository() { + JavaPlugin plugin = mock(JavaPlugin.class); + when(plugin.getLogger()).thenReturn(Logger.getLogger("test")); + Messages messages = mock(Messages.class); + EconomyService economy = mock(EconomyService.class); + EzBoostRepository storage = buildRepo(); + + BoostManager bm = new BoostManager(plugin, null, messages, economy, storage); + assertNotNull(bm); + } + + @Test + public void constructor_nullStorage_throwsNullPointerException() { + JavaPlugin plugin = mock(JavaPlugin.class); + when(plugin.getLogger()).thenReturn(Logger.getLogger("test")); + Messages messages = mock(Messages.class); + EconomyService economy = mock(EconomyService.class); + + assertThrows(NullPointerException.class, + () -> new BoostManager(plugin, null, messages, economy, null)); + } +} diff --git a/src/test/java/com/skyblockexp/ezboost/storage/EzBoostRepositoryTest.java b/src/test/java/com/skyblockexp/ezboost/storage/EzBoostRepositoryTest.java new file mode 100644 index 0000000..ccc7656 --- /dev/null +++ b/src/test/java/com/skyblockexp/ezboost/storage/EzBoostRepositoryTest.java @@ -0,0 +1,168 @@ +package com.skyblockexp.ezboost.storage; + +import com.github.ezframework.jaloquent.model.ModelRepository; +import com.skyblockexp.ezboost.boost.BoostState; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; + +public class EzBoostRepositoryTest { + + @TempDir + Path tempDir; + + private EzBoostRepository repo; + + @BeforeEach + public void setUp() { + Logger logger = Logger.getLogger("test"); + YamlDataStore store = new YamlDataStore(tempDir.resolve("data.yml").toFile(), logger); + ModelRepository stateRepo = + new ModelRepository<>(store, EzBoostRepository.prefixStates(), PlayerBoostStateRecord.FACTORY); + ModelRepository cooldownRepo = + new ModelRepository<>(store, EzBoostRepository.prefixCooldowns(), PlayerCooldownRecord.FACTORY); + repo = new EzBoostRepository(stateRepo, cooldownRepo, logger); + } + + // ── Prefix constants ────────────────────────────────────────────────────── + + @Test + public void prefixStates_returnsBoostStates() { + assertEquals("boost_states", EzBoostRepository.prefixStates()); + } + + @Test + public void prefixCooldowns_returnsCooldowns() { + assertEquals("cooldowns", EzBoostRepository.prefixCooldowns()); + } + + // ── Empty store ─────────────────────────────────────────────────────────── + + @Test + public void load_emptyStore_returnsEmptyMap() { + assertTrue(repo.load().isEmpty()); + } + + // ── Active boost round-trip ─────────────────────────────────────────────── + + @Test + public void saveState_and_load_preservesActiveBoost() { + UUID uuid = UUID.randomUUID(); + BoostState state = new BoostState(); + state.setActiveBoost("speed", System.currentTimeMillis() + 60_000L); + + repo.saveState(uuid, state); + Map loaded = repo.load(); + + assertTrue(loaded.containsKey(uuid)); + assertEquals("speed", loaded.get(uuid).activeBoostKey()); + } + + @Test + public void load_expiredBoost_activeKeyIsNull() { + UUID uuid = UUID.randomUUID(); + BoostState state = new BoostState(); + state.setActiveBoost("speed", System.currentTimeMillis() - 1_000L); // already expired + + repo.saveState(uuid, state); + Map loaded = repo.load(); + + // UUID may appear with an empty BoostState but must not have an active boost key + BoostState ls = loaded.get(uuid); + assertTrue(ls == null || ls.activeBoostKey() == null); + } + + // ── Cooldown round-trip ─────────────────────────────────────────────────── + + @Test + public void saveState_and_load_preservesCooldown() { + UUID uuid = UUID.randomUUID(); + BoostState state = new BoostState(); + state.setCooldownEnd("jump", System.currentTimeMillis() + 30_000L); + + repo.saveState(uuid, state); + Map loaded = repo.load(); + + assertTrue(loaded.containsKey(uuid)); + assertTrue(loaded.get(uuid).cooldownEnd("jump") > System.currentTimeMillis()); + } + + @Test + public void saveState_expiredCooldown_notStoredAndNotLoaded() { + UUID uuid = UUID.randomUUID(); + BoostState state = new BoostState(); + state.setCooldownEnd("jump", System.currentTimeMillis() - 1_000L); // already expired + + repo.saveState(uuid, state); + Map loaded = repo.load(); + + BoostState ls = loaded.get(uuid); + assertTrue(ls == null || ls.cooldownEnd("jump") == 0L); + } + + // ── save (full map) ─────────────────────────────────────────────────────── + + @Test + public void save_fullStateMap_persistsAllPlayers() { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + + BoostState s1 = new BoostState(); + s1.setActiveBoost("speed", System.currentTimeMillis() + 60_000L); + + BoostState s2 = new BoostState(); + s2.setCooldownEnd("jump", System.currentTimeMillis() + 30_000L); + + repo.save(Map.of(uuid1, s1, uuid2, s2)); + Map loaded = repo.load(); + + assertEquals("speed", loaded.get(uuid1).activeBoostKey()); + assertTrue(loaded.get(uuid2).cooldownEnd("jump") > 0L); + } + + // ── deletePlayer ────────────────────────────────────────────────────────── + + @Test + public void deletePlayer_removesBoostStateAndCooldowns() { + UUID uuid = UUID.randomUUID(); + BoostState state = new BoostState(); + state.setActiveBoost("speed", System.currentTimeMillis() + 60_000L); + state.setCooldownEnd("jump", System.currentTimeMillis() + 30_000L); + + repo.saveState(uuid, state); + assertFalse(repo.load().isEmpty()); + + repo.deletePlayer(uuid); + Map loaded = repo.load(); + assertNull(loaded.get(uuid)); + } + + @Test + public void deletePlayer_otherPlayersUnaffected() { + UUID keepUuid = UUID.randomUUID(); + UUID deleteUuid = UUID.randomUUID(); + + BoostState keepState = new BoostState(); + keepState.setActiveBoost("speed", System.currentTimeMillis() + 60_000L); + + BoostState deleteState = new BoostState(); + deleteState.setActiveBoost("jump", System.currentTimeMillis() + 60_000L); + + repo.saveState(keepUuid, keepState); + repo.saveState(deleteUuid, deleteState); + + repo.deletePlayer(deleteUuid); + Map loaded = repo.load(); + + assertNull(loaded.get(deleteUuid)); + assertNotNull(loaded.get(keepUuid)); + assertEquals("speed", loaded.get(keepUuid).activeBoostKey()); + } +} diff --git a/src/test/java/com/skyblockexp/ezboost/storage/PlayerBoostStateRecordTest.java b/src/test/java/com/skyblockexp/ezboost/storage/PlayerBoostStateRecordTest.java new file mode 100644 index 0000000..01268bd --- /dev/null +++ b/src/test/java/com/skyblockexp/ezboost/storage/PlayerBoostStateRecordTest.java @@ -0,0 +1,64 @@ +package com.skyblockexp.ezboost.storage; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class PlayerBoostStateRecordTest { + + private static final String UUID_STR = "550e8400-e29b-41d4-a716-446655440000"; + + @Test + public void constructor_setsId() { + PlayerBoostStateRecord r = new PlayerBoostStateRecord(UUID_STR); + assertEquals(UUID_STR, r.getId()); + } + + @Test + public void activeBoost_defaultIsNull() { + PlayerBoostStateRecord r = new PlayerBoostStateRecord(UUID_STR); + assertNull(r.getActiveBoost()); + } + + @Test + public void activeBoost_roundTrip() { + PlayerBoostStateRecord r = new PlayerBoostStateRecord(UUID_STR); + r.setActiveBoost("speed"); + assertEquals("speed", r.getActiveBoost()); + } + + @Test + public void boostEnd_defaultIsZero() { + PlayerBoostStateRecord r = new PlayerBoostStateRecord(UUID_STR); + assertEquals(0L, r.getBoostEnd()); + } + + @Test + public void boostEnd_roundTrip() { + PlayerBoostStateRecord r = new PlayerBoostStateRecord(UUID_STR); + r.setBoostEnd(9_999_000L); + assertEquals(9_999_000L, r.getBoostEnd()); + } + + @Test + public void boostEnd_handlesIntegerStoredAsNumber() { + // YAML may deserialise long values as Integer when small enough + PlayerBoostStateRecord r = new PlayerBoostStateRecord(UUID_STR); + r.setBoostEnd(42L); + assertEquals(42L, r.getBoostEnd()); + } + + @Test + public void factory_createsRecordFromDataMap() { + Map data = new HashMap<>(); + data.put("active_boost", "jump"); + data.put("boost_end", 12345L); + PlayerBoostStateRecord r = PlayerBoostStateRecord.FACTORY.create(UUID_STR, data); + assertEquals(UUID_STR, r.getId()); + assertEquals("jump", r.getActiveBoost()); + assertEquals(12345L, r.getBoostEnd()); + } +} diff --git a/src/test/java/com/skyblockexp/ezboost/storage/PlayerCooldownRecordTest.java b/src/test/java/com/skyblockexp/ezboost/storage/PlayerCooldownRecordTest.java new file mode 100644 index 0000000..0284e64 --- /dev/null +++ b/src/test/java/com/skyblockexp/ezboost/storage/PlayerCooldownRecordTest.java @@ -0,0 +1,64 @@ +package com.skyblockexp.ezboost.storage; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class PlayerCooldownRecordTest { + + private static final UUID PLAYER_UUID = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"); + private static final String BOOST_KEY = "speed_boost"; + + @Test + public void makeId_formatsAsUuidColonKey() { + String id = PlayerCooldownRecord.makeId(PLAYER_UUID, BOOST_KEY); + assertEquals(PLAYER_UUID.toString() + ":" + BOOST_KEY, id); + } + + @Test + public void extractUUID_roundTrips() { + String id = PlayerCooldownRecord.makeId(PLAYER_UUID, BOOST_KEY); + assertEquals(PLAYER_UUID, PlayerCooldownRecord.extractUUID(id)); + } + + @Test + public void extractBoostKey_roundTrips() { + String id = PlayerCooldownRecord.makeId(PLAYER_UUID, BOOST_KEY); + assertEquals(BOOST_KEY, PlayerCooldownRecord.extractBoostKey(id)); + } + + @Test + public void extractBoostKey_withColonInKey() { + // Boost keys that themselves contain ':' should still be preserved in full + String key = "super:boost"; + String id = PlayerCooldownRecord.makeId(PLAYER_UUID, key); + assertEquals(key, PlayerCooldownRecord.extractBoostKey(id)); + } + + @Test + public void cooldownEnd_defaultIsZero() { + PlayerCooldownRecord r = new PlayerCooldownRecord("x"); + assertEquals(0L, r.getCooldownEnd()); + } + + @Test + public void cooldownEnd_roundTrip() { + PlayerCooldownRecord r = new PlayerCooldownRecord("x"); + r.setCooldownEnd(77_777L); + assertEquals(77_777L, r.getCooldownEnd()); + } + + @Test + public void factory_createsRecordFromDataMap() { + String id = PlayerCooldownRecord.makeId(PLAYER_UUID, BOOST_KEY); + Map data = new HashMap<>(); + data.put("cooldown_end", 54321L); + PlayerCooldownRecord r = PlayerCooldownRecord.FACTORY.create(id, data); + assertEquals(id, r.getId()); + assertEquals(54321L, r.getCooldownEnd()); + } +} diff --git a/src/test/java/com/skyblockexp/ezboost/storage/StorageFactoryTest.java b/src/test/java/com/skyblockexp/ezboost/storage/StorageFactoryTest.java new file mode 100644 index 0000000..895b261 --- /dev/null +++ b/src/test/java/com/skyblockexp/ezboost/storage/StorageFactoryTest.java @@ -0,0 +1,88 @@ +package com.skyblockexp.ezboost.storage; + +import com.github.ezframework.jaloquent.model.ModelRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; + +public class StorageFactoryTest { + + private static final Logger LOGGER = Logger.getLogger("test"); + + // ── YAML backend ────────────────────────────────────────────────────────── + + @Test + public void build_yamlBackend_returnsBundleWithNonNullRepos(@TempDir Path tempDir) { + StorageSettings settings = StorageSettings.defaults(); // yaml backend + StorageFactory.StorageBundle bundle = + StorageFactory.build(settings, tempDir.toFile(), LOGGER); + + assertNotNull(bundle); + assertNotNull(bundle.boostRepository()); + assertNotNull(bundle.leaderboardRepo()); + } + + @Test + public void yamlBundle_boostRepository_startsEmpty(@TempDir Path tempDir) { + StorageSettings settings = StorageSettings.defaults(); + StorageFactory.StorageBundle bundle = + StorageFactory.build(settings, tempDir.toFile(), LOGGER); + + assertTrue(bundle.boostRepository().load().isEmpty()); + } + + @Test + public void yamlBundle_leaderboardRepo_isModelRepository(@TempDir Path tempDir) { + StorageSettings settings = StorageSettings.defaults(); + StorageFactory.StorageBundle bundle = + StorageFactory.build(settings, tempDir.toFile(), LOGGER); + + assertInstanceOf(ModelRepository.class, bundle.leaderboardRepo()); + } + + // ── Prefix constants ────────────────────────────────────────────────────── + + @Test + public void prefixStates_matchesEzBoostRepositoryConstant() { + assertEquals(EzBoostRepository.prefixStates(), StorageFactory.PREFIX_STATES); + } + + @Test + public void prefixCooldowns_matchesEzBoostRepositoryConstant() { + assertEquals(EzBoostRepository.prefixCooldowns(), StorageFactory.PREFIX_COOLDOWNS); + } + + @Test + public void prefixLeaderboard_isLeaderboard() { + assertEquals("leaderboard", StorageFactory.PREFIX_LEADERBOARD); + } + + // ── Unknown backend ─────────────────────────────────────────────────────── + + @Test + public void build_unknownBackend_throwsIllegalArgumentException(@TempDir Path tempDir) { + // "oracle" is not a valid backend — the switch default case must throw + StorageSettings settings = new StorageSettings( + "oracle", "x.db", "localhost", 1521, "db", "user", "", 5, false); + assertThrows(IllegalArgumentException.class, + () -> StorageFactory.build(settings, tempDir.toFile(), LOGGER)); + } + + // ── SQLite backend ──────────────────────────────────────────────────────── + + @Test + public void build_sqliteBackend_returnsBundleWithNonNullRepos(@TempDir Path tempDir) { + StorageSettings settings = new StorageSettings( + "sqlite", "test.db", "localhost", 3306, "ezboost", "", "", 5, false); + StorageFactory.StorageBundle bundle = + StorageFactory.build(settings, tempDir.toFile(), LOGGER); + + assertNotNull(bundle); + assertNotNull(bundle.boostRepository()); + assertNotNull(bundle.leaderboardRepo()); + } +} diff --git a/src/test/java/com/skyblockexp/ezboost/storage/StorageSettingsTest.java b/src/test/java/com/skyblockexp/ezboost/storage/StorageSettingsTest.java new file mode 100644 index 0000000..de71ea0 --- /dev/null +++ b/src/test/java/com/skyblockexp/ezboost/storage/StorageSettingsTest.java @@ -0,0 +1,35 @@ +package com.skyblockexp.ezboost.storage; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class StorageSettingsTest { + + @Test + public void defaults_returnExpectedValues() { + StorageSettings s = StorageSettings.defaults(); + assertEquals("yaml", s.backend()); + assertEquals("ezboost.db", s.dbFile()); + assertEquals("localhost", s.host()); + assertEquals(3306, s.port()); + assertEquals("ezboost", s.database()); + assertNotNull(s.username()); + assertNotNull(s.password()); + assertEquals(10, s.poolSize()); + } + + @Test + public void constructor_storesAllFields() { + StorageSettings s = new StorageSettings( + "sqlite", "my.db", "db.host", 5432, "mydb", "admin", "pass", 5, false); + assertEquals("sqlite", s.backend()); + assertEquals("my.db", s.dbFile()); + assertEquals("db.host", s.host()); + assertEquals(5432, s.port()); + assertEquals("mydb", s.database()); + assertEquals("admin", s.username()); + assertEquals("pass", s.password()); + assertEquals(5, s.poolSize()); + } +} diff --git a/src/test/java/com/skyblockexp/ezboost/storage/YamlDataStoreTest.java b/src/test/java/com/skyblockexp/ezboost/storage/YamlDataStoreTest.java new file mode 100644 index 0000000..d076b7d --- /dev/null +++ b/src/test/java/com/skyblockexp/ezboost/storage/YamlDataStoreTest.java @@ -0,0 +1,141 @@ +package com.skyblockexp.ezboost.storage; + +import com.github.ezframework.jaloquent.model.Model; +import com.github.ezframework.javaquerybuilder.query.Query; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; + +public class YamlDataStoreTest { + + @TempDir + Path tempDir; + + private YamlDataStore store; + + @BeforeEach + public void setUp() { + store = new YamlDataStore(tempDir.resolve("data.yml").toFile(), Logger.getLogger("test")); + } + + // ── save / load ─────────────────────────────────────────────────────────── + + @Test + public void save_and_load_roundTrip() { + store.save("leaderboard.abc123", Map.of("player_name", "Alice", "total_purchases", 5)); + Optional> loaded = store.load("leaderboard.abc123"); + assertTrue(loaded.isPresent()); + assertEquals("Alice", loaded.get().get("player_name")); + assertEquals(5, loaded.get().get("total_purchases")); + } + + @Test + public void load_missingPath_returnsEmpty() { + assertTrue(store.load("nonexistent.id").isEmpty()); + } + + // ── exists / delete ─────────────────────────────────────────────────────── + + @Test + public void exists_returnsFalseBeforeSave_trueAfter() { + assertFalse(store.exists("boost_states.player1")); + store.save("boost_states.player1", Map.of("active_boost", "speed")); + assertTrue(store.exists("boost_states.player1")); + } + + @Test + public void delete_removesEntry() { + store.save("boost_states.player1", Map.of("active_boost", "speed")); + assertTrue(store.exists("boost_states.player1")); + store.delete("boost_states.player1"); + assertFalse(store.exists("boost_states.player1")); + assertTrue(store.load("boost_states.player1").isEmpty()); + } + + // ── query ───────────────────────────────────────────────────────────────── + + @Test + public void query_emptyStore_returnsEmpty() { + List paths = store.query(Model.queryBuilder().build()); + assertTrue(paths.isEmpty()); + } + + @Test + public void query_noConditions_returnsAllRecords() { + store.save("boost_states/uuid1", Map.of("active_boost", "speed", "boost_end", 1000L)); + store.save("boost_states/uuid2", Map.of("active_boost", "jump", "boost_end", 2000L)); + store.save("boost_states/uuid3", Map.of("active_boost", "strength", "boost_end", 3000L)); + + List ids = store.query(Model.queryBuilder().build()); + assertEquals(3, ids.size()); + } + + @Test + public void query_returnsIds() { + store.save("leaderboard/p1", Map.of("player_name", "Alice", "total_purchases", 1)); + List ids = store.query(Model.queryBuilder().build()); + assertEquals(1, ids.size()); + assertEquals("p1", ids.get(0)); + } + + @Test + public void query_withOrderByDescending_sortsByField() { + store.save("leaderboard/p1", Map.of("player_name", "Alice", "total_purchases", 3)); + store.save("leaderboard/p2", Map.of("player_name", "Bob", "total_purchases", 10)); + store.save("leaderboard/p3", Map.of("player_name", "Carol", "total_purchases", 1)); + + Query q = Model.queryBuilder().orderBy("total_purchases", false).build(); + List ids = store.query(q); + + assertEquals(3, ids.size()); + // Bob has the highest total_purchases — must come first with descending order + assertEquals("p2", ids.get(0)); + } + + @Test + public void query_withOrderByAscending_sortsByField() { + store.save("leaderboard/p1", Map.of("player_name", "Alice", "total_purchases", 3)); + store.save("leaderboard/p2", Map.of("player_name", "Bob", "total_purchases", 10)); + store.save("leaderboard/p3", Map.of("player_name", "Carol", "total_purchases", 1)); + + Query q = Model.queryBuilder().orderBy("total_purchases", true).build(); + List ids = store.query(q); + + assertEquals(3, ids.size()); + // Carol has the lowest total_purchases — must come first with ascending order + assertEquals("p3", ids.get(0)); + } + + // ── non-existent file ───────────────────────────────────────────────────── + + @Test + public void nonexistentFile_createsEmptyStore() { + YamlDataStore fresh = new YamlDataStore( + tempDir.resolve("missing.yml").toFile(), Logger.getLogger("test")); + assertTrue(fresh.query(Model.queryBuilder().build()).isEmpty()); + assertFalse(fresh.exists("any.key")); + } + + // ── cross-instance persistence ──────────────────────────────────────────── + + @Test + public void dataPersistedToDisk_isReadableBySecondInstance() { + store.save("leaderboard.p1", Map.of("player_name", "Alice", "total_purchases", 5)); + + // Second instance pointing at the same file + YamlDataStore store2 = new YamlDataStore( + tempDir.resolve("data.yml").toFile(), Logger.getLogger("test")); + Optional> loaded = store2.load("leaderboard.p1"); + assertTrue(loaded.isPresent()); + assertEquals("Alice", loaded.get().get("player_name")); + } +}