From 46f2c3fef31212942b8759df215105736c413703 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Wed, 20 May 2026 00:10:12 +0200 Subject: [PATCH 1/4] feat(factions): add TeamsAPI faction RTP GUI, claim overlays, and update notifier refactor --- CHANGELOG.md | 24 ++ README.md | 10 +- docs/commands.md | 12 + docs/config/faction-gui.md | 44 +++ docs/config/index.md | 9 + .../protection-worldguard-griefprevention.md | 1 + docs/permissions.md | 1 + ezrtp-api/pom.xml | 4 +- ezrtp-bukkit/pom.xml | 11 +- ezrtp-common/pom.xml | 9 +- .../ezrtp/bootstrap/EzRtpPluginBootstrap.java | 18 +- .../component/ConfigurationService.java | 79 ++++ .../component/ListenerRegistrar.java | 9 + .../ezrtp/command/RandomTeleportCommand.java | 17 + .../command/subcommands/FakeSubcommand.java | 96 ++++- .../subcommands/HeatmapSubcommand.java | 100 ++++- .../ezrtp/config/EzRtpConfiguration.java | 26 +- .../ezrtp/config/RandomTeleportSettings.java | 30 +- .../ezrtp/config/gui/FactionGuiSettings.java | 120 ++++++ .../gui/FactionClaimSelectionGuiManager.java | 357 ++++++++++++++++++ .../ezrtp/integration/ClaimOwnerResolver.java | 51 +++ .../integration/TeamsApiSubcommandBridge.java | 113 ++++++ .../ezrtp/message/MessageProvider.java | 10 +- .../teleport/heatmap/ClaimChunkOverlay.java | 8 + .../heatmap/ClaimOverlaySettings.java | 36 ++ .../teleport/heatmap/ClaimOverlayStyle.java | 15 + .../heatmap/HeatmapImageGenerator.java | 54 +++ .../teleport/heatmap/HeatmapMapService.java | 26 +- .../ezrtp/update/ReleaseUpdateChecker.java | 73 ++++ .../ezrtp/update/SpigotUpdateChecker.java | 63 ---- .../config/gui/FactionGuiSettingsTest.java | 43 +++ .../integration/ClaimOwnerResolverTest.java | 71 ++++ .../heatmap/ClaimOverlaySettingsTest.java | 36 ++ .../HeatmapImageGeneratorOverlayTest.java | 30 ++ ezrtp-paper/pom.xml | 6 +- ezrtp-purpur/pom.xml | 6 +- ezrtp-spigot/pom.xml | 6 +- pom.xml | 2 +- src/main/resources/faction-gui.yml | 26 ++ src/main/resources/plugin.yml | 3 + src/main/resources/rtp.yml | 10 + topics/bbcode.txt | 4 +- topics/markdown.md | 2 + 43 files changed, 1547 insertions(+), 124 deletions(-) create mode 100644 docs/config/faction-gui.md create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/config/gui/FactionGuiSettings.java create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/gui/FactionClaimSelectionGuiManager.java create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/ClaimOwnerResolver.java create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandBridge.java create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/heatmap/ClaimChunkOverlay.java create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/heatmap/ClaimOverlaySettings.java create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/heatmap/ClaimOverlayStyle.java create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/update/ReleaseUpdateChecker.java delete mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/update/SpigotUpdateChecker.java create mode 100644 ezrtp-common/src/test/java/com/skyblockexp/ezrtp/config/gui/FactionGuiSettingsTest.java create mode 100644 ezrtp-common/src/test/java/com/skyblockexp/ezrtp/integration/ClaimOwnerResolverTest.java create mode 100644 ezrtp-common/src/test/java/com/skyblockexp/ezrtp/teleport/heatmap/ClaimOverlaySettingsTest.java create mode 100644 ezrtp-common/src/test/java/com/skyblockexp/ezrtp/teleport/heatmap/HeatmapImageGeneratorOverlayTest.java create mode 100644 src/main/resources/faction-gui.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index ddd6dad..d919d02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,30 @@ Release tags use the `v` prefix (e.g. `v3.0.2`). --- +## [3.3.1] - 2026-05-19 + +### Added + +- **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 + +- 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. + ## [3.3.0] - 2026-05-16 ### Added diff --git a/README.md b/README.md index d50284f..8fcd554 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ It is designed for **safety-first teleportation**, **cross-platform compatibilit - **Biome-aware and cache-assisted searching** with optional rare-biome optimization. - **Heatmap/statistics tooling** for operators to inspect RTP distribution and performance. - **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 +95,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 +104,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 +122,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,6 +157,7 @@ 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. @@ -210,6 +216,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 +227,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/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..a56b61c 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -8,3 +8,12 @@ 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` diff --git a/docs/integrations/protection-worldguard-griefprevention.md b/docs/integrations/protection-worldguard-griefprevention.md index 9613335..6c0524a 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). 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..5bb01de 100644 --- a/ezrtp-api/pom.xml +++ b/ezrtp-api/pom.xml @@ -5,13 +5,13 @@ com.skyblockexp ezrtp-parent - 3.3.0 + 3.3.1 ../pom.xml com.skyblockexp ezrtp-api - 3.3.0 + 3.3.1 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..5072365 100644 --- a/ezrtp-bukkit/pom.xml +++ b/ezrtp-bukkit/pom.xml @@ -5,13 +5,13 @@ com.skyblockexp ezrtp-parent - 3.3.0 + 3.3.1 ../pom.xml com.skyblockexp ezrtp-bukkit - 3.3.0 + 3.3.1 jar EzRTP (Bukkit) EzRTP Bukkit-compatible plugin module. @@ -111,7 +111,7 @@ com.skyblockexp ezrtp-common - 3.3.0 + 3.3.1 @@ -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-common/pom.xml b/ezrtp-common/pom.xml index 5a23887..067b05d 100644 --- a/ezrtp-common/pom.xml +++ b/ezrtp-common/pom.xml @@ -5,13 +5,13 @@ com.skyblockexp ezrtp-parent - 3.3.0 + 3.3.1 ../pom.xml com.skyblockexp ezrtp-common - 3.3.0 + 3.3.1 jar EzRTP Common Shared utilities for EzRTP (platform-independent) @@ -79,6 +79,11 @@ 1.4.1 provided + + com.github.ez-plugins + mc-plugin-update-notifier + 1.0.0 + + + com.github.ez-plugins + EzCountdown + v1.4.2 provided diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/bootstrap/EzRtpPluginBootstrap.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/bootstrap/EzRtpPluginBootstrap.java index bbc0e6b..6f95b59 100644 --- a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/bootstrap/EzRtpPluginBootstrap.java +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/bootstrap/EzRtpPluginBootstrap.java @@ -19,6 +19,8 @@ import com.skyblockexp.ezrtp.unsafe.UnsafeLocationMonitor; import com.skyblockexp.ezrtp.unsafe.UnsafeLocationStatistics; import com.skyblockexp.ezrtp.gui.RandomTeleportGuiManager; +import com.skyblockexp.ezrtp.integration.EzCountdownDisplayBridge; +import com.skyblockexp.ezrtp.integration.EzCountdownDisplayBridgeImpl; import com.skyblockexp.ezrtp.integration.TeamsApiSubcommandBridge; import com.skyblockexp.ezrtp.message.MessageProvider; import com.skyblockexp.ezrtp.metrics.EzRtpMetricsRegistrar; @@ -89,6 +91,7 @@ public final class EzRtpPluginBootstrap { private com.skyblockexp.ezrtp.teleport.ChunkyWarmupCoordinator chunkyWarmupCoordinator; private PvpTagService pvpTagService; private TeamsApiSubcommandBridge teamsApiSubcommandBridge; + private EzCountdownDisplayBridge ezCountdownBridge; public EzRtpPluginBootstrap(EzRtpPlugin plugin) { this.plugin = plugin; @@ -264,6 +267,25 @@ public void reloadPluginConfiguration() { } messageProvider = configurationService.getMessageProvider(); + // Initialize EzCountdown integration bridge when EzCountdown is present. + if (ezCountdownBridge == null + && plugin.getServer().getPluginManager().isPluginEnabled("EzCountdown")) { + try { + ezCountdownBridge = new EzCountdownDisplayBridgeImpl(plugin); + plugin.getServer().getPluginManager().registerEvents( + (org.bukkit.event.Listener) ezCountdownBridge, plugin); + plugin.getLogger().info("EzCountdown integration enabled."); + } catch (NoClassDefFoundError e) { + plugin.getLogger().warning("Failed to initialize EzCountdown integration: " + e.getMessage()); + ezCountdownBridge = null; + } catch (Throwable e) { + plugin.getLogger().warning("Failed to initialize EzCountdown integration: " + e.getMessage()); + ezCountdownBridge = null; + } + } + if (teleportService != null) { + teleportService.setEzCountdownBridge(ezCountdownBridge); + } // Initialize text rendering settings (force legacy conversion for older clients) boolean forceLegacy = configurationService.getEffectiveBaseConfiguration().getBoolean("messages.force-legacy-colors", false); com.skyblockexp.ezrtp.util.MessageUtil.setForceLegacyColors(forceLegacy); @@ -298,13 +320,13 @@ public void reloadPluginConfiguration() { configuration.getQueueSettings(), economyService, (player, settings) -> configuration.resolveTeleportCost(player, settings), protectionRegistry, messageProvider, ChunkLoadStrategyRegistry.get(), PlatformRuntimeRegistry.get(), chunkyAPI, chunkyWarmupCoordinator, pvpTagService); + teleportService.setEzCountdownBridge(ezCountdownBridge); try { EzRtpAPI.registerProvider(plugin, teleportService); } catch (Throwable ignored) {} } else { teleportService.reload(defaultSettings, configuration.getQueueSettings()); teleportService.setEconomyService(economyService); teleportService.setCostResolver((player, settings) -> configuration.resolveTeleportCost(player, settings)); - teleportService.setProtectionRegistry(protectionRegistry); - try { EzRtpAPI.registerProvider(plugin, teleportService); } catch (Throwable ignored) {} + teleportService.setProtectionRegistry(protectionRegistry); teleportService.setEzCountdownBridge(ezCountdownBridge); try { EzRtpAPI.registerProvider(plugin, teleportService); } catch (Throwable ignored) {} } networkCoordinator.reload(configuration); RandomTeleportGuiManager guiManager = listenerRegistrar.getGuiManager(); diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/config/EzCountdownIntegrationSettings.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/config/EzCountdownIntegrationSettings.java new file mode 100644 index 0000000..4dfc691 --- /dev/null +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/config/EzCountdownIntegrationSettings.java @@ -0,0 +1,64 @@ +package com.skyblockexp.ezrtp.config; + +import org.bukkit.configuration.ConfigurationSection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.logging.Logger; + +/** Settings controlling the optional EzCountdown-backed countdown display for RTP. */ +public final class EzCountdownIntegrationSettings { + + private static final String DEFAULT_FORMAT = + "Teleporting in {formatted}..."; + + private final boolean enabled; + private final List displayTypes; + private final String formatMessage; + + private EzCountdownIntegrationSettings( + boolean enabled, List displayTypes, String formatMessage) { + this.enabled = enabled; + this.displayTypes = List.copyOf(displayTypes); + this.formatMessage = formatMessage; + } + + public static EzCountdownIntegrationSettings disabled() { + return new EzCountdownIntegrationSettings(false, List.of("ACTION_BAR"), DEFAULT_FORMAT); + } + + public static EzCountdownIntegrationSettings fromConfiguration( + ConfigurationSection section, Logger logger) { + if (section == null) { + return disabled(); + } + boolean enabled = section.getBoolean("enabled", false); + List displayTypes = new ArrayList<>(); + if (section.isList("display-types")) { + for (Object entry : section.getList("display-types")) { + if (entry instanceof String s && !s.isBlank()) { + displayTypes.add(s.toUpperCase(Locale.ROOT)); + } + } + } + if (displayTypes.isEmpty()) { + displayTypes.add("ACTION_BAR"); + } + String format = section.getString("format", DEFAULT_FORMAT); + return new EzCountdownIntegrationSettings( + enabled, displayTypes, format != null ? format : DEFAULT_FORMAT); + } + + public boolean isEnabled() { + return enabled; + } + + public List getDisplayTypes() { + return displayTypes; + } + + public String getFormatMessage() { + return formatMessage; + } +} diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/config/RandomTeleportSettings.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/config/RandomTeleportSettings.java index a6b0fe7..dda0d1a 100644 --- a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/config/RandomTeleportSettings.java +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/config/RandomTeleportSettings.java @@ -64,6 +64,7 @@ public final class RandomTeleportSettings { private final double warnDistance; private final PvpTagIntegrationSettings pvpTagIntegrationSettings; private final boolean heatmapEnabled; + private final EzCountdownIntegrationSettings ezCountdownIntegrationSettings; public RandomTeleportSettings(ConfigurationSection configSection, String worldName, int centerX, int centerZ, int minimumRadius, int maximumRadius, @@ -96,7 +97,7 @@ public RandomTeleportSettings(ConfigurationSection configSection, rareBiomeOptimizationSettings, chunkLoadingSettings, enableFallbackToCache, biomeSearchSettings, biomeFilteringEnabled, biomeSystemEnabled, safetySettings, searchPattern, chunkyIntegrationSettings, false, true, 2.0, 1.0, - PvpTagIntegrationSettings.defaults(), false); + PvpTagIntegrationSettings.defaults(), false, EzCountdownIntegrationSettings.disabled()); } public RandomTeleportSettings(ConfigurationSection configSection, @@ -131,7 +132,7 @@ public RandomTeleportSettings(ConfigurationSection configSection, rareBiomeOptimizationSettings, chunkLoadingSettings, enableFallbackToCache, biomeSearchSettings, biomeFilteringEnabled, biomeSystemEnabled, safetySettings, searchPattern, chunkyIntegrationSettings, suppressPlayerMessages, true, 2.0, 1.0, - PvpTagIntegrationSettings.defaults(), false); + PvpTagIntegrationSettings.defaults(), false, EzCountdownIntegrationSettings.disabled()); } public RandomTeleportSettings(ConfigurationSection configSection, @@ -162,7 +163,8 @@ public RandomTeleportSettings(ConfigurationSection configSection, double cancelDistance, double warnDistance, PvpTagIntegrationSettings pvpTagIntegrationSettings, - boolean heatmapEnabled) { + boolean heatmapEnabled, + EzCountdownIntegrationSettings ezCountdownIntegrationSettings) { this.configSection = configSection; this.worldName = worldName; this.centerX = centerX; @@ -203,6 +205,10 @@ public RandomTeleportSettings(ConfigurationSection configSection, this.pvpTagIntegrationSettings = pvpTagIntegrationSettings != null ? pvpTagIntegrationSettings : PvpTagIntegrationSettings.defaults(); this.heatmapEnabled = heatmapEnabled; + this.ezCountdownIntegrationSettings = + ezCountdownIntegrationSettings != null + ? ezCountdownIntegrationSettings + : EzCountdownIntegrationSettings.disabled(); } public Integer getMinY() { return minY; } public Integer getMaxY() { return maxY; } @@ -246,6 +252,10 @@ public RandomTeleportSettings(ConfigurationSection configSection, public PvpTagIntegrationSettings getPvpTagIntegrationSettings() { return pvpTagIntegrationSettings; } + public EzCountdownIntegrationSettings getEzCountdownIntegrationSettings() { + return ezCountdownIntegrationSettings; + } + /** Returns whether heatmap features are enabled for these effective RTP settings. */ public boolean isHeatmapEnabled() { return heatmapEnabled; @@ -275,7 +285,8 @@ public RandomTeleportSettings withWorldName(String newWorldName) { chunkLoadingSettings, enableFallbackToCache, biomeSearchSettings, biomeFilteringEnabled, biomeSystemEnabled, safetySettings, searchPattern, chunkyIntegrationSettings, suppressPlayerMessages, - cancelOnMove, cancelDistance, warnDistance, pvpTagIntegrationSettings, heatmapEnabled); + cancelOnMove, cancelDistance, warnDistance, pvpTagIntegrationSettings, heatmapEnabled, + ezCountdownIntegrationSettings); } public RandomTeleportSettings withSuppressPlayerMessages(boolean suppress) { @@ -290,7 +301,8 @@ public RandomTeleportSettings withSuppressPlayerMessages(boolean suppress) { chunkLoadingSettings, enableFallbackToCache, biomeSearchSettings, biomeFilteringEnabled, biomeSystemEnabled, safetySettings, searchPattern, chunkyIntegrationSettings, suppress, - cancelOnMove, cancelDistance, warnDistance, pvpTagIntegrationSettings, heatmapEnabled); + cancelOnMove, cancelDistance, warnDistance, pvpTagIntegrationSettings, heatmapEnabled, + ezCountdownIntegrationSettings); } public static RandomTeleportSettings fromConfiguration(ConfigurationSection section, java.util.logging.Logger logger) { @@ -320,7 +332,8 @@ public static RandomTeleportSettings fromConfiguration(ConfigurationSection sect 2.0, 1.0, PvpTagIntegrationSettings.defaults(), - false); + false, + EzCountdownIntegrationSettings.disabled()); } String worldName = section.getString("world", "world"); @@ -443,6 +456,11 @@ public static RandomTeleportSettings fromConfiguration(ConfigurationSection sect section.getConfigurationSection("pvp-tag-integration"), PvpTagIntegrationSettings.defaults()); boolean heatmapEnabled = section.getBoolean("heatmap.enabled", false); + EzCountdownIntegrationSettings ezCountdownIntegrationSettings = + EzCountdownIntegrationSettings.fromConfiguration( + countdownSection != null ? countdownSection.getConfigurationSection("ezcountdown") : null, + logger); + return new RandomTeleportSettings(section, worldName, centerX, centerZ, minRadius, maxRadius, maxAttempts, useWorldBorder, unsafeBlocks, messages, particleSettings, onJoinTeleportSettings, countdownBossBarSettings, countdownParticleSettings, teleportCost, countdownSeconds, countdownChatMessagesEnabled, debugRejectionLogging, minY, maxY, @@ -463,7 +481,8 @@ public static RandomTeleportSettings fromConfiguration(ConfigurationSection sect cancelDistance, warnDistance, pvpTagIntegrationSettings, - heatmapEnabled); + heatmapEnabled, + ezCountdownIntegrationSettings); } public static RandomTeleportSettings fromConfiguration(ConfigurationSection section, java.util.logging.Logger logger, RandomTeleportSettings fallback) { @@ -624,6 +643,18 @@ public static RandomTeleportSettings fromConfiguration(ConfigurationSection sect ? section.getBoolean("heatmap.enabled", false) : (fallback != null && fallback.isHeatmapEnabled()); + EzCountdownIntegrationSettings ezCountdownIntegrationSettings; + ConfigurationSection ezcSection = + countdownSection != null ? countdownSection.getConfigurationSection("ezcountdown") : null; + if (ezcSection != null) { + ezCountdownIntegrationSettings = EzCountdownIntegrationSettings.fromConfiguration(ezcSection, logger); + } else { + ezCountdownIntegrationSettings = + fallback != null + ? fallback.getEzCountdownIntegrationSettings() + : EzCountdownIntegrationSettings.disabled(); + } + return new RandomTeleportSettings(section, worldName, centerX, centerZ, minRadius, maxRadius, maxAttempts, useWorldBorder, unsafeBlocks, messages, particleSettings, onJoinTeleportSettings, countdownBossBarSettings, countdownParticleSettings, teleportCost, countdownSeconds, countdownChatMessagesEnabled, debugRejectionLogging, minY, maxY, @@ -644,7 +675,8 @@ public static RandomTeleportSettings fromConfiguration(ConfigurationSection sect cancelDistance, warnDistance, pvpTagIntegrationSettings, - heatmapEnabled); + heatmapEnabled, + ezCountdownIntegrationSettings); } public int getCountdownSeconds() { return countdownSeconds; diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/EzCountdownDisplayBridge.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/EzCountdownDisplayBridge.java new file mode 100644 index 0000000..9c13610 --- /dev/null +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/EzCountdownDisplayBridge.java @@ -0,0 +1,49 @@ +package com.skyblockexp.ezrtp.integration; + +import com.skyblockexp.ezrtp.config.EzCountdownIntegrationSettings; +import com.skyblockexp.ezrtp.config.RandomTeleportSettings; +import com.skyblockexp.ezrtp.message.MessageProvider; +import com.skyblockexp.ezrtp.pvptag.PvpTagService; +import org.bukkit.entity.Player; + +import java.util.UUID; +import java.util.function.Consumer; + +/** + * Abstraction over EzCountdown-backed countdown display for RTP. + * + *

Implementations reference EzCountdown API classes directly and are only loaded when the + * EzCountdown plugin is present at runtime. This interface itself carries no EzCountdown type + * references so that {@link com.skyblockexp.ezrtp.teleport.CountdownManager} can hold a field of + * this type without triggering a {@link ClassNotFoundException} on servers without EzCountdown. + */ +public interface EzCountdownDisplayBridge { + + /** + * Start an EzCountdown-managed countdown display for the given player. + * + * @return {@code true} if the countdown was successfully created and started, {@code false} + * if EzCountdown could not create the countdown (caller should fall back to built-in + * display) + */ + boolean startCountdown( + Player player, + int seconds, + EzCountdownIntegrationSettings settings, + RandomTeleportSettings teleportSettings, + PvpTagService pvpTagService, + MessageProvider messageProvider, + Consumer callback, + Runnable onComplete); + + /** + * Cancel any active EzCountdown countdown for the player, invoking the failure callback. + * + * @param playerUuid the player's UUID + * @param player the player instance (may be {@code null} if the player has disconnected) + */ + void cancelCountdown(UUID playerUuid, Player player); + + /** Remove all active countdowns and release resources (called on plugin shutdown). */ + void shutdown(); +} diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/EzCountdownDisplayBridgeImpl.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/EzCountdownDisplayBridgeImpl.java new file mode 100644 index 0000000..43d316d --- /dev/null +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/EzCountdownDisplayBridgeImpl.java @@ -0,0 +1,277 @@ +package com.skyblockexp.ezrtp.integration; + +import com.skyblockexp.ezcountdown.EzCountdownPlugin; +import com.skyblockexp.ezcountdown.api.EzCountdownApi; +import com.skyblockexp.ezcountdown.api.event.CountdownEndEvent; +import com.skyblockexp.ezcountdown.api.event.CountdownTickEvent; +import com.skyblockexp.ezcountdown.api.model.Countdown; +import com.skyblockexp.ezcountdown.api.model.CountdownBuilder; +import com.skyblockexp.ezcountdown.api.model.CountdownType; +import com.skyblockexp.ezcountdown.display.DisplayType; +import com.skyblockexp.ezrtp.config.EzCountdownIntegrationSettings; +import com.skyblockexp.ezrtp.config.RandomTeleportSettings; +import com.skyblockexp.ezrtp.message.MessageKey; +import com.skyblockexp.ezrtp.message.MessageProvider; +import com.skyblockexp.ezrtp.pvptag.PvpTagService; +import com.skyblockexp.ezrtp.util.MessageUtil; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.plugin.Plugin; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.logging.Level; + +/** + * EzCountdown-backed implementation of {@link EzCountdownDisplayBridge}. + * + *

Each RTP countdown is created as an ephemeral {@link CountdownType#DURATION} countdown named + * {@code ezrtp-}. A temporary Bukkit {@link PermissionAttachment} grants the player a + * unique visibility permission so that the countdown display is shown only to that player, + * regardless of which display types are active. + * + *

This class must only be instantiated when the EzCountdown plugin is confirmed present; + * otherwise its class loading will throw {@link NoClassDefFoundError}. + */ +public final class EzCountdownDisplayBridgeImpl implements EzCountdownDisplayBridge, Listener { + + private static final String COUNTDOWN_NAME_PREFIX = "ezrtp-"; + private static final String VISIBILITY_PERM_PREFIX = "ezcountdown.visible.ezrtp."; + + private final Plugin plugin; + private final EzCountdownApi api; + private final Map pending = new HashMap<>(); + + public EzCountdownDisplayBridgeImpl(Plugin plugin) { + this.plugin = plugin; + this.api = EzCountdownPlugin.getApi(); + } + + // ------------------------------------------------------------------------- + // EzCountdownDisplayBridge + // ------------------------------------------------------------------------- + + @Override + public boolean startCountdown( + Player player, + int seconds, + EzCountdownIntegrationSettings settings, + RandomTeleportSettings teleportSettings, + PvpTagService pvpTagService, + MessageProvider messageProvider, + Consumer callback, + Runnable onComplete) { + UUID uuid = player.getUniqueId(); + // Cancel any existing countdown for this player first. + cancelCountdownInternal(uuid, player, false); + + String name = COUNTDOWN_NAME_PREFIX + uuid; + String visibilityPerm = VISIBILITY_PERM_PREFIX + uuid; + + // Grant a temporary, player-unique visibility permission so only this player + // sees the EzCountdown display output. + PermissionAttachment attachment = player.addAttachment(plugin); + attachment.setPermission(visibilityPerm, true); + player.recalculatePermissions(); + + // Resolve configured display types. + EnumSet displayTypes = EnumSet.noneOf(DisplayType.class); + for (String typeName : settings.getDisplayTypes()) { + try { + displayTypes.add(DisplayType.valueOf(typeName)); + } catch (IllegalArgumentException ignored) { + plugin.getLogger().warning("Unknown EzCountdown display type '" + typeName + "' — skipping."); + } + } + if (displayTypes.isEmpty()) { + displayTypes.add(DisplayType.ACTION_BAR); + } + + Countdown countdown = + CountdownBuilder.builder(name) + .ephemeral(true) + .type(CountdownType.DURATION) + .durationSeconds(seconds) + .displayTypes(displayTypes) + .formatMessage(settings.getFormatMessage()) + .visibilityPermission(visibilityPerm) + .build(); + + boolean created = api.createCountdown(countdown); + if (!created) { + attachment.remove(); + player.recalculatePermissions(); + plugin.getLogger().log(Level.WARNING, "EzCountdown rejected createCountdown for player " + player.getName()); + return false; + } + api.startCountdown(name); + + Location startLocation = player.getLocation().clone(); + pending.put( + uuid, + new PendingRtpCountdown( + uuid, + startLocation, + callback, + onComplete, + teleportSettings, + pvpTagService, + messageProvider, + attachment, + new boolean[] {false})); + return true; + } + + @Override + public void cancelCountdown(UUID playerUuid, Player player) { + cancelCountdownInternal(playerUuid, player, true); + } + + @Override + public void shutdown() { + for (PendingRtpCountdown p : pending.values()) { + cleanupPermission(p.attachment(), null); + String name = COUNTDOWN_NAME_PREFIX + p.playerUuid(); + api.stopCountdown(name); + api.deleteCountdown(name); + } + pending.clear(); + } + + // ------------------------------------------------------------------------- + // Bukkit event handlers + // ------------------------------------------------------------------------- + + @EventHandler + public void onCountdownTick(CountdownTickEvent event) { + PendingRtpCountdown p = pendingByCountdownName(event.getCountdown().getName()); + if (p == null) return; + + Player player = Bukkit.getPlayer(p.playerUuid()); + if (player == null || !player.isOnline()) { + pending.remove(p.playerUuid()); + return; + } + + RandomTeleportSettings settings = p.teleportSettings(); + + // Movement cancellation check. + if (settings.isCancelOnMove()) { + double movedDistSq = player.getLocation().distanceSquared(p.startLocation()); + double cancelDistSq = settings.getCancelDistance() * settings.getCancelDistance(); + double warnDistSq = settings.getWarnDistance() * settings.getWarnDistance(); + if (movedDistSq >= cancelDistSq) { + String name = event.getCountdown().getName(); + pending.remove(p.playerUuid()); + cleanupPermission(p.attachment(), player); + api.stopCountdown(name); + api.deleteCountdown(name); + if (!settings.isSuppressPlayerMessages()) { + MessageUtil.send( + player, + p.messageProvider().format(MessageKey.COUNTDOWN_MOVE_CANCEL, player)); + } + if (p.callback() != null) p.callback().accept(false); + return; + } + if (!p.warningSent()[0] && warnDistSq > 0 && movedDistSq >= warnDistSq) { + p.warningSent()[0] = true; + if (!settings.isSuppressPlayerMessages()) { + MessageUtil.send( + player, + p.messageProvider().format(MessageKey.COUNTDOWN_MOVE_WARN, player)); + } + } + } + + // PvP tag cancellation check. + if (p.pvpTagService() != null + && settings.getPvpTagIntegrationSettings().isCancelCountdownOnPvpTag() + && p.pvpTagService().isInCombat(player)) { + String name = event.getCountdown().getName(); + pending.remove(p.playerUuid()); + cleanupPermission(p.attachment(), player); + api.stopCountdown(name); + api.deleteCountdown(name); + if (!settings.isSuppressPlayerMessages()) { + MessageUtil.send( + player, p.messageProvider().format(MessageKey.COUNTDOWN_PVP_CANCEL, player)); + } + if (p.callback() != null) p.callback().accept(false); + } + } + + @EventHandler + public void onCountdownEnd(CountdownEndEvent event) { + PendingRtpCountdown p = pendingByCountdownName(event.getCountdown().getName()); + if (p == null) return; + + // Remove before running onComplete so any re-entrant calls don't double-fire. + pending.remove(p.playerUuid()); + Player player = Bukkit.getPlayer(p.playerUuid()); + cleanupPermission(p.attachment(), player); + p.onComplete().run(); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private void cancelCountdownInternal(UUID playerUuid, Player player, boolean fireCallback) { + PendingRtpCountdown p = pending.remove(playerUuid); + if (p == null) return; + cleanupPermission(p.attachment(), player); + String name = COUNTDOWN_NAME_PREFIX + playerUuid; + api.stopCountdown(name); + api.deleteCountdown(name); + if (fireCallback && p.callback() != null) p.callback().accept(false); + } + + private PendingRtpCountdown pendingByCountdownName(String name) { + if (!name.startsWith(COUNTDOWN_NAME_PREFIX)) return null; + String uuidStr = name.substring(COUNTDOWN_NAME_PREFIX.length()); + UUID uuid; + try { + uuid = UUID.fromString(uuidStr); + } catch (IllegalArgumentException ignored) { + return null; + } + return pending.get(uuid); + } + + private static void cleanupPermission(PermissionAttachment attachment, Player player) { + try { + attachment.remove(); + } catch (Throwable ignored) { + // Already removed or player offline — safe to ignore. + } + if (player != null) { + try { + player.recalculatePermissions(); + } catch (Throwable ignored) { + } + } + } + + // ------------------------------------------------------------------------- + // Internal state record + // ------------------------------------------------------------------------- + + private record PendingRtpCountdown( + UUID playerUuid, + Location startLocation, + Consumer callback, + Runnable onComplete, + RandomTeleportSettings teleportSettings, + PvpTagService pvpTagService, + MessageProvider messageProvider, + PermissionAttachment attachment, + boolean[] warningSent) {} +} diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/RtpTeamsSubcommand.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/RtpTeamsSubcommand.java new file mode 100644 index 0000000..8a49378 --- /dev/null +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/RtpTeamsSubcommand.java @@ -0,0 +1,31 @@ +package com.skyblockexp.ezrtp.integration; + +import com.skyblockexp.ezrtp.gui.FactionClaimSelectionGuiManager; +import com.skyblockexp.ezrtp.util.MessageUtil; +import com.skyblockexp.teamsapi.api.AbstractTeamsSubcommand; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +final class RtpTeamsSubcommand extends AbstractTeamsSubcommand { + + private final FactionClaimSelectionGuiManager factionClaimGuiManager; + + RtpTeamsSubcommand(FactionClaimSelectionGuiManager factionClaimGuiManager) { + super("rtp", "Open faction claim RTP GUI", "ezrtp.use"); + this.factionClaimGuiManager = factionClaimGuiManager; + } + + @Override + public boolean execute(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + MessageUtil.send(sender, "This command may only be used by players."); + return true; + } + return factionClaimGuiManager.openSelection(player); + } + + @Override + public String getUsage() { + return "/f rtp"; + } +} diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandBridge.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandBridge.java index b63391e..0d17400 100644 --- a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandBridge.java +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandBridge.java @@ -1,16 +1,10 @@ package com.skyblockexp.ezrtp.integration; import com.skyblockexp.ezrtp.gui.FactionClaimSelectionGuiManager; -import com.skyblockexp.ezrtp.util.MessageUtil; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; +import com.skyblockexp.teamsapi.api.TeamsAPI; +import com.skyblockexp.teamsapi.api.TeamsSubcommand; import org.bukkit.plugin.Plugin; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.Collections; -import java.util.List; import java.util.logging.Level; public final class TeamsApiSubcommandBridge { @@ -18,7 +12,6 @@ public final class TeamsApiSubcommandBridge { private final Plugin plugin; private final FactionClaimSelectionGuiManager factionClaimGuiManager; private Object registeredSubcommand; - private Method unregisterMethod; public TeamsApiSubcommandBridge(Plugin plugin, FactionClaimSelectionGuiManager factionClaimGuiManager) { this.plugin = plugin; @@ -26,88 +19,32 @@ public TeamsApiSubcommandBridge(Plugin plugin, FactionClaimSelectionGuiManager f } public void register() { + if (!plugin.getServer().getPluginManager().isPluginEnabled("TeamsAPI")) { + return; + } try { - Class teamsApiClass = Class.forName("com.skyblockexp.teamsapi.api.TeamsAPI"); - Class subcommandClass = Class.forName("com.skyblockexp.teamsapi.api.TeamsSubcommand"); - - Method registerMethod = teamsApiClass.getMethod("registerSubcommand", Plugin.class, subcommandClass); - unregisterMethod = teamsApiClass.getMethod("unregisterSubcommand", subcommandClass); - - InvocationHandler handler = new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] args) { - String name = method.getName(); - if ("getName".equals(name)) { - return "rtp"; - } - if ("getDescription".equals(name)) { - return "Open faction claim RTP GUI"; - } - if ("getPermission".equals(name)) { - return "ezrtp.use"; - } - if ("getUsage".equals(name)) { - return "/f rtp"; - } - if ("execute".equals(name)) { - CommandSender sender = (CommandSender) args[0]; - return executeSubcommand(sender); - } - if ("tabComplete".equals(name)) { - return Collections.emptyList(); - } - if ("toString".equals(name)) { - return "EzRTPTeamsSubcommandProxy"; - } - if ("hashCode".equals(name)) { - return System.identityHashCode(proxy); - } - if ("equals".equals(name)) { - return proxy == args[0]; - } - return null; - } - }; - - Object proxy = Proxy.newProxyInstance( - subcommandClass.getClassLoader(), - new Class[]{subcommandClass}, - handler); - registerMethod.invoke(null, plugin, proxy); - registeredSubcommand = proxy; - plugin.getLogger().info("Registered TeamsAPI '/f rtp' subcommand."); - } catch (ClassNotFoundException ignored) { - // TeamsAPI without subcommand support (or not installed). - } catch (NoSuchMethodException ignored) { - // TeamsAPI version does not expose subcommand registration. + RtpTeamsSubcommand subcommand = new RtpTeamsSubcommand(factionClaimGuiManager); + TeamsAPI.registerSubcommand(plugin, subcommand); + registeredSubcommand = subcommand; + plugin.getLogger().info("TeamsAPI integration: registered '/f rtp' subcommand."); + } catch (NoClassDefFoundError ignored) { + // TeamsAPI present as a plugin but classes unexpectedly missing. } catch (Throwable throwable) { plugin.getLogger().log(Level.WARNING, "Failed to register TeamsAPI '/f rtp' subcommand.", throwable); } } public void unregister() { - if (registeredSubcommand == null || unregisterMethod == null) { + if (registeredSubcommand == null) { return; } try { - unregisterMethod.invoke(null, registeredSubcommand); + TeamsAPI.unregisterSubcommand((TeamsSubcommand) registeredSubcommand); } catch (Throwable throwable) { plugin.getLogger().log(Level.FINE, "Failed to unregister TeamsAPI subcommand cleanly.", throwable); } finally { registeredSubcommand = null; - unregisterMethod = null; } } - - private boolean executeSubcommand(CommandSender sender) { - if (!(sender instanceof Player player)) { - MessageUtil.send(sender, "This command may only be used by players."); - return true; - } - if (!sender.hasPermission("ezrtp.use")) { - MessageUtil.send(sender, "You do not have permission to use this command."); - return true; - } - return factionClaimGuiManager.openSelection(player); - } } + diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/CountdownManager.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/CountdownManager.java index 47954c4..7623303 100644 --- a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/CountdownManager.java +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/CountdownManager.java @@ -3,6 +3,7 @@ import com.skyblockexp.ezrtp.config.effects.CountdownBossBarSettings; import com.skyblockexp.ezrtp.config.effects.CountdownParticleSettings; import com.skyblockexp.ezrtp.config.RandomTeleportSettings; +import com.skyblockexp.ezrtp.integration.EzCountdownDisplayBridge; import com.skyblockexp.ezrtp.message.MessageKey; import com.skyblockexp.ezrtp.message.MessageProvider; import com.skyblockexp.ezrtp.pvptag.PvpTagService; @@ -31,6 +32,7 @@ public final class CountdownManager { private final MessageProvider messageProvider; private final PvpTagService pvpTagService; private final Map countdownBossBars = new HashMap<>(); + private EzCountdownDisplayBridge ezCountdownBridge; public CountdownManager( org.bukkit.plugin.java.JavaPlugin plugin, @@ -50,6 +52,11 @@ public CountdownManager( this.pvpTagService = pvpTagService; } + /** Sets the optional EzCountdown-backed display bridge. */ + public void setEzCountdownBridge(EzCountdownDisplayBridge bridge) { + this.ezCountdownBridge = bridge; + } + /** * Starts a countdown for the player before teleportation. */ @@ -68,6 +75,15 @@ public void startCountdown(Player player, RandomTeleportSettings teleportSetting Map.of("seconds", String.valueOf(countdown)), player)); } + // Delegate to EzCountdown when available and configured. + if (ezCountdownBridge != null && teleportSettings.getEzCountdownIntegrationSettings().isEnabled()) { + boolean started = ezCountdownBridge.startCountdown( + player, countdown, teleportSettings.getEzCountdownIntegrationSettings(), + teleportSettings, pvpTagService, messageProvider, callback, onComplete); + if (started) return; + // Fall through to built-in countdown if EzCountdown failed to start. + } + Location startLocation = player.getLocation().clone(); boolean[] warningSent = {false}; runCountdown(player, teleportSettings, callback, countdown, countdown, onComplete, startLocation, warningSent); @@ -211,6 +227,9 @@ private void playCountdownParticles(Player player, CountdownParticleSettings set public void shutdown() { countdownBossBars.values().forEach(wrapper -> wrapper.removeAll()); countdownBossBars.clear(); + if (ezCountdownBridge != null) { + ezCountdownBridge.shutdown(); + } } private static String legacy(net.kyori.adventure.text.Component component) { diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/RandomTeleportService.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/RandomTeleportService.java index 7095ccb..967a843 100644 --- a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/RandomTeleportService.java +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/teleport/RandomTeleportService.java @@ -143,6 +143,10 @@ public void setProtectionRegistry(ProtectionRegistry protectionRegistry) { locationValidator.setProtectionRegistry(protectionRegistry); } + public void setEzCountdownBridge(com.skyblockexp.ezrtp.integration.EzCountdownDisplayBridge bridge) { + countdownManager.setEzCountdownBridge(bridge); + } + public RtpStatistics getStatistics() { return statistics; } diff --git a/ezrtp-paper/pom.xml b/ezrtp-paper/pom.xml index 86f89d1..790d585 100644 --- a/ezrtp-paper/pom.xml +++ b/ezrtp-paper/pom.xml @@ -5,13 +5,13 @@ com.skyblockexp ezrtp-parent - 3.3.1 + 3.4.0 ../pom.xml com.skyblockexp ezrtp-paper - 3.3.1 + 3.4.0 jar EzRTP (Paper) Paper-optimized module for EzRTP @@ -32,7 +32,7 @@ com.skyblockexp ezrtp-common - 3.3.1 + 3.4.0 provided diff --git a/ezrtp-purpur/pom.xml b/ezrtp-purpur/pom.xml index 69d19db..d081c3e 100644 --- a/ezrtp-purpur/pom.xml +++ b/ezrtp-purpur/pom.xml @@ -5,7 +5,7 @@ com.skyblockexp ezrtp-parent - 3.3.1 + 3.4.0 ../pom.xml @@ -18,12 +18,12 @@ com.skyblockexp ezrtp-paper - 3.3.1 + 3.4.0 com.skyblockexp ezrtp-common - 3.3.1 + 3.4.0 provided diff --git a/ezrtp-spigot/pom.xml b/ezrtp-spigot/pom.xml index b3d0051..ba227dc 100644 --- a/ezrtp-spigot/pom.xml +++ b/ezrtp-spigot/pom.xml @@ -5,13 +5,13 @@ com.skyblockexp ezrtp-parent - 3.3.1 + 3.4.0 ../pom.xml com.skyblockexp ezrtp-spigot - 3.3.1 + 3.4.0 jar EzRTP (Spigot) Spigot-optimized module for EzRTP @@ -26,7 +26,7 @@ com.skyblockexp ezrtp-common - 3.3.1 + 3.4.0 diff --git a/pom.xml b/pom.xml index c3fa671..a5eca55 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.skyblockexp ezrtp-parent - 3.3.1 + 3.4.0 pom EzRTP Parent Aggregator POM for EzRTP multi-module build diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 61e88ff..c9393ed 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -15,6 +15,7 @@ softdepend: - CombatLogX - PvPManager - CombatLog + - EzCountdown commands: rtp: description: Teleport to a random safe location in the configured world. diff --git a/src/main/resources/rtp.yml b/src/main/resources/rtp.yml index 779eb76..61daa57 100644 --- a/src/main/resources/rtp.yml +++ b/src/main/resources/rtp.yml @@ -66,6 +66,16 @@ countdown: secondary-count: 6 secondary-offset: 0.35 chat-messages: false + # EzCountdown integration — requires the EzCountdown plugin to be installed. + ezcountdown: + # Whether to use EzCountdown for countdown display. + enabled: false + # Display types. Options: ACTION_BAR, BOSS_BAR, CHAT, TITLE, SCOREBOARD, DIALOG + display-types: + - ACTION_BAR + - BOSS_BAR + # Message format. {formatted} = remaining time, {name} = countdown name. + format: "Teleporting in {formatted}..." unsafe-blocks: - WATER From c4746c568fd08733aa9f25711d6961b5a1d3e8ce Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sun, 24 May 2026 12:45:24 +0200 Subject: [PATCH 3/4] fix: make TeamsAPI fully optional in FactionClaimSelectionGuiManager Bukkit's event registration inspects all method descriptors (including private methods) in a Listener class via reflection. When TeamsAPI was absent at runtime, the JVM couldn't resolve TeamClaim/TeamsService types referenced in FactionClaimSelectionGuiManager's private methods and inner Session class, causing a NoClassDefFoundError during registerEvents(). Fix: - Extract ClaimSnapshot record (plain Java types only) as a DTO to carry claim data without any TeamsAPI imports in the Listener class. - Extract TeamsApiClaimFetcher class to hold all TeamsAPI type references; it is loaded lazily from method bodies, not at class-load time. - Refactor FactionClaimSelectionGuiManager to use ClaimSnapshot everywhere; zero TeamsAPI imports remain in the class structure. Tests: - Add mockito-extensions/org.mockito.plugins.MockMaker (mock-maker-inline) to ezrtp-bukkit so the final EzRtpPlugin class can be mocked. - Add SpigotStartupSmokeTest: scheduler (BUKKIT caps) + TeamsAPI-absent regression test. - Add PaperStartupSmokeTest: scheduler (PAPER caps) + TeamsAPI disabled and enabled-but-unavailable path tests. --- .../ezrtp/platform/PaperStartupSmokeTest.java | 175 ++++++++++++++++++ .../platform/SpigotStartupSmokeTest.java | 159 ++++++++++++++++ .../org.mockito.plugins.MockMaker | 1 + .../skyblockexp/ezrtp/gui/ClaimSnapshot.java | 13 ++ .../gui/FactionClaimSelectionGuiManager.java | 101 ++++------ .../integration/TeamsApiClaimFetcher.java | 86 +++++++++ 6 files changed, 473 insertions(+), 62 deletions(-) create mode 100644 ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/PaperStartupSmokeTest.java create mode 100644 ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/SpigotStartupSmokeTest.java create mode 100644 ezrtp-bukkit/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/gui/ClaimSnapshot.java create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiClaimFetcher.java 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..0308e2e --- /dev/null +++ b/ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/SpigotStartupSmokeTest.java @@ -0,0 +1,159 @@ +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 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); + } +} 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/src/main/java/com/skyblockexp/ezrtp/gui/ClaimSnapshot.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/gui/ClaimSnapshot.java new file mode 100644 index 0000000..6feebb3 --- /dev/null +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/gui/ClaimSnapshot.java @@ -0,0 +1,13 @@ +package com.skyblockexp.ezrtp.gui; + +import java.util.UUID; + +/** + * Immutable snapshot of a faction claim's data, using only standard Java types so that + * {@link FactionClaimSelectionGuiManager} (a Bukkit {@code Listener}) can reference it + * without pulling TeamsAPI types into the class structure. + * + *

All TeamsAPI-dependent resolution (owner UUID, sorting) is performed up-front by + * {@link com.skyblockexp.ezrtp.integration.TeamsApiClaimFetcher} before a snapshot is created. + */ +public record ClaimSnapshot(String worldName, int chunkX, int chunkZ, UUID ownerUuid) {} diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/gui/FactionClaimSelectionGuiManager.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/gui/FactionClaimSelectionGuiManager.java index e75101e..bd3a45e 100644 --- a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/gui/FactionClaimSelectionGuiManager.java +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/gui/FactionClaimSelectionGuiManager.java @@ -4,7 +4,7 @@ import com.skyblockexp.ezrtp.config.EzRtpConfiguration; import com.skyblockexp.ezrtp.config.RandomTeleportSettings; import com.skyblockexp.ezrtp.config.gui.FactionGuiSettings; -import com.skyblockexp.ezrtp.integration.ClaimOwnerResolver; +import com.skyblockexp.ezrtp.integration.TeamsApiClaimFetcher; import com.skyblockexp.ezrtp.message.MessageKey; import com.skyblockexp.ezrtp.message.MessageProvider; import com.skyblockexp.ezrtp.platform.PlatformRuntimeRegistry; @@ -12,11 +12,6 @@ import com.skyblockexp.ezrtp.teleport.RandomTeleportService; import com.skyblockexp.ezrtp.teleport.TeleportReason; import com.skyblockexp.ezrtp.util.MessageUtil; -import com.skyblockexp.teamsapi.api.TeamsAPI; -import com.skyblockexp.teamsapi.api.TeamsClaimService; -import com.skyblockexp.teamsapi.api.TeamsService; -import com.skyblockexp.teamsapi.model.Team; -import com.skyblockexp.teamsapi.model.TeamClaim; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.configuration.MemoryConfiguration; @@ -35,8 +30,8 @@ import org.bukkit.inventory.meta.SkullMeta; import java.util.ArrayList; -import java.util.Comparator; import java.util.HashMap; +import java.util.Optional; import java.util.List; import java.util.Map; import java.util.UUID; @@ -64,40 +59,24 @@ public FactionClaimSelectionGuiManager(EzRtpPlugin plugin, } public boolean openSelection(Player player) { - if (player == null || !TeamsAPI.isAvailable() || !TeamsAPI.isClaimAvailable()) { + if (player == null) return true; + if (!plugin.getServer().getPluginManager().isPluginEnabled("TeamsAPI")) { MessageUtil.send(player, "TeamsAPI is not available on this server."); return true; } - - TeamsService teamsService = TeamsAPI.getService(); - TeamsClaimService claimService = TeamsAPI.getClaimService(); - if (teamsService == null || claimService == null) { - MessageUtil.send(player, "TeamsAPI services are not available right now."); - return true; - } - - Team team = teamsService.getPlayerTeam(player.getUniqueId()).orElse(null); - if (team == null) { - MessageUtil.send(player, "You are not in a faction/team."); - return true; - } - - List claims = new ArrayList<>(claimService.getTeamClaims(team.getId())); - if (claims.isEmpty()) { - MessageUtil.send(player, "Your faction/team has no claims."); - return true; - } - claims.sort(Comparator - .comparing(TeamClaim::getWorldName) - .thenComparingInt(TeamClaim::getChunkX) - .thenComparingInt(TeamClaim::getChunkZ)); - EzRtpConfiguration configuration = configurationSupplier.get(); if (configuration == null || !configuration.getFactionGuiSettings().enabled()) { MessageUtil.send(player, "Faction RTP GUI is disabled."); return true; } - openPage(player, new Session(claims, team.getDisplayName(), team.getId(), teamsService, 0)); + try { + Optional fetchResult = TeamsApiClaimFetcher.fetch(player); + if (fetchResult.isEmpty()) return true; + TeamsApiClaimFetcher.FetchResult data = fetchResult.get(); + openPage(player, new Session(data.claims(), data.teamDisplayName(), data.teamId(), 0)); + } catch (NoClassDefFoundError ignored) { + MessageUtil.send(player, "TeamsAPI is not available on this server."); + } return true; } @@ -129,11 +108,11 @@ public void onInventoryClick(InventoryClickEvent event) { } if (slot == session.settings.previousSlot() && session.page > 0) { - openPage(player, new Session(session.claims, session.teamDisplayName, session.teamId, session.teamsService, session.page - 1)); + openPage(player, new Session(session.claims, session.teamDisplayName, session.teamId, session.page - 1)); return; } if (slot == session.settings.nextSlot() && (session.page + 1) * session.claimsPerPage() < session.claims.size()) { - openPage(player, new Session(session.claims, session.teamDisplayName, session.teamId, session.teamsService, session.page + 1)); + openPage(player, new Session(session.claims, session.teamDisplayName, session.teamId, session.page + 1)); return; } if (slot >= session.claimsPerPage()) { @@ -145,7 +124,7 @@ public void onInventoryClick(InventoryClickEvent event) { return; } - TeamClaim claim = session.claims.get(claimIndex); + ClaimSnapshot claim = session.claims.get(claimIndex); teleportToClaimCenter(player, claim); } @@ -188,8 +167,8 @@ private void openPage(Player player, Session session) { int start = page * claimsPerPage; int end = Math.min(start + claimsPerPage, session.claims.size()); for (int i = start; i < end; i++) { - TeamClaim claim = session.claims.get(i); - inventory.setItem(i - start, createClaimItem(claim, i + 1, settings, session.teamsService)); + ClaimSnapshot claim = session.claims.get(i); + inventory.setItem(i - start, createClaimItem(claim, i + 1, settings)); } if (page > 0) { @@ -204,10 +183,10 @@ private void openPage(Player player, Session session) { player.openInventory(inventory); } - private void teleportToClaimCenter(Player player, TeamClaim claim) { - World claimWorld = plugin.getServer().getWorld(claim.getWorldName()); + private void teleportToClaimCenter(Player player, ClaimSnapshot claim) { + World claimWorld = plugin.getServer().getWorld(claim.worldName()); if (claimWorld == null) { - MessageUtil.send(player, "Claim world '" + claim.getWorldName() + "' is not loaded."); + MessageUtil.send(player, "Claim world '" + claim.worldName() + "' is not loaded."); return; } @@ -219,13 +198,13 @@ private void teleportToClaimCenter(Player player, TeamClaim claim) { return; } - RandomTeleportSettings baseSettings = configuration.getSettingsForWorld(claim.getWorldName()); + RandomTeleportSettings baseSettings = configuration.getSettingsForWorld(claim.worldName()); if (baseSettings == null) { - MessageUtil.send(player, "No RTP settings found for world '" + claim.getWorldName() + "'."); + MessageUtil.send(player, "No RTP settings found for world '" + claim.worldName() + "'."); return; } - String worldName = claim.getWorldName(); + String worldName = claim.worldName(); String group = configuration.resolveGroup(player, worldName); boolean bypass = player.isOp(); if (!bypass) { @@ -259,8 +238,8 @@ private void teleportToClaimCenter(Player player, TeamClaim claim) { MemoryConfiguration override = new MemoryConfiguration(); override.set("world", worldName); - override.set("center.x", claim.getChunkX() * 16 + 8); - override.set("center.z", claim.getChunkZ() * 16 + 8); + override.set("center.x", claim.chunkX() * 16 + 8); + override.set("center.z", claim.chunkZ() * 16 + 8); RandomTeleportSettings claimCentered = RandomTeleportSettings.fromConfiguration(override, plugin.getLogger(), baseSettings); player.closeInventory(); @@ -274,8 +253,8 @@ private void teleportToClaimCenter(Player player, TeamClaim claim) { }); } - private ItemStack createClaimItem(TeamClaim claim, int index, FactionGuiSettings settings, TeamsService teamsService) { - ItemStack item = createClaimIcon(claim, settings, teamsService); + private ItemStack createClaimItem(ClaimSnapshot claim, int index, FactionGuiSettings settings) { + ItemStack item = createClaimIcon(claim, settings); ItemMeta meta = item.getItemMeta(); if (meta != null) { meta.setDisplayName(replaceClaimPlaceholders(settings.claimNameFormat(), claim, index)); @@ -289,18 +268,18 @@ private ItemStack createClaimItem(TeamClaim claim, int index, FactionGuiSettings return item; } - private ItemStack createClaimIcon(TeamClaim claim, FactionGuiSettings settings, TeamsService teamsService) { + private ItemStack createClaimIcon(ClaimSnapshot claim, FactionGuiSettings settings) { if (!settings.skullEnabled()) { return new ItemStack(settings.fallbackMaterial()); } - UUID preferredOwner = ClaimOwnerResolver.resolvePreferredOwnerUuid(teamsService, claim).orElse(null); - if (preferredOwner == null) { + UUID ownerUuid = claim.ownerUuid(); + if (ownerUuid == null) { return new ItemStack(settings.fallbackMaterial()); } ItemStack skull = new ItemStack(Material.PLAYER_HEAD); ItemMeta meta = skull.getItemMeta(); if (meta instanceof SkullMeta skullMeta) { - OfflinePlayer offlinePlayer = plugin.getServer().getOfflinePlayer(preferredOwner); + OfflinePlayer offlinePlayer = plugin.getServer().getOfflinePlayer(ownerUuid); skullMeta.setOwningPlayer(offlinePlayer); skull.setItemMeta(skullMeta); return skull; @@ -318,14 +297,14 @@ private ItemStack createNavItem(Material material, String title) { return item; } - private String replaceClaimPlaceholders(String text, TeamClaim claim, int index) { + private String replaceClaimPlaceholders(String text, ClaimSnapshot claim, int index) { return text .replace("", String.valueOf(index)) - .replace("", claim.getWorldName()) - .replace("", String.valueOf(claim.getChunkX())) - .replace("", String.valueOf(claim.getChunkZ())) - .replace("", String.valueOf(claim.getChunkX() * 16 + 8)) - .replace("", String.valueOf(claim.getChunkZ() * 16 + 8)); + .replace("", claim.worldName()) + .replace("", String.valueOf(claim.chunkX())) + .replace("", String.valueOf(claim.chunkZ())) + .replace("", String.valueOf(claim.chunkX() * 16 + 8)) + .replace("", String.valueOf(claim.chunkZ() * 16 + 8)); } private String format(String titleTemplate, int page, int pages) { @@ -333,19 +312,17 @@ private String format(String titleTemplate, int page, int pages) { } private static final class Session { - private final List claims; + private final List claims; private final String teamDisplayName; private final UUID teamId; - private final TeamsService teamsService; private final int page; private FactionGuiSettings settings; private Inventory inventory; - private Session(List claims, String teamDisplayName, UUID teamId, TeamsService teamsService, int page) { + private Session(List claims, String teamDisplayName, UUID teamId, int page) { this.claims = claims; this.teamDisplayName = teamDisplayName; this.teamId = teamId; - this.teamsService = teamsService; this.page = page; } diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiClaimFetcher.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiClaimFetcher.java new file mode 100644 index 0000000..c5cfdfb --- /dev/null +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiClaimFetcher.java @@ -0,0 +1,86 @@ +package com.skyblockexp.ezrtp.integration; + +import com.skyblockexp.ezrtp.gui.ClaimSnapshot; +import com.skyblockexp.ezrtp.util.MessageUtil; +import com.skyblockexp.teamsapi.api.TeamsAPI; +import com.skyblockexp.teamsapi.api.TeamsClaimService; +import com.skyblockexp.teamsapi.api.TeamsService; +import com.skyblockexp.teamsapi.model.Team; +import com.skyblockexp.teamsapi.model.TeamClaim; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Fetches a player's faction claim data from TeamsAPI and converts it to {@link ClaimSnapshot} + * DTOs that contain only standard Java types. + * + *

This class intentionally holds all TeamsAPI type references so that + * {@link com.skyblockexp.ezrtp.gui.FactionClaimSelectionGuiManager} — a Bukkit + * {@code Listener} — can call {@link #fetch(Player)} from a method body without having + * TeamsAPI types appear in its own class structure (field descriptors, method descriptors). + * Bukkit's event registration uses reflection to process listener classes; any unresolvable + * type in a method descriptor causes {@code NoClassDefFoundError} during + * {@code registerEvents}. By isolating those references here, the listener loads cleanly even + * when TeamsAPI is absent. + */ +public final class TeamsApiClaimFetcher { + + private TeamsApiClaimFetcher() {} + + /** + * Fetches the player's team claims as {@link ClaimSnapshot} instances, resolving owner UUIDs + * and sorting the results. Sends an appropriate error message to the player and returns + * {@link Optional#empty()} if TeamsAPI is unavailable or any required data is missing. + * + * @param player the player whose team claims to load + * @return an Optional containing the fetch result, or empty if any precondition fails + */ + public static Optional fetch(Player player) { + if (!TeamsAPI.isAvailable() || !TeamsAPI.isClaimAvailable()) { + MessageUtil.send(player, "TeamsAPI is not available on this server."); + return Optional.empty(); + } + + TeamsService teamsService = TeamsAPI.getService(); + TeamsClaimService claimService = TeamsAPI.getClaimService(); + if (teamsService == null || claimService == null) { + MessageUtil.send(player, "TeamsAPI services are not available right now."); + return Optional.empty(); + } + + Team team = teamsService.getPlayerTeam(player.getUniqueId()).orElse(null); + if (team == null) { + MessageUtil.send(player, "You are not in a faction/team."); + return Optional.empty(); + } + + Collection rawClaims = claimService.getTeamClaims(team.getId()); + if (rawClaims.isEmpty()) { + MessageUtil.send(player, "Your faction/team has no claims."); + return Optional.empty(); + } + + List snapshots = new ArrayList<>(rawClaims.size()); + for (TeamClaim claim : rawClaims) { + UUID ownerUuid = ClaimOwnerResolver.resolvePreferredOwnerUuid(teamsService, claim).orElse(null); + snapshots.add(new ClaimSnapshot(claim.getWorldName(), claim.getChunkX(), claim.getChunkZ(), ownerUuid)); + } + snapshots.sort( + Comparator.comparing(ClaimSnapshot::worldName) + .thenComparingInt(ClaimSnapshot::chunkX) + .thenComparingInt(ClaimSnapshot::chunkZ)); + + return Optional.of(new FetchResult(snapshots, team.getDisplayName(), team.getId())); + } + + /** + * Holds the resolved claim data returned by {@link #fetch(Player)}. + */ + public record FetchResult(List claims, String teamDisplayName, UUID teamId) {} +} From eacda4827f11bcc2b403888a9cc2e7718aebc091 Mon Sep 17 00:00:00 2001 From: ez-plugins Date: Sun, 24 May 2026 13:09:08 +0200 Subject: [PATCH 4/4] fix: remove TeamsSubcommand from TeamsApiSubcommandBridge class structure When TeamsAPI was absent, constructing TeamsApiSubcommandBridge threw NoClassDefFoundError for com/skyblockexp/teamsapi/api/TeamsSubcommand. The JVM's bytecode verifier resolved the checkcast instruction in unregister() at class-load time, before any runtime isPluginEnabled guard could fire. Fix: extract all TeamsAPI type references (registerSubcommand call, unregisterSubcommand call, TeamsSubcommand cast, RtpTeamsSubcommand construction) into a new TeamsApiSubcommandOps class. TeamsApiSubcommandBridge now has zero TeamsAPI imports and delegates from method bodies only, so TeamsApiSubcommandOps is loaded lazily after the isPluginEnabled guard. Regression test: teamsApiSubcommandBridge_register_withoutTeamsApi_doesNotThrow added to SpigotStartupSmokeTest. --- .../platform/SpigotStartupSmokeTest.java | 34 ++++++++++++++++++ .../integration/TeamsApiSubcommandBridge.java | 18 ++++++---- .../integration/TeamsApiSubcommandOps.java | 36 +++++++++++++++++++ 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandOps.java 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 index 0308e2e..a305b27 100644 --- a/ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/SpigotStartupSmokeTest.java +++ b/ezrtp-bukkit/src/test/java/com/skyblockexp/ezrtp/platform/SpigotStartupSmokeTest.java @@ -2,6 +2,7 @@ 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; @@ -156,4 +157,37 @@ void factionClaimGui_openSelection_withNullPlayer_doesNotThrow() { 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-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandBridge.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandBridge.java index 0d17400..8d802c8 100644 --- a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandBridge.java +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandBridge.java @@ -1,12 +1,20 @@ package com.skyblockexp.ezrtp.integration; import com.skyblockexp.ezrtp.gui.FactionClaimSelectionGuiManager; -import com.skyblockexp.teamsapi.api.TeamsAPI; -import com.skyblockexp.teamsapi.api.TeamsSubcommand; import org.bukkit.plugin.Plugin; import java.util.logging.Level; +/** + * Manages the lifecycle of the optional TeamsAPI {@code /f rtp} subcommand. + * + *

This class intentionally contains zero TeamsAPI imports. All TeamsAPI type + * references are confined to {@link TeamsApiSubcommandOps}, which is loaded lazily only when its + * static methods are invoked from inside method bodies here. This prevents + * {@link NoClassDefFoundError} on servers where TeamsAPI is not installed: the JVM's bytecode + * verifier would otherwise attempt to resolve {@code TeamsSubcommand} from this class's constant + * pool at class-load time. + */ public final class TeamsApiSubcommandBridge { private final Plugin plugin; @@ -23,9 +31,7 @@ public void register() { return; } try { - RtpTeamsSubcommand subcommand = new RtpTeamsSubcommand(factionClaimGuiManager); - TeamsAPI.registerSubcommand(plugin, subcommand); - registeredSubcommand = subcommand; + registeredSubcommand = TeamsApiSubcommandOps.registerSubcommand(plugin, factionClaimGuiManager); plugin.getLogger().info("TeamsAPI integration: registered '/f rtp' subcommand."); } catch (NoClassDefFoundError ignored) { // TeamsAPI present as a plugin but classes unexpectedly missing. @@ -39,7 +45,7 @@ public void unregister() { return; } try { - TeamsAPI.unregisterSubcommand((TeamsSubcommand) registeredSubcommand); + TeamsApiSubcommandOps.unregisterSubcommand(registeredSubcommand); } catch (Throwable throwable) { plugin.getLogger().log(Level.FINE, "Failed to unregister TeamsAPI subcommand cleanly.", throwable); } finally { diff --git a/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandOps.java b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandOps.java new file mode 100644 index 0000000..be83a08 --- /dev/null +++ b/ezrtp-common/src/main/java/com/skyblockexp/ezrtp/integration/TeamsApiSubcommandOps.java @@ -0,0 +1,36 @@ +package com.skyblockexp.ezrtp.integration; + +import com.skyblockexp.ezrtp.gui.FactionClaimSelectionGuiManager; +import com.skyblockexp.teamsapi.api.TeamsAPI; +import com.skyblockexp.teamsapi.api.TeamsSubcommand; +import org.bukkit.plugin.Plugin; + +/** + * Holds all {@code TeamsAPI} type references for subcommand registration so that + * {@link TeamsApiSubcommandBridge} can remain free of those imports. + * + *

This class is loaded lazily — only when its static methods are actually invoked from inside + * {@link TeamsApiSubcommandBridge} method bodies — which prevents {@link NoClassDefFoundError} + * on servers where TeamsAPI is not installed. + */ +final class TeamsApiSubcommandOps { + + private TeamsApiSubcommandOps() {} + + /** + * Creates an {@link RtpTeamsSubcommand} and registers it with TeamsAPI. + * + * @return the registered subcommand instance (typed as {@link Object} so the caller need not + * import any TeamsAPI type) + */ + static Object registerSubcommand(Plugin plugin, FactionClaimSelectionGuiManager gui) { + RtpTeamsSubcommand subcommand = new RtpTeamsSubcommand(gui); + TeamsAPI.registerSubcommand(plugin, subcommand); + return subcommand; + } + + /** Unregisters a previously registered subcommand. */ + static void unregisterSubcommand(Object subcommand) { + TeamsAPI.unregisterSubcommand((TeamsSubcommand) subcommand); + } +}