From 896fb8e5c8f2783f263e99dcb86e185fbb82e7af Mon Sep 17 00:00:00 2001 From: TTtie Date: Sat, 3 Jan 2026 15:32:39 +0000 Subject: [PATCH 1/5] 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 bf12356e..8869b1e7 100644 --- a/core/src/main/java/dev/pgm/community/feature/FeatureManager.java +++ b/core/src/main/java/dev/pgm/community/feature/FeatureManager.java @@ -24,6 +24,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; @@ -63,6 +64,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 @@ -99,6 +101,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() { @@ -185,6 +188,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); @@ -207,6 +214,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 @@ -234,5 +242,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 67d0d732..9d659e1a 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 @@ -404,3 +404,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 b15c74c2..cf37f6b8 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] +softdepend: [Database, PGM, Environment, 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 9611336da464e9b546f948c9076775fad1f44235 Mon Sep 17 00:00:00 2001 From: TTtie Date: Sun, 4 Jan 2026 14:47:29 +0000 Subject: [PATCH 2/5] 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 880d3deabd087ceae00064a26bffed08fe3d5390 Mon Sep 17 00:00:00 2001 From: TTtie Date: Tue, 6 Jan 2026 00:42:56 +0000 Subject: [PATCH 3/5] 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 b198c332fd68600a400aa1c70b1742770970e963 Mon Sep 17 00:00:00 2001 From: TTtie Date: Tue, 6 Jan 2026 00:46:51 +0000 Subject: [PATCH 4/5] 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 af3f27fd6f2ae1eb9dd80f326773d25f1d87e293 Mon Sep 17 00:00:00 2001 From: TTtie Date: Fri, 13 Feb 2026 03:02:18 +0000 Subject: [PATCH 5/5] 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"); } }