diff --git a/api/src/main/java/xyz/jonesdev/sonar/api/SonarPlatform.java b/api/src/main/java/xyz/jonesdev/sonar/api/SonarPlatform.java index 02b3f2aa..fefffb69 100644 --- a/api/src/main/java/xyz/jonesdev/sonar/api/SonarPlatform.java +++ b/api/src/main/java/xyz/jonesdev/sonar/api/SonarPlatform.java @@ -30,6 +30,8 @@ public enum SonarPlatform { pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder"), BUNGEE("BungeeCord", 19109, "inbound-boss", pipeline -> "packet-encoder"), + PAPER("Paper", 19110, "packet_handler", + pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder"), VELOCITY("Velocity", 19107, "handler", pipeline -> "minecraft-encoder"); diff --git a/build.gradle.kts b/build.gradle.kts index 52cf78e6..889ff48a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -102,7 +102,7 @@ allprojects { tasks { // This is a small wrapper tasks to simplify the building process register("build-sonar") { - val subprojects = listOf("api", "captcha", "common", "bukkit", "bungeecord", "velocity") + val subprojects = listOf("api", "captcha", "common", "bukkit", "bungeecord", "paper", "velocity") val buildTasks = subprojects.flatMap { listOf("$it:clean", "$it:spotlessApply", "$it:shadowJar") } dependsOn(buildTasks) } 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 cda3773a..3fca756f 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 @@ -68,26 +68,38 @@ void loadLibraries(final @NotNull LibraryManager libraryManager, final @NotNull // Only load adventure if not on Velocity if (platform != SonarPlatform.VELOCITY) { - libraryManager.loadLibraries( - Library.builder() - .groupId("net{}kyori") - .artifactId("adventure-text-minimessage") - .version("4.21.0") - .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori") - .build(), - Library.builder() - .groupId("net{}kyori") - .artifactId("adventure-text-serializer-gson") - .version("4.21.0") - .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori") - .build(), - Library.builder() - .groupId("net{}kyori") - .artifactId("adventure-nbt") - .version("4.21.0") - .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori") - .build() - ); + if (platform != SonarPlatform.PAPER) { + libraryManager.loadLibraries( + Library.builder() + .groupId("net{}kyori") + .artifactId("adventure-text-minimessage") + .version("4.21.0") + .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori") + .build(), + Library.builder() + .groupId("net{}kyori") + .artifactId("adventure-text-serializer-gson") + .version("4.21.0") + .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori") + .build(), + Library.builder() + .groupId("net{}kyori") + .artifactId("adventure-nbt") + .version("4.21.0") + .relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori") + .build() + ); + } else { + // On Paper, we only relocate the nbt package since the rest of adventure is bundled with the server. + libraryManager.loadLibrary( + Library.builder() + .groupId("net{}kyori") + .artifactId("adventure-nbt") + .version("4.21.0") + .relocate("net{}kyori{}adventure{}nbt", "xyz{}jonesdev{}sonar{}libs{}kyori{}adventure{}nbt") + .build() + ); + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4f74104..8468ac05 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ shadow = "com.gradleup.shadow:8.3.8" spotless = "com.diffplug.spotless:7.2.1" pluginyml-bungee = { id = "net.minecrell.plugin-yml.bungee", version.ref = "pluginyml" } pluginyml-bukkit = { id = "net.minecrell.plugin-yml.bukkit", version.ref = "pluginyml" } +pluginyml-paper = { id = "net.minecrell.plugin-yml.paper", version.ref = "pluginyml" } [libraries] netty = { module = "io.netty:netty-all", version.ref = "netty" } @@ -23,6 +24,7 @@ adventure-platform-bungee = { module = "net.kyori:adventure-platform-bungeecord" 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" } +libby-paper = { module = "com.alessiodp.libby:libby-paper", version.ref = "libby" } libby-velocity = { module = "com.alessiodp.libby:libby-velocity", version.ref = "libby" } bstats-bungee = { module = "org.bstats:bstats-bungeecord", version.ref = "bstats" } bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } @@ -35,5 +37,6 @@ ormlite = "com.j256.ormlite:ormlite-jdbc:6.1" lombok = "org.projectlombok:lombok:1.18.42" bungeecord = "net.md-5:bungeecord-proxy:1.21-SNAPSHOT" velocity = "io.papermc.velocity:velocity-proxy:3.4.0-SNAPSHOT" +paper = "io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT" # We have to use 1.16.5 for backwards compatibility spigot = "org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT" diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts new file mode 100644 index 00000000..6b0ff7dd --- /dev/null +++ b/paper/build.gradle.kts @@ -0,0 +1,52 @@ +import net.minecrell.pluginyml.bukkit.BukkitPluginDescription + +plugins { + alias(libs.plugins.pluginyml.paper) apply true +} + +paper { + name = rootProject.name + main = "xyz.jonesdev.sonar.paper.SonarPaperPlugin" + authors = listOf("Jones Development", "Sonar Contributors") + website = "https://jonesdev.xyz/discord/" + load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD + apiVersion = "1.21" // ignore legacy plugin warning + foliaSupported = true + + serverDependencies { + listOf("Geyser-Spigot", "floodgate", "Protocolize", "ProtocolSupport", + "ViaVersion", "packetevents", "ProtocolLib", "FastLogin").forEach { + register(it) { + required = false + } + } + } +} + +java { + // Modern Paper requires Java 21 + java.sourceCompatibility = JavaVersion.VERSION_21 + java.targetCompatibility = JavaVersion.VERSION_21 + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} + +repositories { + maven(url = "https://repo.papermc.io/repository/maven-public/") // Paper +} + +dependencies { + implementation(project(":api")) + implementation(project(":common")) + implementation(project(":bukkit")) + + compileOnly(rootProject.libs.paper) + + implementation(rootProject.libs.bstats.bukkit) + implementation(rootProject.libs.libby.paper) +} + +tasks { + shadowJar { + relocate("net.kyori.adventure.nbt", "xyz.jonesdev.sonar.libs.kyori.adventure.nbt") + } +} diff --git a/paper/src/main/java/xyz/jonesdev/sonar/paper/SonarPaper.java b/paper/src/main/java/xyz/jonesdev/sonar/paper/SonarPaper.java new file mode 100644 index 00000000..034cbc97 --- /dev/null +++ b/paper/src/main/java/xyz/jonesdev/sonar/paper/SonarPaper.java @@ -0,0 +1,117 @@ +/* + * 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.paper; + +import com.alessiodp.libby.PaperLibraryManager; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import lombok.Getter; +import net.kyori.adventure.audience.Audience; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.SimplePie; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.jonesdev.sonar.paper.command.PaperSonarCommand; +import xyz.jonesdev.sonar.api.SonarPlatform; +import xyz.jonesdev.sonar.api.logger.LoggerWrapper; +import xyz.jonesdev.sonar.bukkit.SonarBukkit; +import xyz.jonesdev.sonar.bukkit.antibot.BukkitInjector; +import xyz.jonesdev.sonar.bukkit.listener.BukkitJoinListener; +import xyz.jonesdev.sonar.common.boot.SonarBootstrap; + +import java.util.UUID; + +@Getter +public final class SonarPaper extends SonarBootstrap { + private final LoggerWrapper logger = new LoggerWrapper() { + + @Override + public void info(final String message, final Object... args) { + getPlugin().getLogger().info(buildFullMessage(message, args)); + } + + @Override + public void warn(final String message, final Object... args) { + getPlugin().getLogger().warning(buildFullMessage(message, args)); + } + + @Override + public void error(final String message, final Object... args) { + getPlugin().getLogger().severe(buildFullMessage(message, args)); + } + }; + private Metrics metrics; + + public SonarPaper(final @NotNull SonarPaperPlugin plugin) { + super(plugin, SonarPlatform.PAPER, plugin.getDataFolder(), new PaperLibraryManager(plugin)); + } + + @Override + public @Nullable Audience audience(@Nullable final UUID uniqueId) { + if (uniqueId == null) return null; + return Bukkit.getPlayer(uniqueId); + } + + @Override + public @NotNull Audience sender(@NotNull final Object object) { + return (Audience) object; + } + + @Override + public @NotNull LoggerWrapper getLogger() { + return logger; + } + + @Override + public void enable() { + // Initialize bStats.org metrics + metrics = new Metrics(getPlugin(), getPlatform().getMetricsId()); + + // Add charts for some configuration options + metrics.addCustomChart(new SimplePie("verification", + () -> getConfig().getVerification().getTiming().getDisplayName())); + metrics.addCustomChart(new SimplePie("captcha", + () -> getConfig().getVerification().getMap().getTiming().getDisplayName())); + metrics.addCustomChart(new SimplePie("language", + () -> getConfig().getLanguage().getName())); + metrics.addCustomChart(new SimplePie("database_type", + () -> getConfig().getDatabase().getType().getDisplayName())); + + // Register Sonar command + getPlugin().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> + event.registrar().register("sonar", new PaperSonarCommand())); + + // Try to inject into the server + if (BukkitInjector.isLateBindEnabled()) { + getPlugin().getServer().getScheduler().runTask(getPlugin(), BukkitInjector::inject); + } else { + getPlugin().getServer().getPluginManager().registerEvents(new BukkitJoinListener(), getPlugin()); + } + + // Let the injector know that the plugin has been enabled + SonarBukkit.INITIALIZE_LISTENER.complete(null); + } + + @Override + public void disable() { + // Make sure to properly stop the metrics + if (metrics != null) { + metrics.shutdown(); + } + } +} diff --git a/paper/src/main/java/xyz/jonesdev/sonar/paper/SonarPaperPlugin.java b/paper/src/main/java/xyz/jonesdev/sonar/paper/SonarPaperPlugin.java new file mode 100644 index 00000000..82fc01f7 --- /dev/null +++ b/paper/src/main/java/xyz/jonesdev/sonar/paper/SonarPaperPlugin.java @@ -0,0 +1,44 @@ +/* + * 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.paper; + +import org.bukkit.plugin.java.JavaPlugin; +import xyz.jonesdev.sonar.bukkit.antibot.BukkitInjector; + +public final class SonarPaperPlugin extends JavaPlugin { + private SonarPaper bootstrap; + + @Override + public void onLoad() { + // Inject early if late-bind is disabled + if (!BukkitInjector.isLateBindEnabled()) { + BukkitInjector.inject(); + } + } + + @Override + public void onEnable() { + bootstrap = new SonarPaper(this); + bootstrap.initialize(); + } + + @Override + public void onDisable() { + bootstrap.shutdown(); + } +} diff --git a/paper/src/main/java/xyz/jonesdev/sonar/paper/command/PaperSonarCommand.java b/paper/src/main/java/xyz/jonesdev/sonar/paper/command/PaperSonarCommand.java new file mode 100644 index 00000000..ed01f722 --- /dev/null +++ b/paper/src/main/java/xyz/jonesdev/sonar/paper/command/PaperSonarCommand.java @@ -0,0 +1,49 @@ +/* + * 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.paper.command; + +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; +import xyz.jonesdev.sonar.api.command.InvocationSource; +import xyz.jonesdev.sonar.api.command.SonarCommand; + +import java.util.Collection; +import java.util.Collections; + +public final class PaperSonarCommand implements BasicCommand, SonarCommand { + @Override + public void execute(final CommandSourceStack commandSourceStack, final String @NonNull [] args) { + final CommandSender sender = commandSourceStack.getSender(); + final InvocationSource invocationSource = new InvocationSource( + sender instanceof Player ? ((Player) sender).getUniqueId() : null, + sender, + sender::hasPermission + ); + handle(invocationSource, args); + } + + @Override + public @NonNull Collection suggest(final CommandSourceStack commandSourceStack, final String @NonNull [] args) { + // Do not allow tab completion if the player does not have the required permission + return commandSourceStack.getSender().hasPermission("sonar.command") ? + getCachedTabSuggestions(args) : Collections.emptyList(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3306b5d0..87a09aad 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,6 +6,7 @@ sequenceOf( "common", "bukkit", "bungeecord", + "paper", "velocity" ).forEach { include(":$it")