Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions bungeecord/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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.
* <br>
* 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,26 +36,22 @@

@Getter
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 0 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
Loading