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" }