feat: Add conditional & relational MiniPlaceholders support (#197)

* Added relational mini placeholders support
Fixed some problems
Removed 300ms delay after joining a server
Code refactor

* Updated MiniPlacehodlers dependency
Removed max team length for 1.18+ clients
Fixed problem of backend sending team packets for online players and added a warning message

* Added docs
Added more time/date placeholders

* Added mini condition system

* Fixed problem due to adventure string quoting

* Fixed problem in a rare use case

* Removed debug message

* Fixed conversations
Fixed packet unregistration problem

* Added docs
Fixed a problem

* Added yaml multi-line docs

* Changed docs

* Added papi support for conditions

* Cone clenaup

* Fixed placeholders in conditions

* Fixed conversations

* Fixed problems

* Fixed problems while using minedown or legacy
Added check for team packets tracker

* Fixed problems
Added support for hex colors in legacy formatter

* Fixed problems

* Fixed problem with header & footer

* Resolved conversations
This commit is contained in:
AlexDev_ 2024-06-29 14:32:29 +02:00 committed by GitHub
parent 06268521cf
commit ace3644111
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1591 additions and 454 deletions

View File

@ -24,8 +24,8 @@ ext {
repositories {
mavenCentral()
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://repo.william278.net/velocity/' }
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://repo.william278.net/releases/' }
maven { url = 'https://jitpack.io/' }
maven { url = 'https://repo.minebench.de/' }
@ -35,19 +35,21 @@ dependencies {
compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT"
compileOnly "com.velocitypowered:velocity-proxy:${velocity_api_version}-SNAPSHOT"
compileOnly 'io.netty:netty-codec-http:4.1.110.Final'
compileOnly 'org.projectlombok:lombok:1.18.32'
compileOnly 'net.luckperms:api:5.4'
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.2.3'
compileOnly 'net.william278:PAPIProxyBridge:1.5'
compileOnly 'it.unimi.dsi:fastutil:8.5.13'
compileOnly 'net.kyori:adventure-nbt:4.17.0'
implementation 'org.apache.commons:commons-text:1.12.0'
implementation 'net.william278:DesertWell:2.0.4'
implementation 'net.william278:minedown:1.8.2'
implementation 'org.bstats:bstats-velocity:3.0.2'
implementation 'de.exlll:configlib-yaml:4.5.0'
compileOnly 'io.netty:netty-codec-http:4.1.111.Final'
compileOnly 'net.luckperms:api:5.4'
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.0.0'
compileOnly 'net.william278:PAPIProxyBridge:1.5'
compileOnly 'it.unimi.dsi:fastutil:8.5.13'
compileOnly 'net.kyori:adventure-nbt:4.17.0'
compileOnly 'org.projectlombok:lombok:1.18.32'
implementation 'org.apache.commons:commons-jexl3:3.4.0'
implementation 'net.jodah:expiringmap:0.5.11'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}
@ -92,6 +94,10 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.velocitab.libraries.desertwell'
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')
@ -101,7 +107,9 @@ shadowJar {
destinationDirectory.set(file("$rootDir/target"))
archiveClassifier.set('')
minimize()
minimize() {
exclude dependency('commons-logging:commons-logging')
}
}
jar.dependsOn shadowJar
clean.delete "$rootDir/target"
@ -151,6 +159,11 @@ publishing {
tasks {
runVelocity {
velocityVersion("${velocity_api_version}-SNAPSHOT")
downloadPlugins {
modrinth ("papiproxybridge", "1.6.1")
modrinth ("miniplaceholders", "2.2.4")
}
}
}

View File

@ -0,0 +1,42 @@
# Velocitab Conditional Placeholders Documentation
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>>`.
Currently, this system is only available for the `format` and `nametag` fields in the tab groups configuration.
## Table of Conditional Placeholders
| 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` |
| `<velocitab_rel_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_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` |
| `<velocitab_rel_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_rel_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` |
**Note:** For string comparisons, use double quotes `" "` or single quotes `' '`. For numerical comparisons, quotes are
not needed.
Also if you use `'` for quotes, you need to escape them with `''`. The same applies for `"` and `""`. Example: `''%player_name%''` or `"'%player_name%'"`
In order to use papi placeholders for target you need to use `''%target_player_name%''` in order to get `''%player_name%''` replaced with the target player's name.
If you want to use `:` as a character in the condition or in the true/false value, you need to replace it with `?dp?`. Example: `<velocitab_rel_condition:%player_health% == %target_player_health%:Value?dp?True:Value?dp?False>`.
# Example
If you want to compare audience player's health with target player's health, you can use the following configuration:
```yaml
format: "<velocitab_rel_condition:%player_health% == %target_player_health%:Same health:Not same health>"
```
This is system is based on [JEXL](https://commons.apache.org/proper/commons-jexl/reference/examples.html) expressions.

View File

@ -122,5 +122,39 @@ Velocitab supports basic header and footer animations by adding multiple frames
### Placeholders
You can use various placeholders that will be replaced with values (for example, `%username%`) in your config. Support for PlaceholderAPI is also available through [a bridge library plugin](https://modrinth.com/plugin/papiproxybridge), as is the component-based MiniPlaceholders for users of that plugin with the MiniMessage formatter. See [[Placeholders]] for more information.
### YAML MultiLine Syntax
In order to have a multi-line string in YAML, you can use the `|-` or `|` syntax. The `|-` syntax will remove last newline character, while the `|` syntax will keep it.
You can also use `\n` to add a newline character in a string.
# Example 1
```yaml
foo: |-
bar 1
bar 2
bar 3
```
is equivalent to
```yaml
foo: "bar 1\nbar 2\nbar 3"
```
# Example 2
```yaml
foo: |
bar 1
bar 2
bar 3
```
is equivalent to
```yaml
foo: "bar 1\nbar 2\nbar 3\n"
```
### Server Links
For Minecraft 1.21+ clients, Velocitab supports specifying a list of URLs that will be sent to display in the player pause menu. See [[Server Links]] for more information.

View File

@ -15,6 +15,8 @@ Please click through to the topic you wish to read about.
* 📛 [[Nametags]]
* 📊 [[Sorting]]
* ✍️ [[Placeholders]]
* 🔗 [[Relational Placeholders]]
* 🔀 [[Conditional Placeholders]]
* ✨ [[Animations]]
* 🖼️ [[Custom Logos]]
* 🔗 [[Server Links]]

View File

@ -3,28 +3,42 @@ Velocitab supports a number of Placeholders that will be replaced with their res
## Default placeholders
Placeholders can be included in the header, footer and player name format of the TAB list. The following placeholders are supported out of the box:
| Placeholder | Description | Example |
|---------------------------------|------------------------------------------------------|--------------------|
| `%players_online%` | Players online on the proxy | `6` |
| `%max_players_online%` | Player capacity of the proxy | `500` |
| `%local_players_online%` | Players online on the server the player is on | `3` |
| `%group_players_online_(name)%` | Players online on the group provided | `11` |
| `%group_players_online%` | Players online on player's group | `15` |
| `%current_date%` | Current real-world date of the server | `24 Feb 2023` |
| `%current_time%` | Current real-world time of the server | `21:45:32` |
| `%username%` | The player's username | `William278` |
| `%username_lower%` | The player's username, in lowercase | `william278` |
| `%server%` | Name of the server the player is on | `alpha` |
| `%ping%` | Ping of the player (in ms) | `6` |
| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` |
| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` |
| `%role%` | The player's primary LuckPerms group name | `admin` |
| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` |
| `%role_weight%` | Comparable-formatted primary LuckPerms group weight. | `100` |
| `%luckperms_meta_(key)%` | Formats a meta key from the user's LuckPerms group | (varies) |
| `%server_group%` | The name of the server group the player is on | `default` |
| `%server_group_index%` | Indexed order of the server group in the list | `0` |
| `%debug_team_name%` | (Debug) Player's team name, used for [[Sorting]] | `1alphaWilliam278` |
| Placeholder | Description | Example |
|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| `%players_online%` | Players online on the proxy | `6` |
| `%max_players_online%` | Player capacity of the proxy | `500` |
| `%local_players_online%` | Players online on the server the player is on | `3` |
| `%group_players_online_(name)%` | Players online on the group provided | `11` |
| `%group_players_online%` | Players online on player's group | `15` |
| `%current_date_day%` | Current day of the month | `14` |
| `%current_date_weekday%` | Current day of the week | `Wednesday` |
| `%current_date_weekday_(tag)%` | Current day of the week ([localized](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags)) `it-IT` as example | `Mercoledì` |
| `%current_date_month%` | Current month of the year | `06` |
| `%current_date_year%` | Current year | `2024` |
| `%current_date%` | Current real-world date of the server | `14/06/2023` |
| `%current_date_(tag)%` | Current real-world date ([localized](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags)) `en-US` as example | `06/14/2023` |
| `%current_time_hour%` | Current hour of the day | `21` |
| `%current_time_minute%` | Current minute of the hour | `45` |
| `%current_time_second%` | Current second of the minute | `32` |
| `%current_time%` | Current real-world time of the server | `21:45:32` |
| `%current_time_(tag)%` | Current real-world time ([localized](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags)) `en-US` as example | `9:45 PM` |
| `%username%` | The player's username | `William278` |
| `%username_lower%` | The player's username, in lowercase | `william278` |
| `%server%` | Name of the server the player is on | `alpha` |
| `%ping%` | Ping of the player (in ms) | `6` |
| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` |
| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` |
| `%role%` | The player's primary LuckPerms group name | `admin` |
| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` |
| `%role_weight%` | Comparable-formatted primary LuckPerms group weight | `100` |
| `%luckperms_meta_(key)%` | Formats a meta key from the user's LuckPerms group | (varies) |
| `%server_group%` | The name of the server group the player is on | `default` |
| `%server_group_index%` | Indexed order of the server group in the list | `0` |
| `%debug_team_name%` | (Debug) Player's team name, used for [[Sorting]] | `1alphaWilliam278` |
**Note:** `(tag)` stands for IETF language tag, used for localization of date and time placeholders. For example, `en-US` for American English, `fr-FR` for French, `it-IT` for Italian, etc.
You can find a list of common primary language subtags [here](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags).
### Customising server display names
You can make use of the `server_display_names` feature in `config.yml` to customise how server display name appear when using the `%server%` placeholder. In the below example, if a user is connected to a server with the name "`very-long-server-`name" and the player name format for the group that server belongs to includes a `%server%` placeholder, the placeholder would be replaced with "`VSLN`" instead of the full server name.
@ -46,4 +60,5 @@ To use PlaceholderAPI placeholders in Velocitab, install the [PAPIProxyBridge](h
PlaceholderAPI placeholders are cached to reduce plugin message traffic. By default, placeholders are cached for 30 seconds (30000 milliseconds); if you wish to use PAPI placeholders that update more frequently, you can reduce the cache time in the Velocitab config.yml file by adjusting the `papi_cache_time` value.
## MiniPlaceholders support
If you are using MiniMessage [[Formatting]], you can use [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) with Velocitab for MiniMessage-styled component placeholders provided by other proxy plugins. Install MiniPlaceholders on your Velocity proxy, set the `formatter_type` to `MINIMESSAGE` and ensure `enable_miniplaceholders_hook` is set to `true`
If you are using MiniMessage [[Formatting]], you can use [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) with Velocitab for MiniMessage-styled component placeholders provided by other proxy plugins. Install MiniPlaceholders on your Velocity proxy, set the `formatter_type` to `MINIMESSAGE` and ensure `enable_miniplaceholders_hook` is set to `true`
You can also use [Relational Placeholders](Relational-Placeholders).

View File

@ -0,0 +1,26 @@
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`
In all examples target is the one that sees the message, and the audience is the one that is being seen.
Example:
My username is `William278` and I can see in tablist an audience player named `Player1`.
## Table of Placeholders
| Placeholder | Description | Example Usage |
|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------|
| `<velocitab_rel_who-is-seeing>` | Displays the username of the target player, used for debug | `William278` |
| `<velocitab_rel_perm:(permission):(value)>` | Checks if the target player has a specific permission and, if true parse value with the audience player's name. | See below |
| `<velocitab_rel_vanish>` | Checks if the audience player can see the target player, considering the vanish status. | `true` |
## Examples of `<velocitab_rel_perm:(permission):(value)>` Placeholder
**Note:** In the value, you can [Velocitab](Placeholders.md) placeholders or [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders/wiki/Placeholders#proxy-expansion)
| Placeholder Example Usage | Description | Output Example |
|------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|--------------------------|
| `<velocitab_rel_perm:check.server:Target is on %server%!>` | Checks if the target player has the permission 'check.server' and displays the server of the audience player. | `Target is on survival!` |
| `<velocitab_rel_perm:clientcheck:<player_client>>` | Checks if the target player has the permission 'clientcheck' and displays the client of the audience player. | `LunarClient` |
| `<velocitab_rel_perm:pingcheck:<player_ping>>` | Checks if the target player has the permission 'pingcheck' and displays the ping of the audience player. | `23` |

View File

@ -9,6 +9,8 @@
* 📛 [[Nametags]]
* 📊 [[Sorting]]
* ✍️ [[Placeholders]]
* 🔗 [[Relational Placeholders]]
* 🔀 [[Conditional Placeholders]]
* ✨ [[Animations]]
* 🖼️ [[Custom Logos]]
* 🔗 [[Server Links]]

View File

@ -43,6 +43,7 @@ import net.william278.velocitab.config.Settings;
import net.william278.velocitab.config.TabGroups;
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;
@ -113,9 +114,10 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
@Subscribe
public void onProxyShutdown(@NotNull ProxyShutdownEvent event) {
server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel);
// server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel);
disableScoreboardManager();
getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent);
getMiniPlaceholdersHook().ifPresent(MiniPlaceholdersHook::unregisterExpansion);
unregisterAPI();
logger.info("Successfully disabled Velocitab");
}

View File

@ -29,6 +29,7 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
/**
@ -190,6 +191,33 @@ public class VelocitabAPI {
return getUser(player).map(TabPlayer::getGroup).orElse(null);
}
/**
* Retrieves a list of server groups.
*
* @return A list of Group objects representing server groups.
* @since 1.6.6
*/
@NotNull
public List<Group> getServerGroups() {
return plugin.getTabGroups().getGroups();
}
/**
* Retrieves an optional Group object with the given name.
*
* @param name The name of the group to retrieve.
* @return An optional Group object containing the group with the given name, or an empty optional if no group exists with that name.
* @since 1.6.6
*/
@NotNull
public Optional<Group> getGroup(@NotNull String name) {
return plugin.getTabGroups().getGroup(name);
}
public Optional<Group> getGroupFromServer(@NotNull String server) {
return plugin.getTabGroups().getGroupFromServer(server, plugin);
}
/**
* An exception indicating the Velocitab API was accessed before it was registered.
*

View File

@ -156,7 +156,7 @@ public interface ConfigProvider {
/**
* 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.
* 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

View File

@ -25,8 +25,10 @@ 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 org.apache.commons.lang3.function.TriFunction;
import net.william278.velocitab.util.QuadFunction;
import net.william278.velocitab.util.SerializerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
@ -36,26 +38,33 @@ import java.util.function.Function;
@SuppressWarnings("unused")
public enum Formatter {
MINEDOWN(
(text, player, plugin) -> new MineDown(text).toComponent(),
(text, player, viewer, plugin) -> new MineDown(text).toComponent(),
MineDown::escape,
"MineDown",
(text) -> new MineDown(text).toComponent()
(text) -> new MineDown(text).toComponent(),
(text) -> {
throw new UnsupportedOperationException("MineDown does not support serialization");
}
),
MINIMESSAGE(
(text, player, plugin) -> plugin.getMiniPlaceholdersHook()
.map(hook -> hook.format(text, player.getPlayer()))
(text, player, viewer, plugin) -> plugin.getMiniPlaceholdersHook()
.filter(hook -> viewer != null)
.map(hook -> hook.format(text, player.getPlayer(), viewer.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(text)),
(text) -> MiniMessage.miniMessage().escapeTags(text),
"MiniMessage",
(text) -> MiniMessage.miniMessage().deserialize(text)
(text) -> MiniMessage.miniMessage().deserialize(text),
MiniMessage.miniMessage()::serialize
),
LEGACY(
(text, player, plugin) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text),
(text, player, viewer, plugin) -> SerializerUtil.LEGACY_SERIALIZER.deserialize(text),
Function.identity(),
"Legacy Text",
(text) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text)
SerializerUtil.LEGACY_SERIALIZER::deserialize,
SerializerUtil.LEGACY_SERIALIZER::serialize
);
/**
* Name of the formatter
*/
@ -64,24 +73,50 @@ public enum Formatter {
/**
* Function to apply formatting to a string
*/
private final TriFunction<String, TabPlayer, Velocitab, Component> formatter;
private final QuadFunction<String, TabPlayer, TabPlayer, Velocitab, Component> formatter;
/**
* Function to escape formatting characters in a string
*/
private final Function<String, String> escaper;
private final Function<String, Component> emptyFormatter;
private final Function<Component, String> serializer;
Formatter(@NotNull TriFunction<String, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper,
@NotNull String name, @NotNull Function<String, Component> emptyFormatter) {
Formatter(@NotNull QuadFunction<String, TabPlayer, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper,
@NotNull String name, @NotNull Function<String, Component> emptyFormatter, @NotNull Function<Component, String> serializer) {
this.formatter = formatter;
this.escaper = escaper;
this.name = name;
this.emptyFormatter = emptyFormatter;
this.serializer = serializer;
}
/**
* Formats the given text using a specific formatter.
*
* @param text The text to format
* @param player The TabPlayer object representing the player
* @param tabPlayer The TabPlayer object representing the viewer (can be null)
* @param plugin The Velocitab plugin instance
* @return The formatted Component object
* @throws NullPointerException if any of the parameters (text, player, plugin) is null
*/
@NotNull
public Component format(@NotNull String text, @NotNull TabPlayer player, @Nullable TabPlayer tabPlayer, @NotNull Velocitab plugin) {
return formatter.apply(text, player, tabPlayer, plugin);
}
/**
* Formats the given text using a specific formatter.
*
* @param text The text to format
* @param player The TabPlayer object representing the player
* @param plugin The Velocitab plugin instance
* @return The formatted Component object
* @throws NullPointerException if any of the parameters (text, player, plugin) is null
*/
@NotNull
public Component format(@NotNull String text, @NotNull TabPlayer player, @NotNull Velocitab plugin) {
return formatter.apply(text, player, plugin);
return formatter.apply(text, player, null, plugin);
}
@NotNull
@ -91,7 +126,7 @@ public enum Formatter {
}
@NotNull
public Component emptyFormat(@NotNull String text) {
public Component deserialize(@NotNull String text) {
return emptyFormatter.apply(text);
}
@ -105,4 +140,9 @@ public enum Formatter {
return name;
}
@NotNull
public String serialize(@NotNull Component component) {
return serializer.apply(component);
}
}

View File

@ -64,6 +64,11 @@ public record Group(
.get(Math.max(0, Math.min(index, footers.size() - 1))));
}
public boolean containsServer(@NotNull Velocitab plugin, @NotNull String serverName) {
return registeredServers(plugin).stream()
.anyMatch(registeredServer -> registeredServer.getServerInfo().getName().equalsIgnoreCase(serverName));
}
@NotNull
public Set<RegisteredServer> registeredServers(@NotNull Velocitab plugin) {
return registeredServers(plugin, true);

View File

@ -21,16 +21,23 @@ package net.william278.velocitab.config;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import it.unimi.dsi.fastutil.Pair;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.william278.velocitab.Velocitab;
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.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
@ -45,12 +52,47 @@ public enum Placeholder {
.map(RegisteredServer::getPlayersConnected)
.map(players -> Integer.toString(players.size()))
.orElse("")),
GROUP_PLAYERS_ONLINE_((param, plugin, player) -> plugin.getTabGroups().getGroup(param)
.map(group -> Integer.toString(group.getPlayers(plugin).size()))
.orElse("Group " + param + " not found")),
GROUP_PLAYERS_ONLINE((plugin, player) -> Integer.toString(player.getGroup().getPlayers(plugin).size())),
CURRENT_DATE((plugin, player) -> DateTimeFormatter.ofPattern("dd MMM yyyy").format(LocalDateTime.now())),
CURRENT_TIME((plugin, player) -> DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now())),
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.getServerDisplayName(plugin)),
@ -67,18 +109,26 @@ public enum Placeholder {
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()
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 PLACEHOLDER_PATTERN = Pattern.compile("%.*?%");
private final static Pattern CONDITIONAL_PATTERN = Pattern.compile("<velocitab_rel_condition:[^:]*:[^:]*:[^:]*>");
private final static String DELIMITER = ":::";
private final static String REL_SUBSTITUTE = "-REL-";
public final static Map<String, String> SYMBOL_SUBSTITUTES = Map.of(
"<", "-COND-1",
">", "-COND-2"
);
/**
* Function to replace placeholders with a real value
*/
private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
private final boolean parameterised;
private final Pattern pattern;
private final static Pattern CHECK_PLACEHOLDERS = Pattern.compile("%.*?%");
private final static String DELIMITER = ":::";
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
this.parameterised = false;
@ -89,7 +139,7 @@ public enum Placeholder {
Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) {
this.parameterised = true;
this.replacer = parameterisedReplacer;
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]+%", Pattern.CASE_INSENSITIVE);
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]*%", Pattern.CASE_INSENSITIVE);
}
public static CompletableFuture<Nametag> replace(@NotNull Nametag nametag, @NotNull Velocitab plugin,
@ -107,31 +157,88 @@ public enum Placeholder {
return "";
}
@NotNull
public static String replaceInternal(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
final Pair<Boolean, String> result = processRelationalPlaceholders(format, plugin);
format = result.right();
format = replacePlaceholders(format, plugin, player);
if (result.left()) {
format = format.replace(REL_SUBSTITUTE, "%");
}
return format;
}
private static Pair<Boolean, String> processRelationalPlaceholders(@NotNull String format, @NotNull Velocitab plugin) {
boolean foundRelational = false;
if (format.contains("<vel") && plugin.getFormatter().equals(Formatter.MINIMESSAGE)) {
final Matcher velocitabRelationalMatcher = VELOCITAB_PATTERN.matcher(format);
while (velocitabRelationalMatcher.find()) {
if (velocitabRelationalMatcher.group().contains("rel_condition")) {
continue;
}
foundRelational = true;
final String relationalPlaceholder = velocitabRelationalMatcher.group().substring(1, velocitabRelationalMatcher.group().length() - 1);
final String fixedString = replaceSymbols(relationalPlaceholder);
format = format.replace(relationalPlaceholder, fixedString);
}
format = processConditionalPlaceholders(format);
}
return Pair.of(foundRelational, format);
}
@NotNull
private static String processConditionalPlaceholders(@NotNull String format) {
final Matcher conditionalMatcher = CONDITIONAL_PATTERN.matcher(format);
while (conditionalMatcher.find()) {
String conditionalPlaceholder = conditionalMatcher.group();
conditionalPlaceholder = conditionalPlaceholder.substring(1, conditionalPlaceholder.length() - 1);
final String fixedString = replaceSymbols(conditionalPlaceholder);
format = format.replace(conditionalPlaceholder, fixedString);
}
return format;
}
@NotNull
private static String replacePlaceholders(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
for (Placeholder placeholder : values()) {
Matcher matcher = placeholder.pattern.matcher(format);
if (placeholder.parameterised) {
format = matcher.replaceAll(matchResult ->
Matcher.quoteReplacement(
placeholder.replacer.apply(StringUtils.chop(matchResult.group().replace("%" + placeholder.name().toLowerCase(), "")
.replaceFirst("_", ""))
, plugin, player)
));
} else {
format = matcher.replaceAll(matchResult -> Matcher.quoteReplacement(placeholder.replacer.apply(null, plugin, player)));
}
}
return format;
}
@NotNull
private static String replaceSymbols(@NotNull String input) {
String fixedString = input.replace("%", REL_SUBSTITUTE);
fixedString = MiniMessage.miniMessage().serialize(Formatter.LEGACY.deserialize(fixedString));
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
fixedString = fixedString.replace(entry.getKey(), entry.getValue());
}
return fixedString;
}
public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin,
@NotNull TabPlayer player) {
if (format.equals(DELIMITER)) {
return CompletableFuture.completedFuture("");
}
for (Placeholder placeholder : values()) {
Matcher matcher = placeholder.pattern.matcher(format);
if (placeholder.parameterised) {
// Replace the placeholder with the result of the replacer function with the parameter
format = matcher.replaceAll(matchResult ->
Matcher.quoteReplacement(placeholder.replacer.apply(
StringUtils.chop(matchResult.group().replace(
"%" + placeholder.name().toLowerCase(), ""
)), plugin, player
)));
} else {
// Replace the placeholder with the result of the replacer function
format = matcher.replaceAll(matchResult -> Matcher.quoteReplacement(placeholder.replacer.apply(null, plugin, player)));
}
final String replaced = replaceInternal(format, plugin, player);
}
final String replaced = format;
if (!CHECK_PLACEHOLDERS.matcher(replaced).find()) {
if (!PLACEHOLDER_PATTERN.matcher(replaced).find()) {
return CompletableFuture.completedFuture(replaced);
}

View File

@ -46,7 +46,7 @@ public record ServerUrl(
(type) -> CompletableFuture.completedFuture(ServerLink.serverLink(type, url()))
).orElseGet(
() -> Placeholder.replace(label(), plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
.thenApply(formatted -> ServerLink.serverLink(formatted, url()))
);
}

View File

@ -70,32 +70,34 @@ public class TabGroups implements ConfigValidator {
.orElseThrow(() -> new IllegalStateException("No group with name " + name + " found"));
}
@NotNull
public Optional<Group> getGroup(@NotNull String name) {
return groups.stream()
.filter(group -> group.name().equals(name))
.findFirst();
}
@NotNull
public Group getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
public Optional<Group> getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
final List<Group> groups = new ArrayList<>(this.groups);
final Optional<Group> defaultGroup = getGroup("default");
// Ensure the default group is always checked last
if (defaultGroup.isPresent()) {
groups.remove(defaultGroup.get());
groups.add(defaultGroup.get());
} else {
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 group;
return Optional.of(group);
}
}
return getGroupFromName("default");
if (!plugin.getSettings().isFallbackEnabled()) {
return Optional.empty();
}
return defaultGroup;
}
public int getPosition(@NotNull Group group) {

View File

@ -25,16 +25,38 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
public class MiniPlaceholdersHook extends Hook {
public final static Map<String, String> REPLACE = Map.of(
"\"", "-q-",
"'", "-a-"
);
private final VelocitabMiniExpansion expansion;
public MiniPlaceholdersHook(@NotNull Velocitab plugin) {
super(plugin);
this.expansion = new VelocitabMiniExpansion(plugin);
expansion.registerExpansion();
}
@NotNull
public Component format(@NotNull String text, @NotNull Audience player) {
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getAudienceGlobalPlaceholders(player));
public Component format(@NotNull String text, @NotNull Audience player, @Nullable Audience viewer) {
for (Map.Entry<String, String> entry : REPLACE.entrySet()) {
text = text.replace(entry.getKey(), entry.getValue());
}
if (viewer == null) {
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getAudienceGlobalPlaceholders(player));
}
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getRelationalGlobalPlaceholders(player, viewer));
}
public void unregisterExpansion() {
expansion.unregisterExpansion();
}
}

View File

@ -0,0 +1,125 @@
/*
* 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.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.hook.miniconditions.MiniConditionManager;
import net.william278.velocitab.player.TabPlayer;
import java.util.Map;
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 = queue.pop().value();
String replaced = Placeholder.replaceInternal(value, plugin, targetPlayer);
for (final Map.Entry<String, String> entry : Placeholder.SYMBOL_SUBSTITUTES.entrySet()) {
replaced = replaced.replace(entry.getValue(), entry.getKey());
}
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();
}
}

View File

@ -0,0 +1,196 @@
/*
* 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.hook.MiniPlaceholdersHook;
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 {
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());
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()) {
parameters.add(queue.pop().value());
}
return parameters;
}
@NotNull
private String decodeCondition(@NotNull String condition) {
condition = condition.replace("?lt;", "<").replace("?gt;", ">");
for (Map.Entry<String, String> entry : MiniPlaceholdersHook.REPLACE.entrySet()) {
condition = condition.replace(entry.getValue(), entry.getKey());
condition = condition.replace(entry.getKey() + entry.getKey(), entry.getKey());
}
for (Map.Entry<String, String> entry : Placeholder.SYMBOL_SUBSTITUTES.entrySet()) {
condition = condition.replace(entry.getValue(), 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

@ -19,7 +19,6 @@
package net.william278.velocitab.packet;
import com.google.common.collect.Sets;
import com.velocitypowered.api.event.AwaitingEventExecutor;
import com.velocitypowered.api.event.EventTask;
import com.velocitypowered.api.event.connection.DisconnectEvent;
@ -27,31 +26,20 @@ import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.network.Connections;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.channel.Channel;
import lombok.Getter;
import io.netty.channel.ChannelHandler;
import io.netty.channel.DefaultChannelPipeline;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public class PacketEventManager {
private static final String KEY = "velocitab";
private static final String CITIZENS_PREFIX = "CIT";
private final Velocitab plugin;
@Getter
private final Set<UUID> velocitabEntries;
public PacketEventManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.velocitabEntries = Sets.newConcurrentHashSet();
this.loadPlayers();
this.loadListeners();
}
@ -87,32 +75,17 @@ public class PacketEventManager {
public void removePlayer(@NotNull Player player) {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
final Channel channel = connectedPlayer.getConnection().getChannel();
if (channel.pipeline().get(KEY) != null) {
channel.pipeline().remove(KEY);
final ChannelHandler handler = channel.pipeline().get(KEY);
if (handler == null) {
return;
}
}
protected void handleEntry(@NotNull UpsertPlayerInfoPacket packet, @NotNull Player player) {
final List<TabPlayer> toUpdate = packet.getEntries().stream()
.filter(entry -> entry.getProfile() != null)
.filter(entry -> !entry.getProfile().getName().startsWith(CITIZENS_PREFIX))
.filter(entry -> velocitabEntries.stream().noneMatch(uuid -> uuid.equals(entry.getProfile().getId())))
.map(entry -> entry.getProfile().getId())
.map(id -> plugin.getTabList().getTabPlayer(id))
.filter(Optional::isPresent)
.map(Optional::get)
.filter(TabPlayer::isLoaded)
.toList();
if (toUpdate.isEmpty()) {
if (channel.pipeline() instanceof DefaultChannelPipeline defaultChannelPipeline) {
defaultChannelPipeline.removeIfExists(KEY);
return;
}
toUpdate.forEach(tabPlayer -> packet.getEntries().stream()
.filter(entry -> entry.getProfileId().equals(tabPlayer.getPlayer().getUniqueId()))
.findFirst()
.ifPresent(entry -> entry.setDisplayName(
new ComponentHolder(player.getProtocolVersion(), tabPlayer.getLastDisplayName()))));
plugin.getLogger().warn("Failed to remove player {} from Velocitab packet handler {}",
player.getUsername(), channel.pipeline().getClass().getName());
}
}

View File

@ -30,10 +30,7 @@ import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.function.Supplier;
// Based on VPacketEvents PacketRegistration API
@ -105,7 +102,7 @@ public final class PacketRegistration<P extends MinecraftPacket> {
try {
IntObjectMap<Supplier<?>> packetIdToSupplier = (IntObjectMap<Supplier<?>>) PACKET_REGISTRY$packetIdToSupplier.invoke(protocolRegistry);
Object2IntMap<Class<?>> packetClassToId = (Object2IntMap<Class<?>>) PACKET_REGISTRY$packetClassToId.invoke(protocolRegistry);
packetIdToSupplier.keySet().stream()
Set.copyOf(packetIdToSupplier.keySet()).stream()
.filter(supplier -> packetIdToSupplier.get(supplier).get().getClass().equals(packetClass))
.forEach(packetIdToSupplier::remove);
packetClassToId.values().intStream()

View File

@ -41,40 +41,79 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof final UpdateTeamsPacket updateTeamsPacket && plugin.getSettings().isSendScoreboardPackets()) {
final Optional<ScoreboardManager> scoreboardManager = plugin.getScoreboardManager();
if (scoreboardManager.isEmpty()) {
super.write(ctx, msg, promise);
return;
}
if (updateTeamsPacket.isRemoveTeam()) {
super.write(ctx, msg, promise);
return;
}
if (scoreboardManager.get().isInternalTeam(updateTeamsPacket.teamName())) {
super.write(ctx, msg, promise);
return;
}
if (!updateTeamsPacket.hasEntities()) {
super.write(ctx, msg, promise);
return;
}
if (updateTeamsPacket.entities().stream().noneMatch(entity -> plugin.getServer().getPlayer(entity).isPresent())) {
super.write(ctx, msg, promise);
return;
}
// Cancel packet if the backend is trying to send a team packet with an online player.
// 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",
updateTeamsPacket.teamName(), player.getUsername());
return;
}
if (!(msg instanceof final UpsertPlayerInfoPacket minecraftPacket)) {
super.write(ctx, msg, promise);
return;
}
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
if (tabPlayer.isEmpty()) {
try {
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
if (tabPlayer.isEmpty()) {
super.write(ctx, msg, promise);
return;
}
if (plugin.getSettings().isRemoveSpectatorEffect() && minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE)) {
forceGameMode(minecraftPacket.getEntries());
}
//fix for duplicate entries
if (minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) {
minecraftPacket.getEntries().stream()
.filter(entry -> entry.getProfile() != null && !entry.getProfile().getId().equals(entry.getProfileId()))
.forEach(entry -> entry.setListed(false));
}
if (!minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER) && !minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) {
super.write(ctx, msg, promise);
return;
}
if (minecraftPacket.getEntries().stream().allMatch(entry -> entry.getProfile() != null && entry.getProfile().getName().startsWith("CIT"))) {
super.write(ctx, msg, promise);
return;
}
super.write(ctx, msg, promise);
return;
}
if (plugin.getSettings().isRemoveSpectatorEffect() && minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE)) {
forceGameMode(minecraftPacket.getEntries());
}
//fix for duplicate entries
if (minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) {
minecraftPacket.getEntries().stream()
.filter(entry -> entry.getProfile() != null && !entry.getProfile().getId().equals(entry.getProfileId()))
.forEach(entry -> entry.setListed(false));
}
if (!minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER) && !minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) {
} catch (Exception e) {
plugin.getLogger().error("An error occurred while handling a packet", e);
super.write(ctx, msg, promise);
return;
}
if (minecraftPacket.getEntries().stream().allMatch(entry -> entry.getProfile() != null && entry.getProfile().getName().startsWith("CIT"))) {
super.write(ctx, msg, promise);
return;
}
plugin.getPacketEventManager().handleEntry(minecraftPacket, player);
super.write(ctx, msg, promise);
}
private void forceGameMode(@NotNull List<UpsertPlayerInfoPacket.Entry> entries) {

View File

@ -55,7 +55,34 @@ public class Protocol404Adapter extends TeamsPacketAdapter {
public Protocol404Adapter(@NotNull Velocitab plugin, Set<ProtocolVersion> protocolVersions) {
super(plugin, protocolVersions);
serializer = null;
serializer = GsonComponentSerializer.colorDownsamplingGson();
}
@Override
public void decode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
packet.teamName(ProtocolUtils.readString(byteBuf));
UpdateTeamsPacket.UpdateMode mode = UpdateTeamsPacket.UpdateMode.byId(byteBuf.readByte());
packet.mode(mode);
if (mode == UpdateTeamsPacket.UpdateMode.REMOVE_TEAM) {
return;
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.UPDATE_INFO) {
packet.displayName(readComponent(byteBuf));
packet.friendlyFlags(UpdateTeamsPacket.FriendlyFlag.fromBitMask(byteBuf.readByte()));
packet.nametagVisibility(UpdateTeamsPacket.NametagVisibility.byId(ProtocolUtils.readString(byteBuf)));
packet.collisionRule(UpdateTeamsPacket.CollisionRule.byId(ProtocolUtils.readString(byteBuf)));
packet.color(byteBuf.readByte());
packet.prefix(readComponent(byteBuf));
packet.suffix(readComponent(byteBuf));
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.ADD_PLAYERS || mode == UpdateTeamsPacket.UpdateMode.REMOVE_PLAYERS) {
int count = ProtocolUtils.readVarInt(byteBuf);
List<String> entities = new ArrayList<>();
for (int i = 0; i < count; i++) {
entities.add(ProtocolUtils.readString(byteBuf));
}
packet.entities(entities);
}
}
@Override
@ -84,8 +111,13 @@ public class Protocol404Adapter extends TeamsPacketAdapter {
}
}
protected void writeComponent(ByteBuf buf, Component component) {
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
ProtocolUtils.writeString(buf, serializer.serialize(component));
}
@NotNull
protected Component readComponent(@NotNull ByteBuf buf) {
return serializer.deserialize(ProtocolUtils.readString(buf));
}
}

View File

@ -44,6 +44,35 @@ public class Protocol48Adapter extends TeamsPacketAdapter {
serializer = LegacyComponentSerializer.legacySection();
}
@Override
public void decode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
packet.teamName(ProtocolUtils.readString(byteBuf));
UpdateTeamsPacket.UpdateMode mode = UpdateTeamsPacket.UpdateMode.byId(byteBuf.readByte());
packet.mode(mode);
if (mode == UpdateTeamsPacket.UpdateMode.REMOVE_TEAM) {
return;
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.UPDATE_INFO) {
packet.displayName(readComponent(byteBuf));
packet.prefix(readComponent(byteBuf));
packet.suffix(readComponent(byteBuf));
packet.friendlyFlags(UpdateTeamsPacket.FriendlyFlag.fromBitMask(byteBuf.readByte()));
packet.nametagVisibility(UpdateTeamsPacket.NametagVisibility.byId(ProtocolUtils.readString(byteBuf)));
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_12_2) >= 0) {
packet.collisionRule(UpdateTeamsPacket.CollisionRule.byId(ProtocolUtils.readString(byteBuf)));
}
packet.color(byteBuf.readByte());
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.ADD_PLAYERS || mode == UpdateTeamsPacket.UpdateMode.REMOVE_PLAYERS) {
int count = ProtocolUtils.readVarInt(byteBuf);
List<String> entities = new ArrayList<>();
for (int i = 0; i < count; i++) {
entities.add(ProtocolUtils.readString(byteBuf));
}
packet.entities(entities);
}
}
@Override
public void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
ProtocolUtils.writeString(byteBuf, shrinkString(packet.teamName()));
@ -83,7 +112,12 @@ public class Protocol48Adapter extends TeamsPacketAdapter {
return string.substring(0, Math.min(string.length(), 16));
}
protected void writeComponent(ByteBuf buf, Component component) {
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
ProtocolUtils.writeString(buf, shrinkString(serializer.serialize(component)));
}
@NotNull
protected Component readComponent(@NotNull ByteBuf buf) {
return serializer.deserialize(ProtocolUtils.readString(buf));
}
}

View File

@ -58,8 +58,13 @@ public class Protocol735Adapter extends Protocol404Adapter {
}
@Override
protected void writeComponent(ByteBuf buf, Component component) {
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
ProtocolUtils.writeString(buf, serializer.serialize(component));
}
@NotNull
protected Component readComponent(@NotNull ByteBuf buf) {
return serializer.deserialize(ProtocolUtils.readString(buf));
}
}

View File

@ -44,9 +44,14 @@ public class Protocol765Adapter extends Protocol404Adapter {
));
}
protected void writeComponent(ByteBuf buf, Component component) {
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
final BinaryTag tag = ComponentHolder.serialize(GsonComponentSerializer.gson().serializeToTree(component));
ProtocolUtils.writeBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, tag);
}
@NotNull
protected Component readComponent(@NotNull ByteBuf buf) {
return GsonComponentSerializer.gson().deserializeFromTree(ComponentHolder.deserialize(ProtocolUtils.readBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, null)));
}
}

View File

@ -20,6 +20,8 @@
package net.william278.velocitab.packet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
@ -28,6 +30,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
@ -49,12 +52,14 @@ public class ScoreboardManager {
private final Set<TeamsPacketAdapter> versions;
private final Map<UUID, String> createdTeams;
private final Map<String, Nametag> nametags;
private final Multimap<UUID, String> trackedTeams;
public ScoreboardManager(@NotNull Velocitab velocitab) {
this.plugin = velocitab;
this.createdTeams = Maps.newConcurrentMap();
this.nametags = Maps.newConcurrentMap();
this.versions = Sets.newHashSet();
this.trackedTeams = Multimaps.synchronizedMultimap(Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet));
this.registerVersions();
}
@ -70,6 +75,10 @@ public class ScoreboardManager {
}
}
public boolean isInternalTeam(@NotNull String teamName) {
return nametags.containsKey(teamName);
}
@NotNull
public TeamsPacketAdapter getPacketAdapter(@NotNull ProtocolVersion version) {
return versions.stream()
@ -88,6 +97,7 @@ public class ScoreboardManager {
plugin.getTabList().getTabPlayer(player).ifPresent(tabPlayer ->
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), tabPlayer)
);
trackedTeams.removeAll(player.getUniqueId());
}
}
@ -119,13 +129,15 @@ public class ScoreboardManager {
final Set<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
final Optional<Nametag> cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null));
cachedTag.ifPresent(nametag -> {
final UpdateTeamsPacket packet = vanish ? UpdateTeamsPacket.removeTeam(plugin, teamName) :
UpdateTeamsPacket.create(plugin, tabPlayer, teamName, nametag, player.getUsername());
siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player)
.filter(p -> vanish && !plugin.getVanishManager().canSee(p.getUsername(), player.getUsername()))
.forEach(connected -> dispatchPacket(packet, connected)));
});
cachedTag.ifPresent(nametag -> siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player)
.forEach(connected -> {
if (vanish && !plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername())) {
dispatchPacket(UpdateTeamsPacket.removeTeam(plugin, teamName), connected);
trackedTeams.remove(connected.getUniqueId(), teamName);
} else {
dispatchGroupCreatePacket(plugin, tabPlayer, teamName, nametag, player.getUsername());
}
})));
}
/**
@ -154,16 +166,12 @@ public class ScoreboardManager {
createdTeams.put(player.getUniqueId(), role);
this.nametags.put(role, newTag);
dispatchGroupPacket(
UpdateTeamsPacket.create(plugin, tabPlayer, role, newTag, name),
tabPlayer
);
dispatchGroupCreatePacket(plugin, tabPlayer, role, newTag, name);
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) {
this.nametags.put(role, newTag);
dispatchGroupPacket(
UpdateTeamsPacket.changeNametag(plugin, tabPlayer, role, newTag),
tabPlayer
);
dispatchGroupChangePacket(plugin, tabPlayer, role, newTag);
} else {
updatePlaceholders(tabPlayer);
}
}).exceptionally(e -> {
plugin.log(Level.ERROR, "Failed to update role for " + player.getUsername(), e);
@ -171,6 +179,17 @@ public class ScoreboardManager {
});
}
public void updatePlaceholders(@NotNull TabPlayer tabPlayer) {
final Player player = tabPlayer.getPlayer();
final String role = createdTeams.get(player.getUniqueId());
if (role == null) {
return;
}
final Optional<Nametag> optionalNametag = Optional.ofNullable(nametags.get(role));
optionalNametag.ifPresent(nametag -> dispatchGroupChangePacket(plugin, tabPlayer, role, nametag));
}
public void resendAllTeams(@NotNull TabPlayer tabPlayer) {
if (!plugin.getSettings().isSendScoreboardPackets()) {
@ -196,7 +215,6 @@ public class ScoreboardManager {
}
final Optional<TabPlayer> optionalTabPlayer = plugin.getTabList().getTabPlayer(p);
if (optionalTabPlayer.isEmpty()) {
return;
}
@ -212,14 +230,69 @@ public class ScoreboardManager {
// Send packet
final Nametag tag = nametags.get(role);
if (tag != null) {
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(
plugin, targetTabPlayer, role, tag, p.getUsername()
);
dispatchPacket(packet, player);
dispatchCreatePacket(plugin, targetTabPlayer, role, tag, tabPlayer, p.getUsername());
}
});
}
private void dispatchGroupCreatePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName, @NotNull Nametag nametag,
@NotNull String... teamMembers) {
tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> {
if (!viewer.getPlayer().isActive()) {
return;
}
dispatchCreatePacket(plugin, tabPlayer, teamName, nametag, viewer, teamMembers);
});
}
private void dispatchCreatePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName, @NotNull Nametag nametag,
@NotNull TabPlayer viewer,
@NotNull String... teamMembers) {
final boolean canSee = plugin.getVanishManager().canSee(viewer.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername());
if (!canSee) {
return;
}
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(plugin, tabPlayer, teamName, nametag, viewer, teamMembers);
trackedTeams.put(viewer.getPlayer().getUniqueId(), teamName);
dispatchPacket(packet, viewer.getPlayer());
}
private void dispatchGroupChangePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName,
@NotNull Nametag nametag) {
tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> {
if (viewer == tabPlayer || !viewer.getPlayer().isActive()) {
return;
}
final boolean canSee = plugin.getVanishManager().canSee(viewer.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername());
if (!canSee) {
return;
}
// Prevent sending change nametag packets to players who are not tracking the team
if (!trackedTeams.containsEntry(viewer.getPlayer().getUniqueId(), teamName)) {
return;
}
final UpdateTeamsPacket packet = UpdateTeamsPacket.changeNametag(plugin, tabPlayer, teamName, viewer, nametag);
final Component prefix = packet.prefix();
final Component suffix = packet.suffix();
final Optional<Component[]> cached = tabPlayer.getRelationalNametag(viewer.getPlayer().getUniqueId());
// Skip if the nametag is the same as the cached one
if (cached.isPresent() && cached.get()[0].equals(prefix) && cached.get()[1].equals(suffix)) {
return;
}
tabPlayer.setRelationalNametag(viewer.getPlayer().getUniqueId(), prefix, suffix);
dispatchPacket(packet, viewer.getPlayer());
});
}
private void dispatchPacket(@NotNull UpdateTeamsPacket packet, @NotNull Player player) {
if (!player.isActive()) {
plugin.getTabList().removeOfflinePlayer(player);
@ -235,10 +308,14 @@ public class ScoreboardManager {
}
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Group group) {
final boolean isRemove = packet.isRemoveTeam();
group.registeredServers(plugin).forEach(server -> server.getPlayersConnected().forEach(connected -> {
try {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected;
connectedPlayer.getConnection().write(packet);
if (isRemove) {
trackedTeams.remove(connected.getUniqueId(), packet.teamName());
}
} catch (Throwable e) {
plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e);
}
@ -274,18 +351,18 @@ public class ScoreboardManager {
.direction(ProtocolUtils.Direction.CLIENTBOUND)
.packetSupplier(() -> new UpdateTeamsPacket(plugin))
.stateRegistry(StateRegistry.PLAY)
.mapping(0x3E, MINECRAFT_1_8, true)
.mapping(0x44, MINECRAFT_1_12_2, true)
.mapping(0x47, MINECRAFT_1_13, true)
.mapping(0x4B, MINECRAFT_1_14, true)
.mapping(0x4C, MINECRAFT_1_15, true)
.mapping(0x55, MINECRAFT_1_17, true)
.mapping(0x58, MINECRAFT_1_19_1, true)
.mapping(0x56, MINECRAFT_1_19_3, true)
.mapping(0x5A, MINECRAFT_1_19_4, true)
.mapping(0x5C, MINECRAFT_1_20_2, true)
.mapping(0x5E, MINECRAFT_1_20_3, true)
.mapping(0x60, MINECRAFT_1_20_5, true);
.mapping(0x3E, MINECRAFT_1_8, false)
.mapping(0x44, MINECRAFT_1_12_2, false)
.mapping(0x47, MINECRAFT_1_13, false)
.mapping(0x4B, MINECRAFT_1_14, false)
.mapping(0x4C, MINECRAFT_1_15, false)
.mapping(0x55, MINECRAFT_1_17, false)
.mapping(0x58, MINECRAFT_1_19_1, false)
.mapping(0x56, MINECRAFT_1_19_3, false)
.mapping(0x5A, MINECRAFT_1_19_4, false)
.mapping(0x5C, MINECRAFT_1_20_2, false)
.mapping(0x5E, MINECRAFT_1_20_3, false)
.mapping(0x60, MINECRAFT_1_20_5, false);
packetRegistration.register();
} catch (Throwable e) {
plugin.log(Level.ERROR, "Failed to register UpdateTeamsPacket", e);
@ -320,14 +397,12 @@ public class ScoreboardManager {
final UpdateTeamsPacket removeTeam = UpdateTeamsPacket.removeTeam(plugin, team);
dispatchPacket(removeTeam, player);
trackedTeams.remove(player.getUniqueId(), team);
if (canSee) {
final Nametag tag = nametags.get(team);
if (tag != null) {
final UpdateTeamsPacket addTeam = UpdateTeamsPacket.create(
plugin, tabPlayer, team, tag, target.getPlayer().getUsername()
);
dispatchPacket(addTeam, player);
dispatchCreatePacket(plugin, tabPlayer, team, tag, target, target.getPlayer().getUsername());
}
}
}

View File

@ -38,6 +38,11 @@ public abstract class TeamsPacketAdapter {
public abstract void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion);
protected abstract void writeComponent(ByteBuf buf, Component component);
public abstract void decode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion);
protected abstract void writeComponent(@NotNull ByteBuf buf, @NotNull Component component);
@NotNull
protected abstract Component readComponent(@NotNull ByteBuf buf);
}

View File

@ -64,21 +64,29 @@ public class UpdateTeamsPacket implements MinecraftPacket {
this.plugin = plugin;
}
public boolean isRemoveTeam() {
return mode == UpdateMode.REMOVE_TEAM;
}
public boolean hasEntities() {
return entities != null && !entities.isEmpty();
}
@NotNull
protected static UpdateTeamsPacket create(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName,
@NotNull Nametag nametag,
@NotNull String teamName, @NotNull Nametag nametag,
@NotNull TabPlayer viewer,
@NotNull String... teamMembers) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
.teamName(teamName)
.mode(UpdateMode.CREATE_TEAM)
.displayName(Component.empty())
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
.collisionRule(tabPlayer.getGroup().collisions() ? CollisionRule.ALWAYS : CollisionRule.NEVER)
.color(getLastColor(tabPlayer, nametag.prefix(), plugin))
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer))
.prefix(nametag.getPrefixComponent(plugin, tabPlayer, viewer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer, viewer))
.entities(Arrays.asList(teamMembers));
}
@ -92,25 +100,25 @@ public class UpdateTeamsPacket implements MinecraftPacket {
@NotNull
protected static UpdateTeamsPacket changeNametag(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName,
@NotNull String teamName, @NotNull TabPlayer viewer,
@NotNull Nametag nametag) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
.teamName(teamName)
.mode(UpdateMode.UPDATE_INFO)
.displayName(Component.empty())
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
.collisionRule(tabPlayer.getGroup().collisions() ? CollisionRule.ALWAYS : CollisionRule.NEVER)
.color(getLastColor(tabPlayer, nametag.prefix(), plugin))
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer));
.prefix(nametag.getPrefixComponent(plugin, tabPlayer, viewer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer, viewer));
}
@NotNull
protected static UpdateTeamsPacket addToTeam(@NotNull Velocitab plugin, @NotNull String teamName,
@NotNull String... teamMembers) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
.teamName(teamName)
.mode(UpdateMode.ADD_PLAYERS)
.entities(Arrays.asList(teamMembers));
}
@ -119,7 +127,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
protected static UpdateTeamsPacket removeFromTeam(@NotNull Velocitab plugin, @NotNull String teamName,
@NotNull String... teamMembers) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
.teamName(teamName)
.mode(UpdateMode.REMOVE_PLAYERS)
.entities(Arrays.asList(teamMembers));
}
@ -127,7 +135,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
@NotNull
protected static UpdateTeamsPacket removeTeam(@NotNull Velocitab plugin, @NotNull String teamName) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
.teamName(teamName)
.mode(UpdateMode.REMOVE_TEAM);
}
@ -144,10 +152,10 @@ public class UpdateTeamsPacket implements MinecraftPacket {
text = text + "z";
//serialize & deserialize to downsample rgb to legacy
Component component = plugin.getFormatter().emptyFormat(text);
final Component component = plugin.getFormatter().deserialize(text);
text = LegacyComponentSerializer.legacyAmpersand().serialize(component);
int lastFormatIndex = text.lastIndexOf("&");
final int lastFormatIndex = text.lastIndexOf("&");
if (lastFormatIndex == -1 || lastFormatIndex == text.length() - 1) {
return 15;
}
@ -200,16 +208,21 @@ public class UpdateTeamsPacket implements MinecraftPacket {
@Override
public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException("Operation not supported");
final Optional<ScoreboardManager> optionalManager = plugin.getScoreboardManager();
if (optionalManager.isEmpty()) {
return;
}
optionalManager.get().getPacketAdapter(protocolVersion).decode(byteBuf, this, protocolVersion);
}
@Override
public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
final Optional<ScoreboardManager> optionalManager = plugin.getScoreboardManager();
if (optionalManager.isEmpty()) {
return;
}
optionalManager.get().getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion);
}

View File

@ -19,12 +19,16 @@
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;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Formatter;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.packet.UpdateTeamsPacket;
@ -34,20 +38,29 @@ import org.apache.commons.lang3.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
import java.util.*;
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("%(\\w+)%");
private static final String PLACEHOLDER_DELIMITER = "<-DELIMITER->";
private final Velocitab plugin;
private final Player player;
@Setter
private Role role;
private int headerIndex = 0;
private int footerIndex = 0;
private Component lastDisplayName;
// 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 String lastDisplayName;
private Component lastHeader;
private Component lastFooter;
private String teamName;
@ -72,6 +85,9 @@ public final class TabPlayer implements Comparable<TabPlayer> {
this.player = player;
this.role = role;
this.group = group;
this.relationalDisplayNames = Maps.newConcurrentMap();
this.relationalNametags = Maps.newConcurrentMap();
this.cachedPlaceholders = Maps.newConcurrentMap();
}
@NotNull
@ -115,10 +131,47 @@ public final class TabPlayer implements Comparable<TabPlayer> {
}
@NotNull
public CompletableFuture<Component> getDisplayName(@NotNull Velocitab plugin) {
return Placeholder.replace(group.format(), plugin, this)
.thenApply(formatted -> plugin.getFormatter().format(formatted, this, plugin))
.thenApply(c -> this.lastDisplayName = c);
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 = displayName.replace("\n", "");
final boolean isMiniMessage = plugin.getFormatter().equals(Formatter.MINIMESSAGE);
if (isMiniMessage) {
displayName = Formatter.LEGACY.serialize(MiniMessage.miniMessage().deserialize(displayName));
}
displayName = Placeholder.replaceInternal(displayName, plugin, this);
if (isMiniMessage) {
displayName = MiniMessage.miniMessage().serialize(Formatter.LEGACY.deserialize(displayName))
.replace("\\<", "<");
}
return lastDisplayName = displayName;
}
@NotNull
@ -140,18 +193,18 @@ public final class TabPlayer implements Comparable<TabPlayer> {
return tabList.getHeader(this).thenCompose(header -> tabList.getFooter(this).thenAccept(footer -> {
final boolean disabled = plugin.getSettings().isDisableHeaderFooterIfEmpty();
if (disabled) {
if (!Component.empty().equals(header)) {
if ((!Component.empty().equals(header) && !header.equals(lastHeader)) ||
(!Component.empty().equals(footer) && !footer.equals(lastFooter))) {
lastHeader = header;
player.sendPlayerListHeader(header);
}
if (!Component.empty().equals(footer)) {
lastFooter = footer;
player.sendPlayerListFooter(footer);
player.sendPlayerListHeaderAndFooter(header, footer);
}
} else {
lastHeader = header;
lastFooter = footer;
player.sendPlayerListHeaderAndFooter(header, footer);
if (!header.equals(lastHeader) || !footer.equals(lastFooter)) {
lastHeader = header;
lastFooter = footer;
player.sendPlayerListHeaderAndFooter(header, footer);
}
}
}));
}
@ -175,6 +228,39 @@ public final class TabPlayer implements Comparable<TabPlayer> {
}
}
public void setRelationalDisplayName(@NotNull UUID target, @NotNull Component displayName) {
relationalDisplayNames.put(target, displayName);
}
public void unsetRelationalDisplayName(@NotNull UUID target) {
relationalDisplayNames.remove(target);
}
public Optional<Component> getRelationalDisplayName(@NotNull UUID target) {
return Optional.ofNullable(relationalDisplayNames.get(target));
}
public void setRelationalNametag(@NotNull UUID target, @NotNull Component prefix, @NotNull Component suffix) {
relationalNametags.put(target, new Component[]{prefix, suffix});
}
public void unsetRelationalNametag(@NotNull UUID target) {
relationalNametags.remove(target);
}
public Optional<Component[]> getRelationalNametag(@NotNull UUID target) {
return Optional.ofNullable(relationalNametags.get(target));
}
public void clearCachedData() {
loaded = false;
relationalDisplayNames.clear();
relationalNametags.clear();
lastHeader = null;
lastFooter = null;
role = Role.DEFAULT_ROLE;
}
/**
* Returns the custom name of the TabPlayer, if it has been set.
*
@ -197,4 +283,8 @@ 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

@ -55,6 +55,7 @@ public interface LoggerProvider {
getLogger().warn(message);
}
}
case DEBUG -> getLogger().debug(message);
case INFO -> getLogger().info(message);
}
}

View File

@ -86,7 +86,7 @@ public interface ScoreboardProvider {
*/
default void prepareScoreboard() {
if (getPlugin().getSettings().isSendScoreboardPackets()) {
ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin());
final ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin());
setScoreboardManager(scoreboardManager);
scoreboardManager.registerPacket();
}

View File

@ -20,6 +20,7 @@
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;
@ -49,7 +50,7 @@ public class SortingManager {
return Placeholder.replace(String.join(DELIMITER, player.getGroup().sortingPlaceholders()), plugin, player)
.thenApply(s -> Arrays.asList(s.split(DELIMITER)))
.thenApply(v -> v.stream().map(this::adaptValue).collect(Collectors.toList()))
.thenApply(v -> v.stream().map(s -> adaptValue(s, player)).collect(Collectors.toList()))
.thenApply(v -> handleList(player, v));
}
@ -57,7 +58,7 @@ public class SortingManager {
private String handleList(@NotNull TabPlayer player, @NotNull List<String> values) {
String result = String.join("", values);
if (result.length() > 12) {
if (result.length() > 12 && isLongTeamNotAllowed(player)) {
result = result.substring(0, 12);
}
@ -66,8 +67,13 @@ public class SortingManager {
return result;
}
private boolean isLongTeamNotAllowed(@NotNull TabPlayer player) {
return !player.getGroup().getPlayers(plugin, player).stream()
.allMatch(t -> t.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_18));
}
@NotNull
private String adaptValue(@NotNull String value) {
private String adaptValue(@NotNull String value, @NotNull TabPlayer player) {
if (value.isEmpty()) {
return "";
}
@ -78,7 +84,7 @@ public class SortingManager {
return compressNumber(Integer.MAX_VALUE / 4d - parsed);
}
if (value.length() > 6) {
if (value.length() > 6 && isLongTeamNotAllowed(player)) {
return value.substring(0, 4);
}

View File

@ -30,13 +30,13 @@ import org.jetbrains.annotations.NotNull;
public record Nametag(@NotNull String prefix, @NotNull String suffix) {
@NotNull
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
return plugin.getFormatter().format(prefix, tabPlayer, plugin);
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
return plugin.getFormatter().format(prefix, tabPlayer, target, plugin);
}
@NotNull
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
return plugin.getFormatter().format(suffix, tabPlayer, plugin);
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
return plugin.getFormatter().format(suffix, tabPlayer, target, plugin);
}
@Override

View File

@ -26,7 +26,6 @@ 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.scheduler.ScheduledTask;
import lombok.AccessLevel;
import lombok.Getter;
import net.kyori.adventure.text.Component;
@ -45,6 +44,7 @@ import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* The main class for tracking the server TAB list
@ -55,15 +55,16 @@ public class PlayerTabList {
private final VanishTabList vanishTabList;
@Getter(value = AccessLevel.PUBLIC)
private final Map<UUID, TabPlayer> players;
private final Map<Group, GroupTasks> groupTasks;
private final TaskManager taskManager;
public PlayerTabList(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.vanishTabList = new VanishTabList(plugin, this);
this.players = Maps.newConcurrentMap();
this.groupTasks = Maps.newConcurrentMap();
this.taskManager = new TaskManager(plugin);
this.reloadUpdate();
this.registerListener();
this.ensureDisplayNameTask();
}
private void registerListener() {
@ -102,16 +103,12 @@ public class PlayerTabList {
}
final String serverName = server.get().getServerInfo().getName();
final Group group = getGroup(serverName);
final boolean isDefault = group.registeredServers(plugin)
.stream()
.noneMatch(s -> s.getServerInfo().getName().equals(serverName));
if (isDefault && !plugin.getSettings().isFallbackEnabled()) {
final @NotNull Optional<Group> group = getGroup(serverName);
if (group.isEmpty()) {
return;
}
joinPlayer(p, group);
joinPlayer(p, group.get());
});
}
@ -120,7 +117,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() {
groupTasks.values().forEach(GroupTasks::cancel);
taskManager.cancelAllTasks();
plugin.getServer().getAllPlayers().forEach(p -> {
final Optional<ServerConnection> server = p.getCurrentServer();
if (server.isEmpty()) return;
@ -140,88 +137,38 @@ public class PlayerTabList {
});
}
protected void clearCachedData(@NotNull Player player) {
players.values().forEach(p -> {
p.unsetRelationalDisplayName(player.getUniqueId());
p.unsetRelationalNametag(player.getUniqueId());
});
}
protected void joinPlayer(@NotNull Player joined, @NotNull Group group) {
// Add the player to the tracking list if they are not already listed
final TabPlayer tabPlayer = getTabPlayer(joined).orElseGet(() -> createTabPlayer(joined, group));
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
tabPlayer.setGroup(group);
players.putIfAbsent(joined.getUniqueId(), tabPlayer);
// Store the player's last server, so it's possible to have the last server on disconnect
final Optional<TabPlayer> tabPlayerOptional = getTabPlayer(joined);
if (tabPlayerOptional.isPresent()) {
tabPlayerOptional.get().clearCachedData();
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
tabPlayer.setLastServer(serverName);
// Send server URLs (1.21 clients)
sendPlayerServerLinks(tabPlayer);
// Determine display name, update TAB for player
// Set the player as not loaded until the display name is set
tabPlayer.getDisplayName(plugin).thenAccept(d -> {
joined.getTabList().getEntry(joined.getUniqueId())
.ifPresentOrElse(e -> e.setDisplayName(d),
() -> joined.getTabList().addEntry(createEntry(tabPlayer, joined.getTabList(), d)));
if (d == null) {
plugin.log(Level.ERROR, "Failed to get display name for " + joined.getUsername());
return;
}
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<String> serversInGroup = group.registeredServers(plugin).stream()
.map(server -> server.getServerInfo().getName())
.collect(HashSet::new, HashSet::add, HashSet::addAll);
serversInGroup.remove(serverName);
// Update lists
plugin.getServer().getScheduler()
.buildTask(plugin, () -> {
final TabList tabList = joined.getTabList();
final Set<TabPlayer> tabPlayers = group.getTabPlayers(plugin);
for (final TabPlayer player : tabPlayers) {
// Skip players on other servers if the setting is enabled
if (group.onlyListPlayersInSameServer() && !serverName.equals(getServerName(player.getPlayer()))) {
continue;
}
// check if current player can see the joined player
if (!isVanished || plugin.getVanishManager().canSee(player.getPlayer().getUsername(), joined.getUsername())) {
addPlayerToTabList(player, tabPlayer, d);
} else {
player.getPlayer().getTabList().removeEntry(joined.getUniqueId());
}
// check if joined player can see current player
if ((plugin.getVanishManager().isVanished(player.getPlayer().getUsername()) &&
!plugin.getVanishManager().canSee(joined.getUsername(), player.getPlayer().getUsername())) &&
player.getPlayer() != joined) {
tabList.removeEntry(player.getPlayer().getUniqueId());
} else {
tabList.getEntry(player.getPlayer().getUniqueId()).ifPresentOrElse(
entry -> player.getDisplayName(plugin).thenAccept(entry::setDisplayName)
.exceptionally(throwable -> {
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
player.getPlayer().getUsername(), player.getPlayer().getUniqueId()), throwable);
return null;
}),
() -> createEntry(player, tabList).thenAccept(tabList::addEntry)
);
}
player.sendHeaderAndFooter(this);
}
plugin.getScoreboardManager().ifPresent(s -> {
s.resendAllTeams(tabPlayer);
tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(tabPlayer, t, false));
});
fixDuplicateEntries(joined);
// Fire event without listening for result
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group));
})
.delay(300, TimeUnit.MILLISECONDS)
.schedule();
handleDisplayLoad(tabPlayer);
}).exceptionally(throwable -> {
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
joined.getUsername(), joined.getUniqueId()), throwable);
@ -229,6 +176,74 @@ public class PlayerTabList {
});
}
private void handleDisplayLoad(@NotNull TabPlayer tabPlayer) {
final Player joined = tabPlayer.getPlayer();
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);
updateTabListOnJoin(tabPlayer, group, tabPlayers, isVanished);
}
private void updateTabListOnJoin(@NotNull TabPlayer tabPlayer, @NotNull Group group,
@NotNull Set<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());
List.copyOf(tabPlayer.getPlayer().getTabList().getEntries()).forEach(entry -> {
if (!uuids.contains(entry.getProfile().getId())) {
tabPlayer.getPlayer().getTabList().removeEntry(entry.getProfile().getId());
}
});
for (final TabPlayer iteratedPlayer : tabPlayers) {
final Player player = iteratedPlayer.getPlayer();
final String username = player.getUsername();
final boolean isPlayerVanished = plugin.getVanishManager().isVanished(username);
if (group.onlyListPlayersInSameServer() && !serverName.equals(getServerName(player))) {
continue;
}
// Update lists regarding the joined player
checkVisibilityAndUpdateName(iteratedPlayer, tabPlayer, isJoinedVanished);
// Update lists regarding the iterated player
if (iteratedPlayer != tabPlayer) {
checkVisibilityAndUpdateName(tabPlayer, iteratedPlayer, isPlayerVanished);
}
iteratedPlayer.sendHeaderAndFooter(this);
}
plugin.getScoreboardManager().ifPresent(s -> {
s.resendAllTeams(tabPlayer);
tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(tabPlayer, t, false));
});
fixDuplicateEntries(joined);
// Fire event without listening for result
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group));
}
private void checkVisibilityAndUpdateName(@NotNull TabPlayer observedPlayer, @NotNull TabPlayer observableTabPlayer,
boolean isObservablePlayerVanished) {
final UUID observableUUID = observableTabPlayer.getPlayer().getUniqueId();
final String observedUsername = observedPlayer.getPlayer().getUsername();
final String observableUsername = observableTabPlayer.getPlayer().getUsername();
final TabList observableTabPlayerTabList = observableTabPlayer.getPlayer().getTabList();
if (isObservablePlayerVanished && !plugin.getVanishManager().canSee(observableUsername, observedUsername) &&
!observableUUID.equals(observedPlayer.getPlayer().getUniqueId())) {
observableTabPlayerTabList.removeEntry(observedPlayer.getPlayer().getUniqueId());
} else {
updateDisplayName(observedPlayer, observableTabPlayer);
}
}
@NotNull
private String getServerName(@NotNull Player player) {
return player.getCurrentServer()
@ -236,6 +251,21 @@ public class PlayerTabList {
.orElse("");
}
@NotNull
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer,
@NotNull Component single, @NotNull String toParse) {
if (plugin.getMiniPlaceholdersHook().isEmpty()) {
return single;
}
return plugin.getFormatter().format(toParse, player, viewer, plugin);
}
@NotNull
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull String toParse) {
final Component single = plugin.getFormatter().format(toParse, player, viewer, plugin);
return getRelationalPlaceholder(player, viewer, single, toParse);
}
@SuppressWarnings("unchecked")
private void fixDuplicateEntries(@NotNull Player target) {
try {
@ -248,7 +278,7 @@ public class PlayerTabList {
.filter(entry -> !entry.getKey().equals(target.getUniqueId()))
.forEach(entry -> target.getTabList().removeEntry(entry.getKey()));
} catch (Throwable error) {
plugin.log(Level.ERROR, "Failed to fix duplicate entries", error);
plugin.log(Level.ERROR, "Failed to fix duplicate entries for class " + target.getTabList().getClass().getName() , error);
}
}
@ -273,8 +303,9 @@ public class PlayerTabList {
.forEach(player -> {
player.getPlayer().getTabList().removeEntry(uuid);
player.sendHeaderAndFooter(this);
updatePlayerDisplayName(player);
}))
.delay(500, TimeUnit.MILLISECONDS)
.delay(250, TimeUnit.MILLISECONDS)
.schedule();
// Delete player team
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(target));
@ -283,10 +314,6 @@ public class PlayerTabList {
}
@NotNull
protected CompletableFuture<TabListEntry> createEntry(@NotNull TabPlayer player, @NotNull TabList tabList) {
return player.getDisplayName(plugin).thenApply(name -> createEntry(player, tabList, name));
}
protected TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull Component displayName) {
return TabListEntry.builder()
.profile(player.getPlayer().getGameProfile())
@ -296,25 +323,43 @@ public class PlayerTabList {
.build();
}
private void addPlayerToTabList(@NotNull TabPlayer player, @NotNull TabPlayer newPlayer, @NotNull Component displayName) {
if (newPlayer.getPlayer().getUniqueId().equals(player.getPlayer().getUniqueId())) {
@NotNull
protected TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull TabPlayer viewer) {
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());
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
return TabListEntry.builder()
.profile(player.getPlayer().getGameProfile())
.displayName(displayName)
.latency(Math.max((int) player.getPlayer().getPing(), 0))
.tabList(tabList)
.build();
}
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer) {
final Component displayName = getRelationalPlaceholder(player, viewer, player.getLastDisplayName());
updateDisplayName(player, viewer, displayName);
}
protected void updateDisplayName(@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())
.flatMap(TabListEntry::getDisplayNameComponent).map(displayName::equals)
.orElse(false)
) {
return;
}
plugin.getPacketEventManager().getVelocitabEntries().add(newPlayer.getPlayer().getUniqueId());
plugin.getServer().getScheduler()
.buildTask(plugin, () -> plugin.getPacketEventManager().getVelocitabEntries().remove(newPlayer.getPlayer().getUniqueId()))
.delay(500, TimeUnit.MILLISECONDS)
.schedule();
player.getPlayer()
.getTabList().getEntries().stream()
.filter(e -> e.getProfile().getId().equals(newPlayer.getPlayer().getUniqueId())).findFirst()
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
viewer.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
.ifPresentOrElse(
entry -> entry.setDisplayName(displayName),
() -> player.getPlayer().getTabList()
.addEntry(createEntry(newPlayer, player.getPlayer().getTabList(), displayName))
() -> viewer.getPlayer().getTabList()
.addEntry(createEntry(player, viewer.getPlayer().getTabList(), displayName))
);
}
@ -353,11 +398,12 @@ public class PlayerTabList {
}
public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) {
final Component lastDisplayName = tabPlayer.getLastDisplayName();
tabPlayer.getDisplayName(plugin).thenAccept(displayName -> {
if (displayName == null || displayName.equals(lastDisplayName)) {
if (displayName == null) {
plugin.log(Level.ERROR, "Failed to get display name for " + tabPlayer.getPlayer().getUsername());
return;
}
final Component single = plugin.getFormatter().format(displayName, tabPlayer, plugin);
final boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername());
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
@ -367,18 +413,51 @@ public class PlayerTabList {
return;
}
player.getPlayer().getTabList().getEntries().stream()
.filter(e -> e.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId())).findFirst()
.ifPresent(entry -> entry.setDisplayName(displayName));
final Component relationalPlaceholder = getRelationalPlaceholder(tabPlayer, player, single, displayName);
updateDisplayName(tabPlayer, player, relationalPlaceholder);
});
});
}
public void checkCorrectDisplayName(@NotNull TabPlayer tabPlayer) {
if (!tabPlayer.isLoaded()) {
return;
}
players.values()
.stream()
.filter(TabPlayer::isLoaded)
.forEach(player -> player.getPlayer().getTabList().getEntry(tabPlayer.getPlayer().getUniqueId())
.ifPresent(entry -> {
final Optional<Component> displayNameOptional = tabPlayer.getRelationalDisplayName(player.getPlayer().getUniqueId());
if (displayNameOptional.isEmpty()) {
return;
}
final Component lastDisplayName = displayNameOptional.get();
if (entry.getDisplayNameComponent().isEmpty() || !lastDisplayName.equals(entry.getDisplayNameComponent().get())) {
entry.setDisplayName(lastDisplayName);
}
}));
}
// Update the display names of all listed players
public void updateDisplayNames() {
players.values().forEach(this::updatePlayerDisplayName);
}
public void checkCorrectDisplayNames() {
players.values().forEach(this::checkCorrectDisplayName);
}
public void ensureDisplayNameTask() {
plugin.getServer().getScheduler()
.buildTask(plugin, this::checkCorrectDisplayNames)
.delay(1, TimeUnit.SECONDS)
.repeat(2, TimeUnit.SECONDS)
.schedule();
}
// Get the component for the TAB list header
public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) {
final String header = player.getGroup().getHeader(player.getHeaderIndex());
@ -395,96 +474,11 @@ public class PlayerTabList {
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
}
// Update the tab list periodically
private void updatePeriodically(@NotNull Group group) {
cancelTasks(group);
ScheduledTask headerFooterTask = null;
ScheduledTask updateTask = null;
ScheduledTask latencyTask;
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();
}
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();
}
latencyTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateLatency(group))
.delay(1, TimeUnit.SECONDS)
.repeat(3, TimeUnit.SECONDS)
.schedule();
groupTasks.put(group, new GroupTasks(headerFooterTask, updateTask, latencyTask));
}
private void updateLatency(@NotNull Group group) {
final Set<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
if (groupPlayers.isEmpty()) {
return;
}
groupPlayers.stream()
.filter(player -> player.getPlayer().isActive())
.forEach(player -> {
final int latency = (int) player.getPlayer().getPing();
final Set<TabPlayer> players = group.getTabPlayers(plugin, player);
players.forEach(p -> p.getPlayer().getTabList().getEntries().stream()
.filter(e -> e.getProfile().getId().equals(player.getPlayer().getUniqueId())).findFirst()
.ifPresent(entry -> entry.setLatency(Math.max(latency, 0))));
});
}
/**
* 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()) {
return;
}
groupPlayers.stream()
.filter(player -> player.getPlayer().isActive())
.forEach(player -> {
if (incrementIndexes) {
player.incrementIndexes();
}
if (all) {
this.updatePlayer(player, false);
}
player.sendHeaderAndFooter(this);
});
if (all) {
updateDisplayNames();
}
}
private void cancelTasks(@NotNull Group group) {
final GroupTasks tasks = groupTasks.get(group);
if (tasks != null) {
tasks.cancel();
groupTasks.remove(group);
}
}
/**
* Update the TAB list for all players when a plugin or proxy reload is performed
*/
public void reloadUpdate() {
plugin.getTabGroups().getGroups().forEach(this::updatePeriodically);
plugin.getTabGroups().getGroups().forEach(taskManager::updatePeriodically);
if (players.isEmpty()) {
return;
}
@ -496,8 +490,11 @@ public class PlayerTabList {
return;
}
final String serverName = server.get().getServerInfo().getName();
final Group group = getGroup(serverName);
player.setGroup(group);
final Optional<Group> group = getGroup(serverName);
if (group.isEmpty()) {
return;
}
player.setGroup(group.get());
this.sendPlayerServerLinks(player);
this.updatePlayer(player, true);
player.sendHeaderAndFooter(this);
@ -506,7 +503,7 @@ public class PlayerTabList {
}
@NotNull
public Group getGroup(@NotNull String serverName) {
public Optional<Group> getGroup(@NotNull String serverName) {
return plugin.getTabGroups().getGroupFromServer(serverName, plugin);
}

View File

@ -68,8 +68,12 @@ public class TabListListener {
tabList.removePlayer(event.getPlayer());
} else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer redirectPlayer) {
tabList.removePlayer(event.getPlayer(), redirectPlayer.getServer());
} else if (event.getResult() instanceof KickedFromServerEvent.Notify notify) {
return;
}
event.getPlayer().getTabList().removeEntry(event.getPlayer().getUniqueId());
event.getPlayer().getTabList().clearHeaderAndFooter();
justQuit.add(event.getPlayer().getUniqueId());
plugin.getServer().getScheduler().buildTask(plugin,
@ -88,14 +92,15 @@ public class TabListListener {
.orElse("");
// Get the group the player should now be in
final Group group = tabList.getGroup(serverName);
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group));
final boolean isDefault = group.registeredServers(plugin).stream()
.noneMatch(server -> server.getServerInfo().getName().equalsIgnoreCase(serverName));
final @NotNull Optional<Group> groupOptional = tabList.getGroup(serverName);
final boolean isDefault = groupOptional.map(g -> g.isDefault(plugin)).orElse(false);
// Removes cached relational data of the joined player from all other players
plugin.getTabList().clearCachedData(joined);
// 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()) {
if (isDefault && !plugin.getSettings().isFallbackEnabled() && !groupOptional.map(g -> g.containsServer(plugin, serverName)).orElse(false)) {
final Optional<TabPlayer> tabPlayer = tabList.getTabPlayer(joined);
if (tabPlayer.isEmpty()) {
return;
@ -107,7 +112,6 @@ public class TabListListener {
final Component header = tabPlayer.get().getLastHeader();
final Component footer = tabPlayer.get().getLastFooter();
final Component displayName = tabPlayer.get().getLastDisplayName();
plugin.getServer().getScheduler().buildTask(plugin, () -> {
final Component currentHeader = joined.getPlayerListHeader();
@ -126,6 +130,12 @@ public class TabListListener {
return;
}
if (groupOptional.isEmpty()) {
return;
}
final Group group = groupOptional.get();
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group));
if (justQuit.contains(joined.getUniqueId())) {
plugin.getServer().getScheduler().buildTask(plugin,
() -> tabList.joinPlayer(joined, group))
@ -159,7 +169,6 @@ public class TabListListener {
return;
}
tabList.removePlayer(player);
plugin.log("Player " + player.getUsername() + " was not removed from the tab list, removing now.");
}).delay(500, TimeUnit.MILLISECONDS).schedule();
}

View File

@ -0,0 +1,132 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.tab;
import com.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 org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class TaskManager {
private final Velocitab plugin;
private final Map<Group, GroupTasks> groupTasks;
public TaskManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.groupTasks = Maps.newConcurrentMap();
}
protected void cancelAllTasks() {
groupTasks.values().forEach(GroupTasks::cancel);
groupTasks.clear();
}
protected void updatePeriodically(@NotNull Group group) {
cancelTasks(group);
ScheduledTask headerFooterTask = null;
ScheduledTask updateTask = null;
ScheduledTask latencyTask;
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();
}
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();
}
latencyTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateLatency(group))
.delay(1, TimeUnit.SECONDS)
.repeat(3, TimeUnit.SECONDS)
.schedule();
groupTasks.put(group, new GroupTasks(headerFooterTask, updateTask, latencyTask));
}
/**
* 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()) {
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();
}
}
private void updateLatency(@NotNull Group group) {
final Set<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
if (groupPlayers.isEmpty()) {
return;
}
groupPlayers.stream()
.filter(player -> player.getPlayer().isActive())
.forEach(player -> {
final int latency = (int) player.getPlayer().getPing();
final Set<TabPlayer> players = group.getTabPlayers(plugin, player);
players.forEach(p -> p.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
.ifPresent(entry -> entry.setLatency(Math.max(latency, 0))));
});
}
private void cancelTasks(@NotNull Group group) {
groupTasks.entrySet()
.stream()
.filter(entry -> entry.getKey().name().equals(group.name()))
.map(Map.Entry::getValue)
.findFirst().ifPresent(GroupTasks::cancel);
}
}

View File

@ -20,6 +20,8 @@
package net.william278.velocitab.tab;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.player.TabListEntry;
import lombok.RequiredArgsConstructor;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
@ -30,17 +32,12 @@ import java.util.UUID;
/**
* The VanishTabList handles the tab list for vanished players
*/
@RequiredArgsConstructor
public class VanishTabList {
private final Velocitab plugin;
private final PlayerTabList tabList;
public VanishTabList(Velocitab plugin, PlayerTabList tabList) {
this.plugin = plugin;
this.tabList = tabList;
}
public void vanishPlayer(@NotNull TabPlayer tabPlayer) {
tabList.getPlayers().values().forEach(p -> {
if (p.getPlayer().equals(tabPlayer.getPlayer())) {
@ -56,17 +53,17 @@ public class VanishTabList {
public void unVanishPlayer(@NotNull TabPlayer tabPlayer) {
final UUID uuid = tabPlayer.getPlayer().getUniqueId();
tabPlayer.getDisplayName(plugin).thenAccept(c -> tabList.getPlayers().values().forEach(p -> {
tabList.getPlayers().values().forEach(p -> {
if (p.getPlayer().equals(tabPlayer.getPlayer())) {
return;
}
if (!p.getPlayer().getTabList().containsEntry(uuid)) {
p.getPlayer().getTabList().addEntry(tabList.createEntry(tabPlayer, p.getPlayer().getTabList(), c));
tabList.createEntry(tabPlayer, p.getPlayer().getTabList(), p);
} else {
p.getPlayer().getTabList().getEntry(uuid).ifPresent(entry -> entry.setDisplayName(c));
tabList.updateDisplayName(tabPlayer, p);
}
}));
});
}
@ -104,10 +101,9 @@ public class VanishTabList {
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, false));
} else {
if (!player.getTabList().containsEntry(p.getUniqueId())) {
tabList.createEntry(target, player.getTabList()).thenAccept(e -> {
player.getTabList().addEntry(e);
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, true));
});
final TabListEntry tabListEntry = tabList.createEntry(target, player.getTabList(), tabPlayer);
player.getTabList().addEntry(tabListEntry);
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, true));
}
}
});

View File

@ -0,0 +1,35 @@
/*
* 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;
@FunctionalInterface
public interface QuadFunction<T, U, V, S, R> {
/**
* 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
* @param s the fourth function argument
* @return the function result
*/
R apply(T t, U u, V v, S s);
}

View File

@ -0,0 +1,32 @@
/*
* 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.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
public final class SerializerUtil {
public final static LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder()
.hexCharacter('#')
.character('&')
.hexColors()
.build();
}