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 { repositories {
mavenCentral() mavenCentral()
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://repo.william278.net/velocity/' } 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://repo.william278.net/releases/' }
maven { url = 'https://jitpack.io/' } maven { url = 'https://jitpack.io/' }
maven { url = 'https://repo.minebench.de/' } maven { url = 'https://repo.minebench.de/' }
@ -35,19 +35,21 @@ dependencies {
compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT" compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT"
compileOnly "com.velocitypowered:velocity-proxy:${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 'org.apache.commons:commons-text:1.12.0'
implementation 'net.william278:DesertWell:2.0.4' implementation 'net.william278:DesertWell:2.0.4'
implementation 'net.william278:minedown:1.8.2' implementation 'net.william278:minedown:1.8.2'
implementation 'org.bstats:bstats-velocity:3.0.2' implementation 'org.bstats:bstats-velocity:3.0.2'
implementation 'de.exlll:configlib-yaml:4.5.0' implementation 'de.exlll:configlib-yaml:4.5.0'
implementation 'org.apache.commons:commons-jexl3:3.4.0'
compileOnly 'io.netty:netty-codec-http:4.1.111.Final' implementation 'net.jodah:expiringmap:0.5.11'
compileOnly 'net.luckperms:api:5.4'
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.0.0'
compileOnly 'net.william278:PAPIProxyBridge:1.5'
compileOnly 'it.unimi.dsi:fastutil:8.5.13'
compileOnly 'net.kyori:adventure-nbt:4.17.0'
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.32'
} }
@ -92,6 +94,10 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.velocitab.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.velocitab.libraries.desertwell'
relocate 'org.bstats', 'net.william278.velocitab.libraries.bstats' relocate 'org.bstats', 'net.william278.velocitab.libraries.bstats'
relocate 'de.exlll.configlib', 'net.william278.velocitab.libraries.configlib' 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 { dependencies {
exclude dependency(':slf4j-api') exclude dependency(':slf4j-api')
@ -101,7 +107,9 @@ shadowJar {
destinationDirectory.set(file("$rootDir/target")) destinationDirectory.set(file("$rootDir/target"))
archiveClassifier.set('') archiveClassifier.set('')
minimize() minimize() {
exclude dependency('commons-logging:commons-logging')
}
} }
jar.dependsOn shadowJar jar.dependsOn shadowJar
clean.delete "$rootDir/target" clean.delete "$rootDir/target"
@ -151,6 +159,11 @@ publishing {
tasks { tasks {
runVelocity { runVelocity {
velocityVersion("${velocity_api_version}-SNAPSHOT") 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 ### 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. 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 ### 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. 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]] * 📛 [[Nametags]]
* 📊 [[Sorting]] * 📊 [[Sorting]]
* ✍️ [[Placeholders]] * ✍️ [[Placeholders]]
* 🔗 [[Relational Placeholders]]
* 🔀 [[Conditional Placeholders]]
* ✨ [[Animations]] * ✨ [[Animations]]
* 🖼️ [[Custom Logos]] * 🖼️ [[Custom Logos]]
* 🔗 [[Server Links]] * 🔗 [[Server Links]]

View File

@ -4,14 +4,24 @@ Velocitab supports a number of Placeholders that will be replaced with their res
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: 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 | | Placeholder | Description | Example |
|---------------------------------|------------------------------------------------------|--------------------| |---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| `%players_online%` | Players online on the proxy | `6` | | `%players_online%` | Players online on the proxy | `6` |
| `%max_players_online%` | Player capacity of the proxy | `500` | | `%max_players_online%` | Player capacity of the proxy | `500` |
| `%local_players_online%` | Players online on the server the player is on | `3` | | `%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_(name)%` | Players online on the group provided | `11` |
| `%group_players_online%` | Players online on player's group | `15` | | `%group_players_online%` | Players online on player's group | `15` |
| `%current_date%` | Current real-world date of the server | `24 Feb 2023` | | `%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%` | 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%` | The player's username | `William278` |
| `%username_lower%` | The player's username, in lowercase | `william278` | | `%username_lower%` | The player's username, in lowercase | `william278` |
| `%server%` | Name of the server the player is on | `alpha` | | `%server%` | Name of the server the player is on | `alpha` |
@ -20,12 +30,16 @@ Placeholders can be included in the header, footer and player name format of the
| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` | | `%suffix%` | The player's suffix (from LuckPerms) | `&c ` |
| `%role%` | The player's primary LuckPerms group name | `admin` | | `%role%` | The player's primary LuckPerms group name | `admin` |
| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` | | `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` |
| `%role_weight%` | Comparable-formatted primary LuckPerms group weight. | `100` | | `%role_weight%` | Comparable-formatted primary LuckPerms group weight | `100` |
| `%luckperms_meta_(key)%` | Formats a meta key from the user's LuckPerms group | (varies) | | `%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%` | The name of the server group the player is on | `default` |
| `%server_group_index%` | Indexed order of the server group in the list | `0` | | `%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` | | `%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 ### 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. 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.
@ -47,3 +61,4 @@ PlaceholderAPI placeholders are cached to reduce plugin message traffic. By defa
## MiniPlaceholders support ## 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]] * 📛 [[Nametags]]
* 📊 [[Sorting]] * 📊 [[Sorting]]
* ✍️ [[Placeholders]] * ✍️ [[Placeholders]]
* 🔗 [[Relational Placeholders]]
* 🔀 [[Conditional Placeholders]]
* ✨ [[Animations]] * ✨ [[Animations]]
* 🖼️ [[Custom Logos]] * 🖼️ [[Custom Logos]]
* 🔗 [[Server Links]] * 🔗 [[Server Links]]

View File

@ -43,6 +43,7 @@ import net.william278.velocitab.config.Settings;
import net.william278.velocitab.config.TabGroups; import net.william278.velocitab.config.TabGroups;
import net.william278.velocitab.hook.Hook; import net.william278.velocitab.hook.Hook;
import net.william278.velocitab.hook.LuckPermsHook; import net.william278.velocitab.hook.LuckPermsHook;
import net.william278.velocitab.hook.MiniPlaceholdersHook;
import net.william278.velocitab.packet.PacketEventManager; import net.william278.velocitab.packet.PacketEventManager;
import net.william278.velocitab.packet.ScoreboardManager; import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.providers.HookProvider; import net.william278.velocitab.providers.HookProvider;
@ -113,9 +114,10 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
@Subscribe @Subscribe
public void onProxyShutdown(@NotNull ProxyShutdownEvent event) { public void onProxyShutdown(@NotNull ProxyShutdownEvent event) {
server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel); // server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel);
disableScoreboardManager(); disableScoreboardManager();
getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent); getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent);
getMiniPlaceholdersHook().ifPresent(MiniPlaceholdersHook::unregisterExpansion);
unregisterAPI(); unregisterAPI();
logger.info("Successfully disabled Velocitab"); 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.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
@ -190,6 +191,33 @@ public class VelocitabAPI {
return getUser(player).map(TabPlayer::getGroup).orElse(null); 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. * 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. * 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. * This method assumes that the getConfigDirectory method returns a valid directory path.
* *
* @throws IllegalStateException if the getConfigDirectory method returns null * @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.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.william278.velocitab.Velocitab; import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer; 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.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function; import java.util.function.Function;
@ -36,26 +38,33 @@ import java.util.function.Function;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public enum Formatter { public enum Formatter {
MINEDOWN( MINEDOWN(
(text, player, plugin) -> new MineDown(text).toComponent(), (text, player, viewer, plugin) -> new MineDown(text).toComponent(),
MineDown::escape, MineDown::escape,
"MineDown", "MineDown",
(text) -> new MineDown(text).toComponent() (text) -> new MineDown(text).toComponent(),
(text) -> {
throw new UnsupportedOperationException("MineDown does not support serialization");
}
), ),
MINIMESSAGE( MINIMESSAGE(
(text, player, plugin) -> plugin.getMiniPlaceholdersHook() (text, player, viewer, plugin) -> plugin.getMiniPlaceholdersHook()
.map(hook -> hook.format(text, player.getPlayer())) .filter(hook -> viewer != null)
.map(hook -> hook.format(text, player.getPlayer(), viewer.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(text)), .orElse(MiniMessage.miniMessage().deserialize(text)),
(text) -> MiniMessage.miniMessage().escapeTags(text), (text) -> MiniMessage.miniMessage().escapeTags(text),
"MiniMessage", "MiniMessage",
(text) -> MiniMessage.miniMessage().deserialize(text) (text) -> MiniMessage.miniMessage().deserialize(text),
MiniMessage.miniMessage()::serialize
), ),
LEGACY( LEGACY(
(text, player, plugin) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text), (text, player, viewer, plugin) -> SerializerUtil.LEGACY_SERIALIZER.deserialize(text),
Function.identity(), Function.identity(),
"Legacy Text", "Legacy Text",
(text) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text) SerializerUtil.LEGACY_SERIALIZER::deserialize,
SerializerUtil.LEGACY_SERIALIZER::serialize
); );
/** /**
* Name of the formatter * Name of the formatter
*/ */
@ -64,24 +73,50 @@ public enum Formatter {
/** /**
* Function to apply formatting to a string * 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 * Function to escape formatting characters in a string
*/ */
private final Function<String, String> escaper; private final Function<String, String> escaper;
private final Function<String, Component> emptyFormatter; 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, Formatter(@NotNull QuadFunction<String, TabPlayer, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper,
@NotNull String name, @NotNull Function<String, Component> emptyFormatter) { @NotNull String name, @NotNull Function<String, Component> emptyFormatter, @NotNull Function<Component, String> serializer) {
this.formatter = formatter; this.formatter = formatter;
this.escaper = escaper; this.escaper = escaper;
this.name = name; this.name = name;
this.emptyFormatter = emptyFormatter; 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 @NotNull
public Component format(@NotNull String text, @NotNull TabPlayer player, @NotNull Velocitab plugin) { 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 @NotNull
@ -91,7 +126,7 @@ public enum Formatter {
} }
@NotNull @NotNull
public Component emptyFormat(@NotNull String text) { public Component deserialize(@NotNull String text) {
return emptyFormatter.apply(text); return emptyFormatter.apply(text);
} }
@ -105,4 +140,9 @@ public enum Formatter {
return name; 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)))); .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 @NotNull
public Set<RegisteredServer> registeredServers(@NotNull Velocitab plugin) { public Set<RegisteredServer> registeredServers(@NotNull Velocitab plugin) {
return registeredServers(plugin, true); 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.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer; 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.Velocitab;
import net.william278.velocitab.player.TabPlayer; import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.Nametag; import net.william278.velocitab.tab.Nametag;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.function.TriFunction; import org.apache.commons.lang3.function.TriFunction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.event.Level; import org.slf4j.event.Level;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter; 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.concurrent.CompletableFuture;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -45,12 +52,47 @@ public enum Placeholder {
.map(RegisteredServer::getPlayersConnected) .map(RegisteredServer::getPlayersConnected)
.map(players -> Integer.toString(players.size())) .map(players -> Integer.toString(players.size()))
.orElse("")), .orElse("")),
GROUP_PLAYERS_ONLINE_((param, plugin, player) -> plugin.getTabGroups().getGroup(param) 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())) .map(group -> Integer.toString(group.getPlayers(plugin).size()))
.orElse("Group " + param + " not found")), .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_DATE_DAY((plugin, player) -> DateTimeFormatter.ofPattern("dd").format(LocalDateTime.now())),
CURRENT_TIME((plugin, player) -> DateTimeFormatter.ofPattern("HH:mm:ss").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((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername())),
USERNAME_LOWER((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername()).toLowerCase()), USERNAME_LOWER((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername()).toLowerCase()),
SERVER((plugin, player) -> player.getServerDisplayName(plugin)), SERVER((plugin, player) -> player.getServerDisplayName(plugin)),
@ -67,18 +109,26 @@ public enum Placeholder {
SERVER_GROUP((plugin, player) -> player.getGroup().name()), SERVER_GROUP((plugin, player) -> player.getGroup().name()),
SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))), SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))),
DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse(""))), 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)) .map(hook -> hook.getMeta(player.getPlayer(), param))
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_" + 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 * Function to replace placeholders with a real value
*/ */
private final TriFunction<String, Velocitab, TabPlayer, String> replacer; private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
private final boolean parameterised; private final boolean parameterised;
private final Pattern pattern; private final Pattern pattern;
private final static Pattern CHECK_PLACEHOLDERS = Pattern.compile("%.*?%");
private final static String DELIMITER = ":::";
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) { Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
this.parameterised = false; this.parameterised = false;
@ -89,7 +139,7 @@ public enum Placeholder {
Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) { Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) {
this.parameterised = true; this.parameterised = true;
this.replacer = parameterisedReplacer; 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, public static CompletableFuture<Nametag> replace(@NotNull Nametag nametag, @NotNull Velocitab plugin,
@ -107,31 +157,88 @@ public enum Placeholder {
return ""; 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, public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin,
@NotNull TabPlayer player) { @NotNull TabPlayer player) {
if (format.equals(DELIMITER)) { if (format.equals(DELIMITER)) {
return CompletableFuture.completedFuture(""); return CompletableFuture.completedFuture("");
} }
for (Placeholder placeholder : values()) { final String replaced = replaceInternal(format, plugin, player);
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)));
}
} if (!PLACEHOLDER_PATTERN.matcher(replaced).find()) {
final String replaced = format;
if (!CHECK_PLACEHOLDERS.matcher(replaced).find()) {
return CompletableFuture.completedFuture(replaced); return CompletableFuture.completedFuture(replaced);
} }

View File

@ -70,32 +70,34 @@ public class TabGroups implements ConfigValidator {
.orElseThrow(() -> new IllegalStateException("No group with name " + name + " found")); .orElseThrow(() -> new IllegalStateException("No group with name " + name + " found"));
} }
@NotNull
public Optional<Group> getGroup(@NotNull String name) { public Optional<Group> getGroup(@NotNull String name) {
return groups.stream() return groups.stream()
.filter(group -> group.name().equals(name)) .filter(group -> group.name().equals(name))
.findFirst(); .findFirst();
} }
@NotNull public Optional<Group> getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
public Group getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
final List<Group> groups = new ArrayList<>(this.groups); final List<Group> groups = new ArrayList<>(this.groups);
final Optional<Group> defaultGroup = getGroup("default"); final Optional<Group> defaultGroup = getGroup("default");
// Ensure the default group is always checked last if (defaultGroup.isEmpty()) {
if (defaultGroup.isPresent()) {
groups.remove(defaultGroup.get());
groups.add(defaultGroup.get());
} else {
throw new IllegalStateException("No default group found"); 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) { for (Group group : groups) {
if (group.registeredServers(plugin, false) if (group.registeredServers(plugin, false)
.stream() .stream()
.anyMatch(s -> s.getServerInfo().getName().equalsIgnoreCase(server))) { .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) { 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.kyori.adventure.text.minimessage.MiniMessage;
import net.william278.velocitab.Velocitab; import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
public class MiniPlaceholdersHook extends Hook { 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) { public MiniPlaceholdersHook(@NotNull Velocitab plugin) {
super(plugin); super(plugin);
this.expansion = new VelocitabMiniExpansion(plugin);
expansion.registerExpansion();
} }
@NotNull @NotNull
public Component format(@NotNull String text, @NotNull Audience 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.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; package net.william278.velocitab.packet;
import com.google.common.collect.Sets;
import com.velocitypowered.api.event.AwaitingEventExecutor; import com.velocitypowered.api.event.AwaitingEventExecutor;
import com.velocitypowered.api.event.EventTask; import com.velocitypowered.api.event.EventTask;
import com.velocitypowered.api.event.connection.DisconnectEvent; 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.api.proxy.Player;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.network.Connections; 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 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.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public class PacketEventManager { public class PacketEventManager {
private static final String KEY = "velocitab"; private static final String KEY = "velocitab";
private static final String CITIZENS_PREFIX = "CIT";
private final Velocitab plugin; private final Velocitab plugin;
@Getter
private final Set<UUID> velocitabEntries;
public PacketEventManager(@NotNull Velocitab plugin) { public PacketEventManager(@NotNull Velocitab plugin) {
this.plugin = plugin; this.plugin = plugin;
this.velocitabEntries = Sets.newConcurrentHashSet();
this.loadPlayers(); this.loadPlayers();
this.loadListeners(); this.loadListeners();
} }
@ -87,32 +75,17 @@ public class PacketEventManager {
public void removePlayer(@NotNull Player player) { public void removePlayer(@NotNull Player player) {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player; final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
final Channel channel = connectedPlayer.getConnection().getChannel(); final Channel channel = connectedPlayer.getConnection().getChannel();
if (channel.pipeline().get(KEY) != null) { final ChannelHandler handler = channel.pipeline().get(KEY);
channel.pipeline().remove(KEY); if (handler == null) {
return;
} }
} if (channel.pipeline() instanceof DefaultChannelPipeline defaultChannelPipeline) {
defaultChannelPipeline.removeIfExists(KEY);
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()) {
return; return;
} }
toUpdate.forEach(tabPlayer -> packet.getEntries().stream() plugin.getLogger().warn("Failed to remove player {} from Velocitab packet handler {}",
.filter(entry -> entry.getProfileId().equals(tabPlayer.getPlayer().getUniqueId())) player.getUsername(), channel.pipeline().getClass().getName());
.findFirst()
.ifPresent(entry -> entry.setDisplayName(
new ComponentHolder(player.getProtocolVersion(), tabPlayer.getLastDisplayName()))));
} }
} }

View File

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

View File

@ -41,11 +41,47 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
@Override @Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { 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)) { if (!(msg instanceof final UpsertPlayerInfoPacket minecraftPacket)) {
super.write(ctx, msg, promise); super.write(ctx, msg, promise);
return; return;
} }
try {
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player); final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
if (tabPlayer.isEmpty()) { if (tabPlayer.isEmpty()) {
super.write(ctx, msg, promise); super.write(ctx, msg, promise);
@ -73,8 +109,11 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
return; return;
} }
plugin.getPacketEventManager().handleEntry(minecraftPacket, player);
super.write(ctx, msg, promise); super.write(ctx, msg, promise);
} catch (Exception e) {
plugin.getLogger().error("An error occurred while handling a packet", e);
super.write(ctx, msg, promise);
}
} }
private void forceGameMode(@NotNull List<UpsertPlayerInfoPacket.Entry> entries) { 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) { public Protocol404Adapter(@NotNull Velocitab plugin, Set<ProtocolVersion> protocolVersions) {
super(plugin, 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 @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)); 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(); 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 @Override
public void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) { public void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
ProtocolUtils.writeString(byteBuf, shrinkString(packet.teamName())); ProtocolUtils.writeString(byteBuf, shrinkString(packet.teamName()));
@ -83,7 +112,12 @@ public class Protocol48Adapter extends TeamsPacketAdapter {
return string.substring(0, Math.min(string.length(), 16)); 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))); 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 @Override
protected void writeComponent(ByteBuf buf, Component component) { protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
ProtocolUtils.writeString(buf, serializer.serialize(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)); final BinaryTag tag = ComponentHolder.serialize(GsonComponentSerializer.gson().serializeToTree(component));
ProtocolUtils.writeBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, tag); 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; package net.william278.velocitab.packet;
import com.google.common.collect.Maps; 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.google.common.collect.Sets;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player; 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.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab; import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group; import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer; import net.william278.velocitab.player.TabPlayer;
@ -49,12 +52,14 @@ public class ScoreboardManager {
private final Set<TeamsPacketAdapter> versions; private final Set<TeamsPacketAdapter> versions;
private final Map<UUID, String> createdTeams; private final Map<UUID, String> createdTeams;
private final Map<String, Nametag> nametags; private final Map<String, Nametag> nametags;
private final Multimap<UUID, String> trackedTeams;
public ScoreboardManager(@NotNull Velocitab velocitab) { public ScoreboardManager(@NotNull Velocitab velocitab) {
this.plugin = velocitab; this.plugin = velocitab;
this.createdTeams = Maps.newConcurrentMap(); this.createdTeams = Maps.newConcurrentMap();
this.nametags = Maps.newConcurrentMap(); this.nametags = Maps.newConcurrentMap();
this.versions = Sets.newHashSet(); this.versions = Sets.newHashSet();
this.trackedTeams = Multimaps.synchronizedMultimap(Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet));
this.registerVersions(); this.registerVersions();
} }
@ -70,6 +75,10 @@ public class ScoreboardManager {
} }
} }
public boolean isInternalTeam(@NotNull String teamName) {
return nametags.containsKey(teamName);
}
@NotNull @NotNull
public TeamsPacketAdapter getPacketAdapter(@NotNull ProtocolVersion version) { public TeamsPacketAdapter getPacketAdapter(@NotNull ProtocolVersion version) {
return versions.stream() return versions.stream()
@ -88,6 +97,7 @@ public class ScoreboardManager {
plugin.getTabList().getTabPlayer(player).ifPresent(tabPlayer -> plugin.getTabList().getTabPlayer(player).ifPresent(tabPlayer ->
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), 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 Set<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
final Optional<Nametag> cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null)); final Optional<Nametag> cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null));
cachedTag.ifPresent(nametag -> { cachedTag.ifPresent(nametag -> siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player)
final UpdateTeamsPacket packet = vanish ? UpdateTeamsPacket.removeTeam(plugin, teamName) : .forEach(connected -> {
UpdateTeamsPacket.create(plugin, tabPlayer, teamName, nametag, player.getUsername()); if (vanish && !plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername())) {
siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player) dispatchPacket(UpdateTeamsPacket.removeTeam(plugin, teamName), connected);
.filter(p -> vanish && !plugin.getVanishManager().canSee(p.getUsername(), player.getUsername())) trackedTeams.remove(connected.getUniqueId(), teamName);
.forEach(connected -> dispatchPacket(packet, connected))); } else {
}); dispatchGroupCreatePacket(plugin, tabPlayer, teamName, nametag, player.getUsername());
}
})));
} }
/** /**
@ -154,16 +166,12 @@ public class ScoreboardManager {
createdTeams.put(player.getUniqueId(), role); createdTeams.put(player.getUniqueId(), role);
this.nametags.put(role, newTag); this.nametags.put(role, newTag);
dispatchGroupPacket( dispatchGroupCreatePacket(plugin, tabPlayer, role, newTag, name);
UpdateTeamsPacket.create(plugin, tabPlayer, role, newTag, name),
tabPlayer
);
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) { } else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) {
this.nametags.put(role, newTag); this.nametags.put(role, newTag);
dispatchGroupPacket( dispatchGroupChangePacket(plugin, tabPlayer, role, newTag);
UpdateTeamsPacket.changeNametag(plugin, tabPlayer, role, newTag), } else {
tabPlayer updatePlaceholders(tabPlayer);
);
} }
}).exceptionally(e -> { }).exceptionally(e -> {
plugin.log(Level.ERROR, "Failed to update role for " + player.getUsername(), 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) { public void resendAllTeams(@NotNull TabPlayer tabPlayer) {
if (!plugin.getSettings().isSendScoreboardPackets()) { if (!plugin.getSettings().isSendScoreboardPackets()) {
@ -196,7 +215,6 @@ public class ScoreboardManager {
} }
final Optional<TabPlayer> optionalTabPlayer = plugin.getTabList().getTabPlayer(p); final Optional<TabPlayer> optionalTabPlayer = plugin.getTabList().getTabPlayer(p);
if (optionalTabPlayer.isEmpty()) { if (optionalTabPlayer.isEmpty()) {
return; return;
} }
@ -212,14 +230,69 @@ public class ScoreboardManager {
// Send packet // Send packet
final Nametag tag = nametags.get(role); final Nametag tag = nametags.get(role);
if (tag != null) { if (tag != null) {
final UpdateTeamsPacket packet = UpdateTeamsPacket.create( dispatchCreatePacket(plugin, targetTabPlayer, role, tag, tabPlayer, p.getUsername());
plugin, targetTabPlayer, role, tag, p.getUsername()
);
dispatchPacket(packet, player);
} }
}); });
} }
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) { private void dispatchPacket(@NotNull UpdateTeamsPacket packet, @NotNull Player player) {
if (!player.isActive()) { if (!player.isActive()) {
plugin.getTabList().removeOfflinePlayer(player); plugin.getTabList().removeOfflinePlayer(player);
@ -235,10 +308,14 @@ public class ScoreboardManager {
} }
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Group group) { private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Group group) {
final boolean isRemove = packet.isRemoveTeam();
group.registeredServers(plugin).forEach(server -> server.getPlayersConnected().forEach(connected -> { group.registeredServers(plugin).forEach(server -> server.getPlayersConnected().forEach(connected -> {
try { try {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected; final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected;
connectedPlayer.getConnection().write(packet); connectedPlayer.getConnection().write(packet);
if (isRemove) {
trackedTeams.remove(connected.getUniqueId(), packet.teamName());
}
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", 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) .direction(ProtocolUtils.Direction.CLIENTBOUND)
.packetSupplier(() -> new UpdateTeamsPacket(plugin)) .packetSupplier(() -> new UpdateTeamsPacket(plugin))
.stateRegistry(StateRegistry.PLAY) .stateRegistry(StateRegistry.PLAY)
.mapping(0x3E, MINECRAFT_1_8, true) .mapping(0x3E, MINECRAFT_1_8, false)
.mapping(0x44, MINECRAFT_1_12_2, true) .mapping(0x44, MINECRAFT_1_12_2, false)
.mapping(0x47, MINECRAFT_1_13, true) .mapping(0x47, MINECRAFT_1_13, false)
.mapping(0x4B, MINECRAFT_1_14, true) .mapping(0x4B, MINECRAFT_1_14, false)
.mapping(0x4C, MINECRAFT_1_15, true) .mapping(0x4C, MINECRAFT_1_15, false)
.mapping(0x55, MINECRAFT_1_17, true) .mapping(0x55, MINECRAFT_1_17, false)
.mapping(0x58, MINECRAFT_1_19_1, true) .mapping(0x58, MINECRAFT_1_19_1, false)
.mapping(0x56, MINECRAFT_1_19_3, true) .mapping(0x56, MINECRAFT_1_19_3, false)
.mapping(0x5A, MINECRAFT_1_19_4, true) .mapping(0x5A, MINECRAFT_1_19_4, false)
.mapping(0x5C, MINECRAFT_1_20_2, true) .mapping(0x5C, MINECRAFT_1_20_2, false)
.mapping(0x5E, MINECRAFT_1_20_3, true) .mapping(0x5E, MINECRAFT_1_20_3, false)
.mapping(0x60, MINECRAFT_1_20_5, true); .mapping(0x60, MINECRAFT_1_20_5, false);
packetRegistration.register(); packetRegistration.register();
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.ERROR, "Failed to register UpdateTeamsPacket", e); plugin.log(Level.ERROR, "Failed to register UpdateTeamsPacket", e);
@ -320,14 +397,12 @@ public class ScoreboardManager {
final UpdateTeamsPacket removeTeam = UpdateTeamsPacket.removeTeam(plugin, team); final UpdateTeamsPacket removeTeam = UpdateTeamsPacket.removeTeam(plugin, team);
dispatchPacket(removeTeam, player); dispatchPacket(removeTeam, player);
trackedTeams.remove(player.getUniqueId(), team);
if (canSee) { if (canSee) {
final Nametag tag = nametags.get(team); final Nametag tag = nametags.get(team);
if (tag != null) { if (tag != null) {
final UpdateTeamsPacket addTeam = UpdateTeamsPacket.create( dispatchCreatePacket(plugin, tabPlayer, team, tag, target, target.getPlayer().getUsername());
plugin, tabPlayer, team, tag, target.getPlayer().getUsername()
);
dispatchPacket(addTeam, player);
} }
} }
} }

View File

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

View File

@ -19,12 +19,16 @@
package net.william278.velocitab.player; package net.william278.velocitab.player;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.william278.velocitab.Velocitab; import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Formatter;
import net.william278.velocitab.config.Group; import net.william278.velocitab.config.Group;
import net.william278.velocitab.config.Placeholder; import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.packet.UpdateTeamsPacket; 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.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Optional; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Getter @Getter
@ToString @ToString
public final class TabPlayer implements Comparable<TabPlayer> { 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 Velocitab plugin;
private final Player player; private final Player player;
@Setter @Setter
private Role role; private Role role;
private int headerIndex = 0; private int headerIndex = 0;
private int footerIndex = 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 lastHeader;
private Component lastFooter; private Component lastFooter;
private String teamName; private String teamName;
@ -72,6 +85,9 @@ public final class TabPlayer implements Comparable<TabPlayer> {
this.player = player; this.player = player;
this.role = role; this.role = role;
this.group = group; this.group = group;
this.relationalDisplayNames = Maps.newConcurrentMap();
this.relationalNametags = Maps.newConcurrentMap();
this.cachedPlaceholders = Maps.newConcurrentMap();
} }
@NotNull @NotNull
@ -115,10 +131,47 @@ public final class TabPlayer implements Comparable<TabPlayer> {
} }
@NotNull @NotNull
public CompletableFuture<Component> getDisplayName(@NotNull Velocitab plugin) { public CompletableFuture<String> getDisplayName(@NotNull Velocitab plugin) {
return Placeholder.replace(group.format(), plugin, this) final String format = formatGroup();
.thenApply(formatted -> plugin.getFormatter().format(formatted, this, plugin)) return Placeholder.replace(format, plugin, this)
.thenApply(c -> this.lastDisplayName = c); .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 @NotNull
@ -140,19 +193,19 @@ public final class TabPlayer implements Comparable<TabPlayer> {
return tabList.getHeader(this).thenCompose(header -> tabList.getFooter(this).thenAccept(footer -> { return tabList.getHeader(this).thenCompose(header -> tabList.getFooter(this).thenAccept(footer -> {
final boolean disabled = plugin.getSettings().isDisableHeaderFooterIfEmpty(); final boolean disabled = plugin.getSettings().isDisableHeaderFooterIfEmpty();
if (disabled) { if (disabled) {
if (!Component.empty().equals(header)) { if ((!Component.empty().equals(header) && !header.equals(lastHeader)) ||
lastHeader = header; (!Component.empty().equals(footer) && !footer.equals(lastFooter))) {
player.sendPlayerListHeader(header);
}
if (!Component.empty().equals(footer)) {
lastFooter = footer;
player.sendPlayerListFooter(footer);
}
} else {
lastHeader = header; lastHeader = header;
lastFooter = footer; lastFooter = footer;
player.sendPlayerListHeaderAndFooter(header, footer); player.sendPlayerListHeaderAndFooter(header, footer);
} }
} else {
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. * 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) { public boolean equals(Object obj) {
return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId()); 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); getLogger().warn(message);
} }
} }
case DEBUG -> getLogger().debug(message);
case INFO -> getLogger().info(message); case INFO -> getLogger().info(message);
} }
} }

View File

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

View File

@ -20,6 +20,7 @@
package net.william278.velocitab.sorting; package net.william278.velocitab.sorting;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.velocitypowered.api.network.ProtocolVersion;
import net.william278.velocitab.Velocitab; import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder; import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.player.TabPlayer; import net.william278.velocitab.player.TabPlayer;
@ -49,7 +50,7 @@ public class SortingManager {
return Placeholder.replace(String.join(DELIMITER, player.getGroup().sortingPlaceholders()), plugin, player) return Placeholder.replace(String.join(DELIMITER, player.getGroup().sortingPlaceholders()), plugin, player)
.thenApply(s -> Arrays.asList(s.split(DELIMITER))) .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)); .thenApply(v -> handleList(player, v));
} }
@ -57,7 +58,7 @@ public class SortingManager {
private String handleList(@NotNull TabPlayer player, @NotNull List<String> values) { private String handleList(@NotNull TabPlayer player, @NotNull List<String> values) {
String result = String.join("", values); String result = String.join("", values);
if (result.length() > 12) { if (result.length() > 12 && isLongTeamNotAllowed(player)) {
result = result.substring(0, 12); result = result.substring(0, 12);
} }
@ -66,8 +67,13 @@ public class SortingManager {
return result; 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 @NotNull
private String adaptValue(@NotNull String value) { private String adaptValue(@NotNull String value, @NotNull TabPlayer player) {
if (value.isEmpty()) { if (value.isEmpty()) {
return ""; return "";
} }
@ -78,7 +84,7 @@ public class SortingManager {
return compressNumber(Integer.MAX_VALUE / 4d - parsed); return compressNumber(Integer.MAX_VALUE / 4d - parsed);
} }
if (value.length() > 6) { if (value.length() > 6 && isLongTeamNotAllowed(player)) {
return value.substring(0, 4); 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) { public record Nametag(@NotNull String prefix, @NotNull String suffix) {
@NotNull @NotNull
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) { public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
return plugin.getFormatter().format(prefix, tabPlayer, plugin); return plugin.getFormatter().format(prefix, tabPlayer, target, plugin);
} }
@NotNull @NotNull
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) { public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
return plugin.getFormatter().format(suffix, tabPlayer, plugin); return plugin.getFormatter().format(suffix, tabPlayer, target, plugin);
} }
@Override @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.TabList;
import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.proxy.player.TabListEntry;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.scheduler.ScheduledTask;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -45,6 +44,7 @@ import java.lang.reflect.Field;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/** /**
* The main class for tracking the server TAB list * The main class for tracking the server TAB list
@ -55,15 +55,16 @@ public class PlayerTabList {
private final VanishTabList vanishTabList; private final VanishTabList vanishTabList;
@Getter(value = AccessLevel.PUBLIC) @Getter(value = AccessLevel.PUBLIC)
private final Map<UUID, TabPlayer> players; private final Map<UUID, TabPlayer> players;
private final Map<Group, GroupTasks> groupTasks; private final TaskManager taskManager;
public PlayerTabList(@NotNull Velocitab plugin) { public PlayerTabList(@NotNull Velocitab plugin) {
this.plugin = plugin; this.plugin = plugin;
this.vanishTabList = new VanishTabList(plugin, this); this.vanishTabList = new VanishTabList(plugin, this);
this.players = Maps.newConcurrentMap(); this.players = Maps.newConcurrentMap();
this.groupTasks = Maps.newConcurrentMap(); this.taskManager = new TaskManager(plugin);
this.reloadUpdate(); this.reloadUpdate();
this.registerListener(); this.registerListener();
this.ensureDisplayNameTask();
} }
private void registerListener() { private void registerListener() {
@ -102,16 +103,12 @@ public class PlayerTabList {
} }
final String serverName = server.get().getServerInfo().getName(); final String serverName = server.get().getServerInfo().getName();
final Group group = getGroup(serverName); final @NotNull Optional<Group> group = getGroup(serverName);
final boolean isDefault = group.registeredServers(plugin) if (group.isEmpty()) {
.stream()
.noneMatch(s -> s.getServerInfo().getName().equals(serverName));
if (isDefault && !plugin.getSettings().isFallbackEnabled()) {
return; 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. * Removes the player's entry from the tab list of all other players on the same group servers.
*/ */
public void close() { public void close() {
groupTasks.values().forEach(GroupTasks::cancel); taskManager.cancelAllTasks();
plugin.getServer().getAllPlayers().forEach(p -> { plugin.getServer().getAllPlayers().forEach(p -> {
final Optional<ServerConnection> server = p.getCurrentServer(); final Optional<ServerConnection> server = p.getCurrentServer();
if (server.isEmpty()) return; if (server.isEmpty()) return;
@ -140,27 +137,50 @@ 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) { protected void joinPlayer(@NotNull Player joined, @NotNull Group group) {
// Add the player to the tracking list if they are not already listed // Add the player to the tracking list if they are not already listed
final TabPlayer tabPlayer = getTabPlayer(joined).orElseGet(() -> createTabPlayer(joined, group)); final Optional<TabPlayer> tabPlayerOptional = getTabPlayer(joined);
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername()); if (tabPlayerOptional.isPresent()) {
tabPlayer.setGroup(group); tabPlayerOptional.get().clearCachedData();
players.putIfAbsent(joined.getUniqueId(), tabPlayer); tabPlayerOptional.get().setGroup(group);
tabPlayerOptional.get().setRole(plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(joined)).orElse(Role.DEFAULT_ROLE));
// Store the player's last server, so it's possible to have the last server on disconnect }
final TabPlayer tabPlayer = tabPlayerOptional.orElseGet(() -> createTabPlayer(joined, group));
final String serverName = getServerName(joined); final String serverName = getServerName(joined);
// Store last server, so it's possible to have the last server on disconnect
tabPlayer.setLastServer(serverName); tabPlayer.setLastServer(serverName);
// Send server URLs (1.21 clients) // Send server URLs (1.21 clients)
sendPlayerServerLinks(tabPlayer); 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 -> { tabPlayer.getDisplayName(plugin).thenAccept(d -> {
joined.getTabList().getEntry(joined.getUniqueId()) if (d == null) {
.ifPresentOrElse(e -> e.setDisplayName(d), plugin.log(Level.ERROR, "Failed to get display name for " + joined.getUsername());
() -> joined.getTabList().addEntry(createEntry(tabPlayer, joined.getTabList(), d))); return;
}
handleDisplayLoad(tabPlayer);
}).exceptionally(throwable -> {
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
joined.getUsername(), joined.getUniqueId()), throwable);
return null;
});
}
private void handleDisplayLoad(@NotNull TabPlayer tabPlayer) {
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) tabPlayer.sendHeaderAndFooter(this)
.thenAccept(v -> tabPlayer.setLoaded(true)) .thenAccept(v -> tabPlayer.setLoaded(true))
.exceptionally(throwable -> { .exceptionally(throwable -> {
@ -169,64 +189,59 @@ public class PlayerTabList {
return null; return null;
}); });
final Set<String> serversInGroup = group.registeredServers(plugin).stream() final Set<TabPlayer> tabPlayers = group.getTabPlayers(plugin, tabPlayer);
.map(server -> server.getServerInfo().getName()) updateTabListOnJoin(tabPlayer, group, tabPlayers, isVanished);
.collect(HashSet::new, HashSet::add, HashSet::addAll); }
serversInGroup.remove(serverName);
// Update lists private void updateTabListOnJoin(@NotNull TabPlayer tabPlayer, @NotNull Group group,
plugin.getServer().getScheduler() @NotNull Set<TabPlayer> tabPlayers, boolean isJoinedVanished) {
.buildTask(plugin, () -> { final Player joined = tabPlayer.getPlayer();
final TabList tabList = joined.getTabList(); final String serverName = getServerName(joined);
final Set<TabPlayer> tabPlayers = group.getTabPlayers(plugin); final Set<UUID> uuids = tabPlayers.stream().map(p -> p.getPlayer().getUniqueId()).collect(Collectors.toSet());
for (final TabPlayer player : tabPlayers) { List.copyOf(tabPlayer.getPlayer().getTabList().getEntries()).forEach(entry -> {
// Skip players on other servers if the setting is enabled if (!uuids.contains(entry.getProfile().getId())) {
if (group.onlyListPlayersInSameServer() && !serverName.equals(getServerName(player.getPlayer()))) { 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; 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); // 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 -> { plugin.getScoreboardManager().ifPresent(s -> {
s.resendAllTeams(tabPlayer); s.resendAllTeams(tabPlayer);
tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(tabPlayer, t, false)); tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(tabPlayer, t, false));
}); });
fixDuplicateEntries(joined); fixDuplicateEntries(joined);
// Fire event without listening for result // Fire event without listening for result
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group)); plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group));
}) }
.delay(300, TimeUnit.MILLISECONDS)
.schedule(); private void checkVisibilityAndUpdateName(@NotNull TabPlayer observedPlayer, @NotNull TabPlayer observableTabPlayer,
}).exceptionally(throwable -> { boolean isObservablePlayerVanished) {
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)", final UUID observableUUID = observableTabPlayer.getPlayer().getUniqueId();
joined.getUsername(), joined.getUniqueId()), throwable); final String observedUsername = observedPlayer.getPlayer().getUsername();
return null; 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 @NotNull
@ -236,6 +251,21 @@ public class PlayerTabList {
.orElse(""); .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") @SuppressWarnings("unchecked")
private void fixDuplicateEntries(@NotNull Player target) { private void fixDuplicateEntries(@NotNull Player target) {
try { try {
@ -248,7 +278,7 @@ public class PlayerTabList {
.filter(entry -> !entry.getKey().equals(target.getUniqueId())) .filter(entry -> !entry.getKey().equals(target.getUniqueId()))
.forEach(entry -> target.getTabList().removeEntry(entry.getKey())); .forEach(entry -> target.getTabList().removeEntry(entry.getKey()));
} catch (Throwable error) { } 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 -> { .forEach(player -> {
player.getPlayer().getTabList().removeEntry(uuid); player.getPlayer().getTabList().removeEntry(uuid);
player.sendHeaderAndFooter(this); player.sendHeaderAndFooter(this);
updatePlayerDisplayName(player);
})) }))
.delay(500, TimeUnit.MILLISECONDS) .delay(250, TimeUnit.MILLISECONDS)
.schedule(); .schedule();
// Delete player team // Delete player team
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(target)); plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(target));
@ -283,10 +314,6 @@ public class PlayerTabList {
} }
@NotNull @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) { protected TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull Component displayName) {
return TabListEntry.builder() return TabListEntry.builder()
.profile(player.getPlayer().getGameProfile()) .profile(player.getPlayer().getGameProfile())
@ -296,25 +323,43 @@ public class PlayerTabList {
.build(); .build();
} }
private void addPlayerToTabList(@NotNull TabPlayer player, @NotNull TabPlayer newPlayer, @NotNull Component displayName) { @NotNull
if (newPlayer.getPlayer().getUniqueId().equals(player.getPlayer().getUniqueId())) { 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; return;
} }
plugin.getPacketEventManager().getVelocitabEntries().add(newPlayer.getPlayer().getUniqueId()); player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
viewer.getPlayer().getTabList().getEntry(player.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()
.ifPresentOrElse( .ifPresentOrElse(
entry -> entry.setDisplayName(displayName), entry -> entry.setDisplayName(displayName),
() -> player.getPlayer().getTabList() () -> viewer.getPlayer().getTabList()
.addEntry(createEntry(newPlayer, player.getPlayer().getTabList(), displayName)) .addEntry(createEntry(player, viewer.getPlayer().getTabList(), displayName))
); );
} }
@ -353,11 +398,12 @@ public class PlayerTabList {
} }
public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) { public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) {
final Component lastDisplayName = tabPlayer.getLastDisplayName();
tabPlayer.getDisplayName(plugin).thenAccept(displayName -> { 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; return;
} }
final Component single = plugin.getFormatter().format(displayName, tabPlayer, plugin);
final boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername()); final boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername());
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer); final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
@ -367,18 +413,51 @@ public class PlayerTabList {
return; return;
} }
player.getPlayer().getTabList().getEntries().stream() final Component relationalPlaceholder = getRelationalPlaceholder(tabPlayer, player, single, displayName);
.filter(e -> e.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId())).findFirst() updateDisplayName(tabPlayer, player, relationalPlaceholder);
.ifPresent(entry -> entry.setDisplayName(displayName));
}); });
}); });
} }
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 // Update the display names of all listed players
public void updateDisplayNames() { public void updateDisplayNames() {
players.values().forEach(this::updatePlayerDisplayName); 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 // Get the component for the TAB list header
public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) { public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) {
final String header = player.getGroup().getHeader(player.getHeaderIndex()); final String header = player.getGroup().getHeader(player.getHeaderIndex());
@ -395,96 +474,11 @@ public class PlayerTabList {
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin)); .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 * Update the TAB list for all players when a plugin or proxy reload is performed
*/ */
public void reloadUpdate() { public void reloadUpdate() {
plugin.getTabGroups().getGroups().forEach(this::updatePeriodically); plugin.getTabGroups().getGroups().forEach(taskManager::updatePeriodically);
if (players.isEmpty()) { if (players.isEmpty()) {
return; return;
} }
@ -496,8 +490,11 @@ public class PlayerTabList {
return; return;
} }
final String serverName = server.get().getServerInfo().getName(); final String serverName = server.get().getServerInfo().getName();
final Group group = getGroup(serverName); final Optional<Group> group = getGroup(serverName);
player.setGroup(group); if (group.isEmpty()) {
return;
}
player.setGroup(group.get());
this.sendPlayerServerLinks(player); this.sendPlayerServerLinks(player);
this.updatePlayer(player, true); this.updatePlayer(player, true);
player.sendHeaderAndFooter(this); player.sendHeaderAndFooter(this);
@ -506,7 +503,7 @@ public class PlayerTabList {
} }
@NotNull @NotNull
public Group getGroup(@NotNull String serverName) { public Optional<Group> getGroup(@NotNull String serverName) {
return plugin.getTabGroups().getGroupFromServer(serverName, plugin); return plugin.getTabGroups().getGroupFromServer(serverName, plugin);
} }

View File

@ -68,8 +68,12 @@ public class TabListListener {
tabList.removePlayer(event.getPlayer()); tabList.removePlayer(event.getPlayer());
} else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer redirectPlayer) { } else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer redirectPlayer) {
tabList.removePlayer(event.getPlayer(), redirectPlayer.getServer()); 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()); justQuit.add(event.getPlayer().getUniqueId());
plugin.getServer().getScheduler().buildTask(plugin, plugin.getServer().getScheduler().buildTask(plugin,
@ -88,14 +92,15 @@ public class TabListListener {
.orElse(""); .orElse("");
// Get the group the player should now be in // Get the group the player should now be in
final Group group = tabList.getGroup(serverName); final @NotNull Optional<Group> groupOptional = tabList.getGroup(serverName);
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group)); final boolean isDefault = groupOptional.map(g -> g.isDefault(plugin)).orElse(false);
final boolean isDefault = group.registeredServers(plugin).stream()
.noneMatch(server -> server.getServerInfo().getName().equalsIgnoreCase(serverName)); // 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 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 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); final Optional<TabPlayer> tabPlayer = tabList.getTabPlayer(joined);
if (tabPlayer.isEmpty()) { if (tabPlayer.isEmpty()) {
return; return;
@ -107,7 +112,6 @@ public class TabListListener {
final Component header = tabPlayer.get().getLastHeader(); final Component header = tabPlayer.get().getLastHeader();
final Component footer = tabPlayer.get().getLastFooter(); final Component footer = tabPlayer.get().getLastFooter();
final Component displayName = tabPlayer.get().getLastDisplayName();
plugin.getServer().getScheduler().buildTask(plugin, () -> { plugin.getServer().getScheduler().buildTask(plugin, () -> {
final Component currentHeader = joined.getPlayerListHeader(); final Component currentHeader = joined.getPlayerListHeader();
@ -126,6 +130,12 @@ public class TabListListener {
return; return;
} }
if (groupOptional.isEmpty()) {
return;
}
final Group group = groupOptional.get();
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group));
if (justQuit.contains(joined.getUniqueId())) { if (justQuit.contains(joined.getUniqueId())) {
plugin.getServer().getScheduler().buildTask(plugin, plugin.getServer().getScheduler().buildTask(plugin,
() -> tabList.joinPlayer(joined, group)) () -> tabList.joinPlayer(joined, group))
@ -159,7 +169,6 @@ public class TabListListener {
return; return;
} }
tabList.removePlayer(player); tabList.removePlayer(player);
plugin.log("Player " + player.getUsername() + " was not removed from the tab list, removing now.");
}).delay(500, TimeUnit.MILLISECONDS).schedule(); }).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; package net.william278.velocitab.tab;
import com.velocitypowered.api.proxy.Player; 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.Velocitab;
import net.william278.velocitab.player.TabPlayer; import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -30,17 +32,12 @@ import java.util.UUID;
/** /**
* The VanishTabList handles the tab list for vanished players * The VanishTabList handles the tab list for vanished players
*/ */
@RequiredArgsConstructor
public class VanishTabList { public class VanishTabList {
private final Velocitab plugin; private final Velocitab plugin;
private final PlayerTabList tabList; private final PlayerTabList tabList;
public VanishTabList(Velocitab plugin, PlayerTabList tabList) {
this.plugin = plugin;
this.tabList = tabList;
}
public void vanishPlayer(@NotNull TabPlayer tabPlayer) { public void vanishPlayer(@NotNull TabPlayer tabPlayer) {
tabList.getPlayers().values().forEach(p -> { tabList.getPlayers().values().forEach(p -> {
if (p.getPlayer().equals(tabPlayer.getPlayer())) { if (p.getPlayer().equals(tabPlayer.getPlayer())) {
@ -56,17 +53,17 @@ public class VanishTabList {
public void unVanishPlayer(@NotNull TabPlayer tabPlayer) { public void unVanishPlayer(@NotNull TabPlayer tabPlayer) {
final UUID uuid = tabPlayer.getPlayer().getUniqueId(); 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())) { if (p.getPlayer().equals(tabPlayer.getPlayer())) {
return; return;
} }
if (!p.getPlayer().getTabList().containsEntry(uuid)) { 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 { } 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)); plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, false));
} else { } else {
if (!player.getTabList().containsEntry(p.getUniqueId())) { if (!player.getTabList().containsEntry(p.getUniqueId())) {
tabList.createEntry(target, player.getTabList()).thenAccept(e -> { final TabListEntry tabListEntry = tabList.createEntry(target, player.getTabList(), tabPlayer);
player.getTabList().addEntry(e); player.getTabList().addEntry(tabListEntry);
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, true)); 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();
}