From d7eb663736621077ef74ef716d69b64d9bc5cb2d Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 3 Jan 2026 15:32:39 +0000 Subject: [PATCH 1/6] feat: support setting up 1.21+ server links Adds support for setting up server links in pause menu on supported clients (1.21+). Works on both modern, and 1.8-based servers using ViaVersion. Signed-off-by: TTtie --- .../pgm/community/feature/FeatureManager.java | 9 ++ .../serverlinks/ServerLinksConfig.java | 65 ++++++++++++++ .../serverlinks/ServerLinksFeature.java | 44 ++++++++++ .../serverlinks/types/ServerLink.java | 13 +++ .../types/ServerLinkBuiltinType.java | 18 ++++ core/src/main/resources/config.yml | 14 ++- core/src/main/resources/plugin.yml | 2 +- .../feature/ModernServerLinksPlatform.java | 37 ++++++++ platform/platform-sportpaper/build.gradle.kts | 1 + .../features/SpServerLinksPlatform.java | 86 +++++++++++++++++++ 10 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java create mode 100644 core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java create mode 100644 core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java create mode 100644 core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java create mode 100644 platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/ModernServerLinksPlatform.java create mode 100644 platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/SpServerLinksPlatform.java diff --git a/core/src/main/java/dev/pgm/community/feature/FeatureManager.java b/core/src/main/java/dev/pgm/community/feature/FeatureManager.java index bfe7ade3..200d2afb 100644 --- a/core/src/main/java/dev/pgm/community/feature/FeatureManager.java +++ b/core/src/main/java/dev/pgm/community/feature/FeatureManager.java @@ -25,6 +25,7 @@ import dev.pgm.community.polls.feature.PollFeature; import dev.pgm.community.requests.feature.RequestFeature; import dev.pgm.community.requests.feature.types.RequestFeatureCore; +import dev.pgm.community.serverlinks.ServerLinksFeature; import dev.pgm.community.sessions.feature.SessionFeature; import dev.pgm.community.sessions.feature.types.SessionFeatureCore; import dev.pgm.community.squads.SquadFeature; @@ -65,6 +66,7 @@ public class FeatureManager { private final PollFeature polls; private final SquadFeature squads; private final MatchHistoryFeature history; + private final ServerLinksFeature serverLinks; public FeatureManager(Configuration config, Logger logger, InventoryManager inventory) { // Networking @@ -102,6 +104,7 @@ public FeatureManager(Configuration config, Logger logger, InventoryManager inve this.polls = new PollFeature(config, logger); this.squads = new SquadFeature(config, logger); this.history = new MatchHistoryFeature(config, logger); + this.serverLinks = new ServerLinksFeature(config, logger); } public AssistanceFeature getReports() { @@ -192,6 +195,10 @@ public MatchHistoryFeature getHistory() { return history; } + public ServerLinksFeature getServerLinks() { + return serverLinks; + } + public void reloadConfig(Configuration config) { // Reload all config values here getReports().getConfig().reload(config); @@ -218,6 +225,7 @@ public void reloadConfig(Configuration config) { getPolls().getConfig().reload(config); getSquads().getConfig().reload(config); getHistory().getConfig().reload(config); + getServerLinks().getConfig().reload(config); // TODO: Look into maybe unregister commands for features that have been disabled // commands#unregisterCommand @@ -246,5 +254,6 @@ public void disable() { if (getPolls().isEnabled()) getPolls().disable(); if (getSquads().isEnabled()) getSquads().disable(); if (getHistory().isEnabled()) getHistory().disable(); + if (getServerLinks().isEnabled()) getServerLinks().disable(); } } diff --git a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java new file mode 100644 index 00000000..61bcd407 --- /dev/null +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java @@ -0,0 +1,65 @@ +package dev.pgm.community.serverlinks; + +import static tc.oc.pgm.util.text.TextParser.parseComponent; +import static tc.oc.pgm.util.text.TextParser.parseEnum; +import static tc.oc.pgm.util.text.TextParser.parseUri; + +import dev.pgm.community.feature.config.FeatureConfigImpl; +import dev.pgm.community.serverlinks.types.ServerLink; +import dev.pgm.community.serverlinks.types.ServerLinkBuiltinType; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.bukkit.configuration.Configuration; + +public class ServerLinksConfig extends FeatureConfigImpl { + private static final String KEY = "server-links"; + private static final String LINKS_KEY = "links"; + + private static final String LINK_BUILTIN_KEY = "builtin"; + private static final String LINK_CUSTOM_TEXT_KEY = "text"; + private static final String LINK_URI_KEY = "uri"; + + private List links; + + public ServerLinksConfig(Configuration config) { + super(KEY, config); + } + + public List getLinks() { + return links; + } + + @Override + public void reload(Configuration config) { + super.reload(config); + links = config.getMapList(getKey() + "." + LINKS_KEY).stream() + .map(this::readLink) + .toList(); + } + + private ServerLink readLink(Map configData) { + String builtIn = Objects.toString(configData.get(LINK_BUILTIN_KEY), null); + String customText = Objects.toString(configData.get(LINK_CUSTOM_TEXT_KEY), null); + String uri = Objects.toString(configData.get(LINK_URI_KEY), null); + + if (builtIn == null && customText == null) { + throw new IllegalStateException( + "A server link must have either built-in or custom text defined"); + } else if (builtIn != null && customText != null) { + throw new IllegalStateException( + "A server link cannot have both built-in and custom text defined"); + } + + URI parsedUri = parseUri(uri); + if (!parsedUri.getScheme().equals("http") && !parsedUri.getScheme().equals("https")) { + throw new IllegalStateException("The URL " + uri + " is not a web URL"); + } + + return new ServerLink( + builtIn != null ? parseEnum(builtIn, ServerLinkBuiltinType.class) : null, + customText != null ? parseComponent(customText) : null, + parsedUri); + } +} diff --git a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java new file mode 100644 index 00000000..986a1273 --- /dev/null +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java @@ -0,0 +1,44 @@ +package dev.pgm.community.serverlinks; + +import dev.pgm.community.feature.FeatureBase; +import dev.pgm.community.serverlinks.types.ServerLink; +import dev.pgm.community.util.Platform; +import java.util.List; +import java.util.logging.Logger; +import org.bukkit.configuration.Configuration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; + +public class ServerLinksFeature extends FeatureBase { + private static final ServerLinksPlatform PLATFORM = Platform.get(ServerLinksPlatform.class); + + public interface ServerLinksPlatform { + default boolean isSupported() { + return true; + } + + void sendToPlayer(Player player, List serverLinks); + } + + public ServerLinksFeature(Configuration config, Logger logger) { + super(new ServerLinksConfig(config), logger, "Server Links"); + + if (getConfig().isEnabled()) { + if (!PLATFORM.isSupported()) { + logger.warning("Server links are enabled but not supported by the platform"); + return; + } + enable(); + } + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + PLATFORM.sendToPlayer(event.getPlayer(), getServerLinksConfig().getLinks()); + } + + public ServerLinksConfig getServerLinksConfig() { + return (ServerLinksConfig) getConfig(); + } +} diff --git a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java new file mode 100644 index 00000000..0f2ed101 --- /dev/null +++ b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java @@ -0,0 +1,13 @@ +package dev.pgm.community.serverlinks.types; + +import java.net.URI; +import net.kyori.adventure.text.Component; + +/** + * Represents a Minecraft server link. + * + * @param builtinType The built-in type of the server link, or null if it's a custom link. + * @param customText The custom text for the server link, or null if builtinType is set. + * @param uri The URI of the server link. + */ +public record ServerLink(ServerLinkBuiltinType builtinType, Component customText, URI uri) {} diff --git a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java new file mode 100644 index 00000000..6998f234 --- /dev/null +++ b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java @@ -0,0 +1,18 @@ +package dev.pgm.community.serverlinks.types; + +/** + * Represents a built-in server link type that will be auto-translated by the Minecraft client and + * possibly have special functionality. Keep in sync with Paper's org.bukkit.ServerLinks.Type. + */ +public enum ServerLinkBuiltinType { + REPORT_BUG, + COMMUNITY_GUIDELINES, + SUPPORT, + STATUS, + FEEDBACK, + COMMUNITY, + WEBSITE, + FORUMS, + NEWS, + ANNOUNCEMENTS; +} diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index e8839752..0fadf673 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -262,7 +262,7 @@ command-audit: - "/sudo" - "/community:sudo" -# Freeze - Freeze players via command or observer hotbar tool (when PGM is enabled) +# Freeze - Freeze players via command or observer hotbar tool (when PGM is enabled) freeze: enabled: true pgm-integration: true # Whether Community will attempt to hook into PGM @@ -457,3 +457,15 @@ database: sqlite: # Relative paths resolve under the plugin data folder. file: "community.db" # SQLite file name or absolute path + +# Server Links - Adds links to the pause menu for 1.21+ clients +# Requires ViaVersion to be installed on 1.8-based servers. +server-links: + enabled: false + links: + # A built-in server link type will be auto-translated by the client and may provide some extra functionality. + - builtin: report bug # See https://jd.papermc.io/paper/org/bukkit/ServerLinks.Type.html for a list of built-in types + uri: https://pgm.dev + # Alternatively, custom text can be provided. Custom text is mutually exclusive with built-in types. + - text: "Submit a new map" + uri: https://pgm.dev diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index a8f7008c..e56800a8 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -5,4 +5,4 @@ main: ${mainClass} version: ${version} (git-${commitHash}) website: ${url} author: ${author} -softdepend: [Database, PGM, Environment, packetevents] +softdepend: [Database, PGM, Environment, packetevents, ViaVersion] diff --git a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/ModernServerLinksPlatform.java b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/ModernServerLinksPlatform.java new file mode 100644 index 00000000..6498e17e --- /dev/null +++ b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/ModernServerLinksPlatform.java @@ -0,0 +1,37 @@ +package dev.pgm.community.platform.modern.feature; + +import static dev.pgm.community.util.Supports.Variant.PAPER; + +import dev.pgm.community.serverlinks.ServerLinksFeature; +import dev.pgm.community.serverlinks.types.ServerLink; +import dev.pgm.community.serverlinks.types.ServerLinkBuiltinType; +import dev.pgm.community.util.Supports; +import java.util.List; +import org.bukkit.ServerLinks; +import org.bukkit.craftbukkit.CraftServerLinks; +import org.bukkit.entity.Player; + +@Supports(PAPER) +public class ModernServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform { + @Override + public void sendToPlayer(Player player, List serverLinks) { + player.sendLinks(toPlatformServerLinks(serverLinks)); + } + + private ServerLinks toPlatformServerLinks(List links) { + ServerLinks bukkitLinks = new CraftServerLinks(new net.minecraft.server.ServerLinks(List.of())); + for (ServerLink link : links) { + if (link.builtinType() != null) { + bukkitLinks.addLink(toBukkitType(link.builtinType()), link.uri()); + } else { + bukkitLinks.addLink(link.customText(), link.uri()); + } + } + + return bukkitLinks; + } + + private ServerLinks.Type toBukkitType(ServerLinkBuiltinType type) { + return ServerLinks.Type.values()[type.ordinal()]; + } +} diff --git a/platform/platform-sportpaper/build.gradle.kts b/platform/platform-sportpaper/build.gradle.kts index a1d44a94..8d08a37c 100644 --- a/platform/platform-sportpaper/build.gradle.kts +++ b/platform/platform-sportpaper/build.gradle.kts @@ -6,4 +6,5 @@ dependencies { implementation(project(":core")) implementation(project(":util")) compileOnly("app.ashcon:sportpaper:1.8.8-R0.1-SNAPSHOT") + compileOnly("com.viaversion:viaversion-api:5.0.0") } diff --git a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/SpServerLinksPlatform.java b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/SpServerLinksPlatform.java new file mode 100644 index 00000000..1e67b067 --- /dev/null +++ b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/SpServerLinksPlatform.java @@ -0,0 +1,86 @@ +package dev.pgm.community.platform.sportpaper.features; + +import static dev.pgm.community.util.Supports.Variant.SPORTPAPER; + +import com.viaversion.nbt.tag.Tag; +import com.viaversion.viaversion.api.Via; +import com.viaversion.viaversion.api.connection.UserConnection; +import com.viaversion.viaversion.api.protocol.Protocol; +import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; +import com.viaversion.viaversion.api.protocol.packet.State; +import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; +import com.viaversion.viaversion.api.type.Types; +import com.viaversion.viaversion.libs.gson.JsonParser; +import com.viaversion.viaversion.libs.mcstructs.text.utils.JsonNbtConverter; +import dev.pgm.community.serverlinks.ServerLinksFeature; +import dev.pgm.community.serverlinks.types.ServerLink; +import dev.pgm.community.util.Supports; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import org.bukkit.entity.Player; + +@Supports(SPORTPAPER) +public class SpServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform { + private final Protocol serverLinkProtocol = findServerLinkProtocol(); + private static final boolean hasVia = hasVia(); + + private static boolean hasVia() { + try { + Class.forName("com.viaversion.viaversion.api.Via"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + @Override + public boolean isSupported() { + return hasVia; + } + + @Override + public void sendToPlayer(Player player, List serverLinks) { + if (hasVia && Via.getAPI().isInjected(player.getUniqueId())) { + UserConnection userConnection = Via.getAPI().getConnection(player.getUniqueId()); + if (userConnection != null + && userConnection + .getProtocolInfo() + .protocolVersion() + .newerThanOrEqualTo(ProtocolVersion.v1_21)) { + PacketWrapper serverLinksPacket = createPacket(userConnection, serverLinks); + serverLinksPacket.scheduleSend(serverLinkProtocol.getClass()); + } + } + } + + private PacketWrapper createPacket(UserConnection conn, List links) { + var packetTypes = serverLinkProtocol.getPacketTypesProvider().mappedClientboundPacketTypes(); + var packetType = packetTypes.get(State.PLAY).typeByName("SERVER_LINKS"); + PacketWrapper packet = PacketWrapper.create(packetType, conn); + packet.write(Types.VAR_INT, links.size()); + // TODO: is there a better way to do this? + for (ServerLink link : links) { + packet.write(Types.BOOLEAN, link.builtinType() != null); + if (link.builtinType() != null) { + packet.write(Types.VAR_INT, link.builtinType().ordinal()); + } else { + packet.write(Types.TAG, toViaTag(link.customText())); + } + packet.write(Types.STRING, link.uri().toString()); + } + + return packet; + } + + private Tag toViaTag(Component component) { + return JsonNbtConverter.toNbt( + JsonParser.parseString(GsonComponentSerializer.gson().serialize(component))); + } + + private Protocol findServerLinkProtocol() { + return Via.getManager() + .getProtocolManager() + .getProtocol(/* to */ ProtocolVersion.v1_21, /* from */ ProtocolVersion.v1_20_5); + } +} From 1770f2b405770f0f954d9a1273ca530be67455d2 Mon Sep 17 00:00:00 2001 From: TTtie Date: Sun, 4 Jan 2026 14:47:29 +0000 Subject: [PATCH 2/6] refactor: move via stuff out to a separate class Avoids blowing things up when ViaVersion isn't present. Signed-off-by: TTtie --- .../ModernServerLinksPlatform.java | 2 +- .../serverlinks/SpServerLinksPlatform.java | 35 ++++++++++++ .../ViaServerLinks.java} | 54 ++++++------------- 3 files changed, 52 insertions(+), 39 deletions(-) rename platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/{ => serverlinks}/ModernServerLinksPlatform.java (95%) create mode 100644 platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java rename platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/{SpServerLinksPlatform.java => serverlinks/ViaServerLinks.java} (54%) diff --git a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/ModernServerLinksPlatform.java b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java similarity index 95% rename from platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/ModernServerLinksPlatform.java rename to platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java index 6498e17e..20574b71 100644 --- a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/ModernServerLinksPlatform.java +++ b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java @@ -1,4 +1,4 @@ -package dev.pgm.community.platform.modern.feature; +package dev.pgm.community.platform.modern.feature.serverlinks; import static dev.pgm.community.util.Supports.Variant.PAPER; diff --git a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java new file mode 100644 index 00000000..5125ac3f --- /dev/null +++ b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java @@ -0,0 +1,35 @@ +package dev.pgm.community.platform.sportpaper.features.serverlinks; + +import static dev.pgm.community.util.Supports.Variant.SPORTPAPER; + +import dev.pgm.community.serverlinks.ServerLinksFeature; +import dev.pgm.community.serverlinks.types.ServerLink; +import dev.pgm.community.util.Supports; +import java.util.List; +import org.bukkit.entity.Player; + +@Supports(SPORTPAPER) +public class SpServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform { + private static final boolean HAS_VIA = hasVia(); + + private static boolean hasVia() { + try { + Class.forName("com.viaversion.viaversion.api.Via"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + @Override + public boolean isSupported() { + return HAS_VIA; + } + + @Override + public void sendToPlayer(Player player, List serverLinks) { + if (HAS_VIA) { + ViaServerLinks.sendToPlayer(player, serverLinks); + } + } +} diff --git a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/SpServerLinksPlatform.java b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java similarity index 54% rename from platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/SpServerLinksPlatform.java rename to platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java index 1e67b067..384b3b53 100644 --- a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/SpServerLinksPlatform.java +++ b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java @@ -1,6 +1,4 @@ -package dev.pgm.community.platform.sportpaper.features; - -import static dev.pgm.community.util.Supports.Variant.SPORTPAPER; +package dev.pgm.community.platform.sportpaper.features.serverlinks; import com.viaversion.nbt.tag.Tag; import com.viaversion.viaversion.api.Via; @@ -12,49 +10,29 @@ import com.viaversion.viaversion.api.type.Types; import com.viaversion.viaversion.libs.gson.JsonParser; import com.viaversion.viaversion.libs.mcstructs.text.utils.JsonNbtConverter; -import dev.pgm.community.serverlinks.ServerLinksFeature; import dev.pgm.community.serverlinks.types.ServerLink; -import dev.pgm.community.util.Supports; import java.util.List; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.bukkit.entity.Player; -@Supports(SPORTPAPER) -public class SpServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform { - private final Protocol serverLinkProtocol = findServerLinkProtocol(); - private static final boolean hasVia = hasVia(); - - private static boolean hasVia() { - try { - Class.forName("com.viaversion.viaversion.api.Via"); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } +public class ViaServerLinks { + private static final Protocol serverLinkProtocol = findServerLinkProtocol(); - @Override - public boolean isSupported() { - return hasVia; - } - - @Override - public void sendToPlayer(Player player, List serverLinks) { - if (hasVia && Via.getAPI().isInjected(player.getUniqueId())) { - UserConnection userConnection = Via.getAPI().getConnection(player.getUniqueId()); - if (userConnection != null - && userConnection - .getProtocolInfo() - .protocolVersion() - .newerThanOrEqualTo(ProtocolVersion.v1_21)) { - PacketWrapper serverLinksPacket = createPacket(userConnection, serverLinks); - serverLinksPacket.scheduleSend(serverLinkProtocol.getClass()); - } + public static void sendToPlayer(Player player, List serverLinks) { + if (!Via.getAPI().isInjected(player.getUniqueId())) return; + UserConnection userConnection = Via.getAPI().getConnection(player.getUniqueId()); + if (userConnection != null + && userConnection + .getProtocolInfo() + .protocolVersion() + .newerThanOrEqualTo(ProtocolVersion.v1_21)) { + PacketWrapper serverLinksPacket = createPacket(userConnection, serverLinks); + serverLinksPacket.scheduleSend(serverLinkProtocol.getClass()); } } - private PacketWrapper createPacket(UserConnection conn, List links) { + private static PacketWrapper createPacket(UserConnection conn, List links) { var packetTypes = serverLinkProtocol.getPacketTypesProvider().mappedClientboundPacketTypes(); var packetType = packetTypes.get(State.PLAY).typeByName("SERVER_LINKS"); PacketWrapper packet = PacketWrapper.create(packetType, conn); @@ -73,12 +51,12 @@ private PacketWrapper createPacket(UserConnection conn, List links) return packet; } - private Tag toViaTag(Component component) { + private static Tag toViaTag(Component component) { return JsonNbtConverter.toNbt( JsonParser.parseString(GsonComponentSerializer.gson().serialize(component))); } - private Protocol findServerLinkProtocol() { + private static Protocol findServerLinkProtocol() { return Via.getManager() .getProtocolManager() .getProtocol(/* to */ ProtocolVersion.v1_21, /* from */ ProtocolVersion.v1_20_5); From 603b8f19c1339f20ee792b6c884e39ecc404fed5 Mon Sep 17 00:00:00 2001 From: TTtie Date: Tue, 6 Jan 2026 00:42:56 +0000 Subject: [PATCH 3/6] chore: add ViaVersion repo Local artifacts :( Signed-off-by: TTtie --- platform/platform-sportpaper/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platform/platform-sportpaper/build.gradle.kts b/platform/platform-sportpaper/build.gradle.kts index 8d08a37c..df0a107f 100644 --- a/platform/platform-sportpaper/build.gradle.kts +++ b/platform/platform-sportpaper/build.gradle.kts @@ -2,6 +2,10 @@ plugins { id("buildlogic.java-conventions") } +repositories { + maven("https://repo.viaversion.com") // ViaVersion +} + dependencies { implementation(project(":core")) implementation(project(":util")) From 3ce20c34065204b369c3e0d4cb38a7ceeed33fec Mon Sep 17 00:00:00 2001 From: TTtie Date: Tue, 6 Jan 2026 00:46:51 +0000 Subject: [PATCH 4/6] chore: simplify mutual exclusivity check Co-authored-by: Pablo Herrera Signed-off-by: TTtie --- .../dev/pgm/community/serverlinks/ServerLinksConfig.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java index 61bcd407..55d25ca3 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java @@ -44,12 +44,9 @@ private ServerLink readLink(Map configData) { String customText = Objects.toString(configData.get(LINK_CUSTOM_TEXT_KEY), null); String uri = Objects.toString(configData.get(LINK_URI_KEY), null); - if (builtIn == null && customText == null) { + if ((builtIn == null) == (customText == null)) { throw new IllegalStateException( "A server link must have either built-in or custom text defined"); - } else if (builtIn != null && customText != null) { - throw new IllegalStateException( - "A server link cannot have both built-in and custom text defined"); } URI parsedUri = parseUri(uri); From f8f1fec80ab2a0a73c041b35d8971a6c0a1c2b8f Mon Sep 17 00:00:00 2001 From: TTtie Date: Fri, 13 Feb 2026 03:02:18 +0000 Subject: [PATCH 5/6] chore: remove TODO comment, add nullability annotations Signed-off-by: TTtie --- .../serverlinks/ServerLinksConfig.java | 9 +++++++-- .../serverlinks/ServerLinksFeature.java | 2 ++ .../serverlinks/types/ServerLink.java | 6 +++++- .../types/ServerLinkBuiltinType.java | 3 +++ .../ModernServerLinksPlatform.java | 3 +++ .../serverlinks/SpServerLinksPlatform.java | 2 ++ .../features/serverlinks/ViaServerLinks.java | 20 +++++++++++++------ 7 files changed, 36 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java index 55d25ca3..058c1ac6 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java @@ -12,7 +12,11 @@ import java.util.Map; import java.util.Objects; import org.bukkit.configuration.Configuration; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +@NotNullByDefault public class ServerLinksConfig extends FeatureConfigImpl { private static final String KEY = "server-links"; private static final String LINKS_KEY = "links"; @@ -21,13 +25,14 @@ public class ServerLinksConfig extends FeatureConfigImpl { private static final String LINK_CUSTOM_TEXT_KEY = "text"; private static final String LINK_URI_KEY = "uri"; - private List links; + private @Nullable @Unmodifiable List links; public ServerLinksConfig(Configuration config) { super(KEY, config); } - public List getLinks() { + public @Unmodifiable List getLinks() { + assert links != null; return links; } diff --git a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java index 986a1273..7d19cc99 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java @@ -9,7 +9,9 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.player.PlayerJoinEvent; +import org.jetbrains.annotations.NotNullByDefault; +@NotNullByDefault public class ServerLinksFeature extends FeatureBase { private static final ServerLinksPlatform PLATFORM = Platform.get(ServerLinksPlatform.class); diff --git a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java index 0f2ed101..a8271a50 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java @@ -2,6 +2,8 @@ import java.net.URI; import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; /** * Represents a Minecraft server link. @@ -10,4 +12,6 @@ * @param customText The custom text for the server link, or null if builtinType is set. * @param uri The URI of the server link. */ -public record ServerLink(ServerLinkBuiltinType builtinType, Component customText, URI uri) {} +@NotNullByDefault +public record ServerLink( + @Nullable ServerLinkBuiltinType builtinType, @Nullable Component customText, URI uri) {} diff --git a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java index 6998f234..11450aa9 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java @@ -1,9 +1,12 @@ package dev.pgm.community.serverlinks.types; +import org.jetbrains.annotations.NotNullByDefault; + /** * Represents a built-in server link type that will be auto-translated by the Minecraft client and * possibly have special functionality. Keep in sync with Paper's org.bukkit.ServerLinks.Type. */ +@NotNullByDefault public enum ServerLinkBuiltinType { REPORT_BUG, COMMUNITY_GUIDELINES, diff --git a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java index 20574b71..ee1ad55c 100644 --- a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java +++ b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java @@ -10,8 +10,10 @@ import org.bukkit.ServerLinks; import org.bukkit.craftbukkit.CraftServerLinks; import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNullByDefault; @Supports(PAPER) +@NotNullByDefault public class ModernServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform { @Override public void sendToPlayer(Player player, List serverLinks) { @@ -24,6 +26,7 @@ private ServerLinks toPlatformServerLinks(List links) { if (link.builtinType() != null) { bukkitLinks.addLink(toBukkitType(link.builtinType()), link.uri()); } else { + assert link.customText() != null; bukkitLinks.addLink(link.customText(), link.uri()); } } diff --git a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java index 5125ac3f..b7aa903e 100644 --- a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java +++ b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java @@ -7,8 +7,10 @@ import dev.pgm.community.util.Supports; import java.util.List; import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNullByDefault; @Supports(SPORTPAPER) +@NotNullByDefault public class SpServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform { private static final boolean HAS_VIA = hasVia(); diff --git a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java index 384b3b53..86fe9b5d 100644 --- a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java +++ b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java @@ -1,5 +1,7 @@ package dev.pgm.community.platform.sportpaper.features.serverlinks; +import static tc.oc.pgm.util.Assert.assertNotNull; + import com.viaversion.nbt.tag.Tag; import com.viaversion.viaversion.api.Via; import com.viaversion.viaversion.api.connection.UserConnection; @@ -15,7 +17,9 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNullByDefault; +@NotNullByDefault public class ViaServerLinks { private static final Protocol serverLinkProtocol = findServerLinkProtocol(); @@ -37,12 +41,12 @@ private static PacketWrapper createPacket(UserConnection conn, List var packetType = packetTypes.get(State.PLAY).typeByName("SERVER_LINKS"); PacketWrapper packet = PacketWrapper.create(packetType, conn); packet.write(Types.VAR_INT, links.size()); - // TODO: is there a better way to do this? for (ServerLink link : links) { packet.write(Types.BOOLEAN, link.builtinType() != null); if (link.builtinType() != null) { packet.write(Types.VAR_INT, link.builtinType().ordinal()); } else { + assert link.customText() != null; packet.write(Types.TAG, toViaTag(link.customText())); } packet.write(Types.STRING, link.uri().toString()); @@ -52,13 +56,17 @@ private static PacketWrapper createPacket(UserConnection conn, List } private static Tag toViaTag(Component component) { - return JsonNbtConverter.toNbt( - JsonParser.parseString(GsonComponentSerializer.gson().serialize(component))); + return assertNotNull( + JsonNbtConverter.toNbt( + JsonParser.parseString(GsonComponentSerializer.gson().serialize(component))), + "Component -> NBT conversion failed"); } private static Protocol findServerLinkProtocol() { - return Via.getManager() - .getProtocolManager() - .getProtocol(/* to */ ProtocolVersion.v1_21, /* from */ ProtocolVersion.v1_20_5); + return assertNotNull( + Via.getManager() + .getProtocolManager() + .getProtocol(/* to */ ProtocolVersion.v1_21, /* from */ ProtocolVersion.v1_20_5), + "ViaVersion v1.20.5 -> v1.21 protocol was not found"); } } From 8771e94c691ad15765007d62365f7e6512da87e2 Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 1 Aug 2026 15:35:11 +0000 Subject: [PATCH 6/6] refactor: jspecify, queue the server links packet up when in "config phase" Migrates annotations to JSpecify where appropriate, and makes sure that the server links packet is withheld by ViaVersion if the client is still being tricked to believe it is in configuration phase. Signed-off-by: TTtie --- .../serverlinks/ServerLinksConfig.java | 6 ++--- .../serverlinks/ServerLinksFeature.java | 4 ++-- .../serverlinks/types/ServerLink.java | 6 ++--- .../types/ServerLinkBuiltinType.java | 4 ++-- .../ModernServerLinksPlatform.java | 4 ++-- platform/platform-sportpaper/build.gradle.kts | 2 +- .../serverlinks/SpServerLinksPlatform.java | 4 ++-- .../features/serverlinks/ViaServerLinks.java | 22 ++++++++++++++++--- 8 files changed, 34 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java index 058c1ac6..126313e6 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksConfig.java @@ -12,11 +12,11 @@ import java.util.Map; import java.util.Objects; import org.bukkit.configuration.Configuration; -import org.jetbrains.annotations.NotNullByDefault; -import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; -@NotNullByDefault +@NullMarked public class ServerLinksConfig extends FeatureConfigImpl { private static final String KEY = "server-links"; private static final String LINKS_KEY = "links"; diff --git a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java index 7d19cc99..2a8768f9 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java @@ -9,9 +9,9 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.player.PlayerJoinEvent; -import org.jetbrains.annotations.NotNullByDefault; +import org.jspecify.annotations.NullMarked; -@NotNullByDefault +@NullMarked public class ServerLinksFeature extends FeatureBase { private static final ServerLinksPlatform PLATFORM = Platform.get(ServerLinksPlatform.class); diff --git a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java index a8271a50..ab30f558 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLink.java @@ -2,8 +2,8 @@ import java.net.URI; import net.kyori.adventure.text.Component; -import org.jetbrains.annotations.NotNullByDefault; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Represents a Minecraft server link. @@ -12,6 +12,6 @@ * @param customText The custom text for the server link, or null if builtinType is set. * @param uri The URI of the server link. */ -@NotNullByDefault +@NullMarked public record ServerLink( @Nullable ServerLinkBuiltinType builtinType, @Nullable Component customText, URI uri) {} diff --git a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java index 11450aa9..95b7422e 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/types/ServerLinkBuiltinType.java @@ -1,12 +1,12 @@ package dev.pgm.community.serverlinks.types; -import org.jetbrains.annotations.NotNullByDefault; +import org.jspecify.annotations.NullMarked; /** * Represents a built-in server link type that will be auto-translated by the Minecraft client and * possibly have special functionality. Keep in sync with Paper's org.bukkit.ServerLinks.Type. */ -@NotNullByDefault +@NullMarked public enum ServerLinkBuiltinType { REPORT_BUG, COMMUNITY_GUIDELINES, diff --git a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java index ee1ad55c..a465f82e 100644 --- a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java +++ b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/feature/serverlinks/ModernServerLinksPlatform.java @@ -10,10 +10,10 @@ import org.bukkit.ServerLinks; import org.bukkit.craftbukkit.CraftServerLinks; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNullByDefault; +import org.jspecify.annotations.NullMarked; @Supports(PAPER) -@NotNullByDefault +@NullMarked public class ModernServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform { @Override public void sendToPlayer(Player player, List serverLinks) { diff --git a/platform/platform-sportpaper/build.gradle.kts b/platform/platform-sportpaper/build.gradle.kts index df0a107f..c83efb25 100644 --- a/platform/platform-sportpaper/build.gradle.kts +++ b/platform/platform-sportpaper/build.gradle.kts @@ -10,5 +10,5 @@ dependencies { implementation(project(":core")) implementation(project(":util")) compileOnly("app.ashcon:sportpaper:1.8.8-R0.1-SNAPSHOT") - compileOnly("com.viaversion:viaversion-api:5.0.0") + compileOnly("com.viaversion:viaversion:5.11.0") } diff --git a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java index b7aa903e..0257b451 100644 --- a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java +++ b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/SpServerLinksPlatform.java @@ -7,10 +7,10 @@ import dev.pgm.community.util.Supports; import java.util.List; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNullByDefault; +import org.jspecify.annotations.NullMarked; @Supports(SPORTPAPER) -@NotNullByDefault +@NullMarked public class SpServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform { private static final boolean HAS_VIA = hasVia(); diff --git a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java index 86fe9b5d..831ba9b1 100644 --- a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java +++ b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/features/serverlinks/ViaServerLinks.java @@ -12,14 +12,16 @@ import com.viaversion.viaversion.api.type.Types; import com.viaversion.viaversion.libs.gson.JsonParser; import com.viaversion.viaversion.libs.mcstructs.text.utils.JsonNbtConverter; +import com.viaversion.viaversion.protocols.v1_20to1_20_2.storage.ConfigurationState; +import dev.pgm.community.Community; import dev.pgm.community.serverlinks.types.ServerLink; import java.util.List; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNullByDefault; +import org.jspecify.annotations.NullMarked; -@NotNullByDefault +@NullMarked public class ViaServerLinks { private static final Protocol serverLinkProtocol = findServerLinkProtocol(); @@ -32,7 +34,21 @@ public static void sendToPlayer(Player player, List serverLinks) { .protocolVersion() .newerThanOrEqualTo(ProtocolVersion.v1_21)) { PacketWrapper serverLinksPacket = createPacket(userConnection, serverLinks); - serverLinksPacket.scheduleSend(serverLinkProtocol.getClass()); + var maybeConfigState = userConnection.get(ConfigurationState.class); + assert maybeConfigState != null; // will be present for 1.21+ connections + if (maybeConfigState.bridgePhase() != ConfigurationState.BridgePhase.NONE) { + // The client is in config phase, queue the packet up so the client doesn't crash + // This does get sent through the 1.20 -> 1.20.2 protocol internally, and thus is + // technically wrong, but it seems to go through and upgrade to the client's version just + // fine. + Community.get() + .getLogger() + .info("Queuing SERVER_LINKS packet for player " + player + + " because they are still in config phase"); + maybeConfigState.addClientboundPacketToQueue(serverLinksPacket); + } else { + serverLinksPacket.scheduleSend(serverLinkProtocol.getClass()); + } } }