diff --git a/CHANGELOG.md b/CHANGELOG.md index ddd6dad..e08446a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,52 @@ Release tags use the `v` prefix (e.g. `v3.0.2`). --- +## [3.4.0] - 2026-05-24 + +### Added + +- **EzCountdown integration** (optional soft-dependency): + - RTP countdown display can now be delegated to [EzCountdown](https://modrinth.com/plugin/ezcountdown) for richer, configurable display channels. + - Configurable display types per RTP world: `ACTION_BAR`, `BOSS_BAR`, `TITLE`, `CHAT`, `SCOREBOARD`, `DIALOG`. + - Configurable MiniMessage format string with `{formatted}` placeholder for remaining time. + - Each teleporting player receives an ephemeral, per-player countdown with permission-scoped visibility. + - Falls back to the built-in bossbar/chat countdown automatically when EzCountdown is absent or `ezcountdown.enabled: false`. + - New `countdown.ezcountdown.*` config section in `rtp.yml` (disabled by default). +- `EzCountdown` added as optional soft-dependency in `plugin.yml`. +- **Factions RTP (TeamsAPI)**: + - Added `/rtp faction` to open a claim-selection GUI from all TeamsAPI claims available to the player's team. + - Added TeamsAPI subcommand integration for `/f rtp` via the TeamsAPI subcommand API (when provided by the installed TeamsAPI version). + - Claim selection now sets the selected claim chunk as RTP center and then applies normal per-world RTP behavior/settings. +- **New `faction-gui.yml` file** for full claim-GUI configuration. +- **Heatmap claim overlays** (admin insight): + - Added claim border overlay mode for `/rtp heatmap` and `/rtp heatmap save` via `claims-overlay` flag. + - Added `rtp.yml` options under `heatmap.claims-overlay.*`: + - `enabled` + - `style` (`border`) + - `color` + - `line-width` +- **Claim-constrained fake RTP simulation**: + - Added `/rtp fake claims [world]` to generate simulated points on faction claims owned by the executor’s team. + +### Changed + +- **TeamsAPI** dependency bumped from `1.4.1` to `1.8.0`. +- TeamsAPI `/f rtp` subcommand integration rewritten: replaced the reflection-based `Proxy` approach with a proper `AbstractTeamsSubcommand` subclass (requires TeamsAPI ≥ 1.8.0). +- TeamsAPI subcommand registration now uses an explicit `isPluginEnabled("TeamsAPI")` guard instead of relying on `NoClassDefFoundError` suppression. +- Faction claim GUI icons now attempt to use player skulls when claimant/owner identity is available, with configurable fallback material when unavailable. +- Added configurable title, size, claim item format/lore, skull toggle, and navigation slot/name settings for faction GUI pages. +- Update checker refactored to use `mc-plugin-update-notifier` with Modrinth as primary source and GitHub Releases as fallback source. + +### Fixed + +- Heatmap enablement now resolves correctly for inherited/fallback world settings (`heatmap.enabled`) and no longer reads as disabled unexpectedly in world/GUI override paths. +- `/rtp heatmap` no longer hard-requires biome cache for non-biome heatmaps. Biome cache is only required for biome-filtered heatmap requests. +- Spigot startup compatibility fixed in update checking (`JavaPlugin#getDescription().getVersion()` used instead of Paper-only metadata calls). +- Message loading now supports both top-level keys and nested `messages.*` language-file layouts. +- Language file handling is now non-destructive: startup repair only targets clearly corrupted message files, with automatic backfill of missing message keys while preserving existing translations/customizations. + +--- + ## [3.3.0] - 2026-05-16 ### Added diff --git a/README.md b/README.md index d50284f..d2cc0a1 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,9 @@ It is designed for **safety-first teleportation**, **cross-platform compatibilit - **Queue system** to smooth heavy RTP usage on busy servers. - **Biome-aware and cache-assisted searching** with optional rare-biome optimization. - **Heatmap/statistics tooling** for operators to inspect RTP distribution and performance. +- **Release update notifier** using free APIs with Modrinth primary and GitHub Releases fallback. - **WorldGuard region command mode** for region-scoped RTP entry points. -- **TeamsAPI integration** to skip chunk-claimed areas (works with any TeamsAPI-compatible team plugin). +- **TeamsAPI integration** to skip chunk-claimed areas and power faction-claim RTP selection (works with any TeamsAPI-compatible team plugin). - **Optional first-join/on-join teleport flow**. - **Optional proxy/network destination menu support** for multi-server setups. - **Chunky integration** for pre-generation workflows. @@ -95,6 +96,8 @@ It is designed for **safety-first teleportation**, **cross-platform compatibilit ### Player commands - `/rtp` - Random teleport (or opens GUI when enabled). +- `/rtp faction` - Open faction/team claim selection GUI (TeamsAPI), then RTP around the selected claim center. +- `/f rtp` - TeamsAPI faction subcommand route to the same claim selection GUI. ### Admin / utility subcommands @@ -102,6 +105,8 @@ It is designed for **safety-first teleportation**, **cross-platform compatibilit - `/rtp stats` - Show RTP statistics and performance details. - `/rtp heatmap` - View heatmap information. - `/rtp fake [world]` - Inject/clear simulated heatmap points. +- `/rtp heatmap claims-overlay` - Render heatmap with TeamsAPI claim chunk borders (admin-scoped). +- `/rtp fake claims [world]` - Inject simulated heatmap points constrained to your faction claims. - `/rtp setcenter ` or `/rtp setcenter ` - Update RTP center. - `/rtp pregenerate [world] [radius]` - Trigger Chunky-assisted pre-generation workflow. - `/forcertp [world]` - Force teleport a target player. @@ -118,6 +123,7 @@ It is designed for **safety-first teleportation**, **cross-platform compatibilit - `ezrtp.stats` - Access `/rtp stats`. - `ezrtp.heatmap` - Access `/rtp heatmap`. - `ezrtp.heatmap.fake` - Access `/rtp fake`. +- `ezrtp.heatmap.claims` - Access claim overlay rendering on heatmaps. - `ezrtp.queue.bypass` - Bypass queue restrictions. ### Permissions Reference @@ -152,9 +158,12 @@ EzRTP splits configuration into focused files for maintainability: - `storage.yml` - Usage/cooldown backend (YAML/MySQL). - `queue.yml` - Queue throttling behavior. - `gui.yml` - GUI menu layout, world entries, and icons. +- `faction-gui.yml` - Faction claim GUI layout/icons/navigation for `/rtp faction` and `/f rtp`. - `network.yml` - Proxy/server destination entries. - `force-rtp.yml` - `/forcertp` command behavior. - `messages/*.yml` - Localized messages. + - Supports both top-level keys and `messages.*` nested layout. + - Missing keys are backfilled automatically on startup without overwriting existing translations. --- @@ -210,6 +219,7 @@ Configuration and message references in this repository: - Limits and cooldowns: [`limits.yml`](src/main/resources/limits.yml) - Queue settings: [`queue.yml`](src/main/resources/queue.yml) - GUI settings: [`gui.yml`](src/main/resources/gui.yml) +- Faction GUI settings: [`faction-gui.yml`](src/main/resources/faction-gui.yml) - Network/proxy settings: [`network.yml`](src/main/resources/network.yml) - Storage backend config: [`storage.yml`](src/main/resources/storage.yml) - Force RTP behavior: [`force-rtp.yml`](src/main/resources/force-rtp.yml) @@ -220,6 +230,7 @@ Configuration documentation files: - Main config documentation: [`docs/config/config.md`](docs/config/config.md) - Core configuration reference (`config.yml`, `rtp.yml`, `limits.yml`, `storage.yml`, `force-rtp.yml`): [`docs/config-core-reference.md`](docs/config-core-reference.md) +- Faction GUI config reference: [`docs/config/faction-gui.md`](docs/config/faction-gui.md) - GUI/queue/network reference (`gui.yml`, `queue.yml`, `network.yml`): [`docs/config-gui-queue-network-reference.md`](docs/config-gui-queue-network-reference.md) - Integration docs: diff --git a/docs/commands.md b/docs/commands.md index 2df1b88..e614da4 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -10,6 +10,12 @@ nav_order: 3 - `/rtp` - Opens GUI when GUI is enabled. - Falls back to direct teleport when GUI is disabled or unavailable. +- `/rtp faction` + - Opens a faction/team claim GUI using TeamsAPI claims. + - Selecting a claim uses that claim chunk center and then applies normal configured RTP behavior for that world. + - GUI layout and item rendering are configured in `faction-gui.yml`. +- `/f rtp` + - TeamsAPI subcommand integration that opens the same faction/team claim GUI as `/rtp faction`. - `/rtp ` - Teleports using the named center configured under `centers.named` in `rtp.yml`. - Named center is applied as a center override only; the world's normal RTP settings still apply. @@ -25,8 +31,11 @@ nav_order: 3 - `/rtp stats rare-biomes [page]` - `/rtp heatmap [biome]` - `/rtp heatmap save` +- `/rtp heatmap claims-overlay` +- `/rtp heatmap save claims-overlay` - `/rtp fake [world]` - `/rtp fake clear [world]` +- `/rtp fake claims [world]` - `/rtp setcenter ` - `/rtp setcenter ` - `/rtp addcenter ` @@ -53,8 +62,11 @@ This centers RTP around the specified WorldGuard region and can apply per-region - Stats: `/rtp stats` - Biome stats: `/rtp stats biomes` - Heatmap map item: `/rtp heatmap` +- Heatmap map + claim overlay: `/rtp heatmap claims-overlay` - Save heatmap image: `/rtp heatmap save` +- Save heatmap with claim overlay: `/rtp heatmap save claims-overlay` - Add fake points: `/rtp fake 100 world` +- Add fake points on your faction claims: `/rtp fake 100 claims` - Clear fake points: `/rtp fake clear world` - Set RTP center (current world): `/rtp setcenter 0 0` - Save a named center from your current position: `/rtp addcenter spawn` diff --git a/docs/config-core-reference.md b/docs/config-core-reference.md index 56f8df3..5c495ad 100644 --- a/docs/config-core-reference.md +++ b/docs/config-core-reference.md @@ -45,6 +45,12 @@ This file documents `config.yml` and `force-rtp.yml`. - `countdown.particles.secondary-offset` - `countdown.chat-messages` +### EzCountdown integration + +- `countdown.ezcountdown.enabled` +- `countdown.ezcountdown.display-types` +- `countdown.ezcountdown.format` + ### Safety - `unsafe-blocks` diff --git a/docs/config/faction-gui.md b/docs/config/faction-gui.md new file mode 100644 index 0000000..82db7f5 --- /dev/null +++ b/docs/config/faction-gui.md @@ -0,0 +1,44 @@ +--- +title: faction-gui.yml +nav_order: 5 +parent: Config Reference +--- + +# faction-gui.yml + +Configures the faction/team claim selection GUI used by `/rtp faction` and `/f rtp`. + +## Top-level settings + +| Key | Default | Description | +| :--- | :--- | :--- | +| `enabled` | `true` | Enables faction claim GUI routing. | +| `title` | `Faction RTP Claims (/)` | GUI title. Supports `` and ``. | +| `size` | `54` | Inventory size (multiple of 9, up to 54). | + +## Claim item settings + +| Key | Default | Description | +| :--- | :--- | :--- | +| `items.claim.use-player-skulls` | `true` | Uses player skulls for claim owner/claimer when identity is available. | +| `items.claim.fallback-material` | `GRASS_BLOCK` | Used when skull identity is unavailable. | +| `items.claim.name` | `Claim #` | Item display name. | +| `items.claim.lore` | *(see default file)* | Lore lines with placeholders. | + +Supported placeholders: + +- `` +- `` +- `` +- `` +- `` +- `` + +## Navigation settings + +| Key | Default | Description | +| :--- | :--- | :--- | +| `navigation.previous.slot` | `45` | Previous page button slot. | +| `navigation.previous.name` | `Previous Page` | Previous page button name. | +| `navigation.next.slot` | `53` | Next page button slot. | +| `navigation.next.name` | `Next Page` | Next page button name. | diff --git a/docs/config/index.md b/docs/config/index.md index 75d8eec..768ddfd 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -8,3 +8,18 @@ has_children: true EzRTP uses a set of focused configuration files. Browse the pages below for full details on each file. + +- `config.yml` +- `rtp.yml` +- `limits.yml` +- `storage.yml` +- `gui.yml` +- `faction-gui.yml` +- `queue.yml` +- `network.yml` +- `messages/*.yml` (top-level keys and nested `messages.*` keys are both supported) + +Notes: + +- Missing message keys in language files are backfilled automatically on startup. +- Existing translated/customized message values are preserved during backfill. diff --git a/docs/config/rtp.md b/docs/config/rtp.md index c5e187f..c479ac4 100644 --- a/docs/config/rtp.md +++ b/docs/config/rtp.md @@ -94,6 +94,27 @@ countdown: chat-messages: false ``` +### EzCountdown display (optional) + +When [EzCountdown](../integrations/ezcountdown) is installed, EzRTP can delegate +the countdown display to it for richer, configurable channels. + +| Key | Default | Description | +| :--- | :--- | :--- | +| `countdown.ezcountdown.enabled` | `false` | Hand countdown display off to EzCountdown. Falls back to the built-in display when `false` or when EzCountdown is absent. | +| `countdown.ezcountdown.display-types` | `[ACTION_BAR, BOSS_BAR]` | Display channels EzCountdown renders. Accepted values: `ACTION_BAR`, `BOSS_BAR`, `TITLE`, `CHAT`, `SCOREBOARD`, `DIALOG`. | +| `countdown.ezcountdown.format` | `Teleporting in {formatted}...` | MiniMessage string shown by each active display channel. `{formatted}` is replaced with the remaining time. | + +```yml +countdown: + ezcountdown: + enabled: false + display-types: + - ACTION_BAR + - BOSS_BAR + format: "Teleporting in {formatted}..." +``` + --- ## Safety diff --git a/docs/integrations/ezcountdown.md b/docs/integrations/ezcountdown.md new file mode 100644 index 0000000..de3e9c3 --- /dev/null +++ b/docs/integrations/ezcountdown.md @@ -0,0 +1,112 @@ +--- +title: EzCountdown +nav_order: 6 +parent: Integrations +--- + +# EzCountdown Integration + +Use this integration when you want to show the RTP countdown using +[EzCountdown](https://modrinth.com/plugin/ezcountdown) display effects instead +of (or alongside) the built-in bossbar. EzCountdown supports more display +channels and richer formatting. + +## What this integration does + +When EzCountdown is installed and the integration is enabled in `rtp.yml`, +EzRTP delegates its per-player countdown display to EzCountdown rather than +rendering a bossbar/chat message itself. Each teleporting player receives an +ephemeral, private countdown that expires when the teleport fires or is +cancelled. + +- Movement detection and PvP cancellation still apply exactly as with the + built-in countdown. +- If EzCountdown is absent or `ezcountdown.enabled: false`, EzRTP falls back to + its own bossbar/chat countdown automatically. +- The integration is activated once per server start when EzCountdown is + detected; a `/rtp reload` restarts the display settings but does not + re-initialize the bridge. + +## Requirements + +| Plugin | Where to get it | +| :--- | :--- | +| [EzCountdown](https://modrinth.com/plugin/ezcountdown) | Modrinth · [GitHub](https://github.com/ez-plugins/EzCountdown) | + +EzCountdown is a soft dependency — servers without it work normally with the +built-in display. + +## Where to configure it + +File: `plugins/EzRTP/rtp.yml`, nested under `countdown:` + +```yml +countdown: + # ... existing built-in countdown settings ... + ezcountdown: + enabled: false + display-types: + - ACTION_BAR + - BOSS_BAR + format: "Teleporting in {formatted}..." +``` + +### Key settings + +- `enabled` + - `true`: hand countdown display off to EzCountdown (built-in display is + skipped for players where EzCountdown starts successfully). + - `false`: use the built-in bossbar/chat display (default). +- `display-types` + - One or more display channels EzCountdown renders simultaneously. + - See [Display types](#display-types) below for all valid values. +- `format` + - MiniMessage string shown by each active display channel. + - `{formatted}` is replaced with the remaining time in human-readable form + (e.g. `5s`, `1m 30s`). + +## Display types + +The following values are accepted in the `display-types` list: + +| Value | Where it appears | +| :--- | :--- | +| `ACTION_BAR` | Above the hotbar | +| `BOSS_BAR` | Bar across the top of the screen | +| `TITLE` | Large centre-screen overlay | +| `CHAT` | Chat messages | +| `SCOREBOARD` | Sidebar scoreboard | +| `DIALOG` | Dialogue box (requires server support) | + +You can combine any number of them. Example showing three at once: + +```yml +display-types: + - ACTION_BAR + - BOSS_BAR + - TITLE +``` + +## Startup log + +When EzCountdown is detected, EzRTP prints one line during enable: + +```text +[EzRTP] EzCountdown integration enabled. +``` + +If EzCountdown is not installed, nothing is printed and the built-in countdown +is used without any configuration change. + +## Interaction with the built-in countdown + +The built-in countdown settings (`countdown.bossbar`, `countdown.chat-messages`, +`countdown.particles`, etc.) remain active for all display that does **not** go +through EzCountdown. When `ezcountdown.enabled: true`: + +- The EzCountdown display replaces the EzRTP bossbar for that player. +- Particle effects (if configured) still run independently. +- Movement cancellation and warn-distance checks still apply. + +If EzCountdown fails to start a countdown for a player for any reason, EzRTP +falls back to its built-in display for that player. diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 2611753..2e8a3f0 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -16,4 +16,5 @@ set up each one. | [Chunky](chunky) | Chunky (world pre-generation) | | [Protection](protection-worldguard-griefprevention) | WorldGuard, GriefPrevention, TeamsAPI | | [PvP Tag](pvp-tag) | CombatLogX, PvPManager, Simple Combat Log | +| [EzCountdown](ezcountdown) | EzCountdown (countdown display) | | [Network / Proxy](network-proxy) | BungeeCord / Velocity proxy destinations | diff --git a/docs/integrations/protection-worldguard-griefprevention.md b/docs/integrations/protection-worldguard-griefprevention.md index 9613335..80e5142 100644 --- a/docs/integrations/protection-worldguard-griefprevention.md +++ b/docs/integrations/protection-worldguard-griefprevention.md @@ -17,6 +17,7 @@ Supported provider names in config: - `worldguard` — WorldGuard protected regions - `griefprevention` — GriefPrevention claims - `teamsapi` — Chunk claims managed by any [TeamsAPI](https://modrinth.com/plugin/teams-api)-compatible team plugin + - Also enables faction RTP claim routing with `/rtp faction` and `/f rtp` (TeamsAPI subcommand API). If a provider plugin is not installed, EzRTP continues with available providers (safe fallback behavior). @@ -63,6 +64,19 @@ EzRTP automatically detects this and avoids claimed chunks during RTP destinatio - Claim availability is re-checked dynamically, so a claim plugin that loads after EzRTP is picked up without requiring a reload. +## TeamsAPI `/f rtp` subcommand + +When TeamsAPI is present, EzRTP registers a `/f rtp` subcommand that opens the +faction claim RTP GUI directly from the factions menu. This requires the +`FactionClaimSelectionGuiManager` to be active (see `gui.yml`). + +```text +[EzRTP] TeamsAPI integration: registered '/f rtp' subcommand. +``` + +This line is logged once at startup when TeamsAPI is detected. If TeamsAPI is not +installed, the subcommand is silently skipped and no error is logged. + ## Optional WorldGuard region command mode You can also enable region-centric command routing: diff --git a/docs/overview-installation.md b/docs/overview-installation.md index cfdaac4..92ea9cb 100644 --- a/docs/overview-installation.md +++ b/docs/overview-installation.md @@ -33,6 +33,9 @@ Optional integrations (soft dependencies): - GriefPrevention - PlaceholderAPI - Chunky +- TeamsAPI +- EzCountdown +- CombatLogX / PvPManager / Simple Combat Log If an integration is missing, EzRTP still works and disables only that integration path. diff --git a/docs/permissions.md b/docs/permissions.md index 6eaec8b..ecf1167 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -14,6 +14,7 @@ nav_order: 4 | `ezrtp.stats` | `op` | View RTP statistics with `/rtp stats` | | `ezrtp.heatmap` | `op` | View/save heatmap with `/rtp heatmap` | | `ezrtp.heatmap.fake` | `op` | Add fake heatmap points with `/rtp fake` | +| `ezrtp.heatmap.claims` | `op` (recommended) | View claim border overlays on heatmaps (`claims-overlay`) | | `ezrtp.queue.bypass` | `op` | Skip the teleport queue | | `ezrtp.forcertp` | `op` | Force-teleport players with `/forcertp` | | `ezrtp.setcenter` | `op` | Set or save a named RTP center | diff --git a/ezrtp-api/pom.xml b/ezrtp-api/pom.xml index 9939a2a..6936506 100644 --- a/ezrtp-api/pom.xml +++ b/ezrtp-api/pom.xml @@ -5,13 +5,13 @@ com.skyblockexp ezrtp-parent - 3.3.0 + 3.4.0 ../pom.xml com.skyblockexp ezrtp-api - 3.3.0 + 3.4.0 jar EzRTP API Lightweight public API for EzRTP intended for third-party plugins. diff --git a/ezrtp-bukkit/pom.xml b/ezrtp-bukkit/pom.xml index f81da51..6c8eb60 100644 --- a/ezrtp-bukkit/pom.xml +++ b/ezrtp-bukkit/pom.xml @@ -5,13 +5,13 @@ com.skyblockexp ezrtp-parent - 3.3.0 + 3.4.0 ../pom.xml com.skyblockexp ezrtp-bukkit - 3.3.0 + 3.4.0 jar EzRTP (Bukkit) EzRTP Bukkit-compatible plugin module. @@ -111,7 +111,7 @@ com.skyblockexp ezrtp-common - 3.3.0 + 3.4.0 @@ -225,6 +225,7 @@ net.kyori:adventure-text-serializer-legacy net.kyori:examination-api net.kyori:examination-string + com.github.ez-plugins:mc-plugin-update-notifier @@ -232,6 +233,10 @@ org.bstats com.skyblockexp.ezrtp.libs.org.bstats + + com.github.ezplugins.updater + com.skyblockexp.ezrtp.libs.updater + diff --git a/ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/PaperStartupSmokeTest.java b/ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/PaperStartupSmokeTest.java new file mode 100644 index 0000000..dae7913 --- /dev/null +++ b/ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/PaperStartupSmokeTest.java @@ -0,0 +1,175 @@ +package com.skyblockexp.ezrtp.platform; + +import com.skyblockexp.ezrtp.EzRtpPlugin; +import com.skyblockexp.ezrtp.gui.FactionClaimSelectionGuiManager; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Smoke tests that validate successful plugin startup on a Paper server. + * + *

These tests assert: + *

    + *
  • The fallback {@link BukkitPlatformScheduler} (used when the Paper module is present but + * Folia is not) correctly delegates to the Bukkit scheduler under Paper capabilities.
  • + *
  • Optional integrations such as TeamsAPI do not cause {@link NoClassDefFoundError} or + * other startup failures when the dependency is absent at runtime.
  • + *
+ * + *

Note: Paper-specific async-chunk and tick-loop APIs are tested in the {@code ezrtp-paper} + * module. This suite covers the common {@link BukkitPlatformScheduler} fallback path that is + * also used by Paper (non-Folia) environments. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class PaperStartupSmokeTest { + + @Mock + private Plugin plugin; + + @Mock + private Server server; + + @Mock + private BukkitScheduler bukkitScheduler; + + @BeforeEach + void setUp() { + when(plugin.getServer()).thenReturn(server); + when(server.getScheduler()).thenReturn(bukkitScheduler); + } + + // --- BukkitPlatformScheduler with Paper (non-Folia) capabilities --- + + @Test + void scheduleRepeating_withPaperCapabilities_delegatesToBukkitScheduler() { + BukkitTask mockTask = mock(BukkitTask.class); + when(bukkitScheduler.runTaskTimer( + any(Plugin.class), any(Runnable.class), anyLong(), anyLong())) + .thenReturn(mockTask); + + BukkitPlatformScheduler scheduler = + new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.PAPER); + + PlatformTask task = assertDoesNotThrow(() -> scheduler.scheduleRepeating(() -> {}, 5L, 20L)); + + assertNotNull(task); + verify(bukkitScheduler) + .runTaskTimer(eq(plugin), any(Runnable.class), eq(5L), eq(20L)); + } + + @Test + void executeAsync_withPaperCapabilities_delegatesToBukkitScheduler() { + BukkitTask mockTask = mock(BukkitTask.class); + when(server.getScheduler().runTaskAsynchronously(any(Plugin.class), any(Runnable.class))) + .thenReturn(mockTask); + + BukkitPlatformScheduler scheduler = + new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.PAPER); + + assertDoesNotThrow(() -> scheduler.executeAsync(() -> {})); + verify(bukkitScheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class)); + } + + @Test + void executeGlobal_withPaperCapabilities_delegatesToBukkitScheduler() { + BukkitTask mockTask = mock(BukkitTask.class); + when(server.getScheduler().runTask(any(Plugin.class), any(Runnable.class))) + .thenReturn(mockTask); + + BukkitPlatformScheduler scheduler = + new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.PAPER); + + assertDoesNotThrow(() -> scheduler.executeGlobal(() -> {})); + verify(bukkitScheduler).runTask(eq(plugin), any(Runnable.class)); + } + + // --- TeamsAPI optionality: FactionClaimSelectionGuiManager on Paper --- + + /** + * Verifies that {@link FactionClaimSelectionGuiManager#openSelection(Player)} returns + * {@code true} and does not throw any exception when the TeamsAPI plugin is not + * installed, using a Paper server context. + * + *

Paper servers are the primary target for plugins that include optional integrations. + * This test documents that the TeamsAPI optionality fix works on Paper servers as well as + * Spigot servers. + */ + @Test + void factionClaimGui_openSelection_withoutTeamsApi_onPaper_returnsGracefully() { + EzRtpPlugin ezPlugin = mock(EzRtpPlugin.class); + Server mockServer = mock(Server.class); + PluginManager mockPm = mock(PluginManager.class); + Player player = mock(Player.class); + + when(ezPlugin.getServer()).thenReturn(mockServer); + when(mockServer.getPluginManager()).thenReturn(mockPm); + when(mockPm.isPluginEnabled("TeamsAPI")).thenReturn(false); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + + FactionClaimSelectionGuiManager guiManager = new FactionClaimSelectionGuiManager( + ezPlugin, + () -> null, + () -> null, + () -> null, + null); + + boolean result = assertDoesNotThrow(() -> guiManager.openSelection(player)); + assertTrue(result, "openSelection should return true when TeamsAPI is not enabled"); + } + + /** + * Verifies that when TeamsAPI is reported as enabled by the plugin manager, the handler + * still returns {@code true} gracefully. In the test JVM TeamsAPI is on the classpath as a + * compile-time dependency, but the {@code TeamsAPI.isAvailable()} static check returns + * {@code false} because no server-side plugin has initialised it, so {@link + * FactionClaimSelectionGuiManager#openSelection(Player)} exits cleanly after the fetch guard. + */ + @Test + void factionClaimGui_openSelection_withTeamsApiPluginEnabled_butServiceUnavailable_returnsGracefully() { + EzRtpPlugin ezPlugin = mock(EzRtpPlugin.class); + Server mockServer = mock(Server.class); + PluginManager mockPm = mock(PluginManager.class); + Player player = mock(Player.class); + + when(ezPlugin.getServer()).thenReturn(mockServer); + when(mockServer.getPluginManager()).thenReturn(mockPm); + when(mockPm.isPluginEnabled("TeamsAPI")).thenReturn(true); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + + FactionClaimSelectionGuiManager guiManager = new FactionClaimSelectionGuiManager( + ezPlugin, + () -> null, + () -> null, + () -> null, + null); + + // TeamsAPI.isAvailable() returns false in the test JVM (no live plugin), + // so TeamsApiClaimFetcher.fetch() returns Optional.empty() and openSelection returns true. + boolean result = assertDoesNotThrow(() -> guiManager.openSelection(player)); + assertTrue(result); + } +} diff --git a/ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/SpigotStartupSmokeTest.java b/ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/SpigotStartupSmokeTest.java new file mode 100644 index 0000000..a305b27 --- /dev/null +++ b/ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/SpigotStartupSmokeTest.java @@ -0,0 +1,193 @@ +package com.skyblockexp.ezrtp.platform; + +import com.skyblockexp.ezrtp.EzRtpPlugin; +import com.skyblockexp.ezrtp.gui.FactionClaimSelectionGuiManager; +import com.skyblockexp.ezrtp.integration.TeamsApiSubcommandBridge; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Smoke tests that validate successful plugin startup on a Spigot server. + * + *

These tests assert: + *

    + *
  • The fallback {@link BukkitPlatformScheduler} (used when no Paper module is present) + * correctly delegates to the Bukkit scheduler under standard Spigot capabilities.
  • + *
  • Optional integrations such as TeamsAPI do not cause {@link NoClassDefFoundError} or + * other startup failures when the dependency is absent at runtime.
  • + *
+ */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class SpigotStartupSmokeTest { + + @Mock + private Plugin plugin; + + @Mock + private Server server; + + @Mock + private BukkitScheduler bukkitScheduler; + + @BeforeEach + void setUp() { + when(plugin.getServer()).thenReturn(server); + when(server.getScheduler()).thenReturn(bukkitScheduler); + } + + // --- BukkitPlatformScheduler with Spigot (BUKKIT) capabilities --- + + @Test + void scheduleRepeating_withSpigotCapabilities_delegatesToBukkitScheduler() { + BukkitTask mockTask = mock(BukkitTask.class); + when(bukkitScheduler.runTaskTimer( + any(Plugin.class), any(Runnable.class), anyLong(), anyLong())) + .thenReturn(mockTask); + + BukkitPlatformScheduler scheduler = + new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.BUKKIT); + + PlatformTask task = assertDoesNotThrow(() -> scheduler.scheduleRepeating(() -> {}, 5L, 20L)); + + assertNotNull(task); + verify(bukkitScheduler) + .runTaskTimer(eq(plugin), any(Runnable.class), eq(5L), eq(20L)); + } + + @Test + void executeAsync_withSpigotCapabilities_delegatesToBukkitScheduler() { + BukkitTask mockTask = mock(BukkitTask.class); + when(server.getScheduler().runTaskAsynchronously(any(Plugin.class), any(Runnable.class))) + .thenReturn(mockTask); + + BukkitPlatformScheduler scheduler = + new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.BUKKIT); + + assertDoesNotThrow(() -> scheduler.executeAsync(() -> {})); + verify(bukkitScheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class)); + } + + @Test + void executeGlobal_withSpigotCapabilities_delegatesToBukkitScheduler() { + BukkitTask mockTask = mock(BukkitTask.class); + when(server.getScheduler().runTask(any(Plugin.class), any(Runnable.class))) + .thenReturn(mockTask); + + BukkitPlatformScheduler scheduler = + new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.BUKKIT); + + assertDoesNotThrow(() -> scheduler.executeGlobal(() -> {})); + verify(bukkitScheduler).runTask(eq(plugin), any(Runnable.class)); + } + + // --- TeamsAPI optionality: FactionClaimSelectionGuiManager --- + + /** + * Verifies that {@link FactionClaimSelectionGuiManager#openSelection(Player)} returns + * {@code true} and does not throw any exception (including + * {@link NoClassDefFoundError}) when the TeamsAPI plugin is not installed. + * + *

This is the regression test for the bug where Bukkit's event registration threw + * {@code NoClassDefFoundError} for {@code com/skyblockexp/teamsapi/model/TeamClaim} + * because {@code FactionClaimSelectionGuiManager} referenced that type in its class + * structure (inner-class field descriptors and private method descriptors). + */ + @Test + void factionClaimGui_openSelection_withoutTeamsApi_returnsGracefully() { + EzRtpPlugin ezPlugin = mock(EzRtpPlugin.class); + Server mockServer = mock(Server.class); + PluginManager mockPm = mock(PluginManager.class); + Player player = mock(Player.class); + + when(ezPlugin.getServer()).thenReturn(mockServer); + when(mockServer.getPluginManager()).thenReturn(mockPm); + when(mockPm.isPluginEnabled("TeamsAPI")).thenReturn(false); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + + FactionClaimSelectionGuiManager guiManager = new FactionClaimSelectionGuiManager( + ezPlugin, + () -> null, + () -> null, + () -> null, + null); + + boolean result = assertDoesNotThrow(() -> guiManager.openSelection(player)); + assertTrue(result, "openSelection should return true when TeamsAPI is not enabled"); + } + + /** + * Verifies that passing a {@code null} player to + * {@link FactionClaimSelectionGuiManager#openSelection(Player)} is handled gracefully. + */ + @Test + void factionClaimGui_openSelection_withNullPlayer_doesNotThrow() { + EzRtpPlugin ezPlugin = mock(EzRtpPlugin.class); + + FactionClaimSelectionGuiManager guiManager = new FactionClaimSelectionGuiManager( + ezPlugin, + () -> null, + () -> null, + () -> null, + null); + + boolean result = assertDoesNotThrow(() -> guiManager.openSelection(null)); + assertTrue(result); + } + + // --- TeamsAPI optionality: TeamsApiSubcommandBridge --- + + /** + * Verifies that instantiating {@link TeamsApiSubcommandBridge} and calling + * {@link TeamsApiSubcommandBridge#register()} does not throw + * {@link NoClassDefFoundError} when TeamsAPI is not installed. + * + *

This is the regression test for the crash in + * {@code EzRtpPluginBootstrap.registerCommand()} where constructing + * {@code TeamsApiSubcommandBridge} caused {@code NoClassDefFoundError} for + * {@code com/skyblockexp/teamsapi/api/TeamsSubcommand} because that type was referenced + * in a {@code checkcast} instruction inside {@code unregister()}, which the JVM's bytecode + * verifier resolved at class-load time. + */ + @Test + void teamsApiSubcommandBridge_register_withoutTeamsApi_doesNotThrow() { + Plugin mockPlugin = mock(Plugin.class); + Server mockServer = mock(Server.class); + PluginManager mockPm = mock(PluginManager.class); + + when(mockPlugin.getServer()).thenReturn(mockServer); + when(mockServer.getPluginManager()).thenReturn(mockPm); + when(mockPm.isPluginEnabled("TeamsAPI")).thenReturn(false); + + EzRtpPlugin ezPlugin = mock(EzRtpPlugin.class); + FactionClaimSelectionGuiManager gui = new FactionClaimSelectionGuiManager( + ezPlugin, () -> null, () -> null, () -> null, null); + + TeamsApiSubcommandBridge bridge = + assertDoesNotThrow(() -> new TeamsApiSubcommandBridge(mockPlugin, gui)); + assertDoesNotThrow(bridge::register); + } +} diff --git a/ezrtp-bukkit/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/ezrtp-bukkit/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..1f0955d --- /dev/null +++ b/ezrtp-bukkit/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/ezrtp-common/pom.xml b/ezrtp-common/pom.xml index 5a23887..beee67e 100644 --- a/ezrtp-common/pom.xml +++ b/ezrtp-common/pom.xml @@ -5,13 +5,13 @@ com.skyblockexp ezrtp-parent - 3.3.0 + 3.4.0 ../pom.xml com.skyblockexp ezrtp-common - 3.3.0 + 3.4.0 jar EzRTP Common Shared utilities for EzRTP (platform-independent) @@ -76,9 +76,21 @@ com.github.ez-plugins teams-api - 1.4.1 + 1.8.0 provided + + + com.github.ez-plugins + EzCountdown + v1.4.2 + provided + + + com.github.ez-plugins + mc-plugin-update-notifier + 1.0.0 +