feat: Add configuration for server links (#201)

* feat: add server URLs

* refactor: cleanup imports

* fix: only send server links to 1.21 clients

* feat: update server links on reload

* refactor: minor cleanup

* docs: add docs for server links

* fix: protocol version check issue

* Improved ServerUrl#resolve

---------

Co-authored-by: AlexDev_ <56083016+alexdev03@users.noreply.github.com>
This commit is contained in:
William 2024-06-18 22:42:50 +01:00 committed by GitHub
parent 6f909fbec1
commit 84ae7a9437
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 201 additions and 27 deletions

View File

@ -24,8 +24,8 @@ ext {
repositories {
mavenCentral()
maven { url = 'https://repo.william278.net/velocity/' }
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://repo.william278.net/velocity/' }
maven { url = 'https://repo.william278.net/releases/' }
maven { url = 'https://jitpack.io/' }
maven { url = 'https://repo.minebench.de/' }
@ -35,20 +35,20 @@ dependencies {
compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT"
compileOnly "com.velocitypowered:velocity-proxy:${velocity_api_version}-SNAPSHOT"
compileOnly 'io.netty:netty-codec-http:4.1.111.Final'
compileOnly 'org.projectlombok:lombok:1.18.32'
compileOnly 'net.luckperms:api:5.4'
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.0.0'
compileOnly 'net.william278:PAPIProxyBridge:1.5'
compileOnly 'it.unimi.dsi:fastutil:8.5.13'
compileOnly 'net.kyori:adventure-nbt:4.17.0'
implementation 'org.apache.commons:commons-text:1.12.0'
implementation 'net.william278:DesertWell:2.0.4'
implementation 'net.william278:minedown:1.8.2'
implementation 'org.bstats:bstats-velocity:3.0.2'
implementation 'de.exlll:configlib-yaml:4.5.0'
compileOnly 'io.netty:netty-codec-http:4.1.111.Final'
compileOnly 'net.luckperms:api:5.4'
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.0.0'
compileOnly 'net.william278:PAPIProxyBridge:1.5'
compileOnly 'it.unimi.dsi:fastutil:8.5.13'
compileOnly 'net.kyori:adventure-nbt:4.17.0'
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}

View File

@ -50,6 +50,16 @@ sort_players: true
remove_spectator_effect: false
# Whether to enable the Plugin Message API (allows backend plugins to perform certain operations)
enable_plugin_message_api: true
# A list of URLs that will be sent to display on player pause menus (Minecraft 1.21+ clients only).
# • Labels can be fully custom or built-in (one of 'bug_report', 'community_guidelines', 'support', 'status',
# 'feedback', 'community', 'website', 'forums', 'news', or 'announcements').
# • If you supply a url with a 'bug_report' label, it will be shown if the player is disconnected.
# • Specify a set of server groups each URL should be sent on. Use '*' to show a URL to all groups.
server_links:
- label: '&#00fb9a&About Velocitab'
url: 'https://william278.net/project/velocitab'
groups:
- '*'
```
</details>
@ -111,3 +121,6 @@ Velocitab supports basic header and footer animations by adding multiple frames
### Placeholders
You can use various placeholders that will be replaced with values (for example, `%username%`) in your config. Support for PlaceholderAPI is also available through [a bridge library plugin](https://modrinth.com/plugin/papiproxybridge), as is the component-based MiniPlaceholders for users of that plugin with the MiniMessage formatter. See [[Placeholders]] for more information.
### Server Links
For Minecraft 1.21+ clients, Velocitab supports specifying a list of URLs that will be sent to display in the player pause menu. See [[Server Links]] for more information.

37
docs/Server-Links.md Normal file
View File

@ -0,0 +1,37 @@
> **Note:** This feature will only apply for users connecting with **Minecraft 1.21+** clients
Velocitab supports sending _Server Links_ to players, which will be displayed in the player pause menu by 1.21+ game clients. This can be useful for linking to your server's website, Discord, or other resources.
## Configuring
Server links are configured with the `server_links` section in your `config.yml` file. A link must have:
* A `url` field; a valid web URL to link to
* A `label` field, which is the text to display for the link. Labels can be::
* Fully formatted custom text. You may include placeholders and formatting valid for your chosen formatter.
* One of the following built-in label strings, which will be localized into the user's client language:
* `bug_report` - Will also be shown on the disconnection error screen.
* `community_guidelines`
* `support`
* `status`
* `feedback`
* `community`
* `website`
* `forums`
* `news`
* `announcements`
* A `groups` field, which is a list of server groups the link should be sent to connecting players on.
* Use `'*'` to show the link to all groups.
### Example section
```yaml
server_links:
- url: 'https://william278.net/project/velocitab'
label: 'website'
groups: ['*']
- url: 'https://william278.net/docs/velocitab'
label: 'Documentation'
groups: ['*']
- url: 'https://github.com/William278/Velocitab/issues'
label: 'bug_report' # This will use the bug report built-in label and also be shown on the player disconnect screen
groups: ['*']
```

View File

@ -3,9 +3,9 @@ javaVersion=17
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true
plugin_version=1.6.6
plugin_version=1.7
plugin_archive=velocitab
plugin_description=A beautiful and versatile TAB list plugin for Velocity proxies
velocity_api_version=3.3.0
velocity_minimum_build=398
velocity_minimum_build=400

BIN
images/server-links.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -77,7 +77,7 @@ public enum Placeholder {
private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
private final boolean parameterised;
private final Pattern pattern;
private final static Pattern checkPlaceholders = Pattern.compile("%.*?%");
private final static Pattern CHECK_PLACEHOLDERS = Pattern.compile("%.*?%");
private final static String DELIMITER = ":::";
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
@ -109,7 +109,6 @@ public enum Placeholder {
public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin,
@NotNull TabPlayer player) {
if (format.equals(DELIMITER)) {
return CompletableFuture.completedFuture("");
}
@ -119,19 +118,20 @@ public enum Placeholder {
if (placeholder.parameterised) {
// Replace the placeholder with the result of the replacer function with the parameter
format = matcher.replaceAll(matchResult ->
Matcher.quoteReplacement(
placeholder.replacer.apply(StringUtils.chop(matchResult.group().replace("%" + placeholder.name().toLowerCase(), ""))
, plugin, player)
));
Matcher.quoteReplacement(placeholder.replacer.apply(
StringUtils.chop(matchResult.group().replace(
"%" + placeholder.name().toLowerCase(), ""
)), plugin, player
)));
} else {
// Replace the placeholder with the result of the replacer function
format = matcher.replaceAll(matchResult -> Matcher.quoteReplacement(placeholder.replacer.apply(null, plugin, player)));
}
}
final String replaced = format;
if (!checkPlaceholders.matcher(replaced).find()) {
final String replaced = format;
if (!CHECK_PLACEHOLDERS.matcher(replaced).find()) {
return CompletableFuture.completedFuture(replaced);
}

View File

@ -0,0 +1,90 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.config;
import com.google.common.collect.Lists;
import com.velocitypowered.api.util.ServerLink;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.net.URI;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public record ServerUrl(
@NotNull String label,
@NotNull String url,
@NotNull Set<String> groups
) {
public ServerUrl(@NotNull String label, @NotNull String url) {
this(label, url, Set.of("*"));
}
// Resolve the built-in label or format the custom label, then wrap as a Velocity ServerLink
@NotNull
CompletableFuture<ServerLink> getServerLink(@NotNull Velocitab plugin, @NotNull TabPlayer player) {
return getBuiltInLabel().map(
(type) -> CompletableFuture.completedFuture(ServerLink.serverLink(type, url()))
).orElseGet(
() -> Placeholder.replace(label(), plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
.thenApply(formatted -> ServerLink.serverLink(formatted, url()))
);
}
@NotNull
public static CompletableFuture<List<ServerLink>> resolve(@NotNull Velocitab plugin, @NotNull TabPlayer player,
@NotNull List<ServerUrl> urls) {
final List<CompletableFuture<ServerLink>> futures = new ArrayList<>();
for (ServerUrl url : urls) {
futures.add(url.getServerLink(plugin, player));
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join).toList());
}
private Optional<ServerLink.Type> getBuiltInLabel() {
final String label = label().replaceAll(" ", "_").toUpperCase(Locale.ENGLISH);
return Arrays.stream(ServerLink.Type.values()).filter(type -> type.name().equals(label)).findFirst();
}
// Validate a ServerUrl
void validate() throws IllegalStateException {
if (label().isEmpty()) {
throw new IllegalStateException("Server URL label cannot be empty");
}
if (url().isEmpty()) {
throw new IllegalStateException("Server URL cannot be empty");
}
if (groups().isEmpty()) {
throw new IllegalStateException("Server URL must have at least one group, or '*' to show on all groups");
}
try {
//noinspection ResultOfMethodCallIgnored
URI.create(url());
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Server URL is not a valid URI");
}
}
}

View File

@ -27,6 +27,7 @@ import lombok.NoArgsConstructor;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
@ -96,6 +97,18 @@ public class Settings implements ConfigValidator {
@Comment("Whether to enable the Plugin Message API (allows backend plugins to perform certain operations)")
private boolean enablePluginMessageApi = true;
@Comment({"A list of links that will be sent to display on player pause menus (Minecraft 1.21+ clients only).",
"• Labels can be fully custom or built-in (one of 'bug_report', 'community_guidelines', 'support', 'status',",
" 'feedback', 'community', 'website', 'forums', 'news', or 'announcements').",
"• If you supply a url with a 'bug_report' label, it will be shown if the player is disconnected.",
"• Specify a set of server groups each URL should be sent on. Use '*' to show a URL to all groups."})
private List<ServerUrl> serverLinks = List.of(
new ServerUrl(
"&#00fb9a&About Velocitab",
"https://william278.net/project/velocitab"
)
);
/**
* Get display name for the server
*
@ -107,10 +120,19 @@ public class Settings implements ConfigValidator {
return serverDisplayNames.getOrDefault(serverName, serverName);
}
@NotNull
public List<ServerUrl> getUrlsForGroup(@NotNull Group group) {
return serverLinks.stream()
.filter(link -> link.groups().contains("*") || link.groups().contains(group.name()))
.toList();
}
@Override
public void validateConfig(@NotNull Velocitab plugin) {
if (papiCacheTime < 0) {
throw new IllegalStateException("PAPI cache time must be greater than or equal to 0");
}
serverLinks.forEach(ServerUrl::validate);
}
}

View File

@ -20,6 +20,7 @@
package net.william278.velocitab.tab;
import com.google.common.collect.Maps;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.player.TabList;
@ -33,6 +34,7 @@ import net.william278.velocitab.Velocitab;
import net.william278.velocitab.api.PlayerAddedToTabEvent;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.config.ServerUrl;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
@ -142,18 +144,19 @@ public class PlayerTabList {
protected void joinPlayer(@NotNull Player joined, @NotNull Group group) {
// Add the player to the tracking list if they are not already listed
final TabPlayer tabPlayer = getTabPlayer(joined).orElseGet(() -> createTabPlayer(joined, group));
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
tabPlayer.setGroup(group);
players.putIfAbsent(joined.getUniqueId(), tabPlayer);
// Store the player's last server, so it's possible to have the last server on disconnect
final String serverName = getServerName(joined);
//store last server, so it's possible to have the last server on disconnect
tabPlayer.setLastServer(serverName);
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
// Send server URLs (1.21 clients)
sendPlayerServerLinks(tabPlayer);
// Determine display name, update TAB for player
tabPlayer.getDisplayName(plugin).thenAccept(d -> {
joined.getTabList().getEntry(joined.getUniqueId())
.ifPresentOrElse(e -> e.setDisplayName(d),
() -> joined.getTabList().addEntry(createEntry(tabPlayer, joined.getTabList(), d)));
@ -169,7 +172,6 @@ public class PlayerTabList {
final Set<String> serversInGroup = group.registeredServers(plugin).stream()
.map(server -> server.getServerInfo().getName())
.collect(HashSet::new, HashSet::add, HashSet::addAll);
serversInGroup.remove(serverName);
// Update lists
@ -342,6 +344,14 @@ public class PlayerTabList {
});
}
public void sendPlayerServerLinks(@NotNull TabPlayer player) {
if (player.getPlayer().getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21)) {
return;
}
final List<ServerUrl> urls = plugin.getSettings().getUrlsForGroup(player.getGroup());
ServerUrl.resolve(plugin, player, urls).thenAccept(player.getPlayer()::setServerLinks);
}
public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) {
final Component lastDisplayName = tabPlayer.getLastDisplayName();
tabPlayer.getDisplayName(plugin).thenAccept(displayName -> {
@ -475,10 +485,10 @@ public class PlayerTabList {
*/
public void reloadUpdate() {
plugin.getTabGroups().getGroups().forEach(this::updatePeriodically);
if (players.isEmpty()) {
return;
}
// If the update time is set to 0 do not schedule the updater
players.values().forEach(player -> {
final Optional<ServerConnection> server = player.getPlayer().getCurrentServer();
@ -488,6 +498,7 @@ public class PlayerTabList {
final String serverName = server.get().getServerInfo().getName();
final Group group = getGroup(serverName);
player.setGroup(group);
this.sendPlayerServerLinks(player);
this.updatePlayer(player, true);
player.sendHeaderAndFooter(this);
});

View File

@ -82,11 +82,12 @@ public class TabListListener {
@Subscribe
public void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
final Player joined = event.getPlayer();
final String serverName = joined.getCurrentServer()
.map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("");
// Get the group the player should now be in
final Group group = tabList.getGroup(serverName);
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group));
final boolean isDefault = group.registeredServers(plugin).stream()