From 9d25d309d38a1e3c818166f0c5f994b8af3785cd Mon Sep 17 00:00:00 2001 From: William Date: Tue, 18 Jun 2024 15:29:58 +0100 Subject: [PATCH] feat: Add basic API for server links (#1353) * feat: add basic server links API * refactor: more precondition checking on links * refactor: remove whitespace * refactor: adjust method order, JDs in ServerLink * refactor: remove "throws" from constructors * refactor: add @NotNull annotations * refactor: requested changes * refactor: just use `List#copyOf` --- .../com/velocitypowered/api/proxy/Player.java | 13 +++ .../velocitypowered/api/util/ServerLink.java | 101 ++++++++++++++++++ .../connection/client/ConnectedPlayer.java | 18 ++++ .../config/ClientboundServerLinksPacket.java | 8 ++ 4 files changed, 140 insertions(+) create mode 100644 api/src/main/java/com/velocitypowered/api/util/ServerLink.java diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index dfe9a2bc72..04e65c849b 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -21,6 +21,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.api.util.ServerLink; import java.net.InetSocketAddress; import java.util.Collection; import java.util.List; @@ -461,4 +462,16 @@ default void openBook(@NotNull Book book) { * @sinceMinecraft 1.20.5 */ void requestCookie(Key key); + + /** + * Send the player a list of custom links to display in their client's pause menu. + * + *

Note that later packets sent by the backend server may override links sent by the proxy. + * + * @param links an ordered list of {@link ServerLink}s to send to the player + * @throws IllegalArgumentException if the player is from a version lower than 1.21 + * @since 3.3.0 + * @sinceMinecraft 1.21 + */ + void setServerLinks(@NotNull List links); } \ No newline at end of file diff --git a/api/src/main/java/com/velocitypowered/api/util/ServerLink.java b/api/src/main/java/com/velocitypowered/api/util/ServerLink.java new file mode 100644 index 0000000000..9eb04a9801 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/util/ServerLink.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021-2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.util; + +import com.google.common.base.Preconditions; +import java.net.URI; +import java.util.Optional; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a custom URL servers can show in player pause menus. + * Links can be of a built-in type or use a custom component text label. + */ +public final class ServerLink { + + private @Nullable Type type; + private @Nullable Component label; + private final URI url; + + private ServerLink(Component label, String url) { + this.label = Preconditions.checkNotNull(label, "label"); + this.url = URI.create(url); + } + + private ServerLink(Type type, String url) { + this.type = Preconditions.checkNotNull(type, "type"); + this.url = URI.create(url); + } + + /** + * Construct a server link with a custom component label. + * + * @param label a custom component label to display + * @param link the URL to open when clicked + */ + public static ServerLink serverLink(Component label, String link) { + return new ServerLink(label, link); + } + + /** + * Construct a server link with a built-in type. + * + * @param type the {@link Type built-in type} of link + * @param link the URL to open when clicked + */ + public static ServerLink serverLink(Type type, String link) { + return new ServerLink(type, link); + } + + /** + * Get the type of the server link. + * + * @return the type of the server link + */ + public Optional getBuiltInType() { + return Optional.ofNullable(type); + } + + /** + * Get the custom component label of the server link. + * + * @return the custom component label of the server link + */ + public Optional getCustomLabel() { + return Optional.ofNullable(label); + } + + /** + * Get the link {@link URI}. + * + * @return the link {@link URI} + */ + public URI getUrl() { + return url; + } + + /** + * Built-in types of server links. + * + * @apiNote {@link Type#BUG_REPORT} links are shown on the connection error screen + */ + public enum Type { + BUG_REPORT, + COMMUNITY_GUIDELINES, + SUPPORT, + STATUS, + FEEDBACK, + COMMUNITY, + WEBSITE, + FORUMS, + NEWS, + ANNOUNCEMENTS + } + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index de828382e5..8522dfce79 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -55,6 +55,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.api.util.ServerLink; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -83,6 +84,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatPacket; +import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket; import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput; @@ -1059,6 +1061,22 @@ public void requestCookie(final Key key) { }, connection.eventLoop()); } + @Override + public void setServerLinks(final @NotNull List links) { + Preconditions.checkNotNull(links, "links"); + Preconditions.checkArgument( + this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21), + "Player version must be at least 1.21 to be able to set server links"); + + if (connection.getState() != StateRegistry.PLAY + && connection.getState() != StateRegistry.CONFIG) { + throw new IllegalStateException("Can only send server links in CONFIGURATION or PLAY protocol"); + } + + connection.write(new ClientboundServerLinksPacket(List.copyOf(links).stream() + .map(l -> new ClientboundServerLinksPacket.ServerLink(l, getProtocolVersion())).toList())); + } + @Override public void addCustomChatCompletions(@NotNull Collection completions) { Preconditions.checkNotNull(completions, "completions"); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java index bee080ee82..274bbb8f9e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.protocol.packet.config; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.util.ServerLink; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; @@ -66,6 +67,13 @@ public List getServerLinks() { } public record ServerLink(int id, ComponentHolder displayName, String url) { + + public ServerLink(com.velocitypowered.api.util.ServerLink link, ProtocolVersion protocolVersion) { + this(link.getBuiltInType().map(Enum::ordinal).orElse(-1), + link.getCustomLabel().map(c -> new ComponentHolder(protocolVersion, c)).orElse(null), + link.getUrl().toString()); + } + private static ServerLink read(ByteBuf buf, ProtocolVersion version) { if (buf.readBoolean()) { return new ServerLink(ProtocolUtils.readVarInt(buf), null, ProtocolUtils.readString(buf));