1.7.4 - Bug fixes and performance improvements (#248)

* Incremented join delay

* Refactored the code to cache placeholders and then use them in sync

* More cleanup

* Sorting improvements

* Improvements on server switch

* Increased delay

* Fixed problem with header & footer task
Improved placeholder cache

* Fixed some problems
Not yet stable

* Applied 250ms delay to tab group quit logic
Reworked disconnect handling

* Fixed problem, more stable

* Removed debug messages

* Recoded task manager
Fixed possible synchronization problem
Added possibility of using multiple tabgroups files

* Fixes

* Improvements

* Removed mini expansion
Changed placeholder internal logic

* General improvements

* Performance improvements
Added not relational conditional placeholders
Fixed problems with displayname

* Fixed problem

* Improved code style
Removed useless lines

* Optimized imports

* Code cleanup

* Code cleanup

* Fixed possible when using relational placeholders

* Added Toilet support

* Improved toilet implementation

* Added new DebugSystem
removed old debug

* Changed labels

* Removed useless dependencies
Bumped toilet

* Changed from GeneralUtil to StringUtil

* Added Locales

* Fixed style

* Reverted #238

* Added docs
Removed locales

* Removed confirm sub command from docs

* remove comments

* Fixed FileInclusionRule sorting

* Fixed rowspan

---------
This commit is contained in:
AlexDev_ 2025-02-24 21:54:44 +01:00 committed by GitHub
parent 4920b738a0
commit 4191f13624
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 2365 additions and 1308 deletions

View File

@ -50,8 +50,9 @@ dependencies {
implementation 'net.william278:minedown:1.8.2'
implementation 'org.bstats:bstats-velocity:3.1.0'
implementation 'de.exlll:configlib-yaml:4.5.0'
implementation 'org.apache.commons:commons-jexl3:3.4.0'
implementation 'org.mvel:mvel2:2.5.2.Final'
implementation 'net.jodah:expiringmap:0.5.11'
implementation 'net.william278.toilet:toilet-velocity:1.0.12'
annotationProcessor 'org.projectlombok:lombok:1.18.36'
}
@ -88,24 +89,26 @@ java {
}
shadowJar {
dependencies {
exclude dependency(':slf4j-api')
exclude dependency('org.json:json')
exclude dependency('org.apache.commons:commons-lang3')
}
relocate 'org.apache.commons.text', 'net.william278.velocitab.libraries.commons.text'
relocate 'org.apache.commons.lang3', 'net.william278.velocitab.libraries.commons.lang3'
relocate 'org.jetbrains', 'net.william278.velocitab.libraries'
relocate 'org.intellij', 'net.william278.velocitab.libraries'
relocate 'de.themoep', 'net.william278.velocitab.libraries'
relocate 'net.william278.annotaml', 'net.william278.velocitab.libraries.annotaml'
relocate 'net.william278.desertwell', 'net.william278.velocitab.libraries.desertwell'
relocate 'net.william278.toilet', 'net.william278.velocitab.libraries.toilet'
relocate 'org.bstats', 'net.william278.velocitab.libraries.bstats'
relocate 'de.exlll.configlib', 'net.william278.velocitab.libraries.configlib'
relocate 'org.snakeyaml', 'net.william278.velocitab.libraries.snakeyaml'
relocate 'org.apache.commons.jexl3', 'net.william278.velocitab.libraries.commons.jexl3'
relocate 'org.apache.commons.logging', 'net.william278.velocitab.libraries.commons.logging'
relocate 'net.jodah.expiringmap', 'net.william278.velocitab.libraries.expiringmap'
dependencies {
exclude dependency(':slf4j-api')
exclude dependency('org.json:json')
}
relocate 'org.mvel2', 'net.william278.velocitab.libraries.mvel2'
destinationDirectory.set(file("$rootDir/target"))
archiveClassifier.set('')

View File

@ -11,7 +11,7 @@ This page contains the usage table and associated permissions for the `/velocita
<tbody>
<!-- /velocitab command -->
<tr>
<td rowspan="5"><code>/velocitab</code></td>
<td rowspan="7"><code>/velocitab</code></td>
<td><code>/velocitab</code></td>
<td>View & manage plugin system information</td>
<td><code>velocitab.command</code></td>
@ -36,5 +36,15 @@ This page contains the usage table and associated permissions for the `/velocita
<td>Change or reset your TAB display name</td>
<td><code>velocitab.command.name</code></td>
</tr>
<tr>
<td><code>/velocitab dump</code></td>
<td>Generate a debug dump of the plugin state</td>
<td><code>velocitab.command.dump</code></td>
</tr>
<tr>
<td><code>/velocitab debug tablist &lt;player&gt;</code></td>
<td>Debug the TAB list for a specific player</td>
<td><code>velocitab.command.debug</code></td>
</tr>
</tbody>
</table>
</table>

View File

@ -1,14 +1,34 @@
In order to use these placeholders, install MiniPlaceholders on your Velocity proxy, set the `formatter_type`
to `MINIMESSAGE`, and ensure `enable_miniplaceholders_hook` is set to `true`.
Conditional placeholders allow you to display different values based on certain conditions. The format
is `<velocitab_rel_condition|<condition>|<true>|<false>>`.
is `<velocitab_condition|<condition>|<true>|<false>>` and the relational format is `<velocitab_rel_condition|<condition>|<true>|<false>>`.
Currently, this system is only available for the `format` and `nametag` fields in the tab groups configuration.
**Note:** The difference between the two is that relational placeholders are evaluated from the viewer's perspective, while the conditional placeholders are evaluated from the player's perspective.
So if you have 200 players, if you use a conditional placeholder, the placeholder will be evaluated 200 times, while if you use a relational placeholder, it will be evaluated 200*200 = 40000 times.
Using relational placeholders could be really slow, so it is recommended to not use them unless you need them.
## Table of Conditional Placeholders
| Placeholder Example | Description | Example Output |
| Relational Placeholder Example | Description | Example Output |
|----------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| `<velocitab_condition:%vault_eco_balance% >= 10:rich:poor>` | Checks if the player's vault balance is greater than or equal to 10. If true, displays "rich", else "poor". | `rich` or `poor` |
| `<velocitab_condition:%player_health% < 10:Low Health:Healthy>` | Checks if the player's health is below 10. If true, displays "Low Health", else "Healthy". | `Low Health` or `Healthy` |
| `<velocitab_condition:%player_ping% <= 50:Good Ping:Bad Ping>` | Checks if the player's ping is 50 or below. If true, displays "Good Ping", else "Bad Ping". | `Good Ping` or `Bad Ping` |
| `<velocitab_condition:%player_level% >= 30:High Level:Low Level>` | Checks if the player's level is 30 or above. If true, displays "High Level", else "Low Level". | `High Level` or `Low Level` |
| `<velocitab_condition:%player_exp% >= 1000:XP Master:XP Novice>` | Checks if the player has 1000 or more experience points. If true, displays "XP Master", else "XP Novice". | `XP Master` or `XP Novice` |
| `<velocitab_condition:"%player_name%" == ''AlexDev_'' OR "%player_name%" == ''William278_'':VelocitabDev:>` | Checks if the player's name is either "AlexDev_" or "William278". If true, displays "Developer", else "NotDev". | `Developer` or `NotDev` |
| `<velocitab_condition:"%player_gamemode%" == ''CREATIVE'':Creative Mode:Not Creative Mode>` | Checks if the player is in creative mode. If true, displays "Creative Mode", else "Not Creative Mode". | `Creative Mode` or `Not Creative Mode` |
| `<velocitab_condition:"%player_world%" == ''nether'':In Nether:Not in Nether>` | Checks if the player is in the Nether. If true, displays "In Nether", else "Not in Nether". | `In Nether` or `Not in Nether` |
| `<velocitab_condition:"%player_biome%" == "DESERT":In Desert:Not in Desert>` | Checks if the player is in a desert biome. If true, displays "In Desert", else "Not in Desert". | `In Desert` or `Not in Desert` |
| `<velocitab_condition:''%player_gamemode%''.contains(''S''):Survival or Spectator:Not Survival or Spectator> ` | Checks if the player is in survival or spectator mode. If true, displays "Survival or Spectator", else "Not Survival or Spectator". | `Survival or Spectator` or `Not Survival or Spectator` |
| `<velocitab_condition:%player_health% == %target_player_health%:Same health:Not same health> ` | Checks if the player's health is the same as the target player's health. If true, displays "Same health", else "Not same health". | `Same health` or `Not same health` |
## Table of Conditional Relational Placeholders
**Note:** In order to use relational placeholders you need to give a player the permission `velocitab.relational`. For performance reasons, this permission is not given by default.
| Relational Placeholder Example | Description | Example Output |
|--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| `<velocitab_rel_condition:%vault_eco_balance% >= 10:rich:poor>` | Checks if the player's vault balance is greater than or equal to 10. If true, displays "rich", else "poor". | `rich` or `poor` |
| `<velocitab_rel_condition:%player_health% < 10:Low Health:Healthy>` | Checks if the player's health is below 10. If true, displays "Low Health", else "Healthy". | `Low Health` or `Healthy` |
@ -16,8 +36,6 @@ Currently, this system is only available for the `format` and `nametag` fields i
| `<velocitab_rel_condition:%player_level% >= 30:High Level:Low Level>` | Checks if the player's level is 30 or above. If true, displays "High Level", else "Low Level". | `High Level` or `Low Level` |
| `<velocitab_rel_condition:%player_exp% >= 1000:XP Master:XP Novice>` | Checks if the player has 1000 or more experience points. If true, displays "XP Master", else "XP Novice". | `XP Master` or `XP Novice` |
| `<velocitab_rel_condition:"%player_name%" == ''AlexDev_'' OR "%player_name%" == ''William278_'':VelocitabDev:>` | Checks if the player's name is either "AlexDev_" or "William278". If true, displays "Developer", else "NotDev". | `Developer` or `NotDev` |
| `<velocitab_rel_condition:startsWith(''%player_name%'', ''AlexDe''):IsAlex:NotAlex>` | Checks if the player's name starts with "AlexDe". If true, displays "IsAlex", else "NotAlex". | `IsAlex` or `NotAlex` |
| `<velocitab_rel_condition:endsWith(''%player_name%'', ''278''):EndsWith278:DoesNotEndWith278>` | Checks if the player's name ends with "278". If true, displays "EndsWith278", else "DoesNotEndWith278". | `EndsWith278` or `DoesNotEndWith278` |
| `<velocitab_rel_condition:"%player_gamemode%" == ''CREATIVE'':Creative Mode:Not Creative Mode>` | Checks if the player is in creative mode. If true, displays "Creative Mode", else "Not Creative Mode". | `Creative Mode` or `Not Creative Mode` |
| `<velocitab_rel_condition:"%player_world%" == ''nether'':In Nether:Not in Nether>` | Checks if the player is in the Nether. If true, displays "In Nether", else "Not in Nether". | `In Nether` or `Not in Nether` |
| `<velocitab_rel_condition:"%player_biome%" == "DESERT":In Desert:Not in Desert>` | Checks if the player is in a desert biome. If true, displays "In Desert", else "Not in Desert". | `In Desert` or `Not in Desert` |

View File

@ -6,6 +6,9 @@ the server a player is on. You can also set formatting to use for [[Nametags]] a
Groups are defined in `tab_groups.yml`, as a list of TabGroup elements.
You can also add more tab groups by creating a folder called `tab_groups` in the `plugins/velocitab` folder, and adding
a `other_tab_groups.yml` (you can use a custom name, it needs to be .yml) file to it.
Every group must have a unique name, and a list of servers to include in the group. You can also define a list of
sorting placeholders to use when sorting players in the TAB list, and a header/footer update rate and placeholder update
rate to use for the group.
@ -123,6 +126,34 @@ information.
You can define a header/footer update rate to use for each group, in milliseconds. This will determine how quickly the
headers and footers will cycle through in the TAB list. The default is 1000 milliseconds (1 second).
## Format update rate
<details>
<summary>Example of format update rate</summary>
```yaml
format_update_rate: 1000
```
</details>
You can define a format update rate to use for each group, in milliseconds. This will determine how quickly the
formats will update in the TAB list.
## Nametag update rate (sorting)
<details>
<summary>Example of nametag update rate</summary>
```yaml
nametag_update_rate: 1000
```
</details>
You can define a nametag update rate to use for each group, in milliseconds. This will determine how quickly the
nametags will update in the TAB list. This will also determine how quickly the sorting will update.
## Placeholder update rate
<details>
@ -175,6 +206,8 @@ groups:
- '%role_weight%'
- '%username_lower%'
header_footer_update_rate: 1000
format_update_rate: 1000
nametag_update_rate: 1000
placeholder_update_rate: 1000
- name: survival
headers:
@ -188,6 +221,8 @@ groups:
- '%role_weight%'
- '%username_lower%'
header_footer_update_rate: 1000
format_update_rate: 1000
nametag_update_rate: 1000
placeholder_update_rate: 1000
```

View File

@ -3,7 +3,7 @@ javaVersion=17
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true
plugin_version=1.7.3
plugin_version=1.7.4
plugin_archive=velocitab
plugin_description=A beautiful and versatile TAB list plugin for Velocity proxies

View File

@ -33,24 +33,20 @@ import lombok.Getter;
import lombok.Setter;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.desertwell.util.Version;
import net.william278.toilet.Toilet;
import net.william278.velocitab.api.PluginMessageAPI;
import net.william278.velocitab.api.VelocitabAPI;
import net.william278.velocitab.commands.VelocitabCommand;
import net.william278.velocitab.config.ConfigProvider;
import net.william278.velocitab.config.Formatter;
import net.william278.velocitab.config.Settings;
import net.william278.velocitab.config.TabGroups;
import net.william278.velocitab.config.*;
import net.william278.velocitab.hook.Hook;
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.providers.HookProvider;
import net.william278.velocitab.providers.LoggerProvider;
import net.william278.velocitab.providers.MetricProvider;
import net.william278.velocitab.providers.ScoreboardProvider;
import net.william278.velocitab.placeholder.PlaceholderManager;
import net.william278.velocitab.providers.*;
import net.william278.velocitab.sorting.SortingManager;
import net.william278.velocitab.tab.PlayerTabList;
import net.william278.velocitab.util.DebugSystem;
import net.william278.velocitab.vanish.VanishManager;
import org.bstats.velocity.Metrics;
import org.jetbrains.annotations.NotNull;
@ -62,12 +58,12 @@ import java.util.List;
@Plugin(id = "velocitab")
@Getter
public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProvider, HookProvider, MetricProvider {
public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProvider, HookProvider, MetricProvider, DumpProvider {
@Setter
private Settings settings;
@Setter
private TabGroups tabGroups;
private TabGroupsManager tabGroupsManager;
private final ProxyServer server;
private final Logger logger;
@ -87,6 +83,9 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
private VanishManager vanishManager;
private PacketEventManager packetEventManager;
private PluginMessageAPI pluginMessageAPI;
private PlaceholderManager placeholderManager;
@Setter
private Toilet toilet;
@Inject
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path configDirectory) {
@ -100,13 +99,16 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
checkCompatibility();
loadConfigs();
loadHooks();
preparePlaceholderManager();
prepareVanishManager();
prepareChannelManager();
prepareScoreboard();
registerCommands();
registerMetrics();
checkForUpdates();
prepareAPI();
prepareChannelManager();
initializeToilet();
DebugSystem.initializeTask(this);
logger.info("Successfully enabled Velocitab");
}
@ -114,7 +116,6 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
public void onProxyShutdown(@NotNull ProxyShutdownEvent event) {
disableScoreboardManager();
getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent);
getMiniPlaceholdersHook().ifPresent(MiniPlaceholdersHook::unregisterExpansion);
unregisterAPI();
logger.info("Successfully disabled Velocitab");
}
@ -137,6 +138,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

@ -29,6 +29,7 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ -110,7 +111,7 @@ public class VelocitabAPI {
public void setCustomPlayerName(@NotNull Player player, @Nullable String name) {
getUser(player).ifPresent(tabPlayer -> {
tabPlayer.setCustomName(name);
plugin.getTabList().updatePlayerDisplayName(tabPlayer);
plugin.getTabList().updateDisplayName(tabPlayer);
});
}
@ -199,7 +200,7 @@ public class VelocitabAPI {
*/
@NotNull
public List<Group> getServerGroups() {
return plugin.getTabGroups().getGroups();
return new ArrayList<>(plugin.getTabGroupsManager().getGroups());
}
/**
@ -211,11 +212,11 @@ public class VelocitabAPI {
*/
@NotNull
public Optional<Group> getGroup(@NotNull String name) {
return plugin.getTabGroups().getGroup(name);
return plugin.getTabGroupsManager().getGroup(name);
}
public Optional<Group> getGroupFromServer(@NotNull String server) {
return plugin.getTabGroups().getGroupFromServer(server, plugin);
return plugin.getTabGroupsManager().getGroupFromServer(server, plugin);
}
/**

View File

@ -27,19 +27,35 @@ import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.william278.desertwell.about.AboutMenu;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.UUID;
public final class VelocitabCommand {
private static final TextColor MAIN_COLOR = TextColor.color(0x00FB9A);
private static final TextColor ERROR_COLOR = TextColor.color(0xFF7E5E);
private static final String systemDumpConfirm = """
<color:#00fb9a><bold>Velocitab</bold></color> <color:#00fb9a>| Prepare a system dump? This will include:</color>
<gray> Your latest server logs and Velocitab config files</gray>
<gray> Current plugin system status information</gray>
<gray> Information about your Java & Minecraft server environment</gray>
<gray> A list of other currently installed plugins</gray>
<click:run_command:/velocitab dump confirm><hover:show_text:'<gray>Click to prepare dump'><color:#00fb9a>To confirm click here or use: <italic>/velocitab dump confirm</italic></color></click>
""";
private static final String systemDumpStarted = "<color:#00fb9a><bold>Velocitab</bold></color> <color:#00fb9a>| Preparing system status dump, please wait…</color>";
private static final String systemDumpReady = "<click:open_url:%url%><color:#00fb9a><bold>Velocitab</bold></color> <color:#00fb9a>| System status dump prepared! Click here to view</color></click>";
private static final String systemDumpReadyConsole = "<color:#00fb9a><bold>Velocitab</bold></color> <color:#00fb9a>| System status dump prepared! Url: %url%</color>";
private final AboutMenu aboutMenu;
private final Velocitab plugin;
@ -93,14 +109,13 @@ public final class VelocitabCommand {
}
tabPlayer.get().setCustomName(name);
plugin.getTabList().updatePlayerDisplayName(tabPlayer.get());
plugin.getTabList().updateDisplayName(tabPlayer.get());
ctx.getSource().sendMessage(Component
.text("Your TAB name has been updated!", MAIN_COLOR));
return Command.SINGLE_SUCCESS;
})
)
.requires(src -> src instanceof Player)
.executes(ctx -> {
final Player player = (Player) ctx.getSource();
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
@ -119,7 +134,7 @@ public final class VelocitabCommand {
}
tabPlayer.get().setCustomName(null);
plugin.getTabList().updatePlayerDisplayName(tabPlayer.get());
plugin.getTabList().updateDisplayName(tabPlayer.get());
player.sendMessage(Component.text("Your name has been reset!", MAIN_COLOR));
return Command.SINGLE_SUCCESS;
})
@ -134,6 +149,61 @@ public final class VelocitabCommand {
return Command.SINGLE_SUCCESS;
})
)
.then(LiteralArgumentBuilder.<CommandSource>literal("debug")
.requires(src -> hasPermission(src, "debug"))
.then(LiteralArgumentBuilder.<CommandSource>literal("tablist")
.then(RequiredArgumentBuilder.<CommandSource, String>argument("player", StringArgumentType.string())
.suggests((ctx, builder1) -> {
final String input = ctx.getInput();
if (input.isEmpty()) {
return builder1.buildFuture();
}
plugin.getServer().getAllPlayers().stream()
.map(Player::getUsername)
.filter(s -> s.toLowerCase().startsWith(input.toLowerCase()))
.forEach(builder1::suggest);
return builder1.buildFuture();
})
.executes(ctx -> {
final String input = ctx.getArgument("player", String.class);
final Optional<Player> player = plugin.getServer().getPlayer(input);
if (player.isEmpty()) {
ctx.getSource().sendMessage(Component.text("Player not found!", ERROR_COLOR));
return Command.SINGLE_SUCCESS;
}
player.get().getTabList().getEntries().forEach(entry -> {
final String name = entry.getProfile().getName();
final UUID uuid = entry.getProfile().getId();
final String unformattedDisplayName = entry.getDisplayNameComponent().map(c -> PlainTextComponentSerializer.plainText().serialize(c)).orElse("empty");
ctx.getSource().sendMessage(Component.text("Name: %s, UUID: %s, Unformatted display name: %s".formatted(name, uuid, unformattedDisplayName)));
});
return Command.SINGLE_SUCCESS;
})
)
))
.then(LiteralArgumentBuilder.<CommandSource>literal("dump")
.requires(src -> hasPermission(src, "dump"))
.executes(ctx -> {
ctx.getSource().sendRichMessage(systemDumpConfirm.trim());
return Command.SINGLE_SUCCESS;
})
.then(LiteralArgumentBuilder.<CommandSource>literal("confirm")
.executes(ctx -> {
ctx.getSource().sendRichMessage(systemDumpStarted);
plugin.getServer().getScheduler().buildTask(plugin, () -> {
final String dumpUrl = plugin.createDump(ctx.getSource());
final Component component = MiniMessage.miniMessage().deserialize((ctx.getSource() instanceof Player
? systemDumpReady
: systemDumpReadyConsole)
.replace("%url%", dumpUrl));
ctx.getSource().sendMessage(component);
}).schedule();
return Command.SINGLE_SUCCESS;
})
))
.then(LiteralArgumentBuilder.<CommandSource>literal("update")
.requires(src -> hasPermission(src, "update"))
.executes(ctx -> {

View File

@ -82,7 +82,7 @@ public interface ConfigProvider {
Settings.class,
YAML_CONFIGURATION_PROPERTIES.header(Settings.CONFIG_HEADER).build()
));
getSettings().validateConfig(getPlugin());
getSettings().validateConfig(getPlugin(), "config.yml");
}
/**
@ -92,15 +92,15 @@ public interface ConfigProvider {
* @since 1.0
*/
@NotNull
TabGroups getTabGroups();
TabGroupsManager getTabGroupsManager();
/**
* Set the tab groups
*
* @param tabGroups The tab groups to set
* @since 1.0
* @param tabGroupsManager The tab groups to set
* @since 1.7.4
*/
void setTabGroups(@NotNull TabGroups tabGroups);
void setTabGroupsManager(@NotNull TabGroupsManager tabGroupsManager);
/**
* Load the tab groups from the config file
@ -108,12 +108,8 @@ public interface ConfigProvider {
* @since 1.0
*/
default void loadTabGroups() {
setTabGroups(YamlConfigurations.update(
getConfigDirectory().resolve("tab_groups.yml"),
TabGroups.class,
YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
));
getTabGroups().validateConfig(getPlugin());
setTabGroupsManager(new TabGroupsManager(getPlugin()));
getTabGroupsManager().loadGroups();
}
/**
@ -170,23 +166,6 @@ public interface ConfigProvider {
@NotNull
Version getVelocityVersion();
/**
* Saves the tab groups to the "tab_groups.yml" config file.
* Uses the YamlConfigurations#save method to write the tab groups object to the specified config file path.
* This method assumes that the getConfigDirectory method returns a valid directory path.
*
* @throws IllegalStateException if the getConfigDirectory method returns null
* @since 1.0
*/
default void saveTabGroups() {
YamlConfigurations.save(
getConfigDirectory().resolve("tab_groups.yml"),
TabGroups.class,
getTabGroups(),
YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
);
}
/**
* Get the plugin config directory
*

View File

@ -28,6 +28,6 @@ public interface ConfigValidator {
* Validates the configuration settings.
* @throws IllegalStateException if the configuration is invalid
*/
void validateConfig(@NotNull Velocitab plugin) throws IllegalStateException;
void validateConfig(@NotNull Velocitab plugin, @NotNull String name) throws IllegalStateException;
}

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),
player.getPlayer(), viewer == null ? null : viewer.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(MiniMessageUtil.getINSTANCE().checkForErrors(text))),
(text) -> MiniMessage.miniMessage().escapeTags(text),
"MiniMessage",
(text) -> MiniMessage.miniMessage().deserialize(text),

View File

@ -19,13 +19,15 @@
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;
import net.william278.velocitab.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.event.Level;
@ -50,19 +52,21 @@ public record Group(
Map<String, List<PlaceholderReplacement>> placeholderReplacements,
boolean collisions,
int headerFooterUpdateRate,
int formatUpdateRate,
int nametagUpdateRate,
int placeholderUpdateRate,
boolean onlyListPlayersInSameServer
) {
@NotNull
public String getHeader(int index) {
return headers.isEmpty() ? "" : StringEscapeUtils.unescapeJava(headers
return headers.isEmpty() ? "" : StringUtil.unescapeJava(headers
.get(Math.max(0, Math.min(index, headers.size() - 1))));
}
@NotNull
public String getFooter(int index) {
return footers.isEmpty() ? "" : StringEscapeUtils.unescapeJava(footers
return footers.isEmpty() ? "" : StringUtil.unescapeJava(footers
.get(Math.max(0, Math.min(index, footers.size() - 1))));
}
@ -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);
}
@ -138,35 +157,86 @@ public record Group(
@NotNull
public Set<TabPlayer> getTabPlayers(@NotNull Velocitab plugin) {
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
return Sets.newHashSet(plugin.getTabList().getPlayers().values());
return plugin.getTabList().getPlayers().values().stream().filter(TabPlayer::isLoaded).collect(Collectors.toSet());
}
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 plugin.getTabList().getPlayers().values().stream().filter(TabPlayer::isLoaded).collect(Collectors.toList());
}
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());
return plugin.getTabList().getPlayers().values().stream().filter(TabPlayer::isLoaded).collect(Collectors.toSet());
}
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<TabPlayer> getTabPlayersAsList(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
return plugin.getTabList().getPlayers().values().stream().filter(TabPlayer::isLoaded).collect(Collectors.toList());
}
if (onlyListPlayersInSameServer) {
return plugin.getTabList().getPlayers()
.values()
.stream()
.filter(player -> player.isLoaded() && player.getGroup().equals(this) && player.getServerName().equals(tabPlayer.getServerName()))
.collect(Collectors.toList());
}
return getTabPlayersAsList(plugin);
}
@NotNull
public List<String> getTextsWithPlaceholders(@NotNull Velocitab plugin) {
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);
if (plugin.getLuckPermsHook().isEmpty()) {
texts.add("%luckperms_meta_weight%");
}
return texts;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Group group)) {
return false;
}
return name.equals(group.name);
}
}

View File

@ -1,331 +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()
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_weight%"))),
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,14 +19,15 @@
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.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,
@ -40,26 +41,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,
@NotNull List<ServerUrl> urls) {
final List<CompletableFuture<ServerLink>> futures = new ArrayList<>();
public static List<ServerLink> resolve(@NotNull Velocitab plugin, @NotNull TabPlayer player,
@NotNull List<ServerUrl> urls) {
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

@ -28,7 +28,6 @@ import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
@SuppressWarnings("FieldMayBeFinal")
@ -40,7 +39,8 @@ public class Settings implements ConfigValidator {
public static final String CONFIG_HEADER = """
Velocitab Config
Developed by William278
Developed by
William278 & AlexDev03
Information: https://william278.net/project/velocitab
Documentation: https://william278.net/docs/velocitab""";
@ -96,6 +96,9 @@ public class Settings implements ConfigValidator {
@Comment("Whether to force sending tab list packets to all players, even if a packet for that action has already been sent. This could fix issues with some mods.")
private boolean forceSendingTabListPackets = false;
@Comment("Whether to enable relational placeholders. With an high amount of players, this could cause lag.")
private boolean enableRelationalPlaceholders = false;
@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').",
@ -116,7 +119,7 @@ public class Settings implements ConfigValidator {
}
@Override
public void validateConfig(@NotNull Velocitab plugin) {
public void validateConfig(@NotNull Velocitab plugin, @NotNull String name) {
if (papiCacheTime < 0) {
throw new IllegalStateException("PAPI cache time must be greater than or equal to 0");
}

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
);
@ -84,42 +87,15 @@ public class TabGroups implements ConfigValidator {
.findFirst();
}
public Optional<Group> getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
final List<Group> groups = new ArrayList<>(this.groups);
final Optional<Group> defaultGroup = getGroup("default");
if (defaultGroup.isEmpty()) {
throw new IllegalStateException("No default group found");
}
// Ensure the default group is always checked last
groups.remove(defaultGroup.get());
groups.add(defaultGroup.get());
for (Group group : groups) {
if (group.registeredServers(plugin, false)
.stream()
.anyMatch(s -> s.getServerInfo().getName().equalsIgnoreCase(server))) {
return Optional.of(group);
}
}
if (!plugin.getSettings().isFallbackEnabled()) {
return Optional.empty();
}
return defaultGroup;
}
public int getPosition(@NotNull Group group) {
return groups.indexOf(group) + 1;
}
@Override
public void validateConfig(@NotNull Velocitab plugin) {
if (groups.isEmpty()) {
throw new IllegalStateException("No tab groups defined in config");
}
if (groups.stream().noneMatch(group -> group.name().equals("default"))) {
throw new IllegalStateException("No default tab group defined in config");
public void validateConfig(@NotNull Velocitab plugin, @NotNull String name) {
if(name.equals("tab_groups")) {
if (groups.isEmpty()) {
throw new IllegalStateException("No tab groups defined in config " + name);
}
if (groups.stream().noneMatch(group -> group.name().equals("default"))) {
throw new IllegalStateException("No default tab group defined in config " + name);
}
}
final Multimap<Group, String> missingKeys = getMissingKeys();
@ -127,7 +103,7 @@ public class TabGroups implements ConfigValidator {
return;
}
fixMissingKeys(plugin, missingKeys);
fixMissingKeys(plugin, missingKeys, name);
}
@NotNull
@ -151,12 +127,28 @@ public class TabGroups implements ConfigValidator {
if (group.placeholderReplacements() == null) {
missingKeys.put(group, "placeholderReplacements");
}
if (group.headerFooterUpdateRate() == 0) {
missingKeys.put(group, "headerFooterUpdateRate");
}
if (group.formatUpdateRate() == 0) {
missingKeys.put(group, "formatUpdateRate");
}
if (group.nametagUpdateRate() == 0) {
missingKeys.put(group, "nametagUpdateRate");
}
if (group.placeholderUpdateRate() == 0) {
missingKeys.put(group, "placeholderUpdateRate");
}
}
return missingKeys;
}
private void fixMissingKeys(@NotNull Velocitab plugin, @NotNull Multimap<Group, String> missingKeys) {
private void fixMissingKeys(@NotNull Velocitab plugin, @NotNull Multimap<Group, String> missingKeys, @NotNull String name) {
missingKeys.forEach((group, keys) -> {
plugin.log("Missing required key(s) " + keys + " for group " + group.name());
plugin.log("Using default values for group " + group.name());
@ -173,14 +165,16 @@ public class TabGroups implements ConfigValidator {
group.sortingPlaceholders() == null ? DEFAULT_GROUP.sortingPlaceholders() : group.sortingPlaceholders(),
group.placeholderReplacements() == null ? DEFAULT_GROUP.placeholderReplacements() : group.placeholderReplacements(),
group.collisions(),
group.headerFooterUpdateRate(),
group.placeholderUpdateRate(),
group.headerFooterUpdateRate() == 0 ? DEFAULT_GROUP.headerFooterUpdateRate() : group.headerFooterUpdateRate(),
group.formatUpdateRate() == 0 ? DEFAULT_GROUP.formatUpdateRate() : group.formatUpdateRate(),
group.nametagUpdateRate() == 0 ? DEFAULT_GROUP.nametagUpdateRate() : group.nametagUpdateRate(),
group.placeholderUpdateRate() == 0 ? DEFAULT_GROUP.placeholderUpdateRate() : group.placeholderUpdateRate(),
group.onlyListPlayersInSameServer()
);
groups.add(group);
});
plugin.saveTabGroups();
plugin.getTabGroupsManager().saveGroup(this);
}
}

View File

@ -0,0 +1,197 @@
/*
* 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.google.common.collect.Sets;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import de.exlll.configlib.YamlConfigurationProperties;
import de.exlll.configlib.YamlConfigurations;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
public class TabGroupsManager {
private final Velocitab plugin;
private final Map<String, Group> groups;
private final Map<TabGroups, String> groupsFiles;
private List<Group> groupsList;
public TabGroupsManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.groups = Maps.newConcurrentMap();
this.groupsFiles = Maps.newConcurrentMap();
this.groupsList = Lists.newArrayList();
}
public void loadGroups() {
groups.clear();
groupsFiles.clear();
final Path configDirectory = plugin.getConfigDirectory();
final File defaultFile = configDirectory.resolve("tab_groups.yml").toFile();
final YamlConfigurationProperties properties = ConfigProvider.YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build();
final TabGroups defaultTagGroupsFile = YamlConfigurations.update(
configDirectory.resolve("tab_groups.yml"),
TabGroups.class,
properties
);
final String defaultName = defaultFile.getAbsolutePath().replace(".yml", "");
if (!validateGroups(defaultTagGroupsFile, defaultName)) {
throw new IllegalStateException("Failed to load default tab groups file");
}
final File folder = plugin.getConfigDirectory().resolve("tab_groups").toFile();
if (folder.exists()) {
final File[] filesArray = folder.listFiles();
final List<File> files = filesArray == null ? List.of() : Arrays.asList(filesArray);
for (File file : files) {
if (!file.getName().endsWith(".yml")) {
continue;
}
final TabGroups preCheck = YamlConfigurations.load(file.toPath(), TabGroups.class, properties);
preCheck.groups.removeIf(g -> g.name().equals("default"));
YamlConfigurations.save(file.toPath(), TabGroups.class, preCheck, properties);
final TabGroups group = YamlConfigurations.update(
file.toPath(),
TabGroups.class,
properties
);
final String name = folder.getAbsoluteFile() + "/" + file.getName().replace(".yml", "");
if (!validateGroups(group, name)) {
throw new IllegalStateException("Failed to load tab groups file " + file.getName());
}
}
}
this.groupsList = Lists.newArrayList(getGroups());
}
@NotNull
public List<File> getGroupsFiles() {
return groupsFiles.values().stream().map(f -> new File(f+".yml")).collect(Collectors.toList());
}
public boolean isDefaultFile(@NotNull File file) {
return plugin.getConfigDirectory().resolve("tab_groups.yml").toFile().getAbsolutePath().equals(file.getAbsolutePath());
}
private boolean validateGroups(@NotNull TabGroups group, @NotNull String name) {
this.groupsFiles.put(group, name);
group.validateConfig(plugin, name);
final List<Group> eligibleGroups = Lists.newArrayList();
final Set<RegisteredServer> registeredServers = Sets.newHashSet();
outer:
for (Group group1 : group.groups) {
final Set<RegisteredServer> current = group1.registeredServers(plugin, false);
if (groups.containsKey(group1.name())) {
plugin.getLogger().warn("Group {} is already defined in {} tab groups file. Skipping.", group1.name(), name);
continue;
}
for (RegisteredServer registeredServer : current) {
if (registeredServers.contains(registeredServer)) {
plugin.getLogger().warn("Server {} is already registered for group {} in {}, the same tabgroups file. Skipping.", registeredServer.getServerInfo().getName(), group1.name(), name);
continue outer;
}
}
registeredServers.addAll(current);
eligibleGroups.add(group1);
}
outer:
for (Group group1 : groups.values()) {
final Set<RegisteredServer> current = group1.registeredServers(plugin, false);
for (Group loadingGroup : eligibleGroups) {
final Set<RegisteredServer> loadingGroupServers = loadingGroup.registeredServers(plugin, false);
for (RegisteredServer registeredServer : loadingGroupServers) {
if (current.contains(registeredServer)) {
plugin.getLogger().warn("Server {} in {} tab groups file is already registered for group {}. Skipping.", registeredServer.getServerInfo().getName(), name, group1.name());
eligibleGroups.remove(loadingGroup);
continue outer;
}
}
}
}
for (Group group1 : eligibleGroups) {
groups.put(group1.name(), group1);
}
return true;
}
public void saveGroup(@NotNull TabGroups group) {
final String name = groupsFiles.get(group);
YamlConfigurations.save(
new File("plugins/Velocitab").toPath().resolve(name + ".yml"),
TabGroups.class,
group,
ConfigProvider.YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
);
}
public Optional<Group> getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
final List<Group> groups = new ArrayList<>(this.groups.values());
final Optional<Group> defaultGroup = getGroup("default");
if (defaultGroup.isEmpty()) {
throw new IllegalStateException("No default tab group defined");
}
// Ensure the default group is always checked last
groups.remove(defaultGroup.get());
groups.add(defaultGroup.get());
for (Group group : groups) {
if (group.registeredServers(plugin, false)
.stream()
.anyMatch(s -> s.getServerInfo().getName().equalsIgnoreCase(server))) {
return Optional.of(group);
}
}
return Optional.empty();
}
public Optional<Group> getGroup(@NotNull String name) {
return Optional.ofNullable(groups.get(name));
}
public int getGroupPosition(@NotNull Group group) {
return groupsList.indexOf(group) + 1;
}
@NotNull
public Collection<Group> getGroups() {
return groups.values();
}
}

View File

@ -19,6 +19,7 @@
package net.william278.velocitab.hook;
import lombok.Getter;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
@ -66,9 +67,12 @@ public abstract class Hook {
);
protected final Velocitab plugin;
@Getter
protected final String name;
public Hook(@NotNull Velocitab plugin) {
public Hook(@NotNull Velocitab plugin, @NotNull String name) {
this.plugin = plugin;
this.name = name;
}
private static boolean isPluginAvailable(@NotNull Velocitab plugin, @NotNull String id) {

View File

@ -48,7 +48,7 @@ public class LuckPermsHook extends Hook {
private boolean enabled;
public LuckPermsHook(@NotNull Velocitab plugin) throws IllegalStateException {
super(plugin);
super(plugin, "LuckPerms");
this.api = LuckPermsProvider.get();
this.lastUpdate = Maps.newConcurrentMap();
this.event = api.getEventBus().subscribe(
@ -116,11 +116,11 @@ public class LuckPermsHook extends Hook {
}
tabPlayer.setRole(newRole);
tabList.updatePlayerDisplayName(tabPlayer);
tabList.updateDisplayName(tabPlayer);
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

@ -19,34 +19,48 @@
package net.william278.velocitab.hook;
import com.velocitypowered.api.proxy.Player;
import io.github.miniplaceholders.api.MiniPlaceholders;
import net.kyori.adventure.audience.Audience;
import net.jodah.expiringmap.ExpiringMap;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class MiniPlaceholdersHook extends Hook {
private final VelocitabMiniExpansion expansion;
private final Map<UUID, TagResolver> cache;
public MiniPlaceholdersHook(@NotNull Velocitab plugin) {
super(plugin);
this.expansion = new VelocitabMiniExpansion(plugin);
expansion.registerExpansion();
super(plugin, "MiniPlaceholders");
this.cache = ExpiringMap.builder()
.expiration(5, TimeUnit.MINUTES)
.build();
}
@NotNull
public Component format(@NotNull String text, @NotNull Audience player, @Nullable Audience viewer) {
private TagResolver getResolver(@NotNull Player player, @Nullable Player viewer) {
if (viewer == null) {
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getAudienceGlobalPlaceholders(player));
return cache.computeIfAbsent(player.getUniqueId(), u -> MiniPlaceholders.getAudienceGlobalPlaceholders(player));
}
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getRelationalGlobalPlaceholders(player, viewer));
final UUID merged = new UUID(player.getUniqueId().getMostSignificantBits(), viewer.getUniqueId().getMostSignificantBits());
return cache.computeIfAbsent(merged, u -> MiniPlaceholders.getRelationalGlobalPlaceholders(player, viewer));
}
public void unregisterExpansion() {
expansion.unregisterExpansion();
@NotNull
public Component format(@NotNull String text, @NotNull Player player, @Nullable Player viewer) {
if (viewer == null) {
return MiniMessage.miniMessage().deserialize(text, getResolver(player, null));
}
return MiniMessage.miniMessage().deserialize(text, getResolver(player, viewer));
}
}

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 {
@ -35,7 +31,7 @@ public class PAPIProxyBridgeHook extends Hook {
private final PlaceholderAPI api;
public PAPIProxyBridgeHook(@NotNull Velocitab plugin) {
super(plugin);
super(plugin, "PAPIProxyBridge");
this.api = PlaceholderAPI.createInstance();
this.api.setCacheExpiry(Math.max(0, plugin.getSettings().getPapiCacheTime()));
this.api.setRequestTimeout(1500);
@ -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

@ -1,143 +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.hook;
import com.velocitypowered.api.proxy.Player;
import io.github.miniplaceholders.api.Expansion;
import io.github.miniplaceholders.api.MiniPlaceholders;
import io.github.miniplaceholders.api.utils.TagsUtils;
import net.kyori.adventure.text.Component;
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.hook.miniconditions.MiniConditionManager;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public class VelocitabMiniExpansion {
private final Velocitab plugin;
private final MiniConditionManager miniConditionManager;
private Expansion expansion;
public VelocitabMiniExpansion(Velocitab plugin) {
this.plugin = plugin;
this.miniConditionManager = new MiniConditionManager(plugin);
}
public void registerExpansion() {
final Expansion.Builder builder = Expansion.builder("velocitab");
builder.relationalPlaceholder("condition", ((a1, a2, queue, ctx) -> {
if (!(a2 instanceof Player target)) {
return TagsUtils.EMPTY_TAG;
}
if (!(a1 instanceof Player audience)) {
return TagsUtils.EMPTY_TAG;
}
return Tag.selfClosingInserting(miniConditionManager.checkConditions(target, audience, queue));
}));
builder.relationalPlaceholder("who-is-seeing", ((a1, a2, queue, ctx) -> {
if (!(a2 instanceof Player target)) {
return TagsUtils.EMPTY_TAG;
}
if (!(a1 instanceof Player)) {
return TagsUtils.EMPTY_TAG;
}
return Tag.selfClosingInserting(Component.text(target.getUsername()));
}));
builder.relationalPlaceholder("perm", ((a1, a2, queue, ctx) -> {
if (!(a2 instanceof Player target)) {
return TagsUtils.EMPTY_TAG;
}
if (!(a1 instanceof Player audience)) {
return TagsUtils.EMPTY_TAG;
}
final Optional<TabPlayer> targetOptional = plugin.getTabList().getTabPlayer(audience);
if (targetOptional.isEmpty()) {
return TagsUtils.EMPTY_TAG;
}
final TabPlayer targetPlayer = targetOptional.get();
if (!queue.hasNext()) {
return TagsUtils.EMPTY_TAG;
}
final String permission = queue.pop().value();
if (!queue.hasNext()) {
return TagsUtils.EMPTY_TAG;
}
if (!target.hasPermission(permission)) {
return TagsUtils.EMPTY_TAG;
}
final String value = fixValue(popAll(queue));
final String replaced = Placeholder.replaceInternal(value, plugin, targetPlayer).first();
return Tag.selfClosingInserting(MiniMessage.miniMessage().deserialize(replaced, MiniPlaceholders.getAudienceGlobalPlaceholders(audience)));
}));
builder.relationalPlaceholder("vanish", ((a1, otherAudience, queue, ctx) -> {
if (!(otherAudience instanceof Player target)) {
return TagsUtils.EMPTY_TAG;
}
if (!(a1 instanceof Player audience)) {
return TagsUtils.EMPTY_TAG;
}
return Tag.selfClosingInserting(Component.text(plugin.getVanishManager().getIntegration().canSee(audience.getUsername(), target.getUsername())));
}));
plugin.getLogger().info("Registered Velocitab MiniExpansion");
expansion = builder.build();
expansion.register();
}
public void unregisterExpansion() {
expansion.unregister();
}
@NotNull
private String popAll(@NotNull ArgumentQueue queue) {
final StringBuilder builder = new StringBuilder();
int i = 0;
while (queue.hasNext()) {
if (i > 0) {
builder.append(":");
}
builder.append(queue.pop().value());
i++;
}
return builder.toString();
}
@NotNull
private String fixValue(@NotNull String value) {
return value.replace("*LESS2*", "<").replace("*GREATER2*", ">");
}
}

View File

@ -1,216 +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.hook.miniconditions;
import com.google.common.collect.Lists;
import com.velocitypowered.api.proxy.Player;
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.player.TabPlayer;
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.MapContext;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MiniConditionManager {
public final static Map<String, String> REPLACE = Map.of(
"\"", "-q-",
"'", "-a-"
);
public final static Map<String, String> REPLACE_2 = Map.of(
"*LESS3*", "<",
"*GREATER3*", ">",
"*LESS2*", "<",
"*GREATER2*", ">"
);
private final static Map<String, String> REPLACE_3 = Map.of(
"?dp?", ":"
);
private final Velocitab plugin;
private final JexlEngine jexlEngine;
private final JexlContext jexlContext;
private final Pattern targetPlaceholderPattern;
private final Pattern miniEscapeEndTags;
private final Map<String, Object> cachedExpressions;
public MiniConditionManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.jexlEngine = createJexlEngine();
this.jexlContext = createJexlContext();
this.targetPlaceholderPattern = Pattern.compile("%target_(\\w+)?%");
this.miniEscapeEndTags = Pattern.compile("</(\\w+)>");
this.cachedExpressions = ExpiringMap.builder()
.expiration(5, TimeUnit.MINUTES)
.build();
}
@NotNull
private JexlEngine createJexlEngine() {
return new JexlBuilder().create();
}
@NotNull
private JexlContext createJexlContext() {
final JexlContext jexlContext = new MapContext();
jexlContext.set("startsWith", new StartsWith());
jexlContext.set("endsWith", new EndsWith());
return jexlContext;
}
@NotNull
public Component checkConditions(@NotNull Player target, @NotNull Player audience, @NotNull ArgumentQueue queue) {
final List<String> parameters = collectParameters(queue);
if (parameters.isEmpty()) {
plugin.getLogger().warn("Empty condition");
return Component.empty();
}
String condition = decodeCondition(parameters.get(0));
if (parameters.size() < 3) {
plugin.getLogger().warn("Invalid condition: Missing true/false values for condition: {}", condition);
return Component.empty();
}
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(target);
if (tabPlayer.isEmpty()) {
return Component.empty();
}
condition = Placeholder.replaceInternal(condition, plugin, tabPlayer.get()).first();
final String falseValue = processFalseValue(parameters.get(2));
final String expression = buildExpression(condition);
return evaluateAndFormatCondition(expression, target, audience, parameters.get(1), falseValue);
}
@NotNull
private List<String> collectParameters(@NotNull ArgumentQueue queue) {
final List<String> parameters = Lists.newArrayList();
while (queue.hasNext()) {
String param = queue.pop().value();
for (Map.Entry<String, String> entry : REPLACE_2.entrySet()) {
param = param.replace(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : REPLACE_3.entrySet()) {
param = param.replace(entry.getKey(), entry.getValue());
}
parameters.add(param);
}
return parameters;
}
@NotNull
private String decodeCondition(@NotNull String condition) {
for (Map.Entry<String, String> entry : REPLACE.entrySet()) {
condition = condition.replace(entry.getValue(), entry.getKey());
condition = condition.replace(entry.getKey() + entry.getKey(), entry.getKey());
}
return condition;
}
@NotNull
private String processFalseValue(@NotNull String falseValue) {
final Matcher matcher = miniEscapeEndTags.matcher(falseValue);
if (matcher.find()) {
final String tag = matcher.group(1);
if (falseValue.startsWith("</" + tag + ">")) {
falseValue = falseValue.substring(tag.length() + 3);
}
}
return falseValue;
}
@NotNull
private String buildExpression(@NotNull String condition) {
return condition.replace("and", "&&").replace("or", "||")
.replace("AND", "&&").replace("OR", "||");
}
@NotNull
private Component evaluateAndFormatCondition(@NotNull String expression, @NotNull Player target, @NotNull Player audience, @NotNull String trueValue, @NotNull String falseValue) {
final String targetString = parseTargetPlaceholders(expression, target);
try {
final Object result = evaluateExpression(targetString);
if (result instanceof Boolean) {
final boolean boolResult = (Boolean) result;
final String value = boolResult ? trueValue : falseValue;
return plugin.getMiniPlaceholdersHook().orElseThrow().format(value, target, audience);
}
} catch (Exception e) {
plugin.getLogger().warn("Failed to evaluate condition: {} error: {}", expression, e.getMessage());
}
return Component.empty();
}
@NotNull
private Object evaluateExpression(@NotNull String expression) {
return cachedExpressions.computeIfAbsent(expression, key -> jexlEngine.createExpression(key).evaluate(jexlContext));
}
@NotNull
private String parseTargetPlaceholders(@NotNull String input, @NotNull Player target) {
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(target);
if (tabPlayer.isEmpty()) {
return input;
}
return targetPlaceholderPattern.matcher(input).replaceAll(match -> {
final String placeholder = match.group(1);
if (placeholder == null) {
return "";
}
final String text = "%" + placeholder + "%";
final Optional<String> placeholderValue = tabPlayer.get().getCachedPlaceholderValue(text);
return placeholderValue.orElse(text);
});
}
@SuppressWarnings("unused")
private static class StartsWith {
public boolean startsWith(String str, String prefix) {
return str != null && str.startsWith(prefix);
}
}
@SuppressWarnings("unused")
private static class EndsWith {
public boolean endsWith(String str, String suffix) {
return str != null && str.endsWith(suffix);
}
}
}

View File

@ -32,6 +32,8 @@ import io.netty.channel.DefaultChannelPipeline;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.TimeUnit;
public class PacketEventManager {
private static final String KEY = "velocitab";
@ -45,7 +47,10 @@ public class PacketEventManager {
}
private void loadPlayers() {
plugin.getServer().getAllPlayers().forEach(this::injectPlayer);
plugin.getServer().getScheduler()
.buildTask(plugin, () -> plugin.getServer().getAllPlayers().forEach(this::injectPlayer))
.delay(100, TimeUnit.MILLISECONDS)
.schedule();
}
private void loadListeners() {
@ -72,6 +77,10 @@ public class PacketEventManager {
.addBefore(Connections.HANDLER, KEY, handler);
}
public void removeAllPlayers() {
plugin.getServer().getAllPlayers().forEach(this::removePlayer);
}
public void removePlayer(@NotNull Player player) {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
final Channel channel = connectedPlayer.getConnection().getChannel();

View File

@ -75,7 +75,7 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
// This is to prevent conflicts with Velocitab teams.
plugin.getLogger().warn("Cancelled team \"{}\" packet from backend for player {}. " +
"We suggest disabling \"send_scoreboard_packets\" in Velocitab's config.yml file, " +
"but note this will disable TAB sorting",
"but note this will disable TAB sorting. If you want to use sorting you have to disable team handling on your backend servers (plugin or vanilla scoreboard teams)",
updateTeamsPacket.teamName(), player.getUsername());
return;
}

View File

@ -37,11 +37,11 @@ import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.sorting.SortedSet;
import net.william278.velocitab.tab.Nametag;
import net.william278.velocitab.util.DebugSystem;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import static com.velocitypowered.api.network.ProtocolVersion.*;
@ -100,7 +100,7 @@ public class ScoreboardManager {
@NotNull
public TeamsPacketAdapter getPacketAdapter(@NotNull ProtocolVersion version) {
return Optional.ofNullable(versions.get(version))
.orElseThrow(() -> new IllegalArgumentException("No adapter found for protocol version " + version));
.orElseThrow(() -> new IllegalArgumentException("No adapter found for protocol version " + version + ". Are you sure you're using the latest version of Velocitab and latest build of Velocity?"));
}
public void close() {
@ -129,7 +129,7 @@ public class ScoreboardManager {
private void removeSortedTeam(@NotNull String teamName) {
final boolean result = sortedTeams.removeTeam(teamName);
if (!result) {
plugin.log(Level.ERROR, "Failed to remove team " + teamName + " from sortedTeams");
DebugSystem.log(DebugSystem.DebugLevel.ERROR, "Failed to remove team " + teamName + " from sortedTeams");
}
}
@ -173,46 +173,39 @@ 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);
plugin.getLogger().info("Player {} is not active, removing from tab list", player.getUsername());
return;
}
final String name = player.getUsername();
final CompletableFuture<Void> future = new CompletableFuture<>();
tabPlayer.getNametag(plugin).thenAccept(newTag -> {
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
if (createdTeams.containsKey(player.getUniqueId())) {
dispatchGroupPacket(
UpdateTeamsPacket.removeTeam(plugin, createdTeams.get(player.getUniqueId())),
tabPlayer
);
}
final String oldRole = createdTeams.remove(player.getUniqueId());
if (oldRole != null) {
removeSortedTeam(oldRole);
}
createdTeams.put(player.getUniqueId(), role);
final boolean a = sortedTeams.addTeam(role);
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);
} else {
updatePlaceholders(tabPlayer);
final Nametag nametag = tabPlayer.getNametag(plugin);
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
if (createdTeams.containsKey(player.getUniqueId())) {
dispatchGroupPacket(
UpdateTeamsPacket.removeTeam(plugin, createdTeams.get(player.getUniqueId())),
tabPlayer
);
}
future.complete(null);
}).exceptionally(e -> {
plugin.log(Level.ERROR, "Failed to update role for " + player.getUsername(), e);
return null;
});
return future;
final String oldRole = createdTeams.remove(player.getUniqueId());
if (oldRole != null) {
removeSortedTeam(oldRole);
}
createdTeams.put(player.getUniqueId(), role);
final boolean a = sortedTeams.addTeam(role);
if (!a) {
plugin.log(Level.ERROR, "Failed to add team " + role + " to sortedTeams");
}
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);
}
}
public void updatePlaceholders(@NotNull TabPlayer tabPlayer) {

View File

@ -0,0 +1,199 @@
/*
* 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 net.jodah.expiringmap.ExpiringMap;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvel2.MVEL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ConditionManager {
private static final String VELOCITAB_REL_CONDITION = "velocitab_rel_condition:";
private static final String VELOCITAB_CONDITION = "velocitab_condition:";
private static final String VELOCITAB_REL_PLACEHOLDER_PERM = "velocitab_rel_perm:";
private static final String VELOCITAB_REL_WHO_IS_SEEING = "velocitab_rel_who-is-seeing";
private static final String VELOCITAB_REL_VANISH = "velocitab_rel_vanish";
private final Velocitab plugin;
private final Pattern targetPlaceholderPattern;
private final Pattern miniEscapeEndTags;
private final Map<String, Object> cachedExpressions;
private static final Map<String, String> REPLACE_CHARS = Map.of(
"?dp?", ":"
);
public ConditionManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.targetPlaceholderPattern = Pattern.compile("%target_(\\w+)?%");
this.miniEscapeEndTags = Pattern.compile("</(\\w+)>");
this.cachedExpressions = ExpiringMap.builder()
.expiration(5, TimeUnit.MINUTES)
.build();
}
@NotNull
public String checkConditions(@NotNull TabPlayer target, @NotNull String argument) {
final List<String> parameters = collectParameters(argument);
if (parameters.isEmpty()) {
plugin.getLogger().warn("Empty condition");
return "";
}
String condition = parameters.get(0);
if (parameters.size() < 3) {
plugin.getLogger().warn("Invalid condition: Missing true/false values for condition: {}", condition);
return "";
}
condition = plugin.getPlaceholderManager().applyPlaceholders(target, condition);
final String falseValue = processFalseValue(parameters.get(2));
final String expression = buildExpression(condition);
return evaluateAndFormatCondition(expression, target, parameters.get(1), falseValue);
}
@NotNull
private List<String> collectParameters(@NotNull String argument) {
for (Map.Entry<String, String> entry : REPLACE_CHARS.entrySet()) {
argument = argument.replace(entry.getKey(), entry.getValue());
}
return Arrays.stream(argument.split(":"))
.map(s -> s.replace("''", "\""))
.toList();
}
@NotNull
private String processFalseValue(@NotNull String falseValue) {
final Matcher matcher = miniEscapeEndTags.matcher(falseValue);
if (matcher.find()) {
final String tag = matcher.group(1);
if (falseValue.startsWith("</" + tag + ">")) {
falseValue = falseValue.substring(tag.length() + 3);
}
}
return falseValue;
}
@NotNull
private String buildExpression(@NotNull String condition) {
return condition.replace("and", "&&").replace("or", "||")
.replace("AND", "&&").replace("OR", "||");
}
@NotNull
private String evaluateAndFormatCondition(@NotNull String expression, @NotNull TabPlayer target,
@NotNull String trueValue, @NotNull String falseValue) {
final String targetString = parseTargetPlaceholders(expression, target).trim();
try {
final Object result = evaluateExpression(targetString);
if (result instanceof Boolean) {
final boolean boolResult = (Boolean) result;
return boolResult ? trueValue : falseValue;
}
} catch (Exception e) {
plugin.getLogger().warn("Failed to evaluate condition: {} error: {}", expression, e.getMessage());
}
return "";
}
@NotNull
private Object evaluateExpression(@NotNull String expression) {
return cachedExpressions.computeIfAbsent(expression, MVEL::eval);
}
@NotNull
private String parseTargetPlaceholders(@NotNull String input, @NotNull TabPlayer target) {
return targetPlaceholderPattern.matcher(input).replaceAll(match -> {
final String placeholder = match.group(1);
if (placeholder == null) {
return "";
}
final String text = "%" + placeholder + "%";
final Optional<String> placeholderValue = plugin.getPlaceholderManager().getCachedPlaceholderValue(text, target.getPlayer().getUniqueId());
return placeholderValue.orElse(text);
});
}
public String handleVelocitabPlaceholders(@NotNull String text, @NotNull TabPlayer player, @Nullable TabPlayer viewer) {
if (viewer == null) {
return handleConditionPlaceholders(text, player);
}
return handleRelPlaceholders(text, player, viewer);
}
@NotNull
private String handleRelPlaceholders(@NotNull String text, @NotNull TabPlayer player, @NotNull TabPlayer viewer) {
switch (text) {
case VELOCITAB_REL_WHO_IS_SEEING -> viewer.getPlayer().getUsername();
case VELOCITAB_REL_VANISH -> {
if (plugin.getVanishManager().isVanished(viewer.getPlayer().getUsername())) {
return "true";
}
return "false";
}
}
if (text.length() < VELOCITAB_REL_CONDITION.length()) {
return text;
}
if (text.startsWith(VELOCITAB_REL_CONDITION)) {
return checkConditions(player, text.substring(VELOCITAB_REL_CONDITION.length()));
}
if (text.startsWith(VELOCITAB_REL_PLACEHOLDER_PERM)) {
final String cleaned = text.substring(VELOCITAB_REL_PLACEHOLDER_PERM.length());
final int firstSeparator = cleaned.indexOf(':');
if (firstSeparator == -1) {
return "";
}
final String permission = cleaned.substring(0, firstSeparator);
final String trueValue = cleaned.substring(firstSeparator + 1);
return viewer.getPlayer().hasPermission(permission) ? trueValue : "";
}
return text;
}
@NotNull
private String handleConditionPlaceholders(@NotNull String text, @NotNull TabPlayer player) {
if (text.startsWith(VELOCITAB_CONDITION)) {
return checkConditions(player, text.substring(VELOCITAB_CONDITION.length()));
}
return text;
}
}

View File

@ -0,0 +1,149 @@
/*
* 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 net.william278.velocitab.util.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("")),
GROUP_PLAYERS_ONLINE((param, plugin, player) -> {
if (param.isEmpty()) {
return Integer.toString(player.getGroup().getPlayers(plugin).size());
}
return plugin.getTabGroupsManager().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) -> plugin.getLuckPermsHook()
.map(hook -> player.getRoleWeightString())
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_weight%"))),
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,334 @@
/*
* 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.Group;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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 PLACEHOLDER_PATTERN = Pattern.compile("%.*?%", Pattern.DOTALL);
private static final Pattern VELOCITAB_PLACEHOLDERS = Pattern.compile("<velocitab[^<>]*(?:<(?!v)[^<>]*>[^<>]*)*>");
private static final Pattern VELOCITAB_REL_PLACEHOLDERS = Pattern.compile("<velocitab_rel[^<>]*(?:<(?!v)[^<>]*>[^<>]*)*>");
private final Velocitab plugin;
private final Map<UUID, Map<String, String>> placeholders;
private final Map<UUID, Set<CompletableFuture<?>>> requests;
private final Map<Group, List<String>> cachedTexts;
private final Set<UUID> blocked;
private final ConditionManager conditionManager;
private Map<Group, Map<String, Map<String, String>>> placeholdersReplacements;
public PlaceholderManager(Velocitab plugin) {
this.plugin = plugin;
this.placeholders = Maps.newConcurrentMap();
this.requests = Maps.newConcurrentMap();
this.blocked = Sets.newConcurrentHashSet();
this.cachedTexts = Maps.newConcurrentMap();
this.conditionManager = new ConditionManager(plugin);
this.placeholdersReplacements = Maps.newConcurrentMap();
this.preparePlaceholdersReplacements();
}
public void preparePlaceholdersReplacements() {
placeholdersReplacements = Maps.newConcurrentMap();
for (Group group : plugin.getTabGroupsManager().getGroups()) {
final Map<String, Map<String, String>> map = Maps.newHashMap();
placeholdersReplacements.put(group, map);
for (String placeholder : group.placeholderReplacements().keySet()) {
final Map<String, String> repMap = Maps.newHashMap();
map.put(placeholder, repMap);
for (PlaceholderReplacement replacement : group.placeholderReplacements().get(placeholder)) {
repMap.put(replacement.placeholder(), replacement.replacement());
}
}
}
}
public void fetchPlaceholders(@NotNull Group group) {
final List<String> texts = cachedTexts.computeIfAbsent(group, g -> g.getTextsWithPlaceholders(plugin));
group.getPlayersAsList(plugin).forEach(player -> fetchPlaceholders(player.getUniqueId(), texts, group));
}
public void reload() {
cachedTexts.clear();
}
public void fetchPlaceholders(@NotNull UUID uuid, @NotNull List<String> texts, @NotNull Group group) {
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);
}
final long diff = System.currentTimeMillis() - start;
if (diff > group.placeholderUpdateRate()) {
final long increase = diff + 100;
plugin.getLogger().warn("""
Placeholder {} took more than group placeholder update rate of {} ms to update. This may cause a thread leak.
Please fix the issue of the plugin providing the placeholder.
If you can't fix it, increase the placeholder update rate of the group to at least {} ms.
"""
, placeholder, group.placeholderUpdateRate(), increase);
}
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());
return applyPlaceholderReplacements(text, player, parsed);
}
@NotNull
public String applyPlaceholders(@NotNull TabPlayer player, @NotNull String text, @NotNull TabPlayer viewer) {
final Map<String, String> parsed = placeholders.computeIfAbsent(player.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
final String applied = applyPlaceholderReplacements(text, player, parsed);
final Map<String, String> targetParsed = placeholders.computeIfAbsent(viewer.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
return applyPlaceholderReplacements(applied.replace("%target_", "%"), viewer, targetParsed);
}
@NotNull
public String applyViewerPlaceholders(@NotNull TabPlayer viewer, @NotNull String text) {
final Map<String, String> parsed = placeholders.computeIfAbsent(viewer.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
return applyPlaceholderReplacements(text.replace("%target_", "%"), viewer, parsed);
}
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;
}
@Nullable
private String getReplacement(@NotNull Group group, @NotNull String placeholder, @NotNull String text) {
final Map<String, Map<String, String>> replacements = placeholdersReplacements.get(group);
if (replacements == null) {
return null;
}
final Map<String, String> replacementMap = replacements.get(placeholder);
if (replacementMap == null) {
return null;
}
final String replacement = replacementMap.get(text);
if (replacement == null) {
return replacementMap.get(ELSE_PLACEHOLDER);
}
return replacement;
}
@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()) {
final String replaced = parsed.get(entry.getKey());
final String replacement = getReplacement(player.getGroup(), entry.getKey(), replaced);
if (replacement != null) {
text = text.replace(entry.getKey(), 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 java.util.regex.Matcher matcher = placeholderType.getPattern().matcher(placeholder);
if (matcher.find()) {
final String s = 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));
}
@NotNull
private String chop(@NotNull String text) {
int strLen = text.length();
if (strLen < 2) {
return "";
} else {
int lastIdx = strLen - 1;
String ret = text.substring(0, lastIdx);
char last = text.charAt(lastIdx);
return last == '\n' && ret.charAt(lastIdx - 1) == '\r' ? ret.substring(0, lastIdx - 1) : ret;
}
}
@NotNull
public String formatVelocitabPlaceholders(@NotNull String text, @NotNull TabPlayer player, @Nullable TabPlayer viewer) {
final Matcher matcher = VELOCITAB_PLACEHOLDERS.matcher(text);
if (!matcher.find()) {
return text;
}
final StringBuilder result = new StringBuilder(text.length());
int lastEnd = 0;
do {
String placeholder = matcher.group();
String cleanedPlaceholder = placeholder.substring(1, placeholder.length() - 1);
String replacement;
try {
replacement = conditionManager.handleVelocitabPlaceholders(cleanedPlaceholder, player, viewer);
if (replacement.equals(cleanedPlaceholder)) {
continue;
}
} catch (Exception e) {
plugin.getLogger().warn("Failed to calculate condition for {}", cleanedPlaceholder, e);
replacement = placeholder;
}
result.append(text, lastEnd, matcher.start()).append(replacement);
lastEnd = matcher.end();
} while (matcher.find());
result.append(text.substring(lastEnd));
return result.toString();
}
@NotNull
public String stripVelocitabRelPlaceholders(@NotNull String text) {
final Matcher matcher = VELOCITAB_REL_PLACEHOLDERS.matcher(text);
final StringBuilder result = new StringBuilder();
int lastEnd = 0;
while (matcher.find()) {
String placeholder = matcher.group();
String cleanedPlaceholder = placeholder.substring(1, placeholder.length() - 1);
try {
String replacement = "";
result.append(text, lastEnd, matcher.start()).append(replacement);
lastEnd = matcher.end();
} catch (Exception e) {
plugin.getLogger().warn("Failed to calculate condition for {}", cleanedPlaceholder, e);
}
}
result.append(text.substring(lastEnd));
return result.toString();
}
}

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

@ -30,7 +30,7 @@ import java.util.Optional;
@RequiredArgsConstructor
public class Role implements Comparable<Role> {
public static final int DEFAULT_WEIGHT = -1;
public static final int DEFAULT_WEIGHT = 0;
public static final Role DEFAULT_ROLE = new Role(DEFAULT_WEIGHT, null, null, null, null);
@Getter
private final int weight;
@ -64,12 +64,8 @@ public class Role implements Comparable<Role> {
return Optional.ofNullable(suffix);
}
@NotNull
protected Optional<String> getWeightString() {
if (weight == -1) {
return Optional.empty();
}
return Optional.of(Integer.toString(weight));
protected String getWeightString() {
return Integer.toString(weight);
}
@Override

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,29 +27,20 @@ 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;
import org.apache.commons.lang3.ObjectUtils;
import org.jetbrains.annotations.NotNull;
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,9 +50,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;
private String teamName;
@ -91,12 +78,10 @@ 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
public Optional<String> getRoleWeightString() {
public String getRoleWeightString() {
return getRole().getWeightString();
}
@ -110,7 +95,7 @@ public final class TabPlayer implements Comparable<TabPlayer> {
public String getServerName() {
return player.getCurrentServer()
.map(serverConnection -> serverConnection.getServerInfo().getName())
.orElse(ObjectUtils.firstNonNull(lastServer, "unknown"));
.orElse(lastServer != null ? lastServer : "unknown");
}
/**
@ -120,77 +105,31 @@ public final class TabPlayer implements Comparable<TabPlayer> {
* @return The ordinal position of the server group
*/
public int getServerGroupPosition(@NotNull Velocitab plugin) {
return plugin.getTabGroups().getPosition(group);
return plugin.getTabGroupsManager().getGroupPosition(group);
}
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
public CompletableFuture<String> getDisplayName(@NotNull Velocitab plugin) {
final String format = formatGroup();
return Placeholder.replace(format, plugin, this)
.thenApply(d -> cacheDisplayName(d, format));
}
@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))) {
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 sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
final Component header = tabList.getHeader(this);
final Component footer = tabList.getFooter(this);
lastHeader = header;
lastFooter = footer;
player.sendPlayerListHeaderAndFooter(header, footer);
}
public void incrementIndexes() {
@ -232,10 +171,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 +183,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
lastFooter = null;
role = Role.DEFAULT_ROLE;
teamName = null;
cachedListOrders.clear();
}
/**
@ -273,8 +207,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

@ -0,0 +1,241 @@
/*
* 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.providers;
import com.google.common.collect.Lists;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import net.william278.toilet.DumpOptions;
import net.william278.toilet.Toilet;
import net.william278.toilet.dump.*;
import net.william278.toilet.velocity.VelocityToilet;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.hook.Hook;
import net.william278.velocitab.util.DebugSystem;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import static net.william278.toilet.DumpOptions.*;
import java.awt.*;
import java.io.File;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public interface DumpProvider {
@NotNull PluginInfo.Label VANISH_INCOMPATIBLE = new PluginInfo.Label("Vanish Incompatible", "#bd3b01");
@NotNull Map<String, PluginInfo.Label> INCOMPATIBLE_PLUGINS = Map.ofEntries(
Map.entry("tab", PluginInfo.INCOMPATIBLE_LABEL),
Map.entry("premiumvanish", VANISH_INCOMPATIBLE)
);
@NotNull List<Integer> COLORS = Arrays.asList(
Color.RED.getRGB(),
Color.GREEN.getRGB(),
Color.BLUE.getRGB(),
Color.YELLOW.getRGB(),
Color.ORANGE.getRGB(),
Color.PINK.getRGB(),
Color.CYAN.getRGB(),
Color.MAGENTA.getRGB(),
Color.GRAY.getRGB(),
Color.LIGHT_GRAY.getRGB(),
Color.DARK_GRAY.getRGB(),
Color.BLACK.getRGB(),
Color.WHITE.getRGB(),
new Color(255, 165, 0).getRGB(), // Orange
new Color(128, 0, 128).getRGB(), // Purple
new Color(0, 128, 128).getRGB(), // Teal
new Color(128, 128, 0).getRGB(), // Olive
new Color(255, 192, 203).getRGB(), // Pink
new Color(0, 255, 255).getRGB(), // Aqua
new Color(255, 215, 0).getRGB() // Gold
);
@NotNull String BYTEBIN_URL = "https://bytebin.lucko.me";
@NotNull String VIEWER_URL = "https://william278.net/dump";
@NotNull
Toilet getToilet();
void setToilet(@NotNull Toilet toilet);
default void initializeToilet() {
final Toilet toilet = VelocityToilet.create(getDumpOptions(), getPlugin().getServer());
setToilet(toilet);
}
@NotNull
@Blocking
default String createDump(@NotNull CommandSource u) {
return getToilet().dump(getPluginStatus(), u instanceof Player o
? new DumpUser(o.getUsername(), o.getUniqueId()) : null, getDebugLog()).toString();
}
@NotNull
default DumpOptions getDumpOptions() {
return builder()
.bytebinUrl(BYTEBIN_URL)
.viewerUrl(VIEWER_URL)
.projectMeta(ProjectMeta.builder()
.id("velocitab")
.name("Velocitab")
.version(getPlugin().getVersion().toString())
.md5("unknown")
.author("William278, AlexDev03")
.sourceCode("https://github.com/WiIIiam278/Velocitab")
.website("https://william278.net/project/velocitab")
.support("https://discord.gg/tVYhJfyDWG")
.build())
.compatibilityRules(getCompatibilityRules())
.fileInclusionRules(getFileInclusionRules())
.build();
}
@NotNull
private ExtraFile getDebugLog() {
return new ExtraFile("debug-log", "Internal Debugger", DebugSystem.getLogsAsString());
}
@NotNull
private List<FileInclusionRule> getFileInclusionRules() {
final List<File> tabGroupsFiles = getPlugin().getTabGroupsManager().getGroupsFiles();
final List<FileInclusionRule> rules = Lists.newArrayList();
rules.add(FileInclusionRule.configFile(getPlugin().getConfigDirectory().resolve("config.yml").toFile().getAbsolutePath(), "Config File"));
for (File tabGroupsFile : tabGroupsFiles) {
final boolean isDefault = getPlugin().getTabGroupsManager().isDefaultFile(tabGroupsFile);
final String name = "Tab Groups File (" + (isDefault ? "default" : tabGroupsFile.getName()) + ")";
final FileInclusionRule rule = FileInclusionRule.configFile(tabGroupsFile.getAbsolutePath(), name);
if (isDefault) {
rules.add(1, rule);
} else {
rules.add(rule);
}
}
return rules;
}
@NotNull
private List<CompatibilityRule> getCompatibilityRules() {
return INCOMPATIBLE_PLUGINS.entrySet().stream()
.filter(e -> getPlugin().getServer().getPluginManager().getPlugin(e.getKey()).isPresent())
.map(e -> CompatibilityRule.builder()
.resourceName(e.getKey())
.labelToApply(e.getValue())
.build())
.toList();
}
@NotNull
@Blocking
private PluginStatus getPluginStatus() {
return PluginStatus.builder()
.blocks(List.of(getSystemStatus(), getServersInEachGroup(), getPlayersInEachGroup(), getHookStatus()))
.build();
}
@NotNull
private PluginStatus.MapStatusBlock getSystemStatus() {
return new PluginStatus.MapStatusBlock(
Map.ofEntries(
Map.entry("RemoveNameTags", Boolean.toString(getPlugin().getSettings().isRemoveNametags())),
Map.entry("DisableHeaderFooterIfEmpty", Boolean.toString(getPlugin().getSettings().isDisableHeaderFooterIfEmpty())),
Map.entry("Formatter", getPlugin().getSettings().getFormatter().name()),
Map.entry("FallbackGroupEnabled", Boolean.toString(getPlugin().getSettings().isFallbackEnabled())),
Map.entry("FallbackGroup", getPlugin().getSettings().getFallbackGroup()),
Map.entry("PapiProxyBridge", Boolean.toString(getPlugin().getSettings().isEnablePapiHook())),
Map.entry("PapiCacheTime", Long.toString(getPlugin().getSettings().getPapiCacheTime())),
Map.entry("MiniPlaceholders", Boolean.toString(getPlugin().getSettings().isEnableMiniPlaceholdersHook())),
Map.entry("SendScoreboardPackets", Boolean.toString(getPlugin().getSettings().isSendScoreboardPackets())),
Map.entry("SortPlayers", Boolean.toString(getPlugin().getSettings().isSortPlayers())),
Map.entry("RelationalPlaceholders", Boolean.toString(getPlugin().getSettings().isEnableRelationalPlaceholders())),
Map.entry("VanishIntegration", getPlugin().getVanishManager().getIntegration().getClass().getName())
),
"Plugin Status", "fa6-solid:wrench"
);
}
@NotNull
private PluginStatus.ListStatusBlock getHookStatus() {
return new PluginStatus.ListStatusBlock(
getPlugin().getHooks().stream().map(Hook::getName).toList(),
"Loaded Hooks", "fa6-solid:plug"
);
}
@NotNull
private PluginStatus.ChartStatusBlock getPlayersInEachGroup() {
final AtomicInteger colorIndex = new AtomicInteger(0);
final Map<PluginStatus.ChartKey, Integer> players = getPlugin().getTabGroupsManager().getGroups().stream()
.collect(Collectors.toMap(
g -> new PluginStatus.ChartKey(g.name(), "fa6-solid:server", COLORS.get(colorIndex.getAndIncrement() % COLORS.size())),
group -> group.getTabPlayersAsList(getPlugin()).size()
));
return new PluginStatus.ChartStatusBlock(
players,
PluginStatus.ChartType.PIE,
"Online players per group",
"fa6-solid:users"
);
}
@NotNull
private PluginStatus.MapStatusBlock getServersInEachGroup() {
final Map<String, String> servers = getPlugin().getTabGroupsManager().getGroups().stream()
.sorted(getGroupComparator(getPlugin()).reversed())
.collect(Collectors.toMap(
Group::name,
g -> g.registeredServers(getPlugin()).stream()
.map(RegisteredServer::getServerInfo)
.map(ServerInfo::getName)
.collect(Collectors.joining(", ")),
(e1, e2) -> e1,
LinkedHashMap::new
));
return new PluginStatus.MapStatusBlock(
servers,
"Servers in each group",
"fa6-solid:network-wired"
);
}
default Comparator<Group> getGroupComparator(@NotNull Velocitab plugin) {
return (g1, g2) -> {
final int servers1 = g1.registeredServers(plugin).size();
final int servers2 = g2.registeredServers(plugin).size();
if (servers1 != servers2) {
return servers1 - servers2;
}
return g1.servers().size() - g2.servers().size();
};
}
@NotNull
Velocitab getPlugin();
}

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

@ -20,12 +20,15 @@
package net.william278.velocitab.sorting;
import com.google.common.collect.Maps;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListSet;
@ToString
public class SortedSet {
private final ConcurrentSkipListSet<String> sortedTeams;
@ -36,39 +39,37 @@ public class SortedSet {
positionMap = Maps.newConcurrentMap();
}
public synchronized boolean addTeam(@NotNull String teamName) {
final boolean result = sortedTeams.add(teamName);
if (!result) {
public boolean addTeam(@NotNull String teamName) {
if (!sortedTeams.add(teamName)) {
return false;
}
updatePositions();
updatePositions(teamName);
return true;
}
public synchronized boolean removeTeam(@NotNull String teamName) {
final boolean result = sortedTeams.remove(teamName);
if (!result) {
public boolean removeTeam(@NotNull String teamName) {
if (!sortedTeams.remove(teamName)) {
return false;
}
updatePositions();
updatePositions(null);
return true;
}
private synchronized void updatePositions() {
int index = 0;
positionMap.clear();
for (final String team : sortedTeams) {
positionMap.put(team, index);
index++;
private void updatePositions(@Nullable String newTeam) {
if (newTeam != null) {
int newPosition = sortedTeams.headSet(newTeam).size();
positionMap.put(newTeam, newPosition);
sortedTeams.tailSet(newTeam).forEach(team -> positionMap.put(team, sortedTeams.headSet(team).size()));
} else {
int index = 0;
positionMap.clear();
for (String team : sortedTeams) {
positionMap.put(team, index++);
}
}
}
public synchronized int getPosition(@NotNull String teamName) {
public int getPosition(@NotNull String teamName) {
return positionMap.getOrDefault(teamName, -1);
}
@Override
public String toString() {
return sortedTeams.toString();
}
}

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

@ -21,7 +21,6 @@ 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.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
@ -32,13 +31,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,9 @@ 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.scheduler.ScheduledTask;
import com.velocitypowered.api.util.ServerLink;
import com.velocitypowered.proxy.tablist.KeyedVelocityTabList;
import com.velocitypowered.proxy.tablist.VelocityTabList;
import lombok.AccessLevel;
@ -37,33 +37,32 @@ 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;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.util.DebugSystem;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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
*/
public class PlayerTabList {
private static final String RELATIONAL_PERMISSION = "velocitab.relational";
private final Velocitab plugin;
@Getter
private final VanishTabList vanishTabList;
@Getter(value = AccessLevel.PUBLIC)
private final Map<UUID, TabPlayer> players;
@Getter(value = AccessLevel.PROTECTED)
private final TaskManager taskManager;
private final Map<Class<?>, Field> entriesFields;
@ -73,7 +72,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();
@ -140,8 +138,26 @@ public class PlayerTabList {
return;
}
joinPlayer(p, group.get());
loadPlayer(p, group.get(), 400);
});
reloadUpdate();
}
protected void loadPlayer(@NotNull Player player, @NotNull Group group, int delay) {
final ScheduledTask task = plugin.getServer().getScheduler()
.buildTask(plugin, () -> plugin.getPlaceholderManager().fetchPlaceholders(player.getUniqueId(), group.getTextsWithPlaceholders(plugin), group))
.delay(150, TimeUnit.MILLISECONDS)
.repeat(50, TimeUnit.MILLISECONDS)
.schedule();
//After updating papiproxybridge we can check if redis is used
plugin.getServer().getScheduler().buildTask(plugin, () -> {
task.cancel();
joinPlayer(player, group);
})
.delay(delay, TimeUnit.MILLISECONDS)
.schedule();
}
/**
@ -149,7 +165,7 @@ public class PlayerTabList {
* Removes the player's entry from the tab list of all other players on the same group servers.
*/
public void close() {
taskManager.cancelAllTasks();
taskManager.close();
plugin.getServer().getAllPlayers().forEach(p -> {
final Optional<ServerConnection> server = p.getCurrentServer();
if (server.isEmpty()) return;
@ -167,13 +183,13 @@ public class PlayerTabList {
serversInGroup.remove(server.get().getServer());
serversInGroup.forEach(s -> s.getPlayersConnected().forEach(t -> t.getTabList().removeEntry(p.getUniqueId())));
});
plugin.getPacketEventManager().removeAllPlayers();
}
protected void clearCachedData(@NotNull Player player) {
players.values().forEach(p -> {
p.unsetRelationalDisplayName(player.getUniqueId());
p.unsetRelationalNametag(player.getUniqueId());
p.unsetTabListOrder(player.getUniqueId());
});
}
@ -186,6 +202,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 +211,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;
});
handleDisplayLoad(tabPlayer);
}
private void handleDisplayLoad(@NotNull TabPlayer tabPlayer) {
@ -214,20 +219,14 @@ 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;
});
final Set<TabPlayer> tabPlayers = group.getTabPlayers(plugin, tabPlayer);
tabPlayer.sendHeaderAndFooter(this);
tabPlayer.setLoaded(true);
final List<TabPlayer> tabPlayers = group.getTabPlayersAsList(plugin, tabPlayer);
updateTabListOnJoin(tabPlayer, group, tabPlayers, isVanished);
}
private void updateTabListOnJoin(@NotNull TabPlayer tabPlayer, @NotNull Group group,
@NotNull Set<TabPlayer> tabPlayers, boolean isJoinedVanished) {
@NotNull List<TabPlayer> tabPlayers, boolean isJoinedVanished) {
final Player joined = tabPlayer.getPlayer();
final String serverName = getServerName(joined);
final Set<UUID> uuids = tabPlayers.stream().map(p -> p.getPlayer().getUniqueId()).collect(Collectors.toSet());
@ -268,11 +267,11 @@ public class PlayerTabList {
final String observableUsername = observableTabPlayer.getPlayer().getUsername();
final TabList observableTabPlayerTabList = observableTabPlayer.getPlayer().getTabList();
if (isObservablePlayerVanished && !plugin.getVanishManager().canSee(observableUsername, observedUsername) &&
!observableUUID.equals(observedPlayer.getPlayer().getUniqueId())) {
if ((isObservablePlayerVanished && !plugin.getVanishManager().canSee(observableUsername, observedUsername) &&
!observableUUID.equals(observedPlayer.getPlayer().getUniqueId())) || !observedPlayer.getPlayer().isActive()) {
observableTabPlayerTabList.removeEntry(observedPlayer.getPlayer().getUniqueId());
} else {
updateDisplayName(observedPlayer, observableTabPlayer);
calculateAndSetDisplayName(observedPlayer, observableTabPlayer);
}
}
@ -284,18 +283,14 @@ 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;
}
public Component formatRelationalComponent(@NotNull TabPlayer player, @NotNull TabPlayer viewer,
@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);
public Component formatComponent(@NotNull TabPlayer player, @NotNull String toParse) {
return plugin.getFormatter().format(toParse, player, plugin);
}
@SuppressWarnings("unchecked")
@ -317,10 +312,6 @@ public class PlayerTabList {
}
}
protected void removePlayer(@NotNull Player target) {
removePlayer(target, null);
}
/**
* Remove a player from the tab list
*
@ -332,29 +323,30 @@ public class PlayerTabList {
));
}
protected void removePlayer(@NotNull Player target, @Nullable RegisteredServer server) {
protected void removePlayer(@NotNull Player target) {
final UUID uuid = target.getUniqueId();
plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(uuid));
final Optional<TabPlayer> tabPlayer = getTabPlayer(target.getUniqueId());
if (tabPlayer.isEmpty()) {
return;
}
final Group group = tabPlayer.get().getGroup();
tabPlayer.get().setLoaded(false);
final Set<Player> currentServerPlayers = Optional.ofNullable(server)
.map(RegisteredServer::getPlayersConnected)
.map(HashSet::new)
.orElseGet(HashSet::new);
currentServerPlayers.add(target);
// Update the tab list of all players
plugin.getServer().getScheduler()
.buildTask(plugin, () -> getPlayers().values().stream()
.filter(p -> currentServerPlayers.isEmpty() || !currentServerPlayers.contains(p.getPlayer()))
.forEach(player -> {
player.getPlayer().getTabList().removeEntry(uuid);
player.sendHeaderAndFooter(this);
updatePlayerDisplayName(player);
}))
.buildTask(plugin, () -> {
final List<TabPlayer> list = group.getTabPlayersAsList(plugin);
list.forEach(player -> {
player.getPlayer().getTabList().removeEntry(uuid);
player.sendHeaderAndFooter(this);
});
})
.delay(250, TimeUnit.MILLISECONDS)
.schedule();
// Delete player team
plugin.getScoreboardManager().resetCache(target);
//remove player from tab list cache
getPlayers().remove(uuid);
}
@ -374,8 +366,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 = formatRelationalComponent(player, viewer, displayNameUnformatted);
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
return TabListEntry.builder()
.profile(player.getPlayer().getGameProfile())
@ -385,12 +378,22 @@ public class PlayerTabList {
.build();
}
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer) {
final Component displayName = getRelationalPlaceholder(player, viewer, player.getLastDisplayName());
updateDisplayName(player, viewer, displayName);
protected void calculateAndSetDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer) {
final String withPlaceholders = plugin.getPlaceholderManager().applyPlaceholders(player, player.getGroup().format());
final String unformatted = plugin.getPlaceholderManager().formatVelocitabPlaceholders(withPlaceholders, player, null);
if (!plugin.getSettings().isEnableRelationalPlaceholders()) {
final String stripped = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(unformatted);
final Component displayName = plugin.getFormatter().format(stripped, player, plugin);
updateEntryDisplayName(player, viewer, displayName);
return;
}
final String withRelationalPlaceholders = plugin.getPlaceholderManager().formatVelocitabPlaceholders(unformatted, player, viewer);
final Component displayName = plugin.getFormatter().format(withRelationalPlaceholders, player, viewer, plugin);
updateEntryDisplayName(player, viewer, displayName);
}
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull Component displayName) {
protected void updateEntryDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull Component displayName) {
final Optional<Component> cached = player.getRelationalDisplayName(viewer.getPlayer().getUniqueId());
if (cached.isPresent() && cached.get().equals(displayName) &&
viewer.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
@ -418,6 +421,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,58 +432,180 @@ public class PlayerTabList {
return;
}
updateSorting(tabPlayer, force);
plugin.getPlaceholderManager().fetchPlaceholders(tabPlayer.getPlayer().getUniqueId(), tabPlayer.getGroup().sortingPlaceholders(), tabPlayer.getGroup());
//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) {
final List<TabPlayer> players = group.getTabPlayersAsList(plugin);
players.forEach(p -> updateSorting(p, false, players));
}
private void updateSorting(@NotNull TabPlayer tabPlayer, boolean force) {
tabPlayer.getTeamName(plugin).thenAccept(teamName -> {
if (teamName.isBlank()) {
return;
}
plugin.getScoreboardManager().updateRole(tabPlayer, teamName, force).thenAccept(v -> {
final int order = plugin.getScoreboardManager().getPosition(teamName);
if (order == -1) {
plugin.log(Level.ERROR, "Failed to get position for " + tabPlayer.getPlayer().getUsername());
return;
}
final List<TabPlayer> players = tabPlayer.getGroup().getTabPlayersAsList(plugin, tabPlayer);
updateSorting(tabPlayer, force, players);
}
tabPlayer.setListOrder(order);
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
players.forEach(p -> recalculateSortingForPlayer(p, players));
});
});
private void updateSorting(@NotNull TabPlayer tabPlayer, boolean force, @NotNull List<TabPlayer> players) {
final String teamName = tabPlayer.getTeamName(plugin);
if (teamName.isBlank() || !tabPlayer.getPlayer().isActive()) {
return;
}
plugin.getScoreboardManager().updateRole(tabPlayer, teamName, force);
final int order = plugin.getScoreboardManager().getPosition(teamName);
if (order == -1) {
DebugSystem.log(DebugSystem.DebugLevel.ERROR, "Failed to get position for " + tabPlayer.getPlayer().getUsername());
return;
}
tabPlayer.setListOrder(order);
recalculateSortingForPlayers(tabPlayer, players, order);
}
private boolean hasListOrder(TabPlayer tabPlayer) {
return tabPlayer.getPlayer().getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2);
}
private void updateSorting(TabPlayer tabPlayer, UUID uuid, int position) {
tabPlayer.getPlayer().getTabList().getEntry(uuid)
.filter(entry -> entry.getListOrder() != position)
.ifPresent(entry -> entry.setListOrder(position));
}
public synchronized void recalculateSortingForPlayers(@NotNull TabPlayer tabPlayer, @NotNull List<TabPlayer> players, int order) {
players.stream()
.filter(this::hasListOrder)
.forEach(p -> updateSorting(p, tabPlayer.getPlayer().getUniqueId(), order));
}
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);
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());
public void updateGroupNames(@NotNull Group group) {
final List<TabPlayer> players = group.getTabPlayersAsList(plugin);
if (plugin.getSettings().isEnableRelationalPlaceholders()) {
updateRelationalGroupNames(players, group);
return;
}
updateNormalGroupNames(players, group);
}
private void updateNormalGroupNames(List<TabPlayer> players, @NotNull Group group) {
final String stripped = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(group.format());
checkStrippedString(stripped, group);
for (TabPlayer player : players) {
final String displayName = plugin.getPlaceholderManager().applyPlaceholders(player, stripped);
final String displayNameConditional = plugin.getPlaceholderManager().formatVelocitabPlaceholders(displayName, player, null);
final Component displayNameComponent = formatComponent(player, displayNameConditional);
players.forEach(viewer -> updateEntryDisplayName(player, viewer, displayNameComponent));
}
}
private void updateRelationalGroupNames(@NotNull List<TabPlayer> players, @NotNull Group group) {
for (TabPlayer p1 : players) {
if (!p1.getPlayer().isActive() || !p1.isLoaded()) {
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);
final boolean isVanished = plugin.getVanishManager().isVanished(p1.getPlayer().getUsername());
final String formatPlaceholders = plugin.getPlaceholderManager().applyPlaceholders(p1, group.format());
final String formatConditionalPlaceholders = plugin.getPlaceholderManager().formatVelocitabPlaceholders(formatPlaceholders, p1, null);
players.forEach(player -> {
if (isVanished && !plugin.getVanishManager().canSee(player.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) {
// Handles the case where the player is not
final String formatConditionalPlaceholdersWithoutRelational = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(formatConditionalPlaceholders);
final Component relationalPlaceholder = formatComponent(p1, formatConditionalPlaceholdersWithoutRelational);
for (TabPlayer player : players) {
if (isVanished && !plugin.getVanishManager().canSee(player.getPlayer().getUsername(), p1.getPlayer().getUsername())) {
return;
}
final Component relationalPlaceholder = getRelationalPlaceholder(tabPlayer, player, single, displayName);
updateDisplayName(tabPlayer, player, relationalPlaceholder);
});
if (!player.getPlayer().isActive() || !player.isLoaded()) {
return;
}
if (!player.getPlayer().hasPermission(RELATIONAL_PERMISSION)) {
updateEntryDisplayName(p1, player, relationalPlaceholder);
continue;
}
final String withPlaceholders = plugin.getPlaceholderManager().applyViewerPlaceholders(player, formatConditionalPlaceholders);
final String unformatted = plugin.getPlaceholderManager().formatVelocitabPlaceholders(withPlaceholders, p1, player);
final Component displayName = formatRelationalComponent(p1, player, unformatted);
updateEntryDisplayName(p1, player, displayName);
}
}
}
public void updateDisplayName(@NotNull TabPlayer tabPlayer) {
if (plugin.getSettings().isEnableRelationalPlaceholders()) {
updateRelationalDisplayName(tabPlayer);
return;
}
updateNormalDisplayName(tabPlayer);
}
private void updateNormalDisplayName(@NotNull TabPlayer tabPlayer) {
final Group group = tabPlayer.getGroup();
final String stripped = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(group.format());
checkStrippedString(stripped, group);
final List<TabPlayer> players = group.getTabPlayersAsList(plugin, tabPlayer);
players.forEach(player -> {
if (!player.getPlayer().hasPermission(RELATIONAL_PERMISSION)) {
final String displayName = plugin.getPlaceholderManager().applyPlaceholders(player, stripped);
final String displayNameConditional = plugin.getPlaceholderManager().formatVelocitabPlaceholders(displayName, player, null);
final Component displayNameComponent = formatComponent(player, displayNameConditional);
updateEntryDisplayName(player, tabPlayer, displayNameComponent);
return;
}
final String withPlaceholders = plugin.getPlaceholderManager().applyViewerPlaceholders(player, stripped);
final String unformatted = plugin.getPlaceholderManager().formatVelocitabPlaceholders(withPlaceholders, tabPlayer, player);
final Component displayName = formatRelationalComponent(tabPlayer, player, unformatted);
updateEntryDisplayName(tabPlayer, player, displayName);
});
}
private void updateRelationalDisplayName(@NotNull TabPlayer tabPlayer) {
final Group group = tabPlayer.getGroup();
final String stripped = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(group.format());
checkStrippedString(stripped, group);
final List<TabPlayer> players = group.getTabPlayersAsList(plugin, tabPlayer);
players.forEach(player -> {
final String displayName = plugin.getPlaceholderManager().applyPlaceholders(player, stripped);
final String displayNameConditional = plugin.getPlaceholderManager().formatVelocitabPlaceholders(displayName, player, null);
final Component displayNameComponent = formatRelationalComponent(player, tabPlayer, displayNameConditional);
updateEntryDisplayName(player, tabPlayer, displayNameComponent);
});
}
private void checkStrippedString(@NotNull String text, @NotNull Group group) {
if (text.length() != group.format().length()) {
DebugSystem.log(DebugSystem.DebugLevel.WARNING, "Found relational placeholder in group {} format even though relational placeholders are disabled", group.name());
}
}
public void checkCorrectDisplayName(@NotNull TabPlayer tabPlayer) {
if (!tabPlayer.isLoaded()) {
return;
@ -499,12 +628,6 @@ public class PlayerTabList {
}));
}
// Update the display names of all listed players
public void updateDisplayNames() {
players.values().forEach(this::updatePlayerDisplayName);
}
public void checkCorrectDisplayNames() {
players.values().forEach(this::checkCorrectDisplayName);
}
@ -513,24 +636,24 @@ public class PlayerTabList {
plugin.getServer().getScheduler()
.buildTask(plugin, this::checkCorrectDisplayNames)
.delay(1, TimeUnit.SECONDS)
.repeat(2, TimeUnit.SECONDS)
.repeat(5, TimeUnit.SECONDS)
.schedule();
}
// 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,33 +661,47 @@ public class PlayerTabList {
*/
public void reloadUpdate() {
taskManager.cancelAllTasks();
plugin.getTabGroups().getGroups().forEach(taskManager::updatePeriodically);
plugin.getPlaceholderManager().reload();
plugin.getPlaceholderManager().preparePlaceholdersReplacements();
plugin.getTabGroupsManager().getGroups().forEach(g -> {
plugin.getPlaceholderManager().fetchPlaceholders(g);
taskManager.updatePeriodically(g);
});
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();
if (server.isEmpty()) {
return;
}
final String serverName = server.get().getServerInfo().getName();
final Optional<Group> group = getGroup(serverName);
if (group.isEmpty()) {
return;
}
player.setGroup(group.get());
this.sendPlayerServerLinks(player);
this.updatePlayer(player, true);
player.sendHeaderAndFooter(this);
});
updateDisplayNames();
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();
if (server.isEmpty()) {
return;
}
final String serverName = server.get().getServerInfo().getName();
final Optional<Group> group = getGroup(serverName);
if (group.isEmpty()) {
return;
}
player.setGroup(group.get());
this.sendPlayerServerLinks(player);
this.updatePlayer(player, true);
player.sendHeaderAndFooter(this);
});
plugin.getTabGroupsManager().getGroups().forEach(this::updateGroupNames);
}).delay(500, TimeUnit.MILLISECONDS).schedule();
}
@NotNull
public Optional<Group> getGroup(@NotNull String serverName) {
return plugin.getTabGroups().getGroupFromServer(serverName, plugin);
return plugin.getTabGroupsManager().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.getTabGroupsManager().getGroup("default").orElseThrow());
}
public void removeOldEntry(@NotNull Group group, @NotNull UUID uuid) {
@ -580,45 +717,4 @@ public class PlayerTabList {
public void removeOfflinePlayer(@NotNull Player player) {
players.remove(player.getUniqueId());
}
/**
* Whether the player can use server-side specified TAB list ordering (Minecraft 1.21.2+)
*
* @param tabPlayer player to check
* @return {@code true} if the user is on Minecraft 1.21.2+; {@code false}
*/
private boolean hasListOrder(@NotNull TabPlayer tabPlayer) {
return tabPlayer.getPlayer().getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2);
}
private void updateSorting(@NotNull TabPlayer tabPlayer, @NotNull UUID uuid, int position) {
if (!tabPlayer.getPlayer().getTabList().containsEntry(uuid)) {
return;
}
if (tabPlayer.getCachedListOrders().containsKey(uuid) && tabPlayer.getCachedListOrders().get(uuid) == 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");
}
public synchronized void recalculateSortingForPlayer(@NotNull TabPlayer tabPlayer, @NotNull Set<TabPlayer> players) {
if (!hasListOrder(tabPlayer)) {
return;
}
players.forEach(p -> {
final int order = p.getListOrder();
updateSorting(tabPlayer, p.getPlayer().getUniqueId(), order);
});
}
}

View File

@ -28,6 +28,7 @@ import com.velocitypowered.api.event.player.ServerPostConnectEvent;
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.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
@ -39,6 +40,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* The TabListListener class is responsible for handling events related to the player tab list.
@ -59,7 +61,11 @@ public class TabListListener {
}
@Subscribe
public void onKick(@NotNull KickedFromServerEvent event) {
private void onKick(@NotNull KickedFromServerEvent event) {
plugin.getTabList().getTaskManager().run(() -> handleKick(event));
}
private void handleKick(@NotNull KickedFromServerEvent event) {
event.getPlayer().getTabList().getEntries().stream()
.filter(entry -> entry.getProfile() != null && !entry.getProfile().getId().equals(event.getPlayer().getUniqueId()))
.forEach(entry -> event.getPlayer().getTabList().removeEntry(entry.getProfile().getId()));
@ -68,7 +74,7 @@ public class TabListListener {
if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer) {
tabList.removePlayer(event.getPlayer());
} else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer redirectPlayer) {
tabList.removePlayer(event.getPlayer(), redirectPlayer.getServer());
tabList.removePlayer(event.getPlayer());
} else if (event.getResult() instanceof KickedFromServerEvent.Notify notify) {
return;
}
@ -83,16 +89,23 @@ public class TabListListener {
.schedule();
}
@SuppressWarnings("UnstableApiUsage")
@Subscribe
public void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
@Subscribe(priority = Short.MIN_VALUE)
private void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
plugin.getTabList().getTaskManager().run(() -> handlePlayerJoin(event));
}
@SuppressWarnings("UnstableApiUsage")
private void handlePlayerJoin(@NotNull ServerPostConnectEvent event) {
final Player joined = event.getPlayer();
final String serverName = joined.getCurrentServer()
.map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("");
final Optional<Group> previousGroup = tabList.getTabPlayer(joined)
final Optional<TabPlayer> previousTabPlayer = tabList.getTabPlayer(joined);
final Optional<Group> previousGroup = previousTabPlayer
.map(TabPlayer::getGroup);
// Get the group the player should now be in
@ -101,18 +114,35 @@ public class TabListListener {
// Removes cached relational data of the joined player from all other players
plugin.getTabList().clearCachedData(joined);
plugin.getPlaceholderManager().clearPlaceholders(joined.getUniqueId());
// Mark the previous tab player as unloaded
previousTabPlayer.ifPresent(player -> player.setLoaded(false));
// If the player was in a group and the new group is different or not set, remove the old entry
if (!plugin.getSettings().isShowAllPlayersFromAllGroups() && previousGroup.isPresent()
&& (groupOptional.isPresent() && !previousGroup.get().equals(groupOptional.get())
|| groupOptional.isEmpty())) {
tabList.removeOldEntry(previousGroup.get(), joined.getUniqueId());
&& ((groupOptional.isPresent() && !previousGroup.get().equals(groupOptional.get())) || groupOptional.isEmpty())
) {
tabList.getPlayers().remove(joined.getUniqueId());
removeOldEntry(previousGroup.get(), joined.getUniqueId());
// If a player moved to a server without a group, remove possible entries
if (groupOptional.isEmpty() && joined.getCurrentServer().isPresent()) {
plugin.getTabList().getTaskManager().runDelayed(() -> {
final RegisteredServer server = joined.getCurrentServer().get().getServer();
final Set<UUID> players = server.getPlayersConnected().stream()
.map(Player::getUniqueId).collect(Collectors.toSet());
final Set<UUID> tabPlayers = Sets.newHashSet(tabList.getPlayers().keySet());
tabPlayers.removeAll(players);
tabPlayers.forEach(u -> joined.getTabList().removeEntry(u));
}, 250, TimeUnit.MILLISECONDS);
}
}
// If the server is not in a group, use fallback.
// If fallback is disabled, permit the player to switch excluded servers without a header or footer override
if (isDefault && !plugin.getSettings().isFallbackEnabled() && !groupOptional.map(g -> g.containsServer(plugin, serverName)).orElse(false)) {
final Optional<TabPlayer> tabPlayer = tabList.getTabPlayer(joined);
if (tabPlayer.isEmpty()) {
if (previousTabPlayer.isEmpty()) {
return;
}
@ -120,22 +150,7 @@ public class TabListListener {
return;
}
final Component header = tabPlayer.get().getLastHeader();
final Component footer = tabPlayer.get().getLastFooter();
plugin.getServer().getScheduler().buildTask(plugin, () -> {
final Component currentHeader = joined.getPlayerListHeader();
final Component currentFooter = joined.getPlayerListFooter();
if ((header.equals(currentHeader) && footer.equals(currentFooter)) ||
(currentHeader.equals(Component.empty()) && currentFooter.equals(Component.empty()))
) {
joined.sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
joined.getCurrentServer().ifPresent(serverConnection -> serverConnection.getServer().getPlayersConnected().forEach(player ->
player.getTabList().getEntry(joined.getUniqueId())
.ifPresent(entry -> entry.setDisplayName(Component.text(joined.getUsername())))));
}
}).delay(500, TimeUnit.MILLISECONDS).schedule();
cleanOldHeadersAndFooters(previousTabPlayer.get());
tabList.getPlayers().remove(event.getPlayer().getUniqueId());
return;
}
@ -147,26 +162,59 @@ 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,
() -> tabList.joinPlayer(joined, group))
.delay(delay, TimeUnit.MILLISECONDS)
.schedule();
plugin.getServer().getScheduler().buildTask(plugin, () -> {
plugin.getPlaceholderManager().unblockPlayer(joined.getUniqueId());
}).delay(10, TimeUnit.MILLISECONDS).schedule();
tabList.loadPlayer(joined, group, justQuit.contains(joined.getUniqueId()) ? 400 : 500);
}
@SuppressWarnings("deprecation")
@Subscribe(order = PostOrder.CUSTOM, priority = Short.MIN_VALUE)
public void onPlayerQuit(@NotNull DisconnectEvent event) {
private void onPlayerQuit(@NotNull DisconnectEvent event) {
plugin.getTabList().getTaskManager().run(() -> handlePlayerQuit(event));
}
private void handlePlayerQuit(@NotNull DisconnectEvent event) {
if (event.getLoginStatus() == DisconnectEvent.LoginStatus.CONFLICTING_LOGIN) {
return;
}
if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) {
checkDelayedDisconnect(event);
return;
}
// 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());
}
@Subscribe
private void proxyReload(@NotNull ProxyReloadEvent event) {
plugin.loadConfigs();
tabList.reloadUpdate();
plugin.log("Velocitab has been reloaded!");
}
private void removeOldEntry(@NotNull Group group, @NotNull UUID uuid) {
plugin.getServer().getScheduler().buildTask(plugin, () -> tabList.removeOldEntry(group, uuid))
.delay(100, TimeUnit.MILLISECONDS)
.schedule();
}
private void cleanOldHeadersAndFooters(@NotNull TabPlayer tabPlayer) {
final Component header = tabPlayer.getLastHeader();
final Component footer = tabPlayer.getLastFooter();
plugin.getServer().getScheduler().buildTask(plugin, () -> {
final Component currentHeader = tabPlayer.getPlayer().getPlayerListHeader();
final Component currentFooter = tabPlayer.getPlayer().getPlayerListFooter();
if ((header.equals(currentHeader) && footer.equals(currentFooter)) ||
(currentHeader.equals(Component.empty()) && currentFooter.equals(Component.empty()))
) {
tabPlayer.getPlayer().sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
tabPlayer.getPlayer().getCurrentServer().ifPresent(serverConnection -> serverConnection.getServer().getPlayersConnected().forEach(player ->
player.getTabList().getEntry(tabPlayer.getPlayer().getUniqueId())
.ifPresent(entry -> entry.setDisplayName(Component.text(tabPlayer.getPlayer().getUsername())))));
}
}).delay(500, TimeUnit.MILLISECONDS).schedule();
}
private void checkDelayedDisconnect(@NotNull DisconnectEvent event) {
@ -185,11 +233,5 @@ public class TabListListener {
}).delay(750, TimeUnit.MILLISECONDS).schedule();
}
@Subscribe
public void proxyReload(@NotNull ProxyReloadEvent event) {
plugin.loadConfigs();
tabList.reloadUpdate();
plugin.log("Velocitab has been reloaded!");
}
}

View File

@ -19,88 +19,143 @@
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;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.util.DebugSystem;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class TaskManager {
private final Velocitab plugin;
private final Map<Group, GroupTasks> groupTasks;
private final Map<Group, List<ScheduledFuture<?>>> groupTasks;
private final Map<Group, List<ScheduledTask>> groupTasksOld;
private final ScheduledExecutorService processThread;
public TaskManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.groupTasksOld = Maps.newConcurrentMap();
this.groupTasks = Maps.newConcurrentMap();
this.processThread = Executors.newSingleThreadScheduledExecutor();
}
protected void cancelAllTasks() {
groupTasks.values().forEach(GroupTasks::cancel);
groupTasksOld.values().forEach(c -> c.forEach(ScheduledTask::cancel));
groupTasksOld.clear();
groupTasks.values().forEach(c -> c.forEach(t -> t.cancel(true)));
groupTasks.clear();
}
protected void updatePeriodically(@NotNull Group group) {
ScheduledTask headerFooterTask = null;
ScheduledTask updateTask = null;
ScheduledTask latencyTask;
public void close() {
try {
cancelAllTasks();
processThread.shutdownNow();
} catch (Throwable e) {
plugin.getLogger().error("Failed to close task manager", e);
}
}
protected void updatePeriodically(@NotNull Group group) {
final List<ScheduledFuture<?>> tasks = groupTasks.computeIfAbsent(group, g -> Lists.newArrayList());
if (group.headerFooterUpdateRate() > 0) {
headerFooterTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateGroupPlayers(group, false, true))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.headerFooterUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
final ScheduledFuture<?> headerFooterTask = processThread.scheduleAtFixedRate(() -> {
final long startTime = System.currentTimeMillis();
plugin.getTabList().updateHeaderFooter(group);
final long endTime = System.currentTimeMillis();
final long time = endTime - startTime;
if (time > 30) {
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated header/footer for group {} took {}ms", group.name(), time);
}
},
250,
Math.max(200, group.headerFooterUpdateRate()),
TimeUnit.MILLISECONDS);
tasks.add(headerFooterTask);
}
if (group.formatUpdateRate() > 0) {
final ScheduledFuture<?> formatTask = processThread.scheduleAtFixedRate(() -> {
final long startTime = System.currentTimeMillis();
plugin.getTabList().updateGroupNames(group);
final long endTime = System.currentTimeMillis();
final long time = endTime - startTime;
if (time > 50) {
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated format for group {} took {}ms", group.name(), time);
}
},
500,
Math.max(200, group.formatUpdateRate()),
TimeUnit.MILLISECONDS);
tasks.add(formatTask);
}
if (group.nametagUpdateRate() > 0) {
final ScheduledFuture<?> nametagTask = processThread.scheduleAtFixedRate(() -> {
final long startTime = System.currentTimeMillis();
plugin.getTabList().updateSorting(group);
final long endTime = System.currentTimeMillis();
final long time = endTime - startTime;
if (time > 50) {
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated nametags/sorting for group {} took {}ms", group.name(), time);
}
},
750,
Math.max(200, group.nametagUpdateRate()),
TimeUnit.MILLISECONDS);
tasks.add(nametagTask);
}
if (group.placeholderUpdateRate() > 0) {
updateTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateGroupPlayers(group, true, false))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.placeholderUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
final ScheduledFuture<?> updateTask = processThread.scheduleAtFixedRate(() -> {
final long startTime = System.currentTimeMillis();
updatePlaceholders(group);
final long endTime = System.currentTimeMillis();
final long time = endTime - startTime;
if (time > 10) {
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated placeholders for group {} took {}ms", group.name(), time);
}
},
1000,
Math.max(200, group.placeholderUpdateRate()),
TimeUnit.MILLISECONDS);
tasks.add(updateTask);
}
latencyTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateLatency(group))
.delay(1, TimeUnit.SECONDS)
.repeat(3, TimeUnit.SECONDS)
.schedule();
final ScheduledFuture<?> latencyTask = processThread.scheduleAtFixedRate(() -> {
final long startTime = System.currentTimeMillis();
updateLatency(group);
final long endTime = System.currentTimeMillis();
final long time = endTime - startTime;
if (time > 10) {
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated latency for group {} took {}ms", group.name(), time);
}
},
1250,
2500,
TimeUnit.MILLISECONDS);
groupTasks.put(group, new GroupTasks(headerFooterTask, updateTask, latencyTask));
tasks.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(plugin);
players.forEach(player -> plugin.getPlaceholderManager().fetchPlaceholders(player.getPlayer().getUniqueId(), texts, group));
}
private void updateLatency(@NotNull Group group) {
@ -108,6 +163,7 @@ public class TaskManager {
if (groupPlayers.isEmpty()) {
return;
}
groupPlayers.stream()
.filter(player -> player.getPlayer().isActive())
.forEach(player -> {
@ -118,4 +174,11 @@ public class TaskManager {
});
}
public void run(@NotNull Runnable runnable) {
processThread.execute(runnable);
}
public void runDelayed(@NotNull Runnable runnable, long delay, @NotNull TimeUnit timeUnit) {
processThread.schedule(runnable, delay, timeUnit);
}
}

View File

@ -61,7 +61,7 @@ public class VanishTabList {
if (!p.getPlayer().getTabList().containsEntry(uuid)) {
tabList.createEntry(tabPlayer, p.getPlayer().getTabList(), p);
} else {
tabList.updateDisplayName(tabPlayer, p);
tabList.calculateAndSetDisplayName(tabPlayer, p);
}
});
@ -93,6 +93,10 @@ public class VanishTabList {
return;
}
if(!p.isActive() || !target.isLoaded()) {
return;
}
final boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) ||
plugin.getVanishManager().canSee(player.getUsername(), p.getUsername());

View File

@ -0,0 +1,122 @@
/*
* 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 net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.Iterator;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class DebugSystem {
private static final ConcurrentLinkedQueue<LogEntry> logs = new ConcurrentLinkedQueue<>();
private static final int MAX_LOGS = 10000;
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss");
private static final int REMOVE_HOURS = 6;
public enum DebugLevel {
INFO, WARNING, ERROR, DEBUG
}
private static class LogEntry {
final long timestamp;
final String threadName;
final DebugLevel level;
final String message;
LogEntry(@NotNull final String threadName, @NotNull final DebugLevel level, @NotNull final String message) {
this.timestamp = System.currentTimeMillis();
this.threadName = threadName;
this.level = level;
this.message = message;
}
@NotNull
public String format() {
return "[" + DATE_FORMAT.format(new Date(timestamp)) + "] [" + threadName + "/" + level + "]: " + message;
}
}
public static void log(@NotNull final DebugLevel level, @NotNull final String message) {
logs.add(new LogEntry(Thread.currentThread().getName(), level, message));
if (logs.size() > MAX_LOGS) {
logs.poll();
}
}
public static void log(@NotNull DebugLevel level, @NotNull String message, Object... args) {
logs.add(new LogEntry(Thread.currentThread().getName(), level, formatMessage(message, args)));
if (logs.size() > MAX_LOGS) {
logs.poll();
}
}
@NotNull
private static String formatMessage(@NotNull final String message, @NotNull final Object... args) {
final StringBuilder formattedMessage = new StringBuilder();
int argIndex = 0;
for (int i = 0; i < message.length(); i++) {
if (message.charAt(i) == '{' && i + 1 < message.length() && message.charAt(i + 1) == '}') {
formattedMessage.append(argIndex < args.length ? args[argIndex++] : "{}");
i++;
} else {
formattedMessage.append(message.charAt(i));
}
}
return formattedMessage.toString();
}
@NotNull
public static String getLogsAsString() {
final StringBuilder logBuilder = new StringBuilder();
for (final LogEntry entry : logs) {
logBuilder.append(entry.format()).append("\n");
}
return logBuilder.toString();
}
private static void removeLogsOlderThan() {
final long cutoffTime = System.currentTimeMillis() - (DebugSystem.REMOVE_HOURS * 3600L * 1000L);
final Iterator<LogEntry> iterator = logs.iterator();
while (iterator.hasNext()) {
final LogEntry entry = iterator.next();
if (entry.timestamp < cutoffTime) {
iterator.remove();
} else {
break;
}
}
}
public static void initializeTask(@NotNull Velocitab plugin) {
plugin.getServer().getScheduler().buildTask(plugin, DebugSystem::removeLogsOlderThan)
.delay(REMOVE_HOURS, TimeUnit.HOURS)
.repeat(REMOVE_HOURS, TimeUnit.HOURS)
.schedule();
}
}

View File

@ -0,0 +1,81 @@
/*
* 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 org.jetbrains.annotations.NotNull;
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) {
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++;
DebugSystem.log(DebugSystem.DebugLevel.WARNING, "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()

View File

@ -0,0 +1,92 @@
/*
* 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 org.jetbrains.annotations.NotNull;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StringUtil {
private static final Pattern UNICODE_PATTERN = Pattern.compile("\\\\u([0-9a-fA-F]{4})");
@NotNull
public static String unescapeJava(@NotNull String input) {
final StringBuilder output = new StringBuilder();
int length = input.length();
for (int i = 0; i < length; i++) {
char c = input.charAt(i);
if (c == '\\' && i + 1 < length) {
char next = input.charAt(i + 1);
switch (next) {
case 'b' -> { output.append('\b'); i++; }
case 't' -> { output.append('\t'); i++; }
case 'n' -> { output.append('\n'); i++; }
case 'f' -> { output.append('\f'); i++; }
case 'r' -> { output.append('\r'); i++; }
case '\"' -> { output.append('\"'); i++; }
case '\'' -> { output.append('\''); i++; }
case '\\' -> { output.append('\\'); i++; }
case 'u' -> {
if (i + 5 < length) {
String hex = input.substring(i + 2, i + 6);
try {
int unicode = Integer.parseInt(hex, 16);
output.append((char) unicode);
i += 5;
} catch (NumberFormatException e) {
output.append("\\u").append(hex);
i += 5;
}
} else {
output.append(c);
}
}
default -> {
output.append(c).append(next);
i++;
}
}
} else {
output.append(c);
}
}
return replaceUnicodeEscapes(output.toString());
}
@NotNull
private static String replaceUnicodeEscapes(@NotNull String input) {
final Matcher matcher = UNICODE_PATTERN.matcher(input);
final StringBuilder result = new StringBuilder();
while (matcher.find()) {
final String unicodeStr = matcher.group(1);
final int unicode = Integer.parseInt(unicodeStr, 16);
matcher.appendReplacement(result, Character.toString(unicode));
}
matcher.appendTail(result);
return result.toString();
}
}

View File

@ -17,24 +17,19 @@
* limitations under the License.
*/
package net.william278.velocitab.tab;
package net.william278.velocitab.util;
import com.velocitypowered.api.scheduler.ScheduledTask;
import org.jetbrains.annotations.Nullable;
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
public record GroupTasks(@Nullable ScheduledTask updateTask, @Nullable ScheduledTask headerFooterTask,
@Nullable ScheduledTask latencyTask) {
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @param v the third function argument
* @return the function result
*/
R apply(T t, U u, V v);
public void cancel() {
if (updateTask != null) {
updateTask.cancel();
}
if (headerFooterTask != null) {
headerFooterTask.cancel();
}
if (latencyTask != null) {
latencyTask.cancel();
}
}
}
}

View File

@ -22,6 +22,7 @@ package net.william278.velocitab.vanish;
import com.velocitypowered.api.proxy.Player;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.util.DebugSystem;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
@ -46,11 +47,23 @@ public class VanishManager {
}
public boolean canSee(@NotNull String name, @NotNull String otherName) {
return integration.canSee(name, otherName);
final long start = System.currentTimeMillis();
final boolean result = integration.canSee(name, otherName);
final long end = System.currentTimeMillis();
if (end - start > 2) {
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Vanish isVanished check took " + (end - start) + "ms");
}
return result;
}
public boolean isVanished(@NotNull String name) {
return integration.isVanished(name);
final long start = System.currentTimeMillis();
final boolean result = integration.isVanished(name);
final long end = System.currentTimeMillis();
if (end - start > 2) {
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Vanish isVanished check took " + (end - start) + "ms");
}
return result;
}
public void vanishPlayer(@NotNull Player player) {