From a71dbec98a833d21a6665b1c7a3cf77ae7049c86 Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 3 Jan 2026 15:32:39 +0000 Subject: [PATCH 1/8] 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 558fc29c..c99b3eaa 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); @@ -215,6 +222,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 @@ -243,5 +251,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 6be19862..ae2628bc 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -240,7 +240,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 @@ -435,3 +435,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 7e4edd8c8d088057c54e8c7af8f3171207eba269 Mon Sep 17 00:00:00 2001 From: TTtie Date: Sun, 4 Jan 2026 14:47:29 +0000 Subject: [PATCH 2/8] 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 1dd0e1ddd18ae5fd672166f5185f7b2aefed3bc1 Mon Sep 17 00:00:00 2001 From: TTtie Date: Tue, 6 Jan 2026 00:42:56 +0000 Subject: [PATCH 3/8] 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 0a43e214135eeb1b1c236955f2389a5bd4b603b2 Mon Sep 17 00:00:00 2001 From: TTtie Date: Tue, 6 Jan 2026 00:46:51 +0000 Subject: [PATCH 4/8] 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 5d1e7acbbebbaa0e7ae53e6ca3abeb5315008bfc Mon Sep 17 00:00:00 2001 From: TTtie Date: Fri, 13 Feb 2026 03:02:18 +0000 Subject: [PATCH 5/8] 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 c81c6f77f4f02201e33993368fc43cefc4c77aeb Mon Sep 17 00:00:00 2001 From: TTtie Date: Fri, 24 Apr 2026 23:59:25 +0200 Subject: [PATCH 6/8] fix: "patch" a race condition between login and config phase A more proper fix is TBD Signed-off-by: TTtie --- .../sportpaper/features/serverlinks/ViaServerLinks.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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..0bceec6d 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,10 +12,12 @@ 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.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.Bukkit; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNullByDefault; @@ -32,7 +34,10 @@ public static void sendToPlayer(Player player, List serverLinks) { .protocolVersion() .newerThanOrEqualTo(ProtocolVersion.v1_21)) { PacketWrapper serverLinksPacket = createPacket(userConnection, serverLinks); - serverLinksPacket.scheduleSend(serverLinkProtocol.getClass()); + Runnable r = () -> serverLinksPacket.scheduleSend(serverLinkProtocol.getClass()); + if (userConnection.getProtocolInfo().getClientState() != State.PLAY) { + Bukkit.getScheduler().runTaskLater(Community.get(), r, 20); + } else r.run(); } } From 52c99065aad8bdb708b0084caf9605861a9f2724 Mon Sep 17 00:00:00 2001 From: TTtie Date: Mon, 27 Apr 2026 00:12:26 +0200 Subject: [PATCH 7/8] refactor: hook into server-bound FINISH_CONFIGURATION packet Refactors the implementation and implements a hook for the FINISH_CONFIGURATION packet in the ViaVersion path to be used as the signal to send server links instead of the PlayerJoinEvent. This should reduce the risk of a race condition resulting in a protocol error occurring, as the client should already expect play-state packets after this. The modern path is untested for the time being due to migration to PacketEvents on the PGM dev branch. Signed-off-by: TTtie --- .../serverlinks/ServerLinksConfig.java | 6 +- .../serverlinks/ServerLinksFeature.java | 25 +++++-- .../serverlinks/ServerLinksRequestEvent.java | 32 +++++++++ .../serverlinks/types/ServerLink.java | 6 +- .../types/ServerLinkBuiltinType.java | 4 +- .../ModernServerLinksPlatform.java | 31 +++++++- .../serverlinks/SpServerLinksPlatform.java | 15 +++- .../features/serverlinks/ViaServerLinks.java | 70 +++++++++++++++---- 8 files changed, 156 insertions(+), 33 deletions(-) create mode 100644 core/src/main/java/dev/pgm/community/serverlinks/ServerLinksRequestEvent.java 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..24c79502 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.jspecify.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; -@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..5c02d3e2 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksFeature.java @@ -8,10 +8,9 @@ import org.bukkit.configuration.Configuration; 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); @@ -21,6 +20,10 @@ default boolean isSupported() { } void sendToPlayer(Player player, List serverLinks); + + void hook(); + + void unhook(); } public ServerLinksFeature(Configuration config, Logger logger) { @@ -35,9 +38,21 @@ public ServerLinksFeature(Configuration config, Logger logger) { } } + @Override + public void enable() { + PLATFORM.hook(); + super.enable(); + } + + @Override + public void disable() { + PLATFORM.unhook(); + super.disable(); + } + @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - PLATFORM.sendToPlayer(event.getPlayer(), getServerLinksConfig().getLinks()); + public void onServerLinkRequest(ServerLinksRequestEvent event) { + event.setServerLinks(getServerLinksConfig().getLinks()); } public ServerLinksConfig getServerLinksConfig() { diff --git a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksRequestEvent.java b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksRequestEvent.java new file mode 100644 index 00000000..ee83771d --- /dev/null +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksRequestEvent.java @@ -0,0 +1,32 @@ +package dev.pgm.community.serverlinks; + +import dev.pgm.community.serverlinks.types.ServerLink; +import java.util.List; +import org.bukkit.event.HandlerList; +import org.bukkit.event.server.ServerEvent; +import org.jspecify.annotations.Nullable; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class ServerLinksRequestEvent extends ServerEvent { + public static final HandlerList handlers = new HandlerList(); + + private @Nullable List serverLinks = null; + + public @Nullable List getServerLinks() { + return serverLinks; + } + + public void setServerLinks(@Nullable List serverLinks) { + this.serverLinks = serverLinks; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} 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..d27a2fbf 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.Nullable; +import org.jspecify.annotations.NullMarked; /** * 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..7e6eae08 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 @@ -2,7 +2,9 @@ import static dev.pgm.community.util.Supports.Variant.PAPER; +import dev.pgm.community.Community; import dev.pgm.community.serverlinks.ServerLinksFeature; +import dev.pgm.community.serverlinks.ServerLinksRequestEvent; import dev.pgm.community.serverlinks.types.ServerLink; import dev.pgm.community.serverlinks.types.ServerLinkBuiltinType; import dev.pgm.community.util.Supports; @@ -10,16 +12,30 @@ import org.bukkit.ServerLinks; import org.bukkit.craftbukkit.CraftServerLinks; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNullByDefault; +import org.bukkit.event.EventHandler; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.jspecify.annotations.NullMarked; @Supports(PAPER) -@NotNullByDefault -public class ModernServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform { +@NullMarked +public class ModernServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform, Listener { @Override public void sendToPlayer(Player player, List serverLinks) { player.sendLinks(toPlatformServerLinks(serverLinks)); } + @Override + public void hook() { + Community.get().registerListener(this); + } + + @Override + public void unhook() { + HandlerList.unregisterAll(this); + } + private ServerLinks toPlatformServerLinks(List links) { ServerLinks bukkitLinks = new CraftServerLinks(new net.minecraft.server.ServerLinks(List.of())); for (ServerLink link : links) { @@ -37,4 +53,13 @@ private ServerLinks toPlatformServerLinks(List links) { private ServerLinks.Type toBukkitType(ServerLinkBuiltinType type) { return ServerLinks.Type.values()[type.ordinal()]; } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + ServerLinksRequestEvent request = new ServerLinksRequestEvent(); + request.callEvent(); + + if (request.getServerLinks() == null) return; + event.getPlayer().sendLinks(toPlatformServerLinks(request.getServerLinks())); + } } 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..4272e305 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(); @@ -34,4 +34,15 @@ public void sendToPlayer(Player player, List serverLinks) { ViaServerLinks.sendToPlayer(player, serverLinks); } } + + @Override + public void hook() { + ViaServerLinks.hookProtocol(); + ViaServerLinks.enable(); + } + + @Override + public void unhook() { + ViaServerLinks.disable(); + } } 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 0bceec6d..40683a4b 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 @@ -7,43 +7,61 @@ 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.ServerboundPacketType; 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.Community; +import dev.pgm.community.serverlinks.ServerLinksRequestEvent; 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.Bukkit; 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(); + private static boolean isViaHooked = false; + // Controls whether to run the routine as we cannot unhook the listener from the Via pipeline + private static boolean isEnabled = false; 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)) { + if (userConnection != null) sendToPlayer(userConnection, serverLinks); + } + + public static void sendToPlayer(UserConnection userConnection, List serverLinks) { + if (userConnection + .getProtocolInfo() + .protocolVersion() + .newerThanOrEqualTo(ProtocolVersion.v1_21)) { PacketWrapper serverLinksPacket = createPacket(userConnection, serverLinks); - Runnable r = () -> serverLinksPacket.scheduleSend(serverLinkProtocol.getClass()); - if (userConnection.getProtocolInfo().getClientState() != State.PLAY) { - Bukkit.getScheduler().runTaskLater(Community.get(), r, 20); - } else r.run(); + serverLinksPacket.scheduleSend(serverLinkProtocol.getClass()); } } + public static void hookProtocol() { + hookProtocolInner(serverLinkProtocol); + } + + public static void enable() { + isEnabled = true; + } + + public static void disable() { + isEnabled = false; + } + private static PacketWrapper createPacket(UserConnection conn, List links) { - var packetTypes = serverLinkProtocol.getPacketTypesProvider().mappedClientboundPacketTypes(); - var packetType = packetTypes.get(State.PLAY).typeByName("SERVER_LINKS"); + var packetType = serverLinkProtocol + .getPacketTypesProvider() + .mappedClientboundPacketTypes() + .get(State.PLAY) + .typeByName("SERVER_LINKS"); PacketWrapper packet = PacketWrapper.create(packetType, conn); packet.write(Types.VAR_INT, links.size()); for (ServerLink link : links) { @@ -74,4 +92,26 @@ private static Tag toViaTag(Component component) { .getProtocol(/* to */ ProtocolVersion.v1_21, /* from */ ProtocolVersion.v1_20_5), "ViaVersion v1.20.5 -> v1.21 protocol was not found"); } + + private static void hookProtocolInner( + // Expected - this is only parametrized because of generics + @SuppressWarnings("SameParameterValue") Protocol protocol) { + if (isViaHooked) return; + protocol.appendServerbound( + assertNotNull( + protocol + .getPacketTypesProvider() + .unmappedServerboundType(State.CONFIGURATION, "FINISH_CONFIGURATION"), + "Server-bound FINISH_CONFIGURATION packet was not found"), + wrapper -> { + if (!isEnabled) return; + // When received, the client transitions into PLAY state. Doing this in PlayerJoinEvent + // triggers a race condition + ServerLinksRequestEvent request = new ServerLinksRequestEvent(); + request.callEvent(); + if (request.getServerLinks() == null) return; + sendToPlayer(wrapper.user(), request.getServerLinks()); + }); + isViaHooked = true; + } } From 0cfd86fb39d49b248bfde4425db1f387220cb7fc Mon Sep 17 00:00:00 2001 From: TTtie Date: Mon, 27 Apr 2026 13:56:10 +0200 Subject: [PATCH 8/8] lint Signed-off-by: TTtie --- .../java/dev/pgm/community/serverlinks/ServerLinksConfig.java | 2 +- .../dev/pgm/community/serverlinks/ServerLinksRequestEvent.java | 2 +- .../java/dev/pgm/community/serverlinks/types/ServerLink.java | 2 +- 3 files changed, 3 insertions(+), 3 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 24c79502..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,9 +12,9 @@ import java.util.Map; import java.util.Objects; import org.bukkit.configuration.Configuration; -import org.jspecify.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; @NullMarked public class ServerLinksConfig extends FeatureConfigImpl { diff --git a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksRequestEvent.java b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksRequestEvent.java index ee83771d..d5483648 100644 --- a/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksRequestEvent.java +++ b/core/src/main/java/dev/pgm/community/serverlinks/ServerLinksRequestEvent.java @@ -4,8 +4,8 @@ import java.util.List; import org.bukkit.event.HandlerList; import org.bukkit.event.server.ServerEvent; -import org.jspecify.annotations.Nullable; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; @NullMarked public class ServerLinksRequestEvent extends ServerEvent { 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 d27a2fbf..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.jspecify.annotations.Nullable; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Represents a Minecraft server link.