Compare commits

...

8 Commits

Author SHA1 Message Date
XAP3Y
f09bd7b6bd %server_players_online_<server>% feature
Some checks failed
Build plugin / Build-latest-jar (push) Failing after 3m49s
2025-01-16 19:22:24 +01:00
AlexDev_
94bcd99fe2 Fixed problem with header & footer task
Improved placeholder cache
2025-01-06 23:15:29 +01:00
AlexDev_
99032b4280 Increased delay 2025-01-06 15:55:35 +01:00
AlexDev_
2cf4f4956b Improvements on server switch 2025-01-05 18:48:59 +01:00
AlexDev_
ea40814c71 Sorting improvements 2025-01-05 18:30:39 +01:00
AlexDev_
9593058684 More cleanup 2025-01-05 17:28:58 +01:00
AlexDev_
70bbf01e14 Refactored the code to cache placeholders and then use them in sync 2025-01-05 17:20:57 +01:00
AlexDev_
b78b08d81e Incremented join delay 2024-12-23 17:36:33 +01:00
27 changed files with 887 additions and 697 deletions

View File

@ -0,0 +1,32 @@
name: Build plugin
run-name: Build plugin
on: [push]
jobs:
Build-latest-jar:
runs-on: ubuntu-latest
steps:
- name: Build | Prepare packages
run: |
apt update; apt-get install software-properties-common -y
wget -O- https://apt.corretto.aws/corretto.key | apt-key add -
add-apt-repository 'deb https://apt.corretto.aws stable main'
apt-get update; apt-get install -y java-21-amazon-corretto-jdk
- name: Prepare | Setup git
run: |
git config --global user.name "Radim Lipovčan"
git config --global user.email "radim@lipovcan.cz"
- name: Check out repository code
uses: actions/checkout@v4
- name: Build | Gradle build
run: |
ls -lah
chmod +x ./gradlew
./gradlew clean build && ls -lah && ls */ -lah && ls */* -lah&& ls */*/* -lah
- name: Build | publish jar
run: |
curl --insecure --user username:mypass -T target/Velocitab-1.7.3-94bcd99-indev.jar ftp://192.168.10.133:/
- name: Push | Create release
uses: https://git.lipovcan.cz/Upstream/gitea-release-action.git@v1
with:
files: |-
target/*.jar

View File

@ -45,6 +45,7 @@ import net.william278.velocitab.hook.LuckPermsHook;
import net.william278.velocitab.hook.MiniPlaceholdersHook;
import net.william278.velocitab.packet.PacketEventManager;
import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.placeholder.PlaceholderManager;
import net.william278.velocitab.providers.HookProvider;
import net.william278.velocitab.providers.LoggerProvider;
import net.william278.velocitab.providers.MetricProvider;
@ -87,6 +88,7 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
private VanishManager vanishManager;
private PacketEventManager packetEventManager;
private PluginMessageAPI pluginMessageAPI;
private PlaceholderManager placeholderManager;
@Inject
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path configDirectory) {
@ -100,6 +102,7 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
checkCompatibility();
loadConfigs();
loadHooks();
preparePlaceholderManager();
prepareVanishManager();
prepareChannelManager();
prepareScoreboard();
@ -137,6 +140,10 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
this.packetEventManager = new PacketEventManager(this);
}
private void preparePlaceholderManager() {
this.placeholderManager = new PlaceholderManager(this);
}
@Override
@NotNull
public Velocitab getPlugin() {

View File

@ -25,6 +25,7 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.util.MiniMessageUtil;
import net.william278.velocitab.util.QuadFunction;
import net.william278.velocitab.util.SerializationUtil;
import org.jetbrains.annotations.NotNull;
@ -49,8 +50,9 @@ public enum Formatter {
MINIMESSAGE(
(text, player, viewer, plugin) -> plugin.getMiniPlaceholdersHook()
.filter(hook -> player != null)
.map(hook -> hook.format(text, player.getPlayer(), viewer == null ? null : viewer.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(text)),
.map(hook -> hook.format(MiniMessageUtil.getINSTANCE().checkForErrors(text, plugin),
player.getPlayer(), viewer == null ? null : viewer.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(MiniMessageUtil.getINSTANCE().checkForErrors(text, plugin))),
(text) -> MiniMessage.miniMessage().escapeTags(text),
"MiniMessage",
(text) -> MiniMessage.miniMessage().deserialize(text),

View File

@ -19,10 +19,12 @@
package net.william278.velocitab.config;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.placeholder.PlaceholderReplacement;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.Nametag;
import org.apache.commons.text.StringEscapeUtils;
@ -50,6 +52,8 @@ public record Group(
Map<String, List<PlaceholderReplacement>> placeholderReplacements,
boolean collisions,
int headerFooterUpdateRate,
int formatUpdateRate,
int nametagUpdateRate,
int placeholderUpdateRate,
boolean onlyListPlayersInSameServer
) {
@ -82,6 +86,7 @@ public record Group(
(isDefault(plugin) && plugin.getSettings().isFallbackEnabled())) {
return Sets.newHashSet(plugin.getServer().getAllServers());
}
return getRegexServers(plugin);
}
@ -99,6 +104,7 @@ public record Group(
plugin.getServer().getServer(server).ifPresent(totalServers::add);
}
}
return totalServers;
}
@ -108,10 +114,21 @@ public record Group(
@NotNull
public Set<Player> getPlayers(@NotNull Velocitab plugin) {
Set<Player> players = Sets.newHashSet();
final Set<Player> players = Sets.newHashSet();
for (RegisteredServer server : registeredServers(plugin)) {
players.addAll(server.getPlayersConnected());
}
return players;
}
@NotNull
public List<Player> getPlayersAsList(@NotNull Velocitab plugin) {
final List<Player> players = Lists.newArrayList();
for (RegisteredServer server : registeredServers(plugin)) {
players.addAll(server.getPlayersConnected());
}
return players;
}
@ -120,11 +137,13 @@ public record Group(
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
return Sets.newHashSet(plugin.getServer().getAllPlayers());
}
if (onlyListPlayersInSameServer) {
return tabPlayer.getPlayer().getCurrentServer()
.map(s -> Sets.newHashSet(s.getServer().getPlayersConnected()))
.orElseGet(Sets::newHashSet);
}
return getPlayers(plugin);
}
@ -140,33 +159,62 @@ public record Group(
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
return Sets.newHashSet(plugin.getTabList().getPlayers().values());
}
return plugin.getTabList().getPlayers()
.values()
.stream()
.filter(tabPlayer -> tabPlayer.getGroup().equals(this))
.filter(tabPlayer -> tabPlayer.isLoaded() && tabPlayer.getGroup().equals(this))
.collect(Collectors.toSet());
}
public List<TabPlayer> getTabPlayersAsList(@NotNull Velocitab plugin) {
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
return Lists.newArrayList(plugin.getTabList().getPlayers().values());
}
return plugin.getTabList().getPlayers()
.values()
.stream()
.filter(tabPlayer -> tabPlayer.isLoaded() && tabPlayer.getGroup().equals(this))
.collect(Collectors.toList());
}
@NotNull
public Set<TabPlayer> getTabPlayers(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
return Sets.newHashSet(plugin.getTabList().getPlayers().values());
}
if (onlyListPlayersInSameServer) {
return plugin.getTabList().getPlayers()
.values()
.stream()
.filter(player -> player.getGroup().equals(this) && player.getServerName().equals(tabPlayer.getServerName()))
.filter(player -> player.isLoaded() && player.getGroup().equals(this) && player.getServerName().equals(tabPlayer.getServerName()))
.collect(Collectors.toSet());
}
return getTabPlayers(plugin);
}
@NotNull
public List<String> getTextsWithPlaceholders() {
final List<String> texts = Lists.newArrayList();
texts.add(name);
texts.add(format);
texts.addAll(headers);
texts.addAll(footers);
texts.add(nametag.prefix());
texts.add(nametag.suffix());
texts.addAll(sortingPlaceholders);
return texts;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Group group)) {
return false;
}
return name.equals(group.name);
}
}

View File

@ -1,330 +0,0 @@
/*
* 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.google.common.collect.Maps;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import it.unimi.dsi.fastutil.Pair;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.hook.miniconditions.MiniConditionManager;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.Nametag;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.function.TriFunction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.event.Level;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public enum Placeholder {
PLAYERS_ONLINE((plugin, player) -> Integer.toString(plugin.getServer().getPlayerCount())),
MAX_PLAYERS_ONLINE((plugin, player) -> Integer.toString(plugin.getServer().getConfiguration().getShowMaxPlayers())),
LOCAL_PLAYERS_ONLINE((plugin, player) -> player.getPlayer().getCurrentServer()
.map(ServerConnection::getServer)
.map(RegisteredServer::getPlayersConnected)
.map(players -> Integer.toString(players.size()))
.orElse("")),
GROUP_PLAYERS_ONLINE((param, plugin, player) -> {
if (param.isEmpty()) {
return Integer.toString(player.getGroup().getPlayers(plugin).size());
}
return plugin.getTabGroups().getGroup(param)
.map(group -> Integer.toString(group.getPlayers(plugin).size()))
.orElse("Group " + param + " not found");
}),
CURRENT_DATE_DAY((plugin, player) -> DateTimeFormatter.ofPattern("dd").format(LocalDateTime.now())),
CURRENT_DATE_WEEKDAY((param, plugin, player) -> {
if (param.isEmpty()) {
return DateTimeFormatter.ofPattern("EEEE").format(LocalDateTime.now());
}
final String countryCode = param.toUpperCase();
final Locale locale = Locale.forLanguageTag(countryCode);
return DateTimeFormatter.ofPattern("EEEE").withLocale(locale).format(LocalDateTime.now());
}),
CURRENT_DATE_MONTH((plugin, player) -> DateTimeFormatter.ofPattern("MM").format(LocalDateTime.now())),
CURRENT_DATE_YEAR((plugin, player) -> DateTimeFormatter.ofPattern("yyyy").format(LocalDateTime.now())),
CURRENT_DATE((param, plugin, player) -> {
if (param.isEmpty()) {
return DateTimeFormatter.ofPattern("dd/MM/yyyy").format(LocalDateTime.now());
}
final String countryCode = param.toUpperCase();
final Locale locale = Locale.forLanguageTag(countryCode);
return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale).format(LocalDateTime.now());
}),
CURRENT_TIME_HOUR((plugin, player) -> DateTimeFormatter.ofPattern("HH").format(LocalDateTime.now())),
CURRENT_TIME_MINUTE((plugin, player) -> DateTimeFormatter.ofPattern("mm").format(LocalDateTime.now())),
CURRENT_TIME_SECOND((plugin, player) -> DateTimeFormatter.ofPattern("ss").format(LocalDateTime.now())),
CURRENT_TIME((param, plugin, player) -> {
if (param.isEmpty()) {
return DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now());
}
final String countryCode = param.toUpperCase();
final Locale locale = Locale.forLanguageTag(countryCode);
return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale).format(LocalTime.now());
}),
USERNAME((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername())),
USERNAME_LOWER((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername()).toLowerCase()),
SERVER((plugin, player) -> player.getServerName()),
PING((plugin, player) -> Long.toString(player.getPlayer().getPing())),
PREFIX((plugin, player) -> player.getRole().getPrefix()
.orElse(getPlaceholderFallback(plugin, "%luckperms_prefix%"))),
SUFFIX((plugin, player) -> player.getRole().getSuffix()
.orElse(getPlaceholderFallback(plugin, "%luckperms_suffix%"))),
ROLE((plugin, player) -> player.getRole().getName()
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName()
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()),
SERVER_GROUP((plugin, player) -> player.getGroup().name()),
SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))),
DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse(""))),
LUCKPERMS_META((param, plugin, player) -> plugin.getLuckPermsHook()
.map(hook -> hook.getMeta(player.getPlayer(), param))
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_" + param + "%")));
private final static Pattern VELOCITAB_PATTERN = Pattern.compile("<velocitab_.*?>");
private final static Pattern TEST = Pattern.compile("<.*?>");
private final static Pattern CONDITION_REPLACER = Pattern.compile("<velocitab_rel_condition:[^:]*:");
private final static Pattern PLACEHOLDER_PATTERN = Pattern.compile("%.*?%", Pattern.DOTALL);
private final static String DELIMITER = ":::";
private final static Map<String, String> SYMBOL_SUBSTITUTES = Map.of(
"<", "*LESS*",
">", "*GREATER*"
);
private final static Map<String, String> SYMBOL_SUBSTITUTES_2 = Map.of(
"*LESS*", "*LESS2*",
"*GREATER*", "*GREATER2*"
);
private final static String VEL_PLACEHOLDER = "<vel";
private final static String VELOCITAB_PLACEHOLDER = "<velocitab_rel";
private final static String ELSE_PLACEHOLDER = "ELSE";
/**
* Function to replace placeholders with a real value
*/
private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
private final boolean parameterised;
private final Pattern pattern;
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
this.parameterised = false;
this.replacer = (text, player, plugin) -> replacer.apply(player, plugin);
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "%");
}
Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) {
this.parameterised = true;
this.replacer = parameterisedReplacer;
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]*%", Pattern.CASE_INSENSITIVE);
}
public static CompletableFuture<Nametag> replace(@NotNull Nametag nametag, @NotNull Velocitab plugin,
@NotNull TabPlayer player) {
return replace(nametag.prefix() + DELIMITER + nametag.suffix(), plugin, player)
.thenApply(s -> s.split(DELIMITER, 2))
.thenApply(v -> new Nametag(v[0], v.length > 1 ? v[1] : ""));
}
@NotNull
private static String getPlaceholderFallback(@NotNull Velocitab plugin, @NotNull String fallback) {
if (plugin.getPAPIProxyBridgeHook().isPresent() && plugin.getSettings().isFallbackToPapiIfPlaceholderBlank()) {
return fallback;
}
return "";
}
@NotNull
public static Pair<String, Map<String, String>> replaceInternal(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
format = processRelationalPlaceholders(format, plugin);
return replacePlaceholders(format, plugin, player);
}
private static String processRelationalPlaceholders(@NotNull String format, @NotNull Velocitab plugin) {
if (plugin.getFormatter().equals(Formatter.MINIMESSAGE) && format.contains(VEL_PLACEHOLDER)) {
final Matcher conditionReplacer = CONDITION_REPLACER.matcher(format);
while (conditionReplacer.find()) {
final String search = conditionReplacer.group().split(":")[1];
String condition = search;
for (Map.Entry<String, String> entry : MiniConditionManager.REPLACE.entrySet()) {
condition = condition.replace(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : MiniConditionManager.REPLACE_2.entrySet()) {
condition = condition.replace(entry.getValue(), entry.getKey());
}
format = format.replace(search, condition);
}
final Matcher testMatcher = TEST.matcher(format);
while (testMatcher.find()) {
if (testMatcher.group().startsWith(VELOCITAB_PLACEHOLDER)) {
final Matcher second = TEST.matcher(testMatcher.group().substring(1));
while (second.find()) {
String s = second.group();
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
s = s.replace(entry.getKey(), entry.getValue());
}
format = format.replace(second.group(), s);
}
continue;
}
String s = testMatcher.group();
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
s = s.replace(entry.getKey(), entry.getValue());
}
format = format.replace(testMatcher.group(), s);
}
final Matcher velocitabRelationalMatcher = VELOCITAB_PATTERN.matcher(format);
while (velocitabRelationalMatcher.find()) {
final String relationalPlaceholder = velocitabRelationalMatcher.group().substring(1, velocitabRelationalMatcher.group().length() - 1);
String fixedString = relationalPlaceholder;
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES_2.entrySet()) {
fixedString = fixedString.replace(entry.getKey(), entry.getValue());
}
format = format.replace(relationalPlaceholder, fixedString);
}
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
format = format.replace(entry.getValue(), entry.getKey());
}
}
return format;
}
@NotNull
private static Pair<String, Map<String, String>> replacePlaceholders(@NotNull String format, @NotNull Velocitab plugin,
@Nullable TabPlayer player) {
final Map<String, String> replacedPlaceholders = Maps.newHashMap();
for (Placeholder placeholder : values()) {
final Matcher matcher = placeholder.pattern.matcher(format);
if (placeholder.parameterised) {
format = matcher.replaceAll(matchResult -> {
final String replacement = placeholder.replacer.apply(StringUtils.chop(matchResult.group().replace("%" + placeholder.name().toLowerCase(), "")
.replaceFirst("_", "")), plugin, player);
replacedPlaceholders.put(matchResult.group(), replacement);
return Matcher.quoteReplacement(replacement);
});
} else {
format = matcher.replaceAll(matchResult -> {
final String replacement = placeholder.replacer.apply(null, plugin, player);
replacedPlaceholders.put(matchResult.group(), replacement);
return Matcher.quoteReplacement(replacement);
});
}
}
return Pair.of(format, replacedPlaceholders);
}
@NotNull
private static String applyPlaceholderReplacements(@NotNull String text, @NotNull TabPlayer player,
@NotNull Map<String, String> parsed) {
for (final Map.Entry<String, List<PlaceholderReplacement>> entry : player.getGroup().placeholderReplacements().entrySet()) {
if (!parsed.containsKey(entry.getKey())) {
continue;
}
final String replaced = parsed.get(entry.getKey());
final Optional<PlaceholderReplacement> replacement = entry.getValue().stream()
.filter(r -> r.placeholder().equalsIgnoreCase(replaced))
.findFirst();
if (replacement.isPresent()) {
text = text.replace(entry.getKey(), replacement.get().replacement());
} else {
final Optional<PlaceholderReplacement> elseReplacement = entry.getValue().stream()
.filter(r -> r.placeholder().equalsIgnoreCase(ELSE_PLACEHOLDER))
.findFirst();
if (elseReplacement.isPresent()) {
text = text.replace(entry.getKey(), elseReplacement.get().replacement());
}
}
}
return applyPlaceholders(text, parsed);
}
public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin,
@NotNull TabPlayer player) {
if (format.equals(DELIMITER)) {
return CompletableFuture.completedFuture("");
}
final Pair<String, Map<String, String>> replaced = replaceInternal(format, plugin, player);
if (!PLACEHOLDER_PATTERN.matcher(replaced.first()).find()) {
return CompletableFuture.completedFuture(applyPlaceholderReplacements(format, player, replaced.second()));
}
final List<String> placeholders = extractPlaceholders(replaced.first());
return plugin.getPAPIProxyBridgeHook()
.map(hook -> hook.parsePlaceholders(placeholders, player.getPlayer())
.exceptionally(e -> {
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
return Map.of();
})
)
.orElse(CompletableFuture.completedFuture(Maps.newHashMap()))
.exceptionally(e -> {
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
return Map.of();
})
.thenApply(m -> applyPlaceholderReplacements(format, player, mergeMaps(m, replaced.second())));
}
@NotNull
private static String applyPlaceholders(@NotNull String text, @NotNull Map<String, String> replacements) {
for (Map.Entry<String, String> entry : replacements.entrySet()) {
text = text.replace(entry.getKey(), entry.getValue());
}
return text;
}
@NotNull
private static Map<String, String> mergeMaps(@NotNull Map<String, String> map1, @NotNull Map<String, String> map2) {
map1.putAll(map2);
return map1;
}
@NotNull
private static List<String> extractPlaceholders(@NotNull String text) {
final List<String> placeholders = Lists.newArrayList();
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
while (matcher.find()) {
placeholders.add(matcher.group());
}
return placeholders;
}
}

View File

@ -19,8 +19,11 @@
package net.william278.velocitab.config;
import com.google.common.collect.Lists;
import com.velocitypowered.api.util.ServerLink;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.placeholder.Placeholder;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
@ -40,26 +43,25 @@ public record ServerUrl(
// 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) {
ServerLink getServerLink(@NotNull Velocitab plugin, @NotNull TabPlayer player) {
return getBuiltInLabel().map(
(type) -> CompletableFuture.completedFuture(ServerLink.serverLink(type, url()))
(type) -> ServerLink.serverLink(type, url())
).orElseGet(
() -> Placeholder.replace(label(), plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
.thenApply(formatted -> ServerLink.serverLink(formatted, url()))
);
() -> {
final String replaced = plugin.getPlaceholderManager().applyPlaceholders(player, label());
final Component formatted = plugin.getFormatter().format(replaced, player, plugin);
return ServerLink.serverLink(formatted, url());
});
}
@NotNull
public static CompletableFuture<List<ServerLink>> resolve(@NotNull Velocitab plugin, @NotNull TabPlayer player,
public static List<ServerLink> resolve(@NotNull Velocitab plugin, @NotNull TabPlayer player,
@NotNull List<ServerUrl> urls) {
final List<CompletableFuture<ServerLink>> futures = new ArrayList<>();
final List<ServerLink> serverLinks = Lists.newArrayList();
for (ServerUrl url : urls) {
futures.add(url.getServerLink(plugin, player));
serverLinks.add(url.getServerLink(plugin, player));
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join).toList());
return serverLinks;
}
private Optional<ServerLink.Type> getBuiltInLabel() {

View File

@ -27,6 +27,7 @@ import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.placeholder.PlaceholderReplacement;
import net.william278.velocitab.tab.Nametag;
import org.jetbrains.annotations.NotNull;
@ -64,6 +65,8 @@ public class TabGroups implements ConfigValidator {
false,
1000,
1000,
1000,
1000,
false
);
@ -174,6 +177,8 @@ public class TabGroups implements ConfigValidator {
group.placeholderReplacements() == null ? DEFAULT_GROUP.placeholderReplacements() : group.placeholderReplacements(),
group.collisions(),
group.headerFooterUpdateRate(),
group.formatUpdateRate(),
group.nametagUpdateRate(),
group.placeholderUpdateRate(),
group.onlyListPlayersInSameServer()
);

View File

@ -120,7 +120,7 @@ public class LuckPermsHook extends Hook {
tabList.getVanishTabList().recalculateVanishForPlayer(tabPlayer);
checkRoleUpdate(tabPlayer, oldRole);
})
.delay(500, TimeUnit.MILLISECONDS)
.delay(100, TimeUnit.MILLISECONDS)
.schedule());
}
@ -145,6 +145,7 @@ public class LuckPermsHook extends Hook {
if (oldRole.equals(player.getRole())) {
return;
}
plugin.getTabList().updatePlayer(player, false);
}

View File

@ -26,11 +26,16 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.event.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MiniPlaceholdersHook extends Hook {
private final VelocitabMiniExpansion expansion;
public MiniPlaceholdersHook(@NotNull Velocitab plugin) {
super(plugin);
this.expansion = new VelocitabMiniExpansion(plugin);

View File

@ -19,15 +19,11 @@
package net.william278.velocitab.hook;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.velocitypowered.api.proxy.Player;
import net.william278.papiproxybridge.api.PlaceholderAPI;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class PAPIProxyBridgeHook extends Hook {
@ -45,17 +41,4 @@ public class PAPIProxyBridgeHook extends Hook {
return api.formatPlaceholders(input, player.getUniqueId());
}
public CompletableFuture<Map<String, String>> parsePlaceholders(@NotNull List<String> input, @NotNull Player player) {
final Map<String, String> map = Maps.newConcurrentMap();
final List<CompletableFuture<String>> futures = Lists.newArrayList();
for (String s : input) {
final CompletableFuture<String> future = formatPlaceholders(s, player);
futures.add(future);
future.thenAccept(r -> map.put(s, r));
}
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).thenApply(v -> map);
}
}

View File

@ -28,7 +28,7 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.placeholder.Placeholder;
import net.william278.velocitab.hook.miniconditions.MiniConditionManager;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
@ -98,7 +98,7 @@ public class VelocitabMiniExpansion {
}
final String value = fixValue(popAll(queue));
final String replaced = Placeholder.replaceInternal(value, plugin, targetPlayer).first();
final String replaced = plugin.getPlaceholderManager().applyPlaceholders(targetPlayer, value);
return Tag.selfClosingInserting(MiniMessage.miniMessage().deserialize(replaced, MiniPlaceholders.getAudienceGlobalPlaceholders(audience)));
}));

View File

@ -25,7 +25,7 @@ import net.jodah.expiringmap.ExpiringMap;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.placeholder.Placeholder;
import net.william278.velocitab.player.TabPlayer;
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlContext;
@ -42,19 +42,19 @@ import java.util.regex.Pattern;
public class MiniConditionManager {
public final static Map<String, String> REPLACE = Map.of(
public static final Map<String, String> REPLACE = Map.of(
"\"", "-q-",
"'", "-a-"
);
public final static Map<String, String> REPLACE_2 = Map.of(
public static final Map<String, String> REPLACE_2 = Map.of(
"*LESS3*", "<",
"*GREATER3*", ">",
"*LESS2*", "<",
"*GREATER2*", ">"
);
private final static Map<String, String> REPLACE_3 = Map.of(
private static final Map<String, String> REPLACE_3 = Map.of(
"?dp?", ":"
);
@ -110,7 +110,7 @@ public class MiniConditionManager {
}
condition = Placeholder.replaceInternal(condition, plugin, tabPlayer.get()).first();
condition = plugin.getPlaceholderManager().applyPlaceholders(tabPlayer.get(), condition);
final String falseValue = processFalseValue(parameters.get(2));
final String expression = buildExpression(condition);
return evaluateAndFormatCondition(expression, target, audience, parameters.get(1), falseValue);
@ -194,7 +194,7 @@ public class MiniConditionManager {
}
final String text = "%" + placeholder + "%";
final Optional<String> placeholderValue = tabPlayer.get().getCachedPlaceholderValue(text);
final Optional<String> placeholderValue = plugin.getPlaceholderManager().getCachedPlaceholderValue(text, target.getUniqueId());
return placeholderValue.orElse(text);
});
}

View File

@ -41,7 +41,8 @@ import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static com.velocitypowered.api.network.ProtocolVersion.*;
@ -49,6 +50,7 @@ public class ScoreboardManager {
private PacketRegistration<UpdateTeamsPacket> packetRegistration;
private final Velocitab plugin;
private final ExecutorService executorService;
private final boolean teams;
private final Map<ProtocolVersion, TeamsPacketAdapter> versions;
@Getter
@ -65,8 +67,9 @@ public class ScoreboardManager {
this.nametags = Maps.newConcurrentMap();
this.versions = Maps.newHashMap();
this.trackedTeams = Multimaps.synchronizedMultimap(Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet));
this.sortedTeams = new SortedSet(Comparator.reverseOrder());
this.sortedTeams = new SortedSet(Comparator.reverseOrder()); //Comparator.reverseOrder()
this.registerVersions();
this.executorService = Executors.newFixedThreadPool(2);
}
public boolean handleTeams() {
@ -173,15 +176,15 @@ public class ScoreboardManager {
* @param role The new role of the player. Must not be null.
* @param force Whether to force the update even if the player's nametag is the same.
*/
public CompletableFuture<Void> updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
public void updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
final Player player = tabPlayer.getPlayer();
if (!player.isActive()) {
plugin.getTabList().removeOfflinePlayer(player);
return CompletableFuture.completedFuture(null);
return;
}
final String name = player.getUsername();
final CompletableFuture<Void> future = new CompletableFuture<>();
tabPlayer.getNametag(plugin).thenAccept(newTag -> {
// final CompletableFuture<Void> future = new CompletableFuture<>();
final Nametag nametag = tabPlayer.getNametag(plugin);
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
if (createdTeams.containsKey(player.getUniqueId())) {
dispatchGroupPacket(
@ -198,21 +201,14 @@ public class ScoreboardManager {
if (!a) {
plugin.log(Level.ERROR, "Failed to add team " + role + " to sortedTeams");
}
this.nametags.put(role, newTag);
dispatchGroupCreatePacket(plugin, tabPlayer, role, newTag, name);
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) {
this.nametags.put(role, newTag);
dispatchGroupChangePacket(plugin, tabPlayer, role, newTag);
this.nametags.put(role, nametag);
dispatchGroupCreatePacket(plugin, tabPlayer, role, nametag, name);
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(nametag))) {
this.nametags.put(role, nametag);
dispatchGroupChangePacket(plugin, tabPlayer, role, nametag);
} else {
updatePlaceholders(tabPlayer);
}
future.complete(null);
}).exceptionally(e -> {
plugin.log(Level.ERROR, "Failed to update role for " + player.getUsername(), e);
return null;
});
return future;
}
public void updatePlaceholders(@NotNull TabPlayer tabPlayer) {
@ -397,8 +393,10 @@ public class ScoreboardManager {
return;
}
executorService.submit(() -> {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
connectedPlayer.getConnection().write(packet);
});
}
public void registerPacket() {

View File

@ -0,0 +1,167 @@
/*
* 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.placeholder;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import lombok.Getter;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.apache.commons.lang3.function.TriFunction;
import org.jetbrains.annotations.NotNull;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Getter
public enum Placeholder {
PLAYERS_ONLINE((plugin, player) -> Integer.toString(plugin.getServer().getPlayerCount())),
MAX_PLAYERS_ONLINE((plugin, player) -> Integer.toString(plugin.getServer().getConfiguration().getShowMaxPlayers())),
LOCAL_PLAYERS_ONLINE((plugin, player) -> player.getPlayer().getCurrentServer()
.map(ServerConnection::getServer)
.map(RegisteredServer::getPlayersConnected)
.map(players -> Integer.toString(players.size()))
.orElse("")),
SERVER_PLAYERS_ONLINE((param, plugin, player) -> {
if (param.isEmpty()) {
return "0";
}
Collection<RegisteredServer> list = plugin.getPlugin().getServer().getAllServers();
Integer online = 0;
boolean contains = false;
for (RegisteredServer server : list) {
if (server.getServerInfo().getName().equalsIgnoreCase(param)) {
contains = true;
online = server.getPlayersConnected().size();
break;
}
}
if (!contains) return "0";
return Integer.toString(online);
}),
GROUP_PLAYERS_ONLINE((param, plugin, player) -> {
if (param.isEmpty()) {
return Integer.toString(player.getGroup().getPlayers(plugin).size());
}
return plugin.getTabGroups().getGroup(param)
.map(group -> Integer.toString(group.getPlayers(plugin).size()))
.orElse("Group " + param + " not found");
}),
CURRENT_DATE_DAY((plugin, player) -> DateTimeFormatter.ofPattern("dd").format(LocalDateTime.now())),
CURRENT_DATE_WEEKDAY((param, plugin, player) -> {
if (param.isEmpty()) {
return DateTimeFormatter.ofPattern("EEEE").format(LocalDateTime.now());
}
final String countryCode = param.toUpperCase();
final Locale locale = Locale.forLanguageTag(countryCode);
return DateTimeFormatter.ofPattern("EEEE").withLocale(locale).format(LocalDateTime.now());
}),
CURRENT_DATE_MONTH((plugin, player) -> DateTimeFormatter.ofPattern("MM").format(LocalDateTime.now())),
CURRENT_DATE_YEAR((plugin, player) -> DateTimeFormatter.ofPattern("yyyy").format(LocalDateTime.now())),
CURRENT_DATE((param, plugin, player) -> {
if (param.isEmpty()) {
return DateTimeFormatter.ofPattern("dd/MM/yyyy").format(LocalDateTime.now());
}
final String countryCode = param.toUpperCase();
final Locale locale = Locale.forLanguageTag(countryCode);
return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale).format(LocalDateTime.now());
}),
CURRENT_TIME_HOUR((plugin, player) -> DateTimeFormatter.ofPattern("HH").format(LocalDateTime.now())),
CURRENT_TIME_MINUTE((plugin, player) -> DateTimeFormatter.ofPattern("mm").format(LocalDateTime.now())),
CURRENT_TIME_SECOND((plugin, player) -> DateTimeFormatter.ofPattern("ss").format(LocalDateTime.now())),
CURRENT_TIME((plugin, player) -> {
return DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now());
/*if (param.isEmpty()) {
return DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now());
}
final String countryCode = param.toUpperCase();
final Locale locale = Locale.forLanguageTag(countryCode);
return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale).format(LocalTime.now());*/
}),
USERNAME((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername())),
USERNAME_LOWER((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername()).toLowerCase()),
SERVER((plugin, player) -> player.getServerName()),
PING((plugin, player) -> Long.toString(player.getPlayer().getPing())),
PREFIX((plugin, player) -> player.getRole().getPrefix()
.orElse(getPlaceholderFallback(plugin, "%luckperms_prefix%"))),
SUFFIX((plugin, player) -> player.getRole().getSuffix()
.orElse(getPlaceholderFallback(plugin, "%luckperms_suffix%"))),
ROLE((plugin, player) -> player.getRole().getName()
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName()
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()),
SERVER_GROUP((plugin, player) -> player.getGroup().name()),
SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))),
DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse(""))),
LUCKPERMS_META((param, plugin, player) -> plugin.getLuckPermsHook()
.map(hook -> hook.getMeta(player.getPlayer(), param))
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_" + param + "%")));
private static final List<Placeholder> VALUES = Arrays.asList(values());
private static final Map<String, Placeholder> BY_NAME = VALUES.stream().collect(Collectors.toMap(p -> p.name().toLowerCase(), Function.identity()));
@Getter
private static final List<Placeholder> PARAMETERISED = VALUES.stream().filter(p -> p.parameterised).toList();
/**
* Function to replace placeholders with a real value
*/
private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
private final boolean parameterised;
private final Pattern pattern;
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
this.parameterised = false;
this.replacer = (text, player, plugin) -> replacer.apply(player, plugin);
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "%");
}
Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) {
this.parameterised = true;
this.replacer = parameterisedReplacer;
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]*%", Pattern.CASE_INSENSITIVE);
}
@NotNull
private static String getPlaceholderFallback(@NotNull Velocitab plugin, @NotNull String fallback) {
if (plugin.getPAPIProxyBridgeHook().isPresent() && plugin.getSettings().isFallbackToPapiIfPlaceholderBlank()) {
return fallback;
}
return "";
}
public static Optional<Placeholder> byName(@NotNull String name) {
return Optional.ofNullable(BY_NAME.get(name.toLowerCase().replace("%", "")));
}
}

View File

@ -0,0 +1,288 @@
/*
* 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.placeholder;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.velocitypowered.api.proxy.Player;
import lombok.Setter;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Formatter;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.hook.miniconditions.MiniConditionManager;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PlaceholderManager {
@Setter
private boolean debug = false;
private static final String ELSE_PLACEHOLDER = "ELSE";
private static final Pattern VELOCITAB_PATTERN = Pattern.compile("<velocitab_.*?>");
private static final Pattern TEST = Pattern.compile("<.*?>");
private static final Pattern CONDITION_REPLACER = Pattern.compile("<velocitab_rel_condition:[^:]*:");
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%.*?%", Pattern.DOTALL);
private static final Map<String, String> SYMBOL_SUBSTITUTES = Map.of(
"<", "*LESS*",
">", "*GREATER*"
);
private static final Map<String, String> SYMBOL_SUBSTITUTES_2 = Map.of(
"*LESS*", "*LESS2*",
"*GREATER*", "*GREATER2*"
);
private static final String VEL_PLACEHOLDER = "<vel";
private static final String VELOCITAB_PLACEHOLDER = "<velocitab_rel";
private final Velocitab plugin;
private final Map<UUID, Map<String, String>> placeholders;
private final Map<UUID, Set<CompletableFuture<?>>> requests;
private final Set<UUID> blocked;
public PlaceholderManager(Velocitab plugin) {
this.plugin = plugin;
this.placeholders = Maps.newConcurrentMap();
this.requests = Maps.newConcurrentMap();
this.blocked = Sets.newConcurrentHashSet();
}
public void fetchPlaceholders(@NotNull Group group) {
final List<String> texts = group.getTextsWithPlaceholders();
group.getPlayersAsList(plugin).forEach(player -> fetchPlaceholders(player.getUniqueId(), texts));
}
public void fetchPlaceholders(@NotNull UUID uuid, @NotNull List<String> texts) {
final Player player = plugin.getServer().getPlayer(uuid).orElse(null);
if (player == null) {
return;
}
if (blocked.contains(uuid)) {
return;
}
final Map<String, String> parsed = placeholders.computeIfAbsent(uuid, k -> Maps.newConcurrentMap());
final TabPlayer tabPlayer = plugin.getTabList().getTabPlayer(player)
.orElse(new TabPlayer(plugin, player,
plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE),
plugin.getTabList().getGroupOrDefault(player)));
final List<String> placeholders = texts.stream()
.map(PlaceholderManager::extractPlaceholders)
.flatMap(List::stream)
.map(s -> s.replace("%target_", "%"))
.toList();
final long start = System.currentTimeMillis();
placeholders.forEach(placeholder -> replaceSingle(placeholder, plugin, tabPlayer)
.ifPresentOrElse(replacement -> parsed.put(placeholder, replacement),
() -> plugin.getPAPIProxyBridgeHook().ifPresent(hook -> {
final CompletableFuture<String> future = hook.formatPlaceholders(placeholder, player);
requests.computeIfAbsent(player.getUniqueId(), u -> Sets.newConcurrentHashSet()).add(future);
future.thenAccept(replacement -> {
if (replacement == null || replacement.equals(placeholder)) {
return;
}
if (blocked.contains(player.getUniqueId())) {
return;
}
if (debug) {
plugin.getLogger().info("Placeholder {} replaced with {} in {}ms", placeholder, replacement, System.currentTimeMillis() - start);
}
parsed.put(placeholder, replacement);
requests.get(player.getUniqueId()).remove(future);
});
})));
}
@NotNull
public String applyPlaceholders(@NotNull TabPlayer player, @NotNull String text) {
final Map<String, String> parsed = placeholders.computeIfAbsent(player.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
final String applied = applyPlaceholderReplacements(text, player, parsed);
return processRelationalPlaceholders(applied, plugin);
}
@NotNull
public String applyPlaceholders(@NotNull TabPlayer player, @NotNull String text, @NotNull TabPlayer target) {
final Map<String, String> parsed = placeholders.computeIfAbsent(player.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
final String applied = applyPlaceholderReplacements(text, player, parsed);
final String relational = processRelationalPlaceholders(applied, plugin);
final Map<String, String> targetParsed = placeholders.computeIfAbsent(target.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
final String targetApplied = applyPlaceholderReplacements(relational.replace("%target_", "%"), target, targetParsed);
return processRelationalPlaceholders(targetApplied, plugin);
}
public void clearPlaceholders(@NotNull UUID uuid) {
blocked.add(uuid);
placeholders.remove(uuid);
Optional.ofNullable(requests.get(uuid)).ifPresent(set -> set.forEach(c -> c.cancel(true)));
}
public void unblockPlayer(@NotNull UUID uuid) {
blocked.remove(uuid);
requests.remove(uuid);
}
@NotNull
private static List<String> extractPlaceholders(@NotNull String text) {
final List<String> placeholders = Lists.newArrayList();
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
while (matcher.find()) {
placeholders.add(matcher.group());
}
return placeholders;
}
@NotNull
private String applyPlaceholderReplacements(@NotNull String text, @NotNull TabPlayer player,
@NotNull Map<String, String> parsed) {
for (final Map.Entry<String, List<PlaceholderReplacement>> entry : player.getGroup().placeholderReplacements().entrySet()) {
if (!parsed.containsKey(entry.getKey())) {
continue;
}
final String replaced = parsed.get(entry.getKey());
final Optional<PlaceholderReplacement> replacement = entry.getValue().stream()
.filter(r -> r.placeholder().equalsIgnoreCase(replaced))
.findFirst();
if (replacement.isPresent()) {
text = text.replace(entry.getKey(), replacement.get().replacement());
} else {
final Optional<PlaceholderReplacement> elseReplacement = entry.getValue().stream()
.filter(r -> r.placeholder().equalsIgnoreCase(ELSE_PLACEHOLDER))
.findFirst();
if (elseReplacement.isPresent()) {
text = text.replace(entry.getKey(), elseReplacement.get().replacement());
}
}
}
return applyPlaceholders(text, parsed);
}
@NotNull
private String applyPlaceholders(@NotNull String text, @NotNull Map<String, String> replacements) {
for (Map.Entry<String, String> entry : replacements.entrySet()) {
text = text.replace(entry.getKey(), entry.getValue());
}
return text;
}
public Optional<String> getCachedPlaceholderValue(@NotNull String text, @NotNull UUID uuid) {
if (!placeholders.containsKey(uuid)) {
return Optional.empty();
}
return Optional.ofNullable(placeholders.get(uuid).get(text));
}
private Optional<String> replaceSingle(@NotNull String placeholder, @NotNull Velocitab plugin, @NotNull TabPlayer player) {
final Optional<Placeholder> optionalPlaceholder = Placeholder.byName(placeholder);
if (optionalPlaceholder.isEmpty()) {
//check if it's parameterised
for (Placeholder placeholderType : Placeholder.getPARAMETERISED()) {
final Matcher matcher = placeholderType.getPattern().matcher(placeholder);
if (matcher.find()) {
final String s = StringUtils.chop(matcher.group().replace("%" + placeholderType.name().toLowerCase(), "")
.replaceFirst("_", ""));
return Optional.of(placeholderType.getReplacer().apply(s, plugin, player));
}
}
return Optional.empty();
}
if (optionalPlaceholder.get().isParameterised()) {
throw new IllegalArgumentException("Placeholder " + placeholder + " is parameterised");
}
final Placeholder placeholderType = optionalPlaceholder.get();
return Optional.of(placeholderType.getReplacer().apply(null, plugin, player));
}
private String processRelationalPlaceholders(@NotNull String format, @NotNull Velocitab plugin) {
if (plugin.getFormatter().equals(Formatter.MINIMESSAGE) && format.contains(VEL_PLACEHOLDER)) {
final Matcher conditionReplacer = CONDITION_REPLACER.matcher(format);
while (conditionReplacer.find()) {
final String search = conditionReplacer.group().split(":")[1];
String condition = search;
for (Map.Entry<String, String> entry : MiniConditionManager.REPLACE.entrySet()) {
condition = condition.replace(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : MiniConditionManager.REPLACE_2.entrySet()) {
condition = condition.replace(entry.getValue(), entry.getKey());
}
format = format.replace(search, condition);
}
final Matcher testMatcher = TEST.matcher(format);
while (testMatcher.find()) {
if (testMatcher.group().startsWith(VELOCITAB_PLACEHOLDER)) {
final Matcher second = TEST.matcher(testMatcher.group().substring(1));
while (second.find()) {
String s = second.group();
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
s = s.replace(entry.getKey(), entry.getValue());
}
format = format.replace(second.group(), s);
}
continue;
}
String s = testMatcher.group();
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
s = s.replace(entry.getKey(), entry.getValue());
}
format = format.replace(testMatcher.group(), s);
}
final Matcher velocitabRelationalMatcher = VELOCITAB_PATTERN.matcher(format);
while (velocitabRelationalMatcher.find()) {
final String relationalPlaceholder = velocitabRelationalMatcher.group().substring(1, velocitabRelationalMatcher.group().length() - 1);
String fixedString = relationalPlaceholder;
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES_2.entrySet()) {
fixedString = fixedString.replace(entry.getKey(), entry.getValue());
}
format = format.replace(relationalPlaceholder, fixedString);
}
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
format = format.replace(entry.getValue(), entry.getKey());
}
}
return format;
}
}

View File

@ -17,7 +17,7 @@
* limitations under the License.
*/
package net.william278.velocitab.config;
package net.william278.velocitab.placeholder;
import org.jetbrains.annotations.NotNull;

View File

@ -65,7 +65,7 @@ public class Role implements Comparable<Role> {
}
@NotNull
protected String getWeightString() {
public String getWeightString() {
return Integer.toString(weight);
}

View File

@ -20,7 +20,6 @@
package net.william278.velocitab.player;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.velocitypowered.api.proxy.Player;
import lombok.Getter;
import lombok.Setter;
@ -28,7 +27,6 @@ import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.packet.UpdateTeamsPacket;
import net.william278.velocitab.tab.Nametag;
import net.william278.velocitab.tab.PlayerTabList;
@ -38,19 +36,12 @@ import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Getter
@ToString
public final class TabPlayer implements Comparable<TabPlayer> {
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%");
private static final String PLACEHOLDER_DELIMITER = "<-DELIMITER->";
private final Velocitab plugin;
private final Player player;
@Setter
@ -60,8 +51,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
// Each TabPlayer contains the components for each TabPlayer it's currently viewing this player
private final Map<UUID, Component> relationalDisplayNames;
private final Map<UUID, Component[]> relationalNametags;
private final Map<String, String> cachedPlaceholders;
private final Map<UUID, Integer> cachedListOrders;
private String lastDisplayName;
private Component lastHeader;
private Component lastFooter;
@ -91,8 +80,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
this.group = group;
this.relationalDisplayNames = Maps.newConcurrentMap();
this.relationalNametags = Maps.newConcurrentMap();
this.cachedPlaceholders = Maps.newConcurrentMap();
this.cachedListOrders = Maps.newConcurrentMap();
}
@NotNull
@ -123,75 +110,29 @@ public final class TabPlayer implements Comparable<TabPlayer> {
return plugin.getTabGroups().getPosition(group);
}
@NotNull
public CompletableFuture<String> getDisplayName(@NotNull Velocitab plugin) {
final String format = formatGroup();
return Placeholder.replace(format, plugin, this)
.thenApply(d -> cacheDisplayName(d, format));
public Nametag getNametag(@NotNull Velocitab plugin) {
final String prefix = plugin.getPlaceholderManager().applyPlaceholders(this, group.nametag().prefix());
final String suffix = plugin.getPlaceholderManager().applyPlaceholders(this, group.nametag().suffix());
return new Nametag(prefix, suffix);
}
@NotNull
private String formatGroup() {
final Set<String> placeholders = Sets.newHashSet();
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(group.format());
while (matcher.find()) {
placeholders.add("%" + matcher.group(1) + "%");
}
return String.join(PLACEHOLDER_DELIMITER, placeholders);
}
@NotNull
private String cacheDisplayName(@NotNull String placeholders, @NotNull String keys) {
String displayName = group.format();
final String[] placeholderArray = placeholders.split(PLACEHOLDER_DELIMITER);
final String[] keyArray = keys.split(PLACEHOLDER_DELIMITER);
for (int i = 0; i < placeholderArray.length; i++) {
final String placeholder = keyArray[i];
final String value = placeholderArray[i];
cachedPlaceholders.put(placeholder, value);
displayName = displayName.replace(placeholder, value);
}
displayName = Placeholder.replaceInternal(displayName, plugin, this).first();
return lastDisplayName = displayName;
}
@NotNull
public CompletableFuture<Nametag> getNametag(@NotNull Velocitab plugin) {
return Placeholder.replace(group.nametag(), plugin, this);
}
@NotNull
public CompletableFuture<String> getTeamName(@NotNull Velocitab plugin) {
return plugin.getSortingManager().getTeamName(this)
.thenApply(teamName -> this.teamName = teamName);
public String getTeamName(@NotNull Velocitab plugin) {
final String teamName = plugin.getSortingManager().getTeamName(this);
return this.teamName = teamName;
}
public Optional<String> getLastTeamName() {
return Optional.ofNullable(teamName);
}
public CompletableFuture<Void> sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
return tabList.getHeader(this).thenCompose(header -> tabList.getFooter(this).thenAccept(footer -> {
final boolean disabled = plugin.getSettings().isDisableHeaderFooterIfEmpty();
if (disabled) {
if ((!Component.empty().equals(header) && !header.equals(lastHeader)) ||
(!Component.empty().equals(footer) && !footer.equals(lastFooter))) {
public void sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
final Component header = tabList.getHeader(this);
final Component footer = tabList.getFooter(this);
lastHeader = header;
lastFooter = footer;
player.sendPlayerListHeaderAndFooter(header, footer);
}
} else {
if (!header.equals(lastHeader) || !footer.equals(lastFooter)) {
lastHeader = header;
lastFooter = footer;
player.sendPlayerListHeaderAndFooter(header, footer);
}
}
}));
}
public void incrementIndexes() {
incrementHeaderIndex();
@ -232,10 +173,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
relationalNametags.remove(target);
}
public void unsetTabListOrder(@NotNull UUID target) {
cachedListOrders.remove(target);
}
public Optional<Component[]> getRelationalNametag(@NotNull UUID target) {
return Optional.ofNullable(relationalNametags.get(target));
}
@ -248,7 +185,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
lastFooter = null;
role = Role.DEFAULT_ROLE;
teamName = null;
cachedListOrders.clear();
}
/**
@ -273,8 +209,4 @@ public final class TabPlayer implements Comparable<TabPlayer> {
public boolean equals(Object obj) {
return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId());
}
public Optional<String> getCachedPlaceholderValue(@NotNull String placeholder) {
return Optional.ofNullable(cachedPlaceholders.get(placeholder));
}
}

View File

@ -91,7 +91,7 @@ public interface ScoreboardProvider {
setTabList(tabList);
getPlugin().getServer().getEventManager().register(this, tabList);
getPlugin().getServer().getScheduler().buildTask(this, tabList::load).delay(1, TimeUnit.SECONDS).schedule();
getPlugin().getServer().getScheduler().buildTask(this, tabList::load).delay(250, TimeUnit.MILLISECONDS).schedule();
final SortingManager sortingManager = new SortingManager(getPlugin());
setSortingManager(sortingManager);

View File

@ -22,20 +22,16 @@ package net.william278.velocitab.sorting;
import com.google.common.collect.Lists;
import com.velocitypowered.api.network.ProtocolVersion;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class SortingManager {
private final Velocitab plugin;
private static final String DELIMITER = ":::";
private static final Pattern NUMBER_PATTERN = Pattern.compile("^-?[0-9]\\d*(\\.\\d+)?$");
public SortingManager(@NotNull Velocitab plugin) {
@ -43,15 +39,18 @@ public class SortingManager {
}
@NotNull
public CompletableFuture<String> getTeamName(@NotNull TabPlayer player) {
public String getTeamName(@NotNull TabPlayer player) {
if (!plugin.getSettings().isSortPlayers()) {
return CompletableFuture.completedFuture("");
return "";
}
return Placeholder.replace(String.join(DELIMITER, player.getGroup().sortingPlaceholders()), plugin, player)
.thenApply(s -> Arrays.asList(s.split(DELIMITER)))
.thenApply(v -> v.stream().map(s -> adaptValue(s, player)).collect(Collectors.toList()))
.thenApply(v -> handleList(player, v));
final List<String> placeholders = player.getGroup().sortingPlaceholders()
.stream()
.map(s -> plugin.getPlaceholderManager().applyPlaceholders(player, s))
.map(s -> adaptValue(s, player))
.collect(Collectors.toList());
return handleList(player, placeholders);
}
@NotNull

View File

@ -1,40 +0,0 @@
/*
* 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.tab;
import com.velocitypowered.api.scheduler.ScheduledTask;
import org.jetbrains.annotations.Nullable;
public record GroupTasks(@Nullable ScheduledTask updateTask, @Nullable ScheduledTask headerFooterTask,
@Nullable ScheduledTask latencyTask) {
public void cancel() {
if (updateTask != null) {
updateTask.cancel();
}
if (headerFooterTask != null) {
headerFooterTask.cancel();
}
if (latencyTask != null) {
latencyTask.cancel();
}
}
}

View File

@ -21,7 +21,7 @@ package net.william278.velocitab.tab;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.placeholder.Placeholder;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
@ -32,13 +32,13 @@ public record Nametag(@NotNull String prefix, @NotNull String suffix) {
@NotNull
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
final String formatted = Placeholder.replaceInternal(prefix, plugin, tabPlayer).first();
final String formatted = plugin.getPlaceholderManager().applyPlaceholders(tabPlayer, prefix, target);
return plugin.getFormatter().format(formatted, tabPlayer, target, plugin);
}
@NotNull
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
final String formatted = Placeholder.replaceInternal(suffix, plugin, tabPlayer).first();
final String formatted = plugin.getPlaceholderManager().applyPlaceholders(tabPlayer, suffix, target);
return plugin.getFormatter().format(formatted, tabPlayer, target, plugin);
}

View File

@ -26,9 +26,8 @@ import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.player.TabList;
import com.velocitypowered.api.proxy.player.TabListEntry;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.util.ServerLink;
import com.velocitypowered.proxy.tablist.KeyedVelocityTabList;
import com.velocitypowered.proxy.tablist.VelocityTabList;
import lombok.AccessLevel;
@ -37,7 +36,6 @@ import net.kyori.adventure.text.Component;
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.packet.ScoreboardManager;
import net.william278.velocitab.player.Role;
@ -48,12 +46,9 @@ import org.slf4j.event.Level;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER;
/**
* The main class for tracking the server TAB list for a map of {@link TabPlayer}s
*/
@ -73,7 +68,6 @@ public class PlayerTabList {
this.players = Maps.newConcurrentMap();
this.taskManager = new TaskManager(plugin);
this.entriesFields = Maps.newHashMap();
this.reloadUpdate();
this.registerListener();
this.ensureDisplayNameTask();
this.registerFields();
@ -142,6 +136,8 @@ public class PlayerTabList {
joinPlayer(p, group.get());
});
reloadUpdate();
}
/**
@ -173,7 +169,6 @@ public class PlayerTabList {
players.values().forEach(p -> {
p.unsetRelationalDisplayName(player.getUniqueId());
p.unsetRelationalNametag(player.getUniqueId());
p.unsetTabListOrder(player.getUniqueId());
});
}
@ -186,6 +181,7 @@ public class PlayerTabList {
tabPlayerOptional.get().setGroup(group);
tabPlayerOptional.get().setRole(plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(joined)).orElse(Role.DEFAULT_ROLE));
}
final TabPlayer tabPlayer = tabPlayerOptional.orElseGet(() -> createTabPlayer(joined, group));
final String serverName = getServerName(joined);
// Store last server, so it's possible to have the last server on disconnect
@ -194,19 +190,7 @@ public class PlayerTabList {
// Send server URLs (1.21 clients)
sendPlayerServerLinks(tabPlayer);
// Set the player as not loaded until the display name is set
tabPlayer.getDisplayName(plugin).thenAccept(d -> {
if (d == null) {
plugin.log(Level.ERROR, "Failed to get display name for " + joined.getUsername());
return;
}
handleDisplayLoad(tabPlayer);
}).exceptionally(throwable -> {
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
joined.getUsername(), joined.getUniqueId()), throwable);
return null;
});
}
private void handleDisplayLoad(@NotNull TabPlayer tabPlayer) {
@ -214,14 +198,8 @@ public class PlayerTabList {
final Group group = tabPlayer.getGroup();
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
players.putIfAbsent(joined.getUniqueId(), tabPlayer);
tabPlayer.sendHeaderAndFooter(this)
.thenAccept(v -> tabPlayer.setLoaded(true))
.exceptionally(throwable -> {
plugin.log(Level.ERROR, String.format("Failed to send header and footer for %s (UUID: %s)",
joined.getUsername(), joined.getUniqueId()), throwable);
return null;
});
tabPlayer.sendHeaderAndFooter(this);
tabPlayer.setLoaded(true);
final Set<TabPlayer> tabPlayers = group.getTabPlayers(plugin, tabPlayer);
updateTabListOnJoin(tabPlayer, group, tabPlayers, isVanished);
}
@ -285,19 +263,10 @@ public class PlayerTabList {
@NotNull
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer,
@NotNull Component single, @NotNull String toParse) {
if (plugin.getMiniPlaceholdersHook().isEmpty()) {
return single;
}
@NotNull String toParse) {
return plugin.getFormatter().format(toParse, player, viewer, plugin);
}
@NotNull
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull String toParse) {
final Component single = plugin.getFormatter().format(toParse, player, viewer, plugin);
return getRelationalPlaceholder(player, viewer, single, toParse);
}
@SuppressWarnings("unchecked")
private void fixDuplicateEntries(@NotNull Player target) {
try {
@ -374,8 +343,9 @@ public class PlayerTabList {
if (!viewer.getPlayer().getTabList().equals(tabList)) {
throw new IllegalArgumentException("TabList of viewer is not the same as the TabList of the entry");
}
final Component single = plugin.getFormatter().format(player.getLastDisplayName(), player, viewer, plugin);
final Component displayName = getRelationalPlaceholder(player, viewer, single, player.getGroup().format());
final String displayNameUnformatted = plugin.getPlaceholderManager().applyPlaceholders(player, player.getGroup().format(), viewer);
final Component displayName = getRelationalPlaceholder(player, viewer, displayNameUnformatted);
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
return TabListEntry.builder()
.profile(player.getPlayer().getGameProfile())
@ -386,7 +356,8 @@ public class PlayerTabList {
}
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer) {
final Component displayName = getRelationalPlaceholder(player, viewer, player.getLastDisplayName());
final String displayNameUnformatted = plugin.getPlaceholderManager().applyPlaceholders(player, player.getGroup().format(), viewer);
final Component displayName = getRelationalPlaceholder(player, viewer, displayNameUnformatted);
updateDisplayName(player, viewer, displayName);
}
@ -418,6 +389,10 @@ public class PlayerTabList {
);
}
public void updateHeaderFooter(@NotNull Group group) {
group.getTabPlayers(plugin).forEach(p -> p.sendHeaderAndFooter(this));
}
// Update a player's name in the tab list and scoreboard team
public void updatePlayer(@NotNull TabPlayer tabPlayer, boolean force) {
if (!tabPlayer.getPlayer().isActive()) {
@ -425,15 +400,25 @@ public class PlayerTabList {
return;
}
updateSorting(tabPlayer, force);
plugin.getPlaceholderManager().fetchPlaceholders(tabPlayer.getPlayer().getUniqueId(), tabPlayer.getGroup().sortingPlaceholders());
//to make sure that role placeholder is updated even for a backend placeholder
plugin.getServer().getScheduler().buildTask(plugin,
() -> updateSorting(tabPlayer, force))
.delay(100, TimeUnit.MILLISECONDS)
.schedule();
}
public void updateSorting(@NotNull Group group) {
group.getTabPlayers(plugin).forEach(p -> updateSorting(p, false));
}
private void updateSorting(@NotNull TabPlayer tabPlayer, boolean force) {
tabPlayer.getTeamName(plugin).thenAccept(teamName -> {
final String teamName = tabPlayer.getTeamName(plugin);
if (teamName.isBlank()) {
return;
}
plugin.getScoreboardManager().updateRole(tabPlayer, teamName, force).thenAccept(v -> {
plugin.getScoreboardManager().updateRole(tabPlayer, teamName, force);
final int order = plugin.getScoreboardManager().getPosition(teamName);
if (order == -1) {
plugin.log(Level.ERROR, "Failed to get position for " + tabPlayer.getPlayer().getUsername());
@ -443,8 +428,6 @@ public class PlayerTabList {
tabPlayer.setListOrder(order);
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
players.forEach(p -> recalculateSortingForPlayer(p, players));
});
});
}
public void sendPlayerServerLinks(@NotNull TabPlayer player) {
@ -452,17 +435,11 @@ public class PlayerTabList {
return;
}
final List<ServerUrl> urls = plugin.getSettings().getUrlsForGroup(player.getGroup());
ServerUrl.resolve(plugin, player, urls).thenAccept(player.getPlayer()::setServerLinks);
final List<ServerLink> serverLinks = ServerUrl.resolve(plugin, player, urls);
player.getPlayer().setServerLinks(serverLinks);
}
public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) {
tabPlayer.getDisplayName(plugin).thenAccept(displayName -> {
if (displayName == null) {
plugin.log(Level.ERROR, "Failed to get display name for " + tabPlayer.getPlayer().getUsername());
return;
}
final Component single = plugin.getFormatter().format(displayName, tabPlayer, plugin);
final boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername());
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
@ -471,10 +448,10 @@ public class PlayerTabList {
return;
}
final Component relationalPlaceholder = getRelationalPlaceholder(tabPlayer, player, single, displayName);
final String displayNameUnformatted = plugin.getPlaceholderManager().applyPlaceholders(tabPlayer, tabPlayer.getGroup().format(), player);
final Component relationalPlaceholder = getRelationalPlaceholder(tabPlayer, player, displayNameUnformatted);
updateDisplayName(tabPlayer, player, relationalPlaceholder);
});
});
}
public void checkCorrectDisplayName(@NotNull TabPlayer tabPlayer) {
@ -505,6 +482,10 @@ public class PlayerTabList {
players.values().forEach(this::updatePlayerDisplayName);
}
public void updateDisplayNames(@NotNull Group group) {
group.getTabPlayers(plugin).forEach(this::updatePlayerDisplayName);
}
public void checkCorrectDisplayNames() {
players.values().forEach(this::checkCorrectDisplayName);
}
@ -518,19 +499,19 @@ public class PlayerTabList {
}
// Get the component for the TAB list header
public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) {
public Component getHeader(@NotNull TabPlayer player) {
final String header = player.getGroup().getHeader(player.getHeaderIndex());
final String replaced = plugin.getPlaceholderManager().applyPlaceholders(player, header);
return Placeholder.replace(header, plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
return plugin.getFormatter().format(replaced, player, plugin);
}
// Get the component for the TAB list footer
public CompletableFuture<Component> getFooter(@NotNull TabPlayer player) {
public Component getFooter(@NotNull TabPlayer player) {
final String footer = player.getGroup().getFooter(player.getFooterIndex());
final String replaced = plugin.getPlaceholderManager().applyPlaceholders(player, footer);
return Placeholder.replace(footer, plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
return plugin.getFormatter().format(replaced, player, plugin);
}
/**
@ -538,11 +519,16 @@ public class PlayerTabList {
*/
public void reloadUpdate() {
taskManager.cancelAllTasks();
plugin.getTabGroups().getGroups().forEach(taskManager::updatePeriodically);
plugin.getTabGroups().getGroups().forEach(g -> {
plugin.getPlaceholderManager().fetchPlaceholders(g);
taskManager.updatePeriodically(g);
});
if (players.isEmpty()) {
return;
}
plugin.getServer().getScheduler().buildTask(plugin, () -> {
// If the update time is set to 0 do not schedule the updater
players.values().forEach(player -> {
final Optional<ServerConnection> server = player.getPlayer().getCurrentServer();
@ -560,6 +546,7 @@ public class PlayerTabList {
player.sendHeaderAndFooter(this);
});
updateDisplayNames();
}).delay(500, TimeUnit.MILLISECONDS).schedule();
}
@NotNull
@ -567,6 +554,12 @@ public class PlayerTabList {
return plugin.getTabGroups().getGroupFromServer(serverName, plugin);
}
@NotNull
public Group getGroupOrDefault(@NotNull Player player) {
final Optional<Group> group = getGroup(player.getCurrentServer().map(ServerConnection::getServerInfo).map(ServerInfo::getName).orElse(""));
return group.orElse(plugin.getTabGroups().getGroupFromName("default"));
}
public void removeOldEntry(@NotNull Group group, @NotNull UUID uuid) {
final Set<TabPlayer> players = group.getTabPlayers(plugin);
players.forEach(player -> player.getPlayer().getTabList().removeEntry(uuid));
@ -595,19 +588,13 @@ public class PlayerTabList {
if (!tabPlayer.getPlayer().getTabList().containsEntry(uuid)) {
return;
}
if (tabPlayer.getCachedListOrders().containsKey(uuid) && tabPlayer.getCachedListOrders().get(uuid) == position) {
final Optional<TabListEntry> entry = tabPlayer.getPlayer().getTabList().getEntry(uuid);
if(entry.isEmpty() || entry.get().getListOrder() == position) {
return;
}
tabPlayer.getCachedListOrders().put(uuid, position);
final UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(UPDATE_LIST_ORDER);
final UpsertPlayerInfoPacket.Entry entry = new UpsertPlayerInfoPacket.Entry(uuid);
entry.setListOrder(position);
packet.addEntry(entry);
((ConnectedPlayer) tabPlayer.getPlayer()).getConnection().write(packet);
}
private String getPlayerName(UUID uuid) {
return plugin.getServer().getPlayer(uuid).map(Player::getUsername).orElse("Unknown");
entry.get().setListOrder(position);
}
public synchronized void recalculateSortingForPlayer(@NotNull TabPlayer tabPlayer, @NotNull Set<TabPlayer> players) {
@ -616,7 +603,7 @@ public class PlayerTabList {
}
players.forEach(p -> {
final int order = p.getListOrder();
final int order = plugin.getScoreboardManager().getPosition(p.getLastTeamName().orElse(""));
updateSorting(tabPlayer, p.getPlayer().getUniqueId(), order);
});
}

View File

@ -29,6 +29,7 @@ import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.scheduler.ScheduledTask;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
@ -101,6 +102,7 @@ public class TabListListener {
// Removes cached relational data of the joined player from all other players
plugin.getTabList().clearCachedData(joined);
plugin.getPlaceholderManager().clearPlaceholders(joined.getUniqueId());
if (!plugin.getSettings().isShowAllPlayersFromAllGroups() && previousGroup.isPresent()
&& (groupOptional.isPresent() && !previousGroup.get().equals(groupOptional.get())
@ -147,9 +149,25 @@ public class TabListListener {
final Group group = groupOptional.get();
plugin.getScoreboardManager().resetCache(joined, group);
final int delay = justQuit.contains(joined.getUniqueId()) ? 100 : 250;
plugin.getServer().getScheduler().buildTask(plugin, () -> {
plugin.getPlaceholderManager().unblockPlayer(joined.getUniqueId());
}).delay(10, TimeUnit.MILLISECONDS).schedule();
final ScheduledTask task = plugin.getServer().getScheduler()
.buildTask(plugin, () -> {
plugin.getPlaceholderManager().fetchPlaceholders(joined.getUniqueId(), group.getTextsWithPlaceholders());
})
.delay(15, TimeUnit.MILLISECONDS)
.repeat(50, TimeUnit.MILLISECONDS)
.schedule();
//After updating papiproxybridge we can check if redis is used
final int delay = justQuit.contains(joined.getUniqueId()) ? 400 : 300;
plugin.getServer().getScheduler().buildTask(plugin,
() -> tabList.joinPlayer(joined, group))
() -> {
task.cancel();
tabList.joinPlayer(joined, group);
})
.delay(delay, TimeUnit.MILLISECONDS)
.schedule();
}
@ -167,6 +185,8 @@ public class TabListListener {
// Remove the player from the tab list of all other players
tabList.removePlayer(event.getPlayer());
plugin.getPlaceholderManager().clearPlaceholders(event.getPlayer().getUniqueId());
plugin.getPlaceholderManager().unblockPlayer(event.getPlayer().getUniqueId());
}
private void checkDelayedDisconnect(@NotNull DisconnectEvent event) {

View File

@ -19,6 +19,7 @@
package net.william278.velocitab.tab;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.velocitypowered.api.scheduler.ScheduledTask;
import net.william278.velocitab.Velocitab;
@ -26,6 +27,7 @@ import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -33,7 +35,7 @@ import java.util.concurrent.TimeUnit;
public class TaskManager {
private final Velocitab plugin;
private final Map<Group, GroupTasks> groupTasks;
private final Map<Group, List<ScheduledTask>> groupTasks;
public TaskManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
@ -41,66 +43,65 @@ public class TaskManager {
}
protected void cancelAllTasks() {
groupTasks.values().forEach(GroupTasks::cancel);
groupTasks.values().forEach(c -> c.forEach(ScheduledTask::cancel));
groupTasks.clear();
}
protected void updatePeriodically(@NotNull Group group) {
ScheduledTask headerFooterTask = null;
ScheduledTask updateTask = null;
ScheduledTask latencyTask;
if (group.headerFooterUpdateRate() > 0) {
headerFooterTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateGroupPlayers(group, false, true))
final ScheduledTask headerFooterTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> plugin.getTabList().updateHeaderFooter(group))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.headerFooterUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
groupTasks.computeIfAbsent(group, g -> Lists.newArrayList()).add(headerFooterTask);
}
if (group.formatUpdateRate() > 0) {
final ScheduledTask formatTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> plugin.getTabList().updateDisplayNames(group))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.formatUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
groupTasks.computeIfAbsent(group, g -> Lists.newArrayList()).add(formatTask);
}
if (group.nametagUpdateRate() > 0) {
final ScheduledTask nametagTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> plugin.getTabList().updateSorting(group))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.nametagUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
groupTasks.computeIfAbsent(group, g -> Lists.newArrayList()).add(nametagTask);
}
if (group.placeholderUpdateRate() > 0) {
updateTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateGroupPlayers(group, true, false))
final ScheduledTask updateTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updatePlaceholders(group))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.placeholderUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
groupTasks.computeIfAbsent(group, g -> Lists.newArrayList()).add(updateTask);
}
latencyTask = plugin.getServer().getScheduler()
final ScheduledTask latencyTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateLatency(group))
.delay(1, TimeUnit.SECONDS)
.repeat(3, TimeUnit.SECONDS)
.schedule();
groupTasks.put(group, new GroupTasks(headerFooterTask, updateTask, latencyTask));
groupTasks.computeIfAbsent(group, g -> Lists.newArrayList()).add(latencyTask);
}
/**
* Updates the players in the given group.
*
* @param group The group whose players should be updated.
* @param all Whether to update all player properties, or just the header and footer.
* @param incrementIndexes Whether to increment the header and footer indexes.
*/
private void updateGroupPlayers(@NotNull Group group, boolean all, boolean incrementIndexes) {
final Set<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
if (groupPlayers.isEmpty()) {
private void updatePlaceholders(@NotNull Group group) {
final List<TabPlayer> players = group.getTabPlayersAsList(plugin);
if (players.isEmpty()) {
return;
}
groupPlayers.stream()
.filter(player -> player.getPlayer().isActive())
.forEach(player -> {
if (incrementIndexes) {
player.incrementIndexes();
}
if (all) {
plugin.getTabList().updatePlayer(player, false);
}
player.sendHeaderAndFooter(plugin.getTabList());
});
if (all) {
plugin.getTabList().updateDisplayNames();
}
final List<String> texts = group.getTextsWithPlaceholders();
players.forEach(player -> plugin.getPlaceholderManager().fetchPlaceholders(player.getPlayer().getUniqueId(), texts));
}
private void updateLatency(@NotNull Group group) {

View File

@ -0,0 +1,83 @@
/*
* 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.util;
import com.google.common.collect.Lists;
import lombok.Getter;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MiniMessageUtil {
@Getter
private static final MiniMessageUtil INSTANCE = new MiniMessageUtil();
private final Pattern legacyRGBPattern = Pattern.compile("#&[0-9a-fA-F]{6}");
private final Pattern legacyPattern = Pattern.compile("&[0-9a-fA-F]");
private final Pattern legacySectionPattern = Pattern.compile("§[0-9a-fA-F]");
private int errorsCount;
private MiniMessageUtil() {
errorsCount = 0;
}
@NotNull
public String checkForErrors(@NotNull String text, @NotNull Velocitab plugin) {
final List<String> errors = Lists.newArrayList();
String copy = text;
copy = processLegacySections(errors, copy, legacyRGBPattern);
copy = processLegacySections(errors, copy, legacyPattern);
copy = processLegacySections(errors, copy, legacySectionPattern);
if (errorsCount > 0 && errorsCount % 10 == 0) {
errorsCount++;
plugin.log(Level.WARN, "Found legacy formatting which is not supported if the formatter is set to MINIMESSAGE." +
" Remove the following characters from your config or make sure placeholders don't contain them: " + errors + ". & and § are replaced with * to prevent issues with MINIMESSAGE.");
if(errorsCount > 100000) {
errorsCount = 0;
}
}
return copy;
}
@NotNull
private String processLegacySections(@NotNull List<String> errors, @NotNull String copy, @NotNull Pattern legacySectionPattern) {
final StringBuilder result = new StringBuilder();
final Matcher legacySectionMatcher = legacySectionPattern.matcher(copy);
while (legacySectionMatcher.find()) {
errors.add(legacySectionMatcher.group());
String matched = legacySectionMatcher.group();
String replaced = "*" + matched.substring(1);
legacySectionMatcher.appendReplacement(result, Matcher.quoteReplacement(replaced));
errorsCount++;
}
legacySectionMatcher.appendTail(result);
return result.toString();
}
}

View File

@ -23,7 +23,7 @@ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
public final class SerializationUtil {
public final static LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder()
public static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder()
.hexCharacter('#')
.character('&')
.hexColors()