From 8a591d90d7dd11e55c90d7b7c1a48292ad511854 Mon Sep 17 00:00:00 2001 From: demengc Date: Sun, 17 May 2026 13:40:07 -0700 Subject: [PATCH 1/2] refactor: strip shaded Adventure, declare it compileOnly - Remove the three shaded Adventure libraries (adventure-api, adventure-platform-bukkit, adventure-text-minimessage) and the three matching shadowJar relocations - Declare adventure-api, adventure-text-minimessage, and adventure-text-serializer-legacy as compileOnly; Paper provides them at runtime, Spigot does not - Delete BukkitAudiences plumbing from BaseManager and BasePlugin - Add BasePlugin#isPaper() backed by a testable detectPaper(Function>) predicate - Isolate Adventure usage in Text behind a private static AdventureBridge nested class with a LinkageError -> UnsupportedOperationException boundary, so loading Text on Spigot does not eagerly trigger NoClassDefFoundError - Replace Text.MINI_MESSAGE field with Text.miniMessage() getter - Use ((Audience) player).sendMessage(component) in tellComponent (Paper's Player implements Audience) - Swap BukkitComponentSerializer.legacy() for LegacyComponentSerializer.legacySection() in legacySerialize - Add testImplementation entries so Adventure happy-path tests can resolve types on the build host --- gradle/libs.versions.toml | 25 +++-- pluginbase-core/build.gradle.kts | 19 ++-- .../demeng/pluginbase/plugin/BaseManager.java | 4 - .../demeng/pluginbase/plugin/BasePlugin.java | 33 ++++-- .../java/dev/demeng/pluginbase/text/Text.java | 103 +++++++++++++++--- .../plugin/BasePluginDetectionTest.java | 32 ++++++ .../pluginbase/text/TextAdventureTest.java | 40 +++++++ 7 files changed, 205 insertions(+), 51 deletions(-) create mode 100644 pluginbase-core/src/test/java/dev/demeng/pluginbase/plugin/BasePluginDetectionTest.java create mode 100644 pluginbase-core/src/test/java/dev/demeng/pluginbase/text/TextAdventureTest.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 264496c..0743608 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,34 +1,35 @@ [versions] shadow = "9.4.1" -spotless = "8.4.0" +spotless = "8.5.1" errorprone-plugin = "5.1.0" -errorprone-core = "2.35.1" -nmcp = "1.4.4" +errorprone-core = "2.42.0" +nmcp = "1.5.0" -spigot-api = "1.21.11-R0.1-SNAPSHOT" +spigot-api = "26.1.2-R0.1-SNAPSHOT" annotations = "26.1.0" jsr305 = "3.0.2" -lombok = "1.18.44" +lombok = "1.18.46" authlib = "6.0.57" lamp = "4.0.0-rc.16" -xseries = "13.6.0" +xseries = "13.7.0" adventure-api = "4.26.1" -adventure-platform = "4.4.1" +adventure-text-minimessage = "4.26.1" +adventure-text-serializer-legacy = "4.26.1" expiringmap = "0.5.11" hikaricp = "7.0.2" sqlstreams = "1.0.0" -mongodb-driver = "5.6.4" +mongodb-driver = "5.7.0" -jedis = "7.4.1" +jedis = "7.5.0" commons-pool2 = "2.13.1" junit-jupiter = "6.0.3" assertj = "3.27.7" mockito = "5.23.0" -guava = "33.4.8-jre" +guava = "33.6.0-jre" [libraries] spigot-api = { module = "org.spigotmc:spigot-api", version.ref = "spigot-api" } @@ -42,8 +43,8 @@ lamp-bukkit = { module = "io.github.revxrsal:lamp.bukkit", version.ref = "lamp" lamp-brigadier = { module = "io.github.revxrsal:lamp.brigadier", version.ref = "lamp" } xseries = { module = "com.github.cryptomorin:XSeries", version.ref = "xseries" } adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure-api" } -adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } -adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-api" } +adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-text-minimessage" } +adventure-text-serializer-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure-text-serializer-legacy" } expiringmap = { module = "net.jodah:expiringmap", version.ref = "expiringmap" } hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikaricp" } diff --git a/pluginbase-core/build.gradle.kts b/pluginbase-core/build.gradle.kts index 73d4925..cbe0bd7 100644 --- a/pluginbase-core/build.gradle.kts +++ b/pluginbase-core/build.gradle.kts @@ -4,27 +4,24 @@ plugins { dependencies { compileOnly(libs.authlib) + compileOnly(libs.adventure.api) + compileOnly(libs.adventure.text.minimessage) + compileOnly(libs.adventure.text.serializer.legacy) implementation(libs.bundles.lamp) implementation(libs.xseries) - implementation(libs.adventure.api) { - exclude(group = "net.kyori", module = "adventure-bom") - } - implementation(libs.adventure.platform.bukkit) { - exclude(group = "net.kyori", module = "adventure-bom") - exclude(group = "net.kyori", module = "adventure-api") - } - implementation(libs.adventure.text.minimessage) implementation(libs.expiringmap) + + testImplementation(libs.spigot.api) + testImplementation(libs.adventure.api) + testImplementation(libs.adventure.text.minimessage) + testImplementation(libs.adventure.text.serializer.legacy) } tasks.shadowJar { relocate("revxrsal.commands", "dev.demeng.pluginbase.lib.lamp") relocate("com.cryptomorin.xseries", "dev.demeng.pluginbase.lib.xseries") relocate("net.jodah.expiringmap", "dev.demeng.pluginbase.lib.expiringmap") - relocate("net.kyori.adventure", "dev.demeng.pluginbase.lib.adventure") - relocate("net.kyori.examination", "dev.demeng.pluginbase.lib.examination") - relocate("net.kyori.option", "dev.demeng.pluginbase.lib.option") relocate("com.google.auto.service", "dev.demeng.pluginbase.lib.autoservice") relocate("org.jspecify.annotations", "dev.demeng.pluginbase.lib.jspecify") diff --git a/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BaseManager.java b/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BaseManager.java index e70e489..efaed6a 100644 --- a/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BaseManager.java +++ b/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BaseManager.java @@ -31,7 +31,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import net.kyori.adventure.platform.bukkit.BukkitAudiences; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; @@ -49,9 +48,6 @@ public final class BaseManager { /** The translator used for handling localized messages. */ @NotNull @Getter @Setter private static volatile Translator translator; - /** The BukkitAudiences instance to use for Adventure. Should be set when your plugin enables. */ - @Getter @Setter private static volatile BukkitAudiences adventure; - /** The settings the library should use. */ @NotNull @Getter @Setter private static volatile BaseSettings baseSettings = new BaseSettings() {}; diff --git a/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BasePlugin.java b/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BasePlugin.java index 7ceb001..c2e6369 100644 --- a/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BasePlugin.java +++ b/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BasePlugin.java @@ -25,6 +25,7 @@ package dev.demeng.pluginbase.plugin; import dev.demeng.pluginbase.BaseSettings; +import dev.demeng.pluginbase.Common; import dev.demeng.pluginbase.Schedulers; import dev.demeng.pluginbase.ServerProperties; import dev.demeng.pluginbase.Services; @@ -40,7 +41,7 @@ import dev.demeng.pluginbase.terminable.module.TerminableModule; import java.util.Objects; import java.util.concurrent.TimeUnit; -import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import java.util.function.Function; import org.bukkit.event.Listener; import org.bukkit.plugin.ServicePriority; import org.bukkit.plugin.java.JavaPlugin; @@ -63,6 +64,11 @@ public abstract class BasePlugin extends JavaPlugin implements TerminableConsume /** The dependency injection container for this plugin. */ private DependencyContainer dependencyContainer; + /** + * Cached result of Paper-server detection; {@code null} until {@link #isPaper()} is first called. + */ + private static volatile Boolean paperCache; + @Override public final void onLoad() { BaseManager.setPlugin(this); @@ -85,7 +91,6 @@ public final void onEnable() { .bindWith(this.terminableRegistry); BaseManager.setTranslator(Translator.create()); - BaseManager.setAdventure(BukkitAudiences.create(this)); bindModule(new MenuManager()); @@ -97,11 +102,6 @@ public final void onDisable() { disable(); - if (getAdventure() != null) { - getAdventure().close(); - BaseManager.setAdventure(null); - } - this.terminableRegistry.closeAndReportException(); BaseExecutors.shutdown(); @@ -292,12 +292,23 @@ public Translator getTranslator() { } /** - * Gets the BukkitAudiences instance to use for Adventure. + * Whether the server is running Paper (or a Paper fork like Pufferfish, Purpur, Folia). Returns + * {@code false} on vanilla Spigot or Bukkit. Cached after the first call. Detection looks for + * {@code io.papermc.paper.ServerBuildInfo}, which has shipped in Paper since 2024. * - * @return The BukkitAudiences instance to use for Adventure + * @return true on Paper-derived servers, false otherwise */ - public BukkitAudiences getAdventure() { - return BaseManager.getAdventure(); + public boolean isPaper() { + Boolean cached = paperCache; + if (cached == null) { + cached = detectPaper(Common::checkClass); + paperCache = cached; + } + return cached; + } + + static boolean detectPaper(@NotNull final Function> classLookup) { + return classLookup.apply("io.papermc.paper.ServerBuildInfo") != null; } /** diff --git a/pluginbase-core/src/main/java/dev/demeng/pluginbase/text/Text.java b/pluginbase-core/src/main/java/dev/demeng/pluginbase/text/Text.java index 9ac7fb5..9015215 100644 --- a/pluginbase-core/src/main/java/dev/demeng/pluginbase/text/Text.java +++ b/pluginbase-core/src/main/java/dev/demeng/pluginbase/text/Text.java @@ -41,9 +41,10 @@ import java.util.stream.Collectors; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer; +import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -77,8 +78,6 @@ public final class Text { public static final String CONSOLE_LINE = "*-----------------------------------------------------*"; - public static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build(); - // --------------------------------------------------------------------------------- // LOCALE // --------------------------------------------------------------------------------- @@ -428,21 +427,42 @@ public static String error(@Nullable final String str) { return colorize(getPrefix() + "&c" + str); } + /** + * Returns the {@link MiniMessage} instance used for parsing. + * + *

Throws {@link UnsupportedOperationException} on servers without Adventure (vanilla + * Spigot/Bukkit). + * + * @return the MiniMessage instance + */ + @NotNull + public static MiniMessage miniMessage() { + try { + return AdventureBridge.MINI; + } catch (final LinkageError e) { + throw new UnsupportedOperationException( + "Text.miniMessage() requires Paper (Adventure is not available on Spigot)", e); + } + } + /** * Parses the string using the MiniMessage library. Format: ... * + *

Throws {@link UnsupportedOperationException} on servers without Adventure (vanilla + * Spigot/Bukkit). + * * @param str The raw string * @return The result component for the string, or empty if the provided string is null */ @NotNull public static Component parseMini(@Nullable final String str) { - - if (str == null) { - return Component.empty(); + try { + return AdventureBridge.parseMini(str); + } catch (final LinkageError e) { + throw new UnsupportedOperationException( + "Text.parseMini(String) requires Paper (Adventure is not available on Spigot)", e); } - - return MINI_MESSAGE.deserialize(str); } /** @@ -459,8 +479,11 @@ public static String legacyParseMini(@Nullable final String str) { } /** - * Serializes an Adventure {@link Component} using the legacy Bukkit Component Serializer, which - * can be useful for displaying components in areas other than the chat (ex. item names). + * Serializes an Adventure {@link Component} using the legacy section-prefix serializer, which can + * be useful for displaying components in areas other than the chat (ex. item names). + * + *

Throws {@link UnsupportedOperationException} on servers without Adventure (vanilla + * Spigot/Bukkit). * * @param component The component to serialize * @return The serialized component @@ -468,7 +491,13 @@ public static String legacyParseMini(@Nullable final String str) { */ @NotNull public static String legacySerialize(@NotNull final Component component) { - return BukkitComponentSerializer.legacy().serialize(component); + try { + return AdventureBridge.legacySerialize(component); + } catch (final LinkageError e) { + throw new UnsupportedOperationException( + "Text.legacySerialize(Component) requires Paper (Adventure is not available on Spigot)", + e); + } } /** @@ -727,13 +756,23 @@ public static void tellLocalizedRaw( /** * Sends the {@link Component} to the player as a chat message. * + *

Throws {@link UnsupportedOperationException} on servers without Adventure (vanilla + * Spigot/Bukkit). + * * @param player The player who should receive the component * @param component The component to send * @see #parseMini(String) */ public static void tellComponent( @NotNull final Player player, @NotNull final Component component) { - BaseManager.getAdventure().player(player).sendMessage(component); + try { + AdventureBridge.tell(player, component); + } catch (final LinkageError e) { + throw new UnsupportedOperationException( + "Text.tellComponent(Player, Component) requires Paper" + + " (Adventure is not available on Spigot)", + e); + } } /** @@ -897,7 +936,45 @@ private static boolean attemptTellMini(final CommandSender sender, final String return false; } - tellComponent((Player) sender, parseMini(str.replaceFirst(MINI_PREFIX, ""))); + try { + AdventureBridge.routeMini((Player) sender, str.replaceFirst(MINI_PREFIX, "")); + } catch (final LinkageError e) { + throw new UnsupportedOperationException( + "Text.tell(...) with 'mini:' prefix requires Paper" + + " (Adventure is not available on Spigot)", + e); + } return true; } + + private static final class AdventureBridge { + + private static final MiniMessage MINI = MiniMessage.miniMessage(); + + private AdventureBridge() {} + + @NotNull + static Component parseMini(@Nullable final String str) { + if (str == null) { + return Component.empty(); + } + return MINI.deserialize(str); + } + + @NotNull + static String legacySerialize(@NotNull final Component component) { + return LegacyComponentSerializer.legacySection().serialize(component); + } + + static void tell(@NotNull final Player player, @NotNull final Component component) { + // Paper's Player implements net.kyori.adventure.audience.Audience. + // On Spigot, this whole class fails to load (Audience missing) and the + // calling boundary catches the resulting LinkageError. + ((Audience) player).sendMessage(component); + } + + static void routeMini(@NotNull final Player player, @NotNull final String stripped) { + tell(player, parseMini(stripped)); + } + } } diff --git a/pluginbase-core/src/test/java/dev/demeng/pluginbase/plugin/BasePluginDetectionTest.java b/pluginbase-core/src/test/java/dev/demeng/pluginbase/plugin/BasePluginDetectionTest.java new file mode 100644 index 0000000..b8d8407 --- /dev/null +++ b/pluginbase-core/src/test/java/dev/demeng/pluginbase/plugin/BasePluginDetectionTest.java @@ -0,0 +1,32 @@ +package dev.demeng.pluginbase.plugin; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +class BasePluginDetectionTest { + + @Test + void detectPaper_returnsTrue_whenServerBuildInfoClassResolves() { + Function> lookup = + name -> name.equals("io.papermc.paper.ServerBuildInfo") ? Object.class : null; + + assertThat(BasePlugin.detectPaper(lookup)).isTrue(); + } + + @Test + void detectPaper_returnsFalse_whenServerBuildInfoMissing() { + Function> lookup = name -> null; + + assertThat(BasePlugin.detectPaper(lookup)).isFalse(); + } + + @Test + void detectPaper_isFalse_whenLookupReturnsForUnrelatedClass() { + Function> lookup = + name -> name.equals("java.lang.String") ? String.class : null; + + assertThat(BasePlugin.detectPaper(lookup)).isFalse(); + } +} diff --git a/pluginbase-core/src/test/java/dev/demeng/pluginbase/text/TextAdventureTest.java b/pluginbase-core/src/test/java/dev/demeng/pluginbase/text/TextAdventureTest.java new file mode 100644 index 0000000..4f00ec3 --- /dev/null +++ b/pluginbase-core/src/test/java/dev/demeng/pluginbase/text/TextAdventureTest.java @@ -0,0 +1,40 @@ +package dev.demeng.pluginbase.text; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.junit.jupiter.api.Test; + +class TextAdventureTest { + + @Test + void miniMessage_returnsNonNullInstance() { + MiniMessage instance = Text.miniMessage(); + assertThat(instance).isNotNull(); + } + + @Test + void miniMessage_returnsSameInstanceAcrossCalls() { + assertThat(Text.miniMessage()).isSameAs(Text.miniMessage()); + } + + @Test + void parseMini_returnsEmptyComponent_whenInputIsNull() { + assertThat(Text.parseMini(null)).isEqualTo(Component.empty()); + } + + @Test + void parseMini_parsesMiniMessageMarkup() { + Component result = Text.parseMini("hello"); + String legacy = Text.legacySerialize(result); + assertThat(legacy).contains("hello"); + assertThat(legacy).startsWith("§c"); // §c == red in legacy section format + } + + @Test + void legacySerialize_roundTripsPlainText() { + Component plain = Component.text("hello"); + assertThat(Text.legacySerialize(plain)).isEqualTo("hello"); + } +} From c04bf7b35eecdaea019780ec80908a44e849648b Mon Sep 17 00:00:00 2001 From: demengc Date: Sun, 17 May 2026 14:45:48 -0700 Subject: [PATCH 2/2] docs: flag Paper-only Adventure APIs after the shading strip Calls out that Text.parseMini, Text.tellComponent, Text.miniMessage(), Text.legacySerialize, Text.legacyParseMini, and the "mini:" prefix in Text.tell/tellRaw/tellLocalized throw UnsupportedOperationException on vanilla Spigot, since Adventure is no longer shaded. - Text class-level Javadoc enumerates the six Paper-only entry points - legacyParseMini Javadoc inherits the same caveat from its delegates - BasePlugin#isPaper() Javadoc clarifies it is a downstream-facing guard (Text uses a LinkageError boundary internally) and notes the 2024 ServerBuildInfo lower bound - README and docs site call out the Paper requirement; stale upper version pin removed from docs/README.md - Drop redundant non-null assertion from TextAdventureTest (the isSameAs identity check already covers it) --- README.md | 3 ++ docs/README.md | 8 +++-- docs/core-features/text-and-localization.md | 12 ++++++-- .../demeng/pluginbase/plugin/BasePlugin.java | 9 +++++- .../java/dev/demeng/pluginbase/text/Text.java | 29 +++++++++++++++++-- .../pluginbase/text/TextAdventureTest.java | 7 ----- 6 files changed, 52 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ead6fa5..0fc7bae 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,9 @@ of handling objects. - **Java 17+** - **Spigot 1.8.8+** (compiled against latest, runtime-compatible back to 1.8.8) +- **Paper** for Adventure-based APIs (`Text.parseMini`, `Text.tellComponent`, `mini:` message + prefix). These methods throw `UnsupportedOperationException` on vanilla Spigot. Use + `BasePlugin#isPaper()` to gate calls if you target both platforms. ## Documentation diff --git a/docs/README.md b/docs/README.md index 53136b1..d2170a1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@ --- -description: Spigot plugin framework with DI, commands, menus, schedulers, and cross-version support for Minecraft 1.8.8 - 1.21.11. +description: Spigot plugin framework with DI, commands, menus, schedulers, and cross-version support back to Minecraft 1.8.8. --- # Introduction @@ -30,7 +30,11 @@ PluginBase streamlines Spigot plugin development by providing ready-to-use compo ## Version Compatibility -Minecraft 1.8.8 - 1.21.11. Java 17+. +Minecraft 1.8.8+. Java 17+. + +Adventure-based APIs (`Text.parseMini`, `Text.tellComponent`, `mini:` message prefix) require +Paper at runtime and throw `UnsupportedOperationException` on vanilla Spigot. All other features +work on both Paper and Spigot. ## Quick Start diff --git a/docs/core-features/text-and-localization.md b/docs/core-features/text-and-localization.md index 3725227..187dbda 100644 --- a/docs/core-features/text-and-localization.md +++ b/docs/core-features/text-and-localization.md @@ -12,7 +12,7 @@ The `Text` utility class handles all message formatting and sending. It supports |---|---|---|---| | Legacy codes | `&` + code | `&a` (green), `&l` (bold) | Any version | | HEX colors | `<#RRGGBB>` | `<#FF5733>` | Spigot 1.16+ | -| MiniMessage | `mini:` prefix | `mini:text` | Adventure | +| MiniMessage | `mini:` prefix | `mini:text` | Paper (Adventure) | | Color scheme | `&p`, `&s`, `&t` | `&pPrimary color` | `ColorScheme` configured | ### Legacy and HEX Colors @@ -49,7 +49,7 @@ Text.tell(player, "&pPrimary &sSecondary &tTertiary"); |---|---|---|---| | `tell(CommandSender, String)` | Yes | Yes | Standard message | | `tellRaw(CommandSender, String)` | No | Yes | Colors only, no prefix | -| `tellComponent(Player, Component)` | No | No | Sends an Adventure `Component` directly | +| `tellComponent(Player, Component)` | No | No | Sends an Adventure `Component` directly (Paper only) | | `tellCentered(Player, String)` | No | Yes | Centered in chat; may not work with custom fonts or HEX colors | ```java @@ -127,7 +127,13 @@ Component comp = Text.parseMini("Text"); Text.tellComponent(player, comp); ``` -The `MINI_MESSAGE` field on `Text` exposes the underlying `MiniMessage` instance if you need direct access. +Call `Text.miniMessage()` to access the underlying `MiniMessage` instance directly. + +> **Paper required.** `parseMini`, `legacyParseMini`, `legacySerialize`, `tellComponent`, +> `miniMessage()`, and the `mini:` prefix in `tell`/`tellRaw`/`tellLocalized`/`tellLocalizedRaw` +> all require a Paper-derived server (Paper, Folia, Pufferfish, Purpur). They throw +> `UnsupportedOperationException` on vanilla Spigot, where Adventure is not provided. Use +> `BasePlugin#isPaper()` to gate calls if you target both platforms. ## Formatting Utilities diff --git a/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BasePlugin.java b/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BasePlugin.java index c2e6369..489e661 100644 --- a/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BasePlugin.java +++ b/pluginbase-core/src/main/java/dev/demeng/pluginbase/plugin/BasePlugin.java @@ -294,7 +294,14 @@ public Translator getTranslator() { /** * Whether the server is running Paper (or a Paper fork like Pufferfish, Purpur, Folia). Returns * {@code false} on vanilla Spigot or Bukkit. Cached after the first call. Detection looks for - * {@code io.papermc.paper.ServerBuildInfo}, which has shipped in Paper since 2024. + * {@code io.papermc.paper.ServerBuildInfo}, which has shipped in Paper since 2024; older Paper + * builds will be reported as non-Paper. + * + *

This is exposed for downstream plugins that need to gate Adventure-using code (e.g. {@link + * dev.demeng.pluginbase.text.Text#parseMini(String)}, {@link + * dev.demeng.pluginbase.text.Text#tellComponent}). PluginBase itself does not consult this flag + * internally; {@code Text} relies on a {@link LinkageError} boundary instead so that detection + * cannot drift out of sync with actual class availability. * * @return true on Paper-derived servers, false otherwise */ diff --git a/pluginbase-core/src/main/java/dev/demeng/pluginbase/text/Text.java b/pluginbase-core/src/main/java/dev/demeng/pluginbase/text/Text.java index 9015215..9090a31 100644 --- a/pluginbase-core/src/main/java/dev/demeng/pluginbase/text/Text.java +++ b/pluginbase-core/src/main/java/dev/demeng/pluginbase/text/Text.java @@ -52,7 +52,27 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** Message-related utilities, including console and chat messages. */ +/** + * Message-related utilities, including console and chat messages. + * + *

Paper-only methods: the following methods touch Adventure and therefore require a + * Paper-derived server at runtime. They throw {@link UnsupportedOperationException} on vanilla + * Spigot or Bukkit, where Adventure is not provided: + * + *

    + *
  • {@link #miniMessage()} + *
  • {@link #parseMini(String)} + *
  • {@link #legacyParseMini(String)} (delegates to the above) + *
  • {@link #legacySerialize(Component)} + *
  • {@link #tellComponent(Player, Component)} + *
  • {@link #tell(CommandSender, String)} (and its {@code tellRaw}/{@code tellLocalized} + * siblings) only when the message starts with the {@code "mini:"} prefix + *
+ * + *

Plugins that target both Paper and Spigot should guard these calls with {@link + * dev.demeng.pluginbase.plugin.BasePlugin#isPaper()} or catch {@link + * UnsupportedOperationException}. + */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class Text { @@ -466,10 +486,13 @@ public static Component parseMini(@Nullable final String str) { } /** - * Parses the string using the MiniMessage library, and then uses the legacy Bukkit Component - * Serializer to return a String rather than a Component. Format: ... * + *

Throws {@link UnsupportedOperationException} on servers without Adventure (vanilla + * Spigot/Bukkit), inherited from {@link #parseMini(String)} and {@link #legacySerialize}. + * * @param str The raw string(s) * @return The serialized component for the string, or empty if the provided string is null */ diff --git a/pluginbase-core/src/test/java/dev/demeng/pluginbase/text/TextAdventureTest.java b/pluginbase-core/src/test/java/dev/demeng/pluginbase/text/TextAdventureTest.java index 4f00ec3..b1c3241 100644 --- a/pluginbase-core/src/test/java/dev/demeng/pluginbase/text/TextAdventureTest.java +++ b/pluginbase-core/src/test/java/dev/demeng/pluginbase/text/TextAdventureTest.java @@ -3,17 +3,10 @@ import static org.assertj.core.api.Assertions.assertThat; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.junit.jupiter.api.Test; class TextAdventureTest { - @Test - void miniMessage_returnsNonNullInstance() { - MiniMessage instance = Text.miniMessage(); - assertThat(instance).isNotNull(); - } - @Test void miniMessage_returnsSameInstanceAcrossCalls() { assertThat(Text.miniMessage()).isSameAs(Text.miniMessage());