diff --git a/bungeecord/build.gradle.kts b/bungeecord/build.gradle.kts
index 05ef27151..cc09077b8 100644
--- a/bungeecord/build.gradle.kts
+++ b/bungeecord/build.gradle.kts
@@ -15,9 +15,6 @@ dependencies {
compileOnly(rootProject.libs.bungeecord)
- implementation(rootProject.libs.adventure.platform.bungee) {
- exclude(module = "adventure-nbt")
- }
implementation(rootProject.libs.bstats.bungee)
implementation(rootProject.libs.libby.bungee)
}
diff --git a/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/BungeeAudience.java b/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/BungeeAudience.java
new file mode 100644
index 000000000..683fb4f90
--- /dev/null
+++ b/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/BungeeAudience.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2026 Sonar Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.jonesdev.sonar.bungee;
+
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.kyori.adventure.title.Title;
+import net.md_5.bungee.api.ChatMessageType;
+import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.ProxyServer;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.chat.ComponentSerializer;
+import org.jetbrains.annotations.NotNull;
+import xyz.jonesdev.sonar.api.antibot.protocol.ProtocolVersion;
+
+import java.time.Duration;
+
+/**
+ * Minimal {@link Audience} implementation backed by the native BungeeCord chat API.
+ *
+ * BungeeCord does not ship Adventure, so we used to bundle adventure-platform-bungeecord (Adventure 4.x).
+ * That conflicted with the Adventure 5.x libraries Sonar loads at runtime, because both end up
+ * in the same relocated package. Converting components to JSON and passing them to BungeeCord's own
+ * {@link ComponentSerializer} allows us to depend on exactly one Adventure version.
+ */
+final class BungeeAudience implements Audience {
+ private final CommandSender sender;
+
+ BungeeAudience(final @NotNull CommandSender sender) {
+ this.sender = sender;
+ }
+
+ @Override
+ public void sendMessage(final @NotNull Component message) {
+ sender.sendMessage(toBungee(message));
+ }
+
+ @Override
+ public void sendActionBar(final @NotNull Component message) {
+ // Only players have an action bar
+ if (sender instanceof ProxiedPlayer player) {
+ player.sendMessage(ChatMessageType.ACTION_BAR, toBungee(message));
+ }
+ }
+
+ @Override
+ public void showTitle(final @NotNull Title title) {
+ // Only players can see titles
+ if (sender instanceof ProxiedPlayer player) {
+ final Title.Times times = title.times();
+ ProxyServer.getInstance().createTitle()
+ .title(toBungee(title.title()))
+ .subTitle(toBungee(title.subtitle()))
+ // Use the same defaults as Title#DEFAULT_TIMES (0.5s, 3.5s, 1s)
+ .fadeIn(times == null ? 10 : toTicks(times.fadeIn()))
+ .stay(times == null ? 70 : toTicks(times.stay()))
+ .fadeOut(times == null ? 20 : toTicks(times.fadeOut()))
+ .send(player);
+ }
+ }
+
+ private static int toTicks(final @NotNull Duration duration) {
+ return (int) (duration.toMillis() / 50L);
+ }
+
+ private @NotNull BaseComponent[] toBungee(final @NotNull Component component) {
+ // Downsample hex colors for clients that do not support them
+ final boolean legacyClient = sender instanceof ProxiedPlayer player
+ && player.getPendingConnection().getVersion() < ProtocolVersion.MINECRAFT_1_16.getProtocol();
+ final GsonComponentSerializer serializer = legacyClient
+ ? GsonComponentSerializer.colorDownsamplingGson()
+ : GsonComponentSerializer.gson();
+ return ComponentSerializer.parse(serializer.serialize(component));
+ }
+}
diff --git a/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/SonarBungee.java b/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/SonarBungee.java
index 77cc5fff7..de373bba7 100644
--- a/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/SonarBungee.java
+++ b/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/SonarBungee.java
@@ -20,8 +20,8 @@
import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
-import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.CommandSender;
+import net.md_5.bungee.api.connection.ProxiedPlayer;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
@@ -36,26 +36,22 @@
@Getter
public final class SonarBungee extends SonarBootstrap {
- public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
- super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), new BungeeLibraryManager(plugin));
+ public SonarBungee(final @NotNull SonarBungeePlugin plugin, final @NotNull BungeeLibraryManager libraryManager) {
+ super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), libraryManager);
}
- /**
- * Wrapper for BungeeCord audiences
- */
- private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
-
@Override
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return null;
}
- return bungeeAudiences.player(uniqueId);
+ final ProxiedPlayer player = getPlugin().getServer().getPlayer(uniqueId);
+ return player == null ? null : new BungeeAudience(player);
}
@Override
public @NotNull Audience sender(final @NotNull Object object) {
- return bungeeAudiences.sender((CommandSender) object);
+ return new BungeeAudience((CommandSender) object);
}
/**
diff --git a/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/SonarBungeePlugin.java b/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/SonarBungeePlugin.java
index 3f79cb2ed..fe80ca343 100644
--- a/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/SonarBungeePlugin.java
+++ b/bungeecord/src/main/java/xyz/jonesdev/sonar/bungee/SonarBungeePlugin.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 Sonar Contributors
+ * Copyright (C) 2026 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,9 +17,12 @@
package xyz.jonesdev.sonar.bungee;
+import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin;
+import xyz.jonesdev.sonar.api.SonarPlatform;
+import xyz.jonesdev.sonar.common.boot.LibraryLoader;
@Getter
public final class SonarBungeePlugin extends Plugin {
@@ -30,7 +33,13 @@ public final class SonarBungeePlugin extends Plugin {
public void onEnable() {
server = ProxyServer.getInstance();
- bootstrap = new SonarBungee(this);
+ // HACK:
+ // Adventure is not bundled in the jar, so the libraries have to be on the classpath
+ // before SonarBungee is loaded. The JVM resolves its Adventure types during class verification.
+ final BungeeLibraryManager libraryManager = new BungeeLibraryManager(this);
+ LibraryLoader.loadLibraries(libraryManager, SonarPlatform.BUNGEE);
+
+ bootstrap = new SonarBungee(this, libraryManager);
bootstrap.initialize();
}
diff --git a/common/src/main/java/xyz/jonesdev/sonar/common/boot/LibraryLoader.java b/common/src/main/java/xyz/jonesdev/sonar/common/boot/LibraryLoader.java
index dec610f63..f9d09c269 100644
--- a/common/src/main/java/xyz/jonesdev/sonar/common/boot/LibraryLoader.java
+++ b/common/src/main/java/xyz/jonesdev/sonar/common/boot/LibraryLoader.java
@@ -24,8 +24,8 @@
import xyz.jonesdev.sonar.api.SonarPlatform;
@UtilityClass
-class LibraryLoader {
- void loadLibraries(final @NotNull LibraryManager libraryManager, final @NotNull SonarPlatform platform) {
+public class LibraryLoader {
+ public void loadLibraries(final @NotNull LibraryManager libraryManager, final @NotNull SonarPlatform platform) {
libraryManager.addMavenCentral();
libraryManager.addJitPack();
libraryManager.loadLibraries(
@@ -69,25 +69,14 @@ void loadLibraries(final @NotNull LibraryManager libraryManager, final @NotNull
// Only load adventure if not on Velocity
if (platform != SonarPlatform.VELOCITY) {
if (platform != SonarPlatform.PAPER) {
+ // BungeeCord does not ship Adventure, and we do not bundle adventure-platform there anymore
+ // (mixing Adventure 4.x and 5.x in the same relocated package causes NoSuchMethodErrors).
+ // Therefore, adventure-api and everything else Adventure needs has to be resolved at runtime.
+ final boolean resolveTransitive = platform == SonarPlatform.BUNGEE;
libraryManager.loadLibraries(
- Library.builder()
- .groupId("net{}kyori")
- .artifactId("adventure-text-minimessage")
- .version("5.2.0")
- .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
- .build(),
- Library.builder()
- .groupId("net{}kyori")
- .artifactId("adventure-text-serializer-gson")
- .version("5.2.0")
- .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
- .build(),
- Library.builder()
- .groupId("net{}kyori")
- .artifactId("adventure-nbt")
- .version("5.2.0")
- .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
- .build()
+ adventureLibrary("adventure-text-minimessage", resolveTransitive),
+ adventureLibrary("adventure-text-serializer-gson", resolveTransitive),
+ adventureLibrary("adventure-nbt", resolveTransitive)
);
} else {
// On Paper, we only relocate the nbt package since the rest of adventure is bundled with the server.
@@ -102,4 +91,18 @@ void loadLibraries(final @NotNull LibraryManager libraryManager, final @NotNull
}
}
}
+
+ private @NotNull Library adventureLibrary(final @NotNull String artifactId, final boolean resolveTransitive) {
+ final Library.Builder builder = Library.builder()
+ .groupId("net{}kyori")
+ .artifactId(artifactId)
+ .version("5.2.0")
+ .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori");
+ if (resolveTransitive) {
+ builder.resolveTransitiveDependencies(true)
+ // Gson is already provided by the proxy, so we must not load another copy
+ .excludeTransitiveDependency("com.google.code.gson", "gson");
+ }
+ return builder.build();
+ }
}
diff --git a/common/src/main/java/xyz/jonesdev/sonar/common/boot/SonarBootstrap.java b/common/src/main/java/xyz/jonesdev/sonar/common/boot/SonarBootstrap.java
index ba7bb700f..af721008a 100644
--- a/common/src/main/java/xyz/jonesdev/sonar/common/boot/SonarBootstrap.java
+++ b/common/src/main/java/xyz/jonesdev/sonar/common/boot/SonarBootstrap.java
@@ -68,7 +68,13 @@ public SonarBootstrap(final @NotNull T plugin,
final @NotNull File dataDirectory,
final @NotNull LibraryManager libraryManager) {
// Load all libraries before anything else
- LibraryLoader.loadLibraries(libraryManager, platform);
+ // HACK:
+ // We manually load the libraries on BungeeCord beforehand,
+ // so we skip this call to avoid loading libraries twice.
+ // See SonarBungeePlugin.java#L40
+ if (platform != SonarPlatform.BUNGEE) {
+ LibraryLoader.loadLibraries(libraryManager, platform);
+ }
this.libraryManager = libraryManager;
// Check if Netty is up to date
ProtocolUtil.checkNettyVersion();
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3122068a0..6202c34e0 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -20,7 +20,6 @@ adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", versi
adventure-serializer = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" }
adventure-nbt = { module = "net.kyori:adventure-nbt", version.ref = "adventure" }
adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" }
-adventure-platform-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" }
libby-core = { module = "com.alessiodp.libby:libby-core", version.ref = "libby" }
libby-bungee = { module = "com.alessiodp.libby:libby-bungee", version.ref = "libby" }
libby-bukkit = { module = "com.alessiodp.libby:libby-bukkit", version.ref = "libby" }