Skip to content

Commit c69806d

Browse files
committed
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 <me@tttie.cz>
1 parent eb27740 commit c69806d

10 files changed

Lines changed: 286 additions & 1 deletion

File tree

core/src/main/java/dev/pgm/community/feature/FeatureManager.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import dev.pgm.community.polls.feature.PollFeature;
2525
import dev.pgm.community.requests.feature.RequestFeature;
2626
import dev.pgm.community.requests.feature.types.SQLRequestFeature;
27+
import dev.pgm.community.serverlinks.ServerLinksFeature;
2728
import dev.pgm.community.sessions.feature.SessionFeature;
2829
import dev.pgm.community.sessions.feature.types.SQLSessionFeature;
2930
import dev.pgm.community.squads.SquadFeature;
@@ -60,6 +61,7 @@ public class FeatureManager {
6061
private final PollFeature polls;
6162
private final SquadFeature squads;
6263
private final MatchHistoryFeature history;
64+
private final ServerLinksFeature serverLinks;
6365

6466
public FeatureManager(
6567
Configuration config,
@@ -97,6 +99,7 @@ public FeatureManager(
9799
this.polls = new PollFeature(config, logger);
98100
this.squads = new SquadFeature(config, logger);
99101
this.history = new MatchHistoryFeature(config, logger);
102+
this.serverLinks = new ServerLinksFeature(config, logger);
100103
}
101104

102105
public AssistanceFeature getReports() {
@@ -179,6 +182,10 @@ public MatchHistoryFeature getHistory() {
179182
return history;
180183
}
181184

185+
public ServerLinksFeature getServerLinks() {
186+
return serverLinks;
187+
}
188+
182189
public void reloadConfig(Configuration config) {
183190
// Reload all config values here
184191
getReports().getConfig().reload(config);
@@ -200,6 +207,7 @@ public void reloadConfig(Configuration config) {
200207
getPolls().getConfig().reload(config);
201208
getSquads().getConfig().reload(config);
202209
getHistory().getConfig().reload(config);
210+
getServerLinks().getConfig().reload(config);
203211

204212
// TODO: Look into maybe unregister commands for features that have been disabled
205213
// commands#unregisterCommand
@@ -226,5 +234,6 @@ public void disable() {
226234
if (getPolls().isEnabled()) getPolls().disable();
227235
if (getSquads().isEnabled()) getSquads().disable();
228236
if (getHistory().isEnabled()) getHistory().disable();
237+
if (getServerLinks().isEnabled()) getServerLinks().disable();
229238
}
230239
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package dev.pgm.community.serverlinks;
2+
3+
import static tc.oc.pgm.util.text.TextParser.parseComponent;
4+
import static tc.oc.pgm.util.text.TextParser.parseEnum;
5+
import static tc.oc.pgm.util.text.TextParser.parseUri;
6+
7+
import dev.pgm.community.feature.config.FeatureConfigImpl;
8+
import dev.pgm.community.serverlinks.types.ServerLink;
9+
import dev.pgm.community.serverlinks.types.ServerLinkBuiltinType;
10+
import java.net.URI;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Objects;
14+
import org.bukkit.configuration.Configuration;
15+
16+
public class ServerLinksConfig extends FeatureConfigImpl {
17+
private static final String KEY = "server-links";
18+
private static final String LINKS_KEY = "links";
19+
20+
private static final String LINK_BUILTIN_KEY = "builtin";
21+
private static final String LINK_CUSTOM_TEXT_KEY = "text";
22+
private static final String LINK_URI_KEY = "uri";
23+
24+
private List<ServerLink> links;
25+
26+
public ServerLinksConfig(Configuration config) {
27+
super(KEY, config);
28+
}
29+
30+
public List<ServerLink> getLinks() {
31+
return links;
32+
}
33+
34+
@Override
35+
public void reload(Configuration config) {
36+
super.reload(config);
37+
links = config.getMapList(getKey() + "." + LINKS_KEY).stream()
38+
.map(this::readLink)
39+
.toList();
40+
}
41+
42+
private ServerLink readLink(Map<?, ?> configData) {
43+
String builtIn = Objects.toString(configData.get(LINK_BUILTIN_KEY), null);
44+
String customText = Objects.toString(configData.get(LINK_CUSTOM_TEXT_KEY), null);
45+
String uri = Objects.toString(configData.get(LINK_URI_KEY), null);
46+
47+
if (builtIn == null && customText == null) {
48+
throw new IllegalStateException(
49+
"A server link must have either built-in or custom text defined");
50+
} else if (builtIn != null && customText != null) {
51+
throw new IllegalStateException(
52+
"A server link cannot have both built-in and custom text defined");
53+
}
54+
55+
URI parsedUri = parseUri(uri);
56+
if (!parsedUri.getScheme().equals("http") && !parsedUri.getScheme().equals("https")) {
57+
throw new IllegalStateException("The URL " + uri + " is not a web URL");
58+
}
59+
60+
return new ServerLink(
61+
builtIn != null ? parseEnum(builtIn, ServerLinkBuiltinType.class) : null,
62+
customText != null ? parseComponent(customText) : null,
63+
parsedUri);
64+
}
65+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package dev.pgm.community.serverlinks;
2+
3+
import dev.pgm.community.feature.FeatureBase;
4+
import dev.pgm.community.serverlinks.types.ServerLink;
5+
import dev.pgm.community.util.Platform;
6+
import java.util.List;
7+
import java.util.logging.Logger;
8+
import org.bukkit.configuration.Configuration;
9+
import org.bukkit.entity.Player;
10+
import org.bukkit.event.EventHandler;
11+
import org.bukkit.event.player.PlayerJoinEvent;
12+
13+
public class ServerLinksFeature extends FeatureBase {
14+
private static final ServerLinksPlatform PLATFORM = Platform.get(ServerLinksPlatform.class);
15+
16+
public interface ServerLinksPlatform {
17+
default boolean isSupported() {
18+
return true;
19+
}
20+
21+
void sendToPlayer(Player player, List<ServerLink> serverLinks);
22+
}
23+
24+
public ServerLinksFeature(Configuration config, Logger logger) {
25+
super(new ServerLinksConfig(config), logger, "Server Links");
26+
27+
if (getConfig().isEnabled()) {
28+
if (!PLATFORM.isSupported()) {
29+
logger.warning("Server links are enabled but not supported by the platform");
30+
return;
31+
}
32+
enable();
33+
}
34+
}
35+
36+
@EventHandler
37+
public void onPlayerJoin(PlayerJoinEvent event) {
38+
PLATFORM.sendToPlayer(event.getPlayer(), getServerLinksConfig().getLinks());
39+
}
40+
41+
public ServerLinksConfig getServerLinksConfig() {
42+
return (ServerLinksConfig) getConfig();
43+
}
44+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dev.pgm.community.serverlinks.types;
2+
3+
import java.net.URI;
4+
import net.kyori.adventure.text.Component;
5+
6+
/**
7+
* Represents a Minecraft server link.
8+
*
9+
* @param builtinType The built-in type of the server link, or null if it's a custom link.
10+
* @param customText The custom text for the server link, or null if builtinType is set.
11+
* @param uri The URI of the server link.
12+
*/
13+
public record ServerLink(ServerLinkBuiltinType builtinType, Component customText, URI uri) {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package dev.pgm.community.serverlinks.types;
2+
3+
/**
4+
* Represents a built-in server link type that will be auto-translated by the Minecraft client and
5+
* possibly have special functionality. Keep in sync with Paper's org.bukkit.ServerLinks.Type.
6+
*/
7+
public enum ServerLinkBuiltinType {
8+
REPORT_BUG,
9+
COMMUNITY_GUIDELINES,
10+
SUPPORT,
11+
STATUS,
12+
FEEDBACK,
13+
COMMUNITY,
14+
WEBSITE,
15+
FORUMS,
16+
NEWS,
17+
ANNOUNCEMENTS;
18+
}

core/src/main/resources/config.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,15 @@ database:
385385
host: "localhost:3306" # host and port
386386
timezone: "America/Los_Angeles" # Database timezone
387387
max-connections: 2 # Maximum simultaneous connections (does not impact SQLite)
388+
389+
# Server Links - Adds links to the pause menu for 1.21+ clients
390+
# Requires ViaVersion to be installed on 1.8-based servers.
391+
server-links:
392+
enabled: false
393+
links:
394+
# A built-in server link type will be auto-translated by the client and may provide some extra functionality.
395+
- builtin: report bug # See https://jd.papermc.io/paper/org/bukkit/ServerLinks.Type.html for a list of built-in types
396+
uri: https://pgm.dev
397+
# Alternatively, custom text can be provided. Custom text is mutually exclusive with built-in types.
398+
- text: "Submit a new map"
399+
uri: https://pgm.dev

core/src/main/resources/plugin.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ main: ${mainClass}
44
version: ${version} (git-${commitHash})
55
website: ${url}
66
author: ${author}
7-
softdepend: [PGM, Environment, AFK]
7+
softdepend: [PGM, Environment, AFK, ViaVersion]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package dev.pgm.community.platform.modern.feature;
2+
3+
import static dev.pgm.community.util.Supports.Variant.PAPER;
4+
5+
import dev.pgm.community.serverlinks.ServerLinksFeature;
6+
import dev.pgm.community.serverlinks.types.ServerLink;
7+
import dev.pgm.community.serverlinks.types.ServerLinkBuiltinType;
8+
import dev.pgm.community.util.Supports;
9+
import java.util.List;
10+
import org.bukkit.ServerLinks;
11+
import org.bukkit.craftbukkit.CraftServerLinks;
12+
import org.bukkit.entity.Player;
13+
14+
@Supports(PAPER)
15+
public class ModernServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform {
16+
@Override
17+
public void sendToPlayer(Player player, List<ServerLink> serverLinks) {
18+
player.sendLinks(toPlatformServerLinks(serverLinks));
19+
}
20+
21+
private ServerLinks toPlatformServerLinks(List<ServerLink> links) {
22+
ServerLinks bukkitLinks = new CraftServerLinks(new net.minecraft.server.ServerLinks(List.of()));
23+
for (ServerLink link : links) {
24+
if (link.builtinType() != null) {
25+
bukkitLinks.addLink(toBukkitType(link.builtinType()), link.uri());
26+
} else {
27+
bukkitLinks.addLink(link.customText(), link.uri());
28+
}
29+
}
30+
31+
return bukkitLinks;
32+
}
33+
34+
private ServerLinks.Type toBukkitType(ServerLinkBuiltinType type) {
35+
return ServerLinks.Type.values()[type.ordinal()];
36+
}
37+
}

platform/platform-sportpaper/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ dependencies {
66
implementation(project(":core"))
77
implementation(project(":util"))
88
compileOnly("app.ashcon:sportpaper:1.8.8-R0.1-SNAPSHOT")
9+
compileOnly("com.viaversion:viaversion-api:5.0.0")
910
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package dev.pgm.community.platform.sportpaper.features;
2+
3+
import static dev.pgm.community.util.Supports.Variant.SPORTPAPER;
4+
5+
import com.viaversion.nbt.tag.Tag;
6+
import com.viaversion.viaversion.api.Via;
7+
import com.viaversion.viaversion.api.connection.UserConnection;
8+
import com.viaversion.viaversion.api.protocol.Protocol;
9+
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
10+
import com.viaversion.viaversion.api.protocol.packet.State;
11+
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
12+
import com.viaversion.viaversion.api.type.Types;
13+
import com.viaversion.viaversion.libs.gson.JsonParser;
14+
import com.viaversion.viaversion.libs.mcstructs.text.utils.JsonNbtConverter;
15+
import dev.pgm.community.serverlinks.ServerLinksFeature;
16+
import dev.pgm.community.serverlinks.types.ServerLink;
17+
import dev.pgm.community.util.Supports;
18+
import java.util.List;
19+
import net.kyori.adventure.text.Component;
20+
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
21+
import org.bukkit.entity.Player;
22+
23+
@Supports(SPORTPAPER)
24+
public class SpServerLinksPlatform implements ServerLinksFeature.ServerLinksPlatform {
25+
private final Protocol<?, ?, ?, ?> serverLinkProtocol = findServerLinkProtocol();
26+
private static final boolean hasVia = hasVia();
27+
28+
private static boolean hasVia() {
29+
try {
30+
Class.forName("com.viaversion.viaversion.api.Via");
31+
return true;
32+
} catch (ClassNotFoundException e) {
33+
return false;
34+
}
35+
}
36+
37+
@Override
38+
public boolean isSupported() {
39+
return hasVia;
40+
}
41+
42+
@Override
43+
public void sendToPlayer(Player player, List<ServerLink> serverLinks) {
44+
if (hasVia && Via.getAPI().isInjected(player.getUniqueId())) {
45+
UserConnection userConnection = Via.getAPI().getConnection(player.getUniqueId());
46+
if (userConnection != null
47+
&& userConnection
48+
.getProtocolInfo()
49+
.protocolVersion()
50+
.newerThanOrEqualTo(ProtocolVersion.v1_21)) {
51+
PacketWrapper serverLinksPacket = createPacket(userConnection, serverLinks);
52+
serverLinksPacket.scheduleSend(serverLinkProtocol.getClass());
53+
}
54+
}
55+
}
56+
57+
private PacketWrapper createPacket(UserConnection conn, List<ServerLink> links) {
58+
var packetTypes = serverLinkProtocol.getPacketTypesProvider().mappedClientboundPacketTypes();
59+
var packetType = packetTypes.get(State.PLAY).typeByName("SERVER_LINKS");
60+
PacketWrapper packet = PacketWrapper.create(packetType, conn);
61+
packet.write(Types.VAR_INT, links.size());
62+
// TODO: is there a better way to do this?
63+
for (ServerLink link : links) {
64+
packet.write(Types.BOOLEAN, link.builtinType() != null);
65+
if (link.builtinType() != null) {
66+
packet.write(Types.VAR_INT, link.builtinType().ordinal());
67+
} else {
68+
packet.write(Types.TAG, toViaTag(link.customText()));
69+
}
70+
packet.write(Types.STRING, link.uri().toString());
71+
}
72+
73+
return packet;
74+
}
75+
76+
private Tag toViaTag(Component component) {
77+
return JsonNbtConverter.toNbt(
78+
JsonParser.parseString(GsonComponentSerializer.gson().serialize(component)));
79+
}
80+
81+
private Protocol<?, ?, ?, ?> findServerLinkProtocol() {
82+
return Via.getManager()
83+
.getProtocolManager()
84+
.getProtocol(/* to */ ProtocolVersion.v1_21, /* from */ ProtocolVersion.v1_20_5);
85+
}
86+
}

0 commit comments

Comments
 (0)