forked from Upstream/Velocitab
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:
parent
06268521cf
commit
ace3644111
33
build.gradle
33
build.gradle
@ -24,8 +24,8 @@ ext {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||
maven { url = 'https://repo.william278.net/velocity/' }
|
||||
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||
maven { url = 'https://repo.william278.net/releases/' }
|
||||
maven { url = 'https://jitpack.io/' }
|
||||
maven { url = 'https://repo.minebench.de/' }
|
||||
@ -35,19 +35,21 @@ dependencies {
|
||||
compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT"
|
||||
compileOnly "com.velocitypowered:velocity-proxy:${velocity_api_version}-SNAPSHOT"
|
||||
|
||||
compileOnly 'io.netty:netty-codec-http:4.1.110.Final'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
compileOnly 'net.luckperms:api:5.4'
|
||||
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.2.3'
|
||||
compileOnly 'net.william278:PAPIProxyBridge:1.5'
|
||||
compileOnly 'it.unimi.dsi:fastutil:8.5.13'
|
||||
compileOnly 'net.kyori:adventure-nbt:4.17.0'
|
||||
|
||||
implementation 'org.apache.commons:commons-text:1.12.0'
|
||||
implementation 'net.william278:DesertWell:2.0.4'
|
||||
implementation 'net.william278:minedown:1.8.2'
|
||||
implementation 'org.bstats:bstats-velocity:3.0.2'
|
||||
implementation 'de.exlll:configlib-yaml:4.5.0'
|
||||
|
||||
compileOnly 'io.netty:netty-codec-http:4.1.111.Final'
|
||||
compileOnly 'net.luckperms:api:5.4'
|
||||
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.0.0'
|
||||
compileOnly 'net.william278:PAPIProxyBridge:1.5'
|
||||
compileOnly 'it.unimi.dsi:fastutil:8.5.13'
|
||||
compileOnly 'net.kyori:adventure-nbt:4.17.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
implementation 'org.apache.commons:commons-jexl3:3.4.0'
|
||||
implementation 'net.jodah:expiringmap:0.5.11'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
}
|
||||
@ -92,6 +94,10 @@ shadowJar {
|
||||
relocate 'net.william278.desertwell', 'net.william278.velocitab.libraries.desertwell'
|
||||
relocate 'org.bstats', 'net.william278.velocitab.libraries.bstats'
|
||||
relocate 'de.exlll.configlib', 'net.william278.velocitab.libraries.configlib'
|
||||
relocate 'org.snakeyaml', 'net.william278.velocitab.libraries.snakeyaml'
|
||||
relocate 'org.apache.commons.jexl3', 'net.william278.velocitab.libraries.commons.jexl3'
|
||||
relocate 'org.apache.commons.logging', 'net.william278.velocitab.libraries.commons.logging'
|
||||
relocate 'net.jodah.expiringmap', 'net.william278.velocitab.libraries.expiringmap'
|
||||
|
||||
dependencies {
|
||||
exclude dependency(':slf4j-api')
|
||||
@ -101,7 +107,9 @@ shadowJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
|
||||
minimize()
|
||||
minimize() {
|
||||
exclude dependency('commons-logging:commons-logging')
|
||||
}
|
||||
}
|
||||
jar.dependsOn shadowJar
|
||||
clean.delete "$rootDir/target"
|
||||
@ -151,6 +159,11 @@ publishing {
|
||||
tasks {
|
||||
runVelocity {
|
||||
velocityVersion("${velocity_api_version}-SNAPSHOT")
|
||||
|
||||
downloadPlugins {
|
||||
modrinth ("papiproxybridge", "1.6.1")
|
||||
modrinth ("miniplaceholders", "2.2.4")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
42
docs/Conditional-Placeholders.md
Normal file
42
docs/Conditional-Placeholders.md
Normal 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.
|
@ -122,5 +122,39 @@ Velocitab supports basic header and footer animations by adding multiple frames
|
||||
### Placeholders
|
||||
You can use various placeholders that will be replaced with values (for example, `%username%`) in your config. Support for PlaceholderAPI is also available through [a bridge library plugin](https://modrinth.com/plugin/papiproxybridge), as is the component-based MiniPlaceholders for users of that plugin with the MiniMessage formatter. See [[Placeholders]] for more information.
|
||||
|
||||
### YAML MultiLine Syntax
|
||||
|
||||
In order to have a multi-line string in YAML, you can use the `|-` or `|` syntax. The `|-` syntax will remove last newline character, while the `|` syntax will keep it.
|
||||
You can also use `\n` to add a newline character in a string.
|
||||
|
||||
# Example 1
|
||||
```yaml
|
||||
foo: |-
|
||||
bar 1
|
||||
bar 2
|
||||
bar 3
|
||||
```
|
||||
|
||||
is equivalent to
|
||||
|
||||
```yaml
|
||||
foo: "bar 1\nbar 2\nbar 3"
|
||||
```
|
||||
|
||||
# Example 2
|
||||
|
||||
```yaml
|
||||
foo: |
|
||||
bar 1
|
||||
bar 2
|
||||
bar 3
|
||||
```
|
||||
|
||||
is equivalent to
|
||||
|
||||
```yaml
|
||||
foo: "bar 1\nbar 2\nbar 3\n"
|
||||
```
|
||||
|
||||
### Server Links
|
||||
For Minecraft 1.21+ clients, Velocitab supports specifying a list of URLs that will be sent to display in the player pause menu. See [[Server Links]] for more information.
|
@ -15,6 +15,8 @@ Please click through to the topic you wish to read about.
|
||||
* 📛 [[Nametags]]
|
||||
* 📊 [[Sorting]]
|
||||
* ✍️ [[Placeholders]]
|
||||
* 🔗 [[Relational Placeholders]]
|
||||
* 🔀 [[Conditional Placeholders]]
|
||||
* ✨ [[Animations]]
|
||||
* 🖼️ [[Custom Logos]]
|
||||
* 🔗 [[Server Links]]
|
||||
|
@ -3,28 +3,42 @@ Velocitab supports a number of Placeholders that will be replaced with their res
|
||||
## Default placeholders
|
||||
Placeholders can be included in the header, footer and player name format of the TAB list. The following placeholders are supported out of the box:
|
||||
|
||||
| Placeholder | Description | Example |
|
||||
|---------------------------------|------------------------------------------------------|--------------------|
|
||||
| `%players_online%` | Players online on the proxy | `6` |
|
||||
| `%max_players_online%` | Player capacity of the proxy | `500` |
|
||||
| `%local_players_online%` | Players online on the server the player is on | `3` |
|
||||
| `%group_players_online_(name)%` | Players online on the group provided | `11` |
|
||||
| `%group_players_online%` | Players online on player's group | `15` |
|
||||
| `%current_date%` | Current real-world date of the server | `24 Feb 2023` |
|
||||
| `%current_time%` | Current real-world time of the server | `21:45:32` |
|
||||
| `%username%` | The player's username | `William278` |
|
||||
| `%username_lower%` | The player's username, in lowercase | `william278` |
|
||||
| `%server%` | Name of the server the player is on | `alpha` |
|
||||
| `%ping%` | Ping of the player (in ms) | `6` |
|
||||
| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` |
|
||||
| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` |
|
||||
| `%role%` | The player's primary LuckPerms group name | `admin` |
|
||||
| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` |
|
||||
| `%role_weight%` | Comparable-formatted primary LuckPerms group weight. | `100` |
|
||||
| `%luckperms_meta_(key)%` | Formats a meta key from the user's LuckPerms group | (varies) |
|
||||
| `%server_group%` | The name of the server group the player is on | `default` |
|
||||
| `%server_group_index%` | Indexed order of the server group in the list | `0` |
|
||||
| `%debug_team_name%` | (Debug) Player's team name, used for [[Sorting]] | `1alphaWilliam278` |
|
||||
| Placeholder | Description | Example |
|
||||
|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
|
||||
| `%players_online%` | Players online on the proxy | `6` |
|
||||
| `%max_players_online%` | Player capacity of the proxy | `500` |
|
||||
| `%local_players_online%` | Players online on the server the player is on | `3` |
|
||||
| `%group_players_online_(name)%` | Players online on the group provided | `11` |
|
||||
| `%group_players_online%` | Players online on player's group | `15` |
|
||||
| `%current_date_day%` | Current day of the month | `14` |
|
||||
| `%current_date_weekday%` | Current day of the week | `Wednesday` |
|
||||
| `%current_date_weekday_(tag)%` | Current day of the week ([localized](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags)) `it-IT` as example | `Mercoledì` |
|
||||
| `%current_date_month%` | Current month of the year | `06` |
|
||||
| `%current_date_year%` | Current year | `2024` |
|
||||
| `%current_date%` | Current real-world date of the server | `14/06/2023` |
|
||||
| `%current_date_(tag)%` | Current real-world date ([localized](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags)) `en-US` as example | `06/14/2023` |
|
||||
| `%current_time_hour%` | Current hour of the day | `21` |
|
||||
| `%current_time_minute%` | Current minute of the hour | `45` |
|
||||
| `%current_time_second%` | Current second of the minute | `32` |
|
||||
| `%current_time%` | Current real-world time of the server | `21:45:32` |
|
||||
| `%current_time_(tag)%` | Current real-world time ([localized](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags)) `en-US` as example | `9:45 PM` |
|
||||
| `%username%` | The player's username | `William278` |
|
||||
| `%username_lower%` | The player's username, in lowercase | `william278` |
|
||||
| `%server%` | Name of the server the player is on | `alpha` |
|
||||
| `%ping%` | Ping of the player (in ms) | `6` |
|
||||
| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` |
|
||||
| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` |
|
||||
| `%role%` | The player's primary LuckPerms group name | `admin` |
|
||||
| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` |
|
||||
| `%role_weight%` | Comparable-formatted primary LuckPerms group weight | `100` |
|
||||
| `%luckperms_meta_(key)%` | Formats a meta key from the user's LuckPerms group | (varies) |
|
||||
| `%server_group%` | The name of the server group the player is on | `default` |
|
||||
| `%server_group_index%` | Indexed order of the server group in the list | `0` |
|
||||
| `%debug_team_name%` | (Debug) Player's team name, used for [[Sorting]] | `1alphaWilliam278` |
|
||||
|
||||
**Note:** `(tag)` stands for IETF language tag, used for localization of date and time placeholders. For example, `en-US` for American English, `fr-FR` for French, `it-IT` for Italian, etc.
|
||||
You can find a list of common primary language subtags [here](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags).
|
||||
|
||||
|
||||
### Customising server display names
|
||||
You can make use of the `server_display_names` feature in `config.yml` to customise how server display name appear when using the `%server%` placeholder. In the below example, if a user is connected to a server with the name "`very-long-server-`name" and the player name format for the group that server belongs to includes a `%server%` placeholder, the placeholder would be replaced with "`VSLN`" instead of the full server name.
|
||||
@ -46,4 +60,5 @@ To use PlaceholderAPI placeholders in Velocitab, install the [PAPIProxyBridge](h
|
||||
PlaceholderAPI placeholders are cached to reduce plugin message traffic. By default, placeholders are cached for 30 seconds (30000 milliseconds); if you wish to use PAPI placeholders that update more frequently, you can reduce the cache time in the Velocitab config.yml file by adjusting the `papi_cache_time` value.
|
||||
|
||||
## MiniPlaceholders support
|
||||
If you are using MiniMessage [[Formatting]], you can use [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) with Velocitab for MiniMessage-styled component placeholders provided by other proxy plugins. Install MiniPlaceholders on your Velocity proxy, set the `formatter_type` to `MINIMESSAGE` and ensure `enable_miniplaceholders_hook` is set to `true`
|
||||
If you are using MiniMessage [[Formatting]], you can use [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) with Velocitab for MiniMessage-styled component placeholders provided by other proxy plugins. Install MiniPlaceholders on your Velocity proxy, set the `formatter_type` to `MINIMESSAGE` and ensure `enable_miniplaceholders_hook` is set to `true`
|
||||
You can also use [Relational Placeholders](Relational-Placeholders).
|
26
docs/Relational-Placeholders.md
Normal file
26
docs/Relational-Placeholders.md
Normal 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` |
|
||||
|
||||
|
@ -9,6 +9,8 @@
|
||||
* 📛 [[Nametags]]
|
||||
* 📊 [[Sorting]]
|
||||
* ✍️ [[Placeholders]]
|
||||
* 🔗 [[Relational Placeholders]]
|
||||
* 🔀 [[Conditional Placeholders]]
|
||||
* ✨ [[Animations]]
|
||||
* 🖼️ [[Custom Logos]]
|
||||
* 🔗 [[Server Links]]
|
||||
|
@ -43,6 +43,7 @@ import net.william278.velocitab.config.Settings;
|
||||
import net.william278.velocitab.config.TabGroups;
|
||||
import net.william278.velocitab.hook.Hook;
|
||||
import net.william278.velocitab.hook.LuckPermsHook;
|
||||
import net.william278.velocitab.hook.MiniPlaceholdersHook;
|
||||
import net.william278.velocitab.packet.PacketEventManager;
|
||||
import net.william278.velocitab.packet.ScoreboardManager;
|
||||
import net.william278.velocitab.providers.HookProvider;
|
||||
@ -113,9 +114,10 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
|
||||
@Subscribe
|
||||
public void onProxyShutdown(@NotNull ProxyShutdownEvent event) {
|
||||
server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel);
|
||||
// server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel);
|
||||
disableScoreboardManager();
|
||||
getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent);
|
||||
getMiniPlaceholdersHook().ifPresent(MiniPlaceholdersHook::unregisterExpansion);
|
||||
unregisterAPI();
|
||||
logger.info("Successfully disabled Velocitab");
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@ -190,6 +191,33 @@ public class VelocitabAPI {
|
||||
return getUser(player).map(TabPlayer::getGroup).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of server groups.
|
||||
*
|
||||
* @return A list of Group objects representing server groups.
|
||||
* @since 1.6.6
|
||||
*/
|
||||
@NotNull
|
||||
public List<Group> getServerGroups() {
|
||||
return plugin.getTabGroups().getGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an optional Group object with the given name.
|
||||
*
|
||||
* @param name The name of the group to retrieve.
|
||||
* @return An optional Group object containing the group with the given name, or an empty optional if no group exists with that name.
|
||||
* @since 1.6.6
|
||||
*/
|
||||
@NotNull
|
||||
public Optional<Group> getGroup(@NotNull String name) {
|
||||
return plugin.getTabGroups().getGroup(name);
|
||||
}
|
||||
|
||||
public Optional<Group> getGroupFromServer(@NotNull String server) {
|
||||
return plugin.getTabGroups().getGroupFromServer(server, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception indicating the Velocitab API was accessed before it was registered.
|
||||
*
|
||||
|
@ -156,7 +156,7 @@ public interface ConfigProvider {
|
||||
|
||||
/**
|
||||
* Saves the tab groups to the "tab_groups.yml" config file.
|
||||
* Uses the YamlConfigurations.save method to write the tab groups object to the specified config file path.
|
||||
* Uses the YamlConfigurations#save method to write the tab groups object to the specified config file path.
|
||||
* This method assumes that the getConfigDirectory method returns a valid directory path.
|
||||
*
|
||||
* @throws IllegalStateException if the getConfigDirectory method returns null
|
||||
|
@ -25,8 +25,10 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.apache.commons.lang3.function.TriFunction;
|
||||
import net.william278.velocitab.util.QuadFunction;
|
||||
import net.william278.velocitab.util.SerializerUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@ -36,26 +38,33 @@ import java.util.function.Function;
|
||||
@SuppressWarnings("unused")
|
||||
public enum Formatter {
|
||||
MINEDOWN(
|
||||
(text, player, plugin) -> new MineDown(text).toComponent(),
|
||||
(text, player, viewer, plugin) -> new MineDown(text).toComponent(),
|
||||
MineDown::escape,
|
||||
"MineDown",
|
||||
(text) -> new MineDown(text).toComponent()
|
||||
(text) -> new MineDown(text).toComponent(),
|
||||
(text) -> {
|
||||
throw new UnsupportedOperationException("MineDown does not support serialization");
|
||||
}
|
||||
),
|
||||
MINIMESSAGE(
|
||||
(text, player, plugin) -> plugin.getMiniPlaceholdersHook()
|
||||
.map(hook -> hook.format(text, player.getPlayer()))
|
||||
(text, player, viewer, plugin) -> plugin.getMiniPlaceholdersHook()
|
||||
.filter(hook -> viewer != null)
|
||||
.map(hook -> hook.format(text, player.getPlayer(), viewer.getPlayer()))
|
||||
.orElse(MiniMessage.miniMessage().deserialize(text)),
|
||||
(text) -> MiniMessage.miniMessage().escapeTags(text),
|
||||
"MiniMessage",
|
||||
(text) -> MiniMessage.miniMessage().deserialize(text)
|
||||
(text) -> MiniMessage.miniMessage().deserialize(text),
|
||||
MiniMessage.miniMessage()::serialize
|
||||
),
|
||||
LEGACY(
|
||||
(text, player, plugin) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text),
|
||||
(text, player, viewer, plugin) -> SerializerUtil.LEGACY_SERIALIZER.deserialize(text),
|
||||
Function.identity(),
|
||||
"Legacy Text",
|
||||
(text) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text)
|
||||
SerializerUtil.LEGACY_SERIALIZER::deserialize,
|
||||
SerializerUtil.LEGACY_SERIALIZER::serialize
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Name of the formatter
|
||||
*/
|
||||
@ -64,24 +73,50 @@ public enum Formatter {
|
||||
/**
|
||||
* Function to apply formatting to a string
|
||||
*/
|
||||
private final TriFunction<String, TabPlayer, Velocitab, Component> formatter;
|
||||
private final QuadFunction<String, TabPlayer, TabPlayer, Velocitab, Component> formatter;
|
||||
/**
|
||||
* Function to escape formatting characters in a string
|
||||
*/
|
||||
private final Function<String, String> escaper;
|
||||
private final Function<String, Component> emptyFormatter;
|
||||
private final Function<Component, String> serializer;
|
||||
|
||||
Formatter(@NotNull TriFunction<String, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper,
|
||||
@NotNull String name, @NotNull Function<String, Component> emptyFormatter) {
|
||||
Formatter(@NotNull QuadFunction<String, TabPlayer, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper,
|
||||
@NotNull String name, @NotNull Function<String, Component> emptyFormatter, @NotNull Function<Component, String> serializer) {
|
||||
this.formatter = formatter;
|
||||
this.escaper = escaper;
|
||||
this.name = name;
|
||||
this.emptyFormatter = emptyFormatter;
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given text using a specific formatter.
|
||||
*
|
||||
* @param text The text to format
|
||||
* @param player The TabPlayer object representing the player
|
||||
* @param tabPlayer The TabPlayer object representing the viewer (can be null)
|
||||
* @param plugin The Velocitab plugin instance
|
||||
* @return The formatted Component object
|
||||
* @throws NullPointerException if any of the parameters (text, player, plugin) is null
|
||||
*/
|
||||
@NotNull
|
||||
public Component format(@NotNull String text, @NotNull TabPlayer player, @Nullable TabPlayer tabPlayer, @NotNull Velocitab plugin) {
|
||||
return formatter.apply(text, player, tabPlayer, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given text using a specific formatter.
|
||||
*
|
||||
* @param text The text to format
|
||||
* @param player The TabPlayer object representing the player
|
||||
* @param plugin The Velocitab plugin instance
|
||||
* @return The formatted Component object
|
||||
* @throws NullPointerException if any of the parameters (text, player, plugin) is null
|
||||
*/
|
||||
@NotNull
|
||||
public Component format(@NotNull String text, @NotNull TabPlayer player, @NotNull Velocitab plugin) {
|
||||
return formatter.apply(text, player, plugin);
|
||||
return formatter.apply(text, player, null, plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -91,7 +126,7 @@ public enum Formatter {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component emptyFormat(@NotNull String text) {
|
||||
public Component deserialize(@NotNull String text) {
|
||||
return emptyFormatter.apply(text);
|
||||
}
|
||||
|
||||
@ -105,4 +140,9 @@ public enum Formatter {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String serialize(@NotNull Component component) {
|
||||
return serializer.apply(component);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -64,6 +64,11 @@ public record Group(
|
||||
.get(Math.max(0, Math.min(index, footers.size() - 1))));
|
||||
}
|
||||
|
||||
public boolean containsServer(@NotNull Velocitab plugin, @NotNull String serverName) {
|
||||
return registeredServers(plugin).stream()
|
||||
.anyMatch(registeredServer -> registeredServer.getServerInfo().getName().equalsIgnoreCase(serverName));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Set<RegisteredServer> registeredServers(@NotNull Velocitab plugin) {
|
||||
return registeredServers(plugin, true);
|
||||
|
@ -21,16 +21,23 @@ package net.william278.velocitab.config;
|
||||
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.tab.Nametag;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.function.TriFunction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
@ -45,12 +52,47 @@ public enum Placeholder {
|
||||
.map(RegisteredServer::getPlayersConnected)
|
||||
.map(players -> Integer.toString(players.size()))
|
||||
.orElse("")),
|
||||
GROUP_PLAYERS_ONLINE_((param, plugin, player) -> plugin.getTabGroups().getGroup(param)
|
||||
.map(group -> Integer.toString(group.getPlayers(plugin).size()))
|
||||
.orElse("Group " + param + " not found")),
|
||||
GROUP_PLAYERS_ONLINE((plugin, player) -> Integer.toString(player.getGroup().getPlayers(plugin).size())),
|
||||
CURRENT_DATE((plugin, player) -> DateTimeFormatter.ofPattern("dd MMM yyyy").format(LocalDateTime.now())),
|
||||
CURRENT_TIME((plugin, player) -> DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now())),
|
||||
GROUP_PLAYERS_ONLINE((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return Integer.toString(player.getGroup().getPlayers(plugin).size());
|
||||
}
|
||||
return plugin.getTabGroups().getGroup(param)
|
||||
.map(group -> Integer.toString(group.getPlayers(plugin).size()))
|
||||
.orElse("Group " + param + " not found");
|
||||
}),
|
||||
CURRENT_DATE_DAY((plugin, player) -> DateTimeFormatter.ofPattern("dd").format(LocalDateTime.now())),
|
||||
CURRENT_DATE_WEEKDAY((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return DateTimeFormatter.ofPattern("EEEE").format(LocalDateTime.now());
|
||||
}
|
||||
|
||||
final String countryCode = param.toUpperCase();
|
||||
final Locale locale = Locale.forLanguageTag(countryCode);
|
||||
return DateTimeFormatter.ofPattern("EEEE").withLocale(locale).format(LocalDateTime.now());
|
||||
}),
|
||||
CURRENT_DATE_MONTH((plugin, player) -> DateTimeFormatter.ofPattern("MM").format(LocalDateTime.now())),
|
||||
CURRENT_DATE_YEAR((plugin, player) -> DateTimeFormatter.ofPattern("yyyy").format(LocalDateTime.now())),
|
||||
CURRENT_DATE((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return DateTimeFormatter.ofPattern("dd/MM/yyyy").format(LocalDateTime.now());
|
||||
}
|
||||
|
||||
final String countryCode = param.toUpperCase();
|
||||
final Locale locale = Locale.forLanguageTag(countryCode);
|
||||
return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale).format(LocalDateTime.now());
|
||||
}),
|
||||
CURRENT_TIME_HOUR((plugin, player) -> DateTimeFormatter.ofPattern("HH").format(LocalDateTime.now())),
|
||||
CURRENT_TIME_MINUTE((plugin, player) -> DateTimeFormatter.ofPattern("mm").format(LocalDateTime.now())),
|
||||
CURRENT_TIME_SECOND((plugin, player) -> DateTimeFormatter.ofPattern("ss").format(LocalDateTime.now())),
|
||||
CURRENT_TIME((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now());
|
||||
}
|
||||
|
||||
final String countryCode = param.toUpperCase();
|
||||
final Locale locale = Locale.forLanguageTag(countryCode);
|
||||
return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale).format(LocalTime.now());
|
||||
}),
|
||||
USERNAME((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername())),
|
||||
USERNAME_LOWER((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername()).toLowerCase()),
|
||||
SERVER((plugin, player) -> player.getServerDisplayName(plugin)),
|
||||
@ -67,18 +109,26 @@ public enum Placeholder {
|
||||
SERVER_GROUP((plugin, player) -> player.getGroup().name()),
|
||||
SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))),
|
||||
DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse(""))),
|
||||
LUCKPERMS_META_((param, plugin, player) -> plugin.getLuckPermsHook()
|
||||
LUCKPERMS_META((param, plugin, player) -> plugin.getLuckPermsHook()
|
||||
.map(hook -> hook.getMeta(player.getPlayer(), param))
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_" + param + "%")));
|
||||
|
||||
private final static Pattern VELOCITAB_PATTERN = Pattern.compile("<velocitab_.*?>");
|
||||
private final static Pattern PLACEHOLDER_PATTERN = Pattern.compile("%.*?%");
|
||||
private final static Pattern CONDITIONAL_PATTERN = Pattern.compile("<velocitab_rel_condition:[^:]*:[^:]*:[^:]*>");
|
||||
private final static String DELIMITER = ":::";
|
||||
private final static String REL_SUBSTITUTE = "-REL-";
|
||||
public final static Map<String, String> SYMBOL_SUBSTITUTES = Map.of(
|
||||
"<", "-COND-1",
|
||||
">", "-COND-2"
|
||||
);
|
||||
|
||||
/**
|
||||
* Function to replace placeholders with a real value
|
||||
*/
|
||||
private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
|
||||
private final boolean parameterised;
|
||||
private final Pattern pattern;
|
||||
private final static Pattern CHECK_PLACEHOLDERS = Pattern.compile("%.*?%");
|
||||
private final static String DELIMITER = ":::";
|
||||
|
||||
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
|
||||
this.parameterised = false;
|
||||
@ -89,7 +139,7 @@ public enum Placeholder {
|
||||
Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) {
|
||||
this.parameterised = true;
|
||||
this.replacer = parameterisedReplacer;
|
||||
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]+%", Pattern.CASE_INSENSITIVE);
|
||||
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]*%", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
public static CompletableFuture<Nametag> replace(@NotNull Nametag nametag, @NotNull Velocitab plugin,
|
||||
@ -107,31 +157,88 @@ public enum Placeholder {
|
||||
return "";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String replaceInternal(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
|
||||
final Pair<Boolean, String> result = processRelationalPlaceholders(format, plugin);
|
||||
format = result.right();
|
||||
format = replacePlaceholders(format, plugin, player);
|
||||
|
||||
if (result.left()) {
|
||||
format = format.replace(REL_SUBSTITUTE, "%");
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
private static Pair<Boolean, String> processRelationalPlaceholders(@NotNull String format, @NotNull Velocitab plugin) {
|
||||
boolean foundRelational = false;
|
||||
if (format.contains("<vel") && plugin.getFormatter().equals(Formatter.MINIMESSAGE)) {
|
||||
final Matcher velocitabRelationalMatcher = VELOCITAB_PATTERN.matcher(format);
|
||||
while (velocitabRelationalMatcher.find()) {
|
||||
if (velocitabRelationalMatcher.group().contains("rel_condition")) {
|
||||
continue;
|
||||
}
|
||||
foundRelational = true;
|
||||
final String relationalPlaceholder = velocitabRelationalMatcher.group().substring(1, velocitabRelationalMatcher.group().length() - 1);
|
||||
final String fixedString = replaceSymbols(relationalPlaceholder);
|
||||
format = format.replace(relationalPlaceholder, fixedString);
|
||||
}
|
||||
|
||||
format = processConditionalPlaceholders(format);
|
||||
}
|
||||
return Pair.of(foundRelational, format);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String processConditionalPlaceholders(@NotNull String format) {
|
||||
final Matcher conditionalMatcher = CONDITIONAL_PATTERN.matcher(format);
|
||||
while (conditionalMatcher.find()) {
|
||||
String conditionalPlaceholder = conditionalMatcher.group();
|
||||
conditionalPlaceholder = conditionalPlaceholder.substring(1, conditionalPlaceholder.length() - 1);
|
||||
final String fixedString = replaceSymbols(conditionalPlaceholder);
|
||||
format = format.replace(conditionalPlaceholder, fixedString);
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String replacePlaceholders(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
|
||||
for (Placeholder placeholder : values()) {
|
||||
Matcher matcher = placeholder.pattern.matcher(format);
|
||||
if (placeholder.parameterised) {
|
||||
format = matcher.replaceAll(matchResult ->
|
||||
Matcher.quoteReplacement(
|
||||
placeholder.replacer.apply(StringUtils.chop(matchResult.group().replace("%" + placeholder.name().toLowerCase(), "")
|
||||
.replaceFirst("_", ""))
|
||||
, plugin, player)
|
||||
));
|
||||
} else {
|
||||
format = matcher.replaceAll(matchResult -> Matcher.quoteReplacement(placeholder.replacer.apply(null, plugin, player)));
|
||||
}
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String replaceSymbols(@NotNull String input) {
|
||||
String fixedString = input.replace("%", REL_SUBSTITUTE);
|
||||
fixedString = MiniMessage.miniMessage().serialize(Formatter.LEGACY.deserialize(fixedString));
|
||||
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
|
||||
fixedString = fixedString.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return fixedString;
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin,
|
||||
@NotNull TabPlayer player) {
|
||||
|
||||
if (format.equals(DELIMITER)) {
|
||||
return CompletableFuture.completedFuture("");
|
||||
}
|
||||
|
||||
for (Placeholder placeholder : values()) {
|
||||
Matcher matcher = placeholder.pattern.matcher(format);
|
||||
if (placeholder.parameterised) {
|
||||
// Replace the placeholder with the result of the replacer function with the parameter
|
||||
format = matcher.replaceAll(matchResult ->
|
||||
Matcher.quoteReplacement(placeholder.replacer.apply(
|
||||
StringUtils.chop(matchResult.group().replace(
|
||||
"%" + placeholder.name().toLowerCase(), ""
|
||||
)), plugin, player
|
||||
)));
|
||||
} else {
|
||||
// Replace the placeholder with the result of the replacer function
|
||||
format = matcher.replaceAll(matchResult -> Matcher.quoteReplacement(placeholder.replacer.apply(null, plugin, player)));
|
||||
}
|
||||
final String replaced = replaceInternal(format, plugin, player);
|
||||
|
||||
}
|
||||
|
||||
final String replaced = format;
|
||||
if (!CHECK_PLACEHOLDERS.matcher(replaced).find()) {
|
||||
if (!PLACEHOLDER_PATTERN.matcher(replaced).find()) {
|
||||
return CompletableFuture.completedFuture(replaced);
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ public record ServerUrl(
|
||||
(type) -> CompletableFuture.completedFuture(ServerLink.serverLink(type, url()))
|
||||
).orElseGet(
|
||||
() -> Placeholder.replace(label(), plugin, player)
|
||||
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
|
||||
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
|
||||
.thenApply(formatted -> ServerLink.serverLink(formatted, url()))
|
||||
);
|
||||
}
|
||||
|
@ -70,32 +70,34 @@ public class TabGroups implements ConfigValidator {
|
||||
.orElseThrow(() -> new IllegalStateException("No group with name " + name + " found"));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Optional<Group> getGroup(@NotNull String name) {
|
||||
return groups.stream()
|
||||
.filter(group -> group.name().equals(name))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Group getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
|
||||
public Optional<Group> getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
|
||||
final List<Group> groups = new ArrayList<>(this.groups);
|
||||
final Optional<Group> defaultGroup = getGroup("default");
|
||||
// Ensure the default group is always checked last
|
||||
if (defaultGroup.isPresent()) {
|
||||
groups.remove(defaultGroup.get());
|
||||
groups.add(defaultGroup.get());
|
||||
} else {
|
||||
if (defaultGroup.isEmpty()) {
|
||||
throw new IllegalStateException("No default group found");
|
||||
}
|
||||
// Ensure the default group is always checked last
|
||||
groups.remove(defaultGroup.get());
|
||||
groups.add(defaultGroup.get());
|
||||
for (Group group : groups) {
|
||||
if (group.registeredServers(plugin, false)
|
||||
.stream()
|
||||
.anyMatch(s -> s.getServerInfo().getName().equalsIgnoreCase(server))) {
|
||||
return group;
|
||||
return Optional.of(group);
|
||||
}
|
||||
}
|
||||
return getGroupFromName("default");
|
||||
|
||||
if (!plugin.getSettings().isFallbackEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return defaultGroup;
|
||||
}
|
||||
|
||||
public int getPosition(@NotNull Group group) {
|
||||
|
@ -25,16 +25,38 @@ import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class MiniPlaceholdersHook extends Hook {
|
||||
|
||||
public final static Map<String, String> REPLACE = Map.of(
|
||||
"\"", "-q-",
|
||||
"'", "-a-"
|
||||
);
|
||||
|
||||
private final VelocitabMiniExpansion expansion;
|
||||
|
||||
public MiniPlaceholdersHook(@NotNull Velocitab plugin) {
|
||||
super(plugin);
|
||||
this.expansion = new VelocitabMiniExpansion(plugin);
|
||||
expansion.registerExpansion();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component format(@NotNull String text, @NotNull Audience player) {
|
||||
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getAudienceGlobalPlaceholders(player));
|
||||
public Component format(@NotNull String text, @NotNull Audience player, @Nullable Audience viewer) {
|
||||
for (Map.Entry<String, String> entry : REPLACE.entrySet()) {
|
||||
text = text.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (viewer == null) {
|
||||
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getAudienceGlobalPlaceholders(player));
|
||||
}
|
||||
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getRelationalGlobalPlaceholders(player, viewer));
|
||||
}
|
||||
|
||||
public void unregisterExpansion() {
|
||||
expansion.unregisterExpansion();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -19,7 +19,6 @@
|
||||
|
||||
package net.william278.velocitab.packet;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.velocitypowered.api.event.AwaitingEventExecutor;
|
||||
import com.velocitypowered.api.event.EventTask;
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
@ -27,31 +26,20 @@ import com.velocitypowered.api.event.connection.PostLoginEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.network.Connections;
|
||||
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
||||
import io.netty.channel.Channel;
|
||||
import lombok.Getter;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.DefaultChannelPipeline;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PacketEventManager {
|
||||
|
||||
private static final String KEY = "velocitab";
|
||||
private static final String CITIZENS_PREFIX = "CIT";
|
||||
|
||||
private final Velocitab plugin;
|
||||
@Getter
|
||||
private final Set<UUID> velocitabEntries;
|
||||
|
||||
public PacketEventManager(@NotNull Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.velocitabEntries = Sets.newConcurrentHashSet();
|
||||
this.loadPlayers();
|
||||
this.loadListeners();
|
||||
}
|
||||
@ -87,32 +75,17 @@ public class PacketEventManager {
|
||||
public void removePlayer(@NotNull Player player) {
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
|
||||
final Channel channel = connectedPlayer.getConnection().getChannel();
|
||||
if (channel.pipeline().get(KEY) != null) {
|
||||
channel.pipeline().remove(KEY);
|
||||
final ChannelHandler handler = channel.pipeline().get(KEY);
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleEntry(@NotNull UpsertPlayerInfoPacket packet, @NotNull Player player) {
|
||||
final List<TabPlayer> toUpdate = packet.getEntries().stream()
|
||||
.filter(entry -> entry.getProfile() != null)
|
||||
.filter(entry -> !entry.getProfile().getName().startsWith(CITIZENS_PREFIX))
|
||||
.filter(entry -> velocitabEntries.stream().noneMatch(uuid -> uuid.equals(entry.getProfile().getId())))
|
||||
.map(entry -> entry.getProfile().getId())
|
||||
.map(id -> plugin.getTabList().getTabPlayer(id))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.filter(TabPlayer::isLoaded)
|
||||
.toList();
|
||||
|
||||
if (toUpdate.isEmpty()) {
|
||||
if (channel.pipeline() instanceof DefaultChannelPipeline defaultChannelPipeline) {
|
||||
defaultChannelPipeline.removeIfExists(KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
toUpdate.forEach(tabPlayer -> packet.getEntries().stream()
|
||||
.filter(entry -> entry.getProfileId().equals(tabPlayer.getPlayer().getUniqueId()))
|
||||
.findFirst()
|
||||
.ifPresent(entry -> entry.setDisplayName(
|
||||
new ComponentHolder(player.getProtocolVersion(), tabPlayer.getLastDisplayName()))));
|
||||
plugin.getLogger().warn("Failed to remove player {} from Velocitab packet handler {}",
|
||||
player.getUsername(), channel.pipeline().getClass().getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,10 +30,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
// Based on VPacketEvents PacketRegistration API
|
||||
@ -105,7 +102,7 @@ public final class PacketRegistration<P extends MinecraftPacket> {
|
||||
try {
|
||||
IntObjectMap<Supplier<?>> packetIdToSupplier = (IntObjectMap<Supplier<?>>) PACKET_REGISTRY$packetIdToSupplier.invoke(protocolRegistry);
|
||||
Object2IntMap<Class<?>> packetClassToId = (Object2IntMap<Class<?>>) PACKET_REGISTRY$packetClassToId.invoke(protocolRegistry);
|
||||
packetIdToSupplier.keySet().stream()
|
||||
Set.copyOf(packetIdToSupplier.keySet()).stream()
|
||||
.filter(supplier -> packetIdToSupplier.get(supplier).get().getClass().equals(packetClass))
|
||||
.forEach(packetIdToSupplier::remove);
|
||||
packetClassToId.values().intStream()
|
||||
|
@ -41,40 +41,79 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
|
||||
|
||||
@Override
|
||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
||||
if (msg instanceof final UpdateTeamsPacket updateTeamsPacket && plugin.getSettings().isSendScoreboardPackets()) {
|
||||
final Optional<ScoreboardManager> scoreboardManager = plugin.getScoreboardManager();
|
||||
if (scoreboardManager.isEmpty()) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateTeamsPacket.isRemoveTeam()) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scoreboardManager.get().isInternalTeam(updateTeamsPacket.teamName())) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updateTeamsPacket.hasEntities()) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateTeamsPacket.entities().stream().noneMatch(entity -> plugin.getServer().getPlayer(entity).isPresent())) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel packet if the backend is trying to send a team packet with an online player.
|
||||
// This is to prevent conflicts with Velocitab teams.
|
||||
plugin.getLogger().warn("Cancelled team \"{}\" packet from backend for player {}. " +
|
||||
"We suggest disabling \"send_scoreboard_packets\" in Velocitab's config.yml file, " +
|
||||
"but note this will disable TAB sorting",
|
||||
updateTeamsPacket.teamName(), player.getUsername());
|
||||
return;
|
||||
}
|
||||
if (!(msg instanceof final UpsertPlayerInfoPacket minecraftPacket)) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
|
||||
if (tabPlayer.isEmpty()) {
|
||||
try {
|
||||
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
|
||||
if (tabPlayer.isEmpty()) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plugin.getSettings().isRemoveSpectatorEffect() && minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE)) {
|
||||
forceGameMode(minecraftPacket.getEntries());
|
||||
}
|
||||
|
||||
//fix for duplicate entries
|
||||
if (minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) {
|
||||
minecraftPacket.getEntries().stream()
|
||||
.filter(entry -> entry.getProfile() != null && !entry.getProfile().getId().equals(entry.getProfileId()))
|
||||
.forEach(entry -> entry.setListed(false));
|
||||
}
|
||||
|
||||
if (!minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER) && !minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
if (minecraftPacket.getEntries().stream().allMatch(entry -> entry.getProfile() != null && entry.getProfile().getName().startsWith("CIT"))) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plugin.getSettings().isRemoveSpectatorEffect() && minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE)) {
|
||||
forceGameMode(minecraftPacket.getEntries());
|
||||
}
|
||||
|
||||
//fix for duplicate entries
|
||||
if (minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) {
|
||||
minecraftPacket.getEntries().stream()
|
||||
.filter(entry -> entry.getProfile() != null && !entry.getProfile().getId().equals(entry.getProfileId()))
|
||||
.forEach(entry -> entry.setListed(false));
|
||||
}
|
||||
|
||||
if (!minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER) && !minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) {
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().error("An error occurred while handling a packet", e);
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
if (minecraftPacket.getEntries().stream().allMatch(entry -> entry.getProfile() != null && entry.getProfile().getName().startsWith("CIT"))) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getPacketEventManager().handleEntry(minecraftPacket, player);
|
||||
super.write(ctx, msg, promise);
|
||||
}
|
||||
|
||||
private void forceGameMode(@NotNull List<UpsertPlayerInfoPacket.Entry> entries) {
|
||||
|
@ -55,7 +55,34 @@ public class Protocol404Adapter extends TeamsPacketAdapter {
|
||||
|
||||
public Protocol404Adapter(@NotNull Velocitab plugin, Set<ProtocolVersion> protocolVersions) {
|
||||
super(plugin, protocolVersions);
|
||||
serializer = null;
|
||||
serializer = GsonComponentSerializer.colorDownsamplingGson();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
|
||||
packet.teamName(ProtocolUtils.readString(byteBuf));
|
||||
UpdateTeamsPacket.UpdateMode mode = UpdateTeamsPacket.UpdateMode.byId(byteBuf.readByte());
|
||||
packet.mode(mode);
|
||||
if (mode == UpdateTeamsPacket.UpdateMode.REMOVE_TEAM) {
|
||||
return;
|
||||
}
|
||||
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.UPDATE_INFO) {
|
||||
packet.displayName(readComponent(byteBuf));
|
||||
packet.friendlyFlags(UpdateTeamsPacket.FriendlyFlag.fromBitMask(byteBuf.readByte()));
|
||||
packet.nametagVisibility(UpdateTeamsPacket.NametagVisibility.byId(ProtocolUtils.readString(byteBuf)));
|
||||
packet.collisionRule(UpdateTeamsPacket.CollisionRule.byId(ProtocolUtils.readString(byteBuf)));
|
||||
packet.color(byteBuf.readByte());
|
||||
packet.prefix(readComponent(byteBuf));
|
||||
packet.suffix(readComponent(byteBuf));
|
||||
}
|
||||
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.ADD_PLAYERS || mode == UpdateTeamsPacket.UpdateMode.REMOVE_PLAYERS) {
|
||||
int count = ProtocolUtils.readVarInt(byteBuf);
|
||||
List<String> entities = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
entities.add(ProtocolUtils.readString(byteBuf));
|
||||
}
|
||||
packet.entities(entities);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -84,8 +111,13 @@ public class Protocol404Adapter extends TeamsPacketAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeComponent(ByteBuf buf, Component component) {
|
||||
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
|
||||
ProtocolUtils.writeString(buf, serializer.serialize(component));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Component readComponent(@NotNull ByteBuf buf) {
|
||||
return serializer.deserialize(ProtocolUtils.readString(buf));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -44,6 +44,35 @@ public class Protocol48Adapter extends TeamsPacketAdapter {
|
||||
serializer = LegacyComponentSerializer.legacySection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
|
||||
packet.teamName(ProtocolUtils.readString(byteBuf));
|
||||
UpdateTeamsPacket.UpdateMode mode = UpdateTeamsPacket.UpdateMode.byId(byteBuf.readByte());
|
||||
packet.mode(mode);
|
||||
if (mode == UpdateTeamsPacket.UpdateMode.REMOVE_TEAM) {
|
||||
return;
|
||||
}
|
||||
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.UPDATE_INFO) {
|
||||
packet.displayName(readComponent(byteBuf));
|
||||
packet.prefix(readComponent(byteBuf));
|
||||
packet.suffix(readComponent(byteBuf));
|
||||
packet.friendlyFlags(UpdateTeamsPacket.FriendlyFlag.fromBitMask(byteBuf.readByte()));
|
||||
packet.nametagVisibility(UpdateTeamsPacket.NametagVisibility.byId(ProtocolUtils.readString(byteBuf)));
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_12_2) >= 0) {
|
||||
packet.collisionRule(UpdateTeamsPacket.CollisionRule.byId(ProtocolUtils.readString(byteBuf)));
|
||||
}
|
||||
packet.color(byteBuf.readByte());
|
||||
}
|
||||
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.ADD_PLAYERS || mode == UpdateTeamsPacket.UpdateMode.REMOVE_PLAYERS) {
|
||||
int count = ProtocolUtils.readVarInt(byteBuf);
|
||||
List<String> entities = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
entities.add(ProtocolUtils.readString(byteBuf));
|
||||
}
|
||||
packet.entities(entities);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
|
||||
ProtocolUtils.writeString(byteBuf, shrinkString(packet.teamName()));
|
||||
@ -83,7 +112,12 @@ public class Protocol48Adapter extends TeamsPacketAdapter {
|
||||
return string.substring(0, Math.min(string.length(), 16));
|
||||
}
|
||||
|
||||
protected void writeComponent(ByteBuf buf, Component component) {
|
||||
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
|
||||
ProtocolUtils.writeString(buf, shrinkString(serializer.serialize(component)));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Component readComponent(@NotNull ByteBuf buf) {
|
||||
return serializer.deserialize(ProtocolUtils.readString(buf));
|
||||
}
|
||||
}
|
||||
|
@ -58,8 +58,13 @@ public class Protocol735Adapter extends Protocol404Adapter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeComponent(ByteBuf buf, Component component) {
|
||||
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
|
||||
ProtocolUtils.writeString(buf, serializer.serialize(component));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Component readComponent(@NotNull ByteBuf buf) {
|
||||
return serializer.deserialize(ProtocolUtils.readString(buf));
|
||||
}
|
||||
|
||||
}
|
@ -44,9 +44,14 @@ public class Protocol765Adapter extends Protocol404Adapter {
|
||||
));
|
||||
}
|
||||
|
||||
protected void writeComponent(ByteBuf buf, Component component) {
|
||||
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
|
||||
final BinaryTag tag = ComponentHolder.serialize(GsonComponentSerializer.gson().serializeToTree(component));
|
||||
ProtocolUtils.writeBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, tag);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Component readComponent(@NotNull ByteBuf buf) {
|
||||
return GsonComponentSerializer.gson().deserializeFromTree(ComponentHolder.deserialize(ProtocolUtils.readBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, null)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,6 +20,8 @@
|
||||
package net.william278.velocitab.packet;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
@ -28,6 +30,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
@ -49,12 +52,14 @@ public class ScoreboardManager {
|
||||
private final Set<TeamsPacketAdapter> versions;
|
||||
private final Map<UUID, String> createdTeams;
|
||||
private final Map<String, Nametag> nametags;
|
||||
private final Multimap<UUID, String> trackedTeams;
|
||||
|
||||
public ScoreboardManager(@NotNull Velocitab velocitab) {
|
||||
this.plugin = velocitab;
|
||||
this.createdTeams = Maps.newConcurrentMap();
|
||||
this.nametags = Maps.newConcurrentMap();
|
||||
this.versions = Sets.newHashSet();
|
||||
this.trackedTeams = Multimaps.synchronizedMultimap(Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet));
|
||||
this.registerVersions();
|
||||
}
|
||||
|
||||
@ -70,6 +75,10 @@ public class ScoreboardManager {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInternalTeam(@NotNull String teamName) {
|
||||
return nametags.containsKey(teamName);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public TeamsPacketAdapter getPacketAdapter(@NotNull ProtocolVersion version) {
|
||||
return versions.stream()
|
||||
@ -88,6 +97,7 @@ public class ScoreboardManager {
|
||||
plugin.getTabList().getTabPlayer(player).ifPresent(tabPlayer ->
|
||||
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), tabPlayer)
|
||||
);
|
||||
trackedTeams.removeAll(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,13 +129,15 @@ public class ScoreboardManager {
|
||||
final Set<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
|
||||
|
||||
final Optional<Nametag> cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null));
|
||||
cachedTag.ifPresent(nametag -> {
|
||||
final UpdateTeamsPacket packet = vanish ? UpdateTeamsPacket.removeTeam(plugin, teamName) :
|
||||
UpdateTeamsPacket.create(plugin, tabPlayer, teamName, nametag, player.getUsername());
|
||||
siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player)
|
||||
.filter(p -> vanish && !plugin.getVanishManager().canSee(p.getUsername(), player.getUsername()))
|
||||
.forEach(connected -> dispatchPacket(packet, connected)));
|
||||
});
|
||||
cachedTag.ifPresent(nametag -> siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player)
|
||||
.forEach(connected -> {
|
||||
if (vanish && !plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername())) {
|
||||
dispatchPacket(UpdateTeamsPacket.removeTeam(plugin, teamName), connected);
|
||||
trackedTeams.remove(connected.getUniqueId(), teamName);
|
||||
} else {
|
||||
dispatchGroupCreatePacket(plugin, tabPlayer, teamName, nametag, player.getUsername());
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,16 +166,12 @@ public class ScoreboardManager {
|
||||
|
||||
createdTeams.put(player.getUniqueId(), role);
|
||||
this.nametags.put(role, newTag);
|
||||
dispatchGroupPacket(
|
||||
UpdateTeamsPacket.create(plugin, tabPlayer, role, newTag, name),
|
||||
tabPlayer
|
||||
);
|
||||
dispatchGroupCreatePacket(plugin, tabPlayer, role, newTag, name);
|
||||
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) {
|
||||
this.nametags.put(role, newTag);
|
||||
dispatchGroupPacket(
|
||||
UpdateTeamsPacket.changeNametag(plugin, tabPlayer, role, newTag),
|
||||
tabPlayer
|
||||
);
|
||||
dispatchGroupChangePacket(plugin, tabPlayer, role, newTag);
|
||||
} else {
|
||||
updatePlaceholders(tabPlayer);
|
||||
}
|
||||
}).exceptionally(e -> {
|
||||
plugin.log(Level.ERROR, "Failed to update role for " + player.getUsername(), e);
|
||||
@ -171,6 +179,17 @@ public class ScoreboardManager {
|
||||
});
|
||||
}
|
||||
|
||||
public void updatePlaceholders(@NotNull TabPlayer tabPlayer) {
|
||||
final Player player = tabPlayer.getPlayer();
|
||||
|
||||
final String role = createdTeams.get(player.getUniqueId());
|
||||
if (role == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Optional<Nametag> optionalNametag = Optional.ofNullable(nametags.get(role));
|
||||
optionalNametag.ifPresent(nametag -> dispatchGroupChangePacket(plugin, tabPlayer, role, nametag));
|
||||
}
|
||||
|
||||
public void resendAllTeams(@NotNull TabPlayer tabPlayer) {
|
||||
if (!plugin.getSettings().isSendScoreboardPackets()) {
|
||||
@ -196,7 +215,6 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
final Optional<TabPlayer> optionalTabPlayer = plugin.getTabList().getTabPlayer(p);
|
||||
|
||||
if (optionalTabPlayer.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@ -212,14 +230,69 @@ public class ScoreboardManager {
|
||||
// Send packet
|
||||
final Nametag tag = nametags.get(role);
|
||||
if (tag != null) {
|
||||
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(
|
||||
plugin, targetTabPlayer, role, tag, p.getUsername()
|
||||
);
|
||||
dispatchPacket(packet, player);
|
||||
dispatchCreatePacket(plugin, targetTabPlayer, role, tag, tabPlayer, p.getUsername());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchGroupCreatePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
|
||||
@NotNull String teamName, @NotNull Nametag nametag,
|
||||
@NotNull String... teamMembers) {
|
||||
tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> {
|
||||
if (!viewer.getPlayer().isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
dispatchCreatePacket(plugin, tabPlayer, teamName, nametag, viewer, teamMembers);
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchCreatePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
|
||||
@NotNull String teamName, @NotNull Nametag nametag,
|
||||
@NotNull TabPlayer viewer,
|
||||
@NotNull String... teamMembers) {
|
||||
final boolean canSee = plugin.getVanishManager().canSee(viewer.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername());
|
||||
if (!canSee) {
|
||||
return;
|
||||
}
|
||||
|
||||
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(plugin, tabPlayer, teamName, nametag, viewer, teamMembers);
|
||||
trackedTeams.put(viewer.getPlayer().getUniqueId(), teamName);
|
||||
dispatchPacket(packet, viewer.getPlayer());
|
||||
}
|
||||
|
||||
private void dispatchGroupChangePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
|
||||
@NotNull String teamName,
|
||||
@NotNull Nametag nametag) {
|
||||
tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> {
|
||||
if (viewer == tabPlayer || !viewer.getPlayer().isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean canSee = plugin.getVanishManager().canSee(viewer.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername());
|
||||
if (!canSee) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent sending change nametag packets to players who are not tracking the team
|
||||
if (!trackedTeams.containsEntry(viewer.getPlayer().getUniqueId(), teamName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final UpdateTeamsPacket packet = UpdateTeamsPacket.changeNametag(plugin, tabPlayer, teamName, viewer, nametag);
|
||||
final Component prefix = packet.prefix();
|
||||
final Component suffix = packet.suffix();
|
||||
final Optional<Component[]> cached = tabPlayer.getRelationalNametag(viewer.getPlayer().getUniqueId());
|
||||
// Skip if the nametag is the same as the cached one
|
||||
if (cached.isPresent() && cached.get()[0].equals(prefix) && cached.get()[1].equals(suffix)) {
|
||||
return;
|
||||
}
|
||||
tabPlayer.setRelationalNametag(viewer.getPlayer().getUniqueId(), prefix, suffix);
|
||||
dispatchPacket(packet, viewer.getPlayer());
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchPacket(@NotNull UpdateTeamsPacket packet, @NotNull Player player) {
|
||||
if (!player.isActive()) {
|
||||
plugin.getTabList().removeOfflinePlayer(player);
|
||||
@ -235,10 +308,14 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Group group) {
|
||||
final boolean isRemove = packet.isRemoveTeam();
|
||||
group.registeredServers(plugin).forEach(server -> server.getPlayersConnected().forEach(connected -> {
|
||||
try {
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected;
|
||||
connectedPlayer.getConnection().write(packet);
|
||||
if (isRemove) {
|
||||
trackedTeams.remove(connected.getUniqueId(), packet.teamName());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e);
|
||||
}
|
||||
@ -274,18 +351,18 @@ public class ScoreboardManager {
|
||||
.direction(ProtocolUtils.Direction.CLIENTBOUND)
|
||||
.packetSupplier(() -> new UpdateTeamsPacket(plugin))
|
||||
.stateRegistry(StateRegistry.PLAY)
|
||||
.mapping(0x3E, MINECRAFT_1_8, true)
|
||||
.mapping(0x44, MINECRAFT_1_12_2, true)
|
||||
.mapping(0x47, MINECRAFT_1_13, true)
|
||||
.mapping(0x4B, MINECRAFT_1_14, true)
|
||||
.mapping(0x4C, MINECRAFT_1_15, true)
|
||||
.mapping(0x55, MINECRAFT_1_17, true)
|
||||
.mapping(0x58, MINECRAFT_1_19_1, true)
|
||||
.mapping(0x56, MINECRAFT_1_19_3, true)
|
||||
.mapping(0x5A, MINECRAFT_1_19_4, true)
|
||||
.mapping(0x5C, MINECRAFT_1_20_2, true)
|
||||
.mapping(0x5E, MINECRAFT_1_20_3, true)
|
||||
.mapping(0x60, MINECRAFT_1_20_5, true);
|
||||
.mapping(0x3E, MINECRAFT_1_8, false)
|
||||
.mapping(0x44, MINECRAFT_1_12_2, false)
|
||||
.mapping(0x47, MINECRAFT_1_13, false)
|
||||
.mapping(0x4B, MINECRAFT_1_14, false)
|
||||
.mapping(0x4C, MINECRAFT_1_15, false)
|
||||
.mapping(0x55, MINECRAFT_1_17, false)
|
||||
.mapping(0x58, MINECRAFT_1_19_1, false)
|
||||
.mapping(0x56, MINECRAFT_1_19_3, false)
|
||||
.mapping(0x5A, MINECRAFT_1_19_4, false)
|
||||
.mapping(0x5C, MINECRAFT_1_20_2, false)
|
||||
.mapping(0x5E, MINECRAFT_1_20_3, false)
|
||||
.mapping(0x60, MINECRAFT_1_20_5, false);
|
||||
packetRegistration.register();
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.ERROR, "Failed to register UpdateTeamsPacket", e);
|
||||
@ -320,14 +397,12 @@ public class ScoreboardManager {
|
||||
|
||||
final UpdateTeamsPacket removeTeam = UpdateTeamsPacket.removeTeam(plugin, team);
|
||||
dispatchPacket(removeTeam, player);
|
||||
trackedTeams.remove(player.getUniqueId(), team);
|
||||
|
||||
if (canSee) {
|
||||
final Nametag tag = nametags.get(team);
|
||||
if (tag != null) {
|
||||
final UpdateTeamsPacket addTeam = UpdateTeamsPacket.create(
|
||||
plugin, tabPlayer, team, tag, target.getPlayer().getUsername()
|
||||
);
|
||||
dispatchPacket(addTeam, player);
|
||||
dispatchCreatePacket(plugin, tabPlayer, team, tag, target, target.getPlayer().getUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,11 @@ public abstract class TeamsPacketAdapter {
|
||||
|
||||
public abstract void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion);
|
||||
|
||||
protected abstract void writeComponent(ByteBuf buf, Component component);
|
||||
public abstract void decode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion);
|
||||
|
||||
protected abstract void writeComponent(@NotNull ByteBuf buf, @NotNull Component component);
|
||||
|
||||
@NotNull
|
||||
protected abstract Component readComponent(@NotNull ByteBuf buf);
|
||||
|
||||
}
|
||||
|
@ -64,21 +64,29 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean isRemoveTeam() {
|
||||
return mode == UpdateMode.REMOVE_TEAM;
|
||||
}
|
||||
|
||||
public boolean hasEntities() {
|
||||
return entities != null && !entities.isEmpty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static UpdateTeamsPacket create(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
|
||||
@NotNull String teamName,
|
||||
@NotNull Nametag nametag,
|
||||
@NotNull String teamName, @NotNull Nametag nametag,
|
||||
@NotNull TabPlayer viewer,
|
||||
@NotNull String... teamMembers) {
|
||||
return new UpdateTeamsPacket(plugin)
|
||||
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.teamName(teamName)
|
||||
.mode(UpdateMode.CREATE_TEAM)
|
||||
.displayName(Component.empty())
|
||||
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
|
||||
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
|
||||
.collisionRule(tabPlayer.getGroup().collisions() ? CollisionRule.ALWAYS : CollisionRule.NEVER)
|
||||
.color(getLastColor(tabPlayer, nametag.prefix(), plugin))
|
||||
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
|
||||
.suffix(nametag.getSuffixComponent(plugin, tabPlayer))
|
||||
.prefix(nametag.getPrefixComponent(plugin, tabPlayer, viewer))
|
||||
.suffix(nametag.getSuffixComponent(plugin, tabPlayer, viewer))
|
||||
.entities(Arrays.asList(teamMembers));
|
||||
}
|
||||
|
||||
@ -92,25 +100,25 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
|
||||
@NotNull
|
||||
protected static UpdateTeamsPacket changeNametag(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
|
||||
@NotNull String teamName,
|
||||
@NotNull String teamName, @NotNull TabPlayer viewer,
|
||||
@NotNull Nametag nametag) {
|
||||
return new UpdateTeamsPacket(plugin)
|
||||
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.teamName(teamName)
|
||||
.mode(UpdateMode.UPDATE_INFO)
|
||||
.displayName(Component.empty())
|
||||
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
|
||||
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
|
||||
.collisionRule(tabPlayer.getGroup().collisions() ? CollisionRule.ALWAYS : CollisionRule.NEVER)
|
||||
.color(getLastColor(tabPlayer, nametag.prefix(), plugin))
|
||||
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
|
||||
.suffix(nametag.getSuffixComponent(plugin, tabPlayer));
|
||||
.prefix(nametag.getPrefixComponent(plugin, tabPlayer, viewer))
|
||||
.suffix(nametag.getSuffixComponent(plugin, tabPlayer, viewer));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static UpdateTeamsPacket addToTeam(@NotNull Velocitab plugin, @NotNull String teamName,
|
||||
@NotNull String... teamMembers) {
|
||||
return new UpdateTeamsPacket(plugin)
|
||||
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.teamName(teamName)
|
||||
.mode(UpdateMode.ADD_PLAYERS)
|
||||
.entities(Arrays.asList(teamMembers));
|
||||
}
|
||||
@ -119,7 +127,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
protected static UpdateTeamsPacket removeFromTeam(@NotNull Velocitab plugin, @NotNull String teamName,
|
||||
@NotNull String... teamMembers) {
|
||||
return new UpdateTeamsPacket(plugin)
|
||||
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.teamName(teamName)
|
||||
.mode(UpdateMode.REMOVE_PLAYERS)
|
||||
.entities(Arrays.asList(teamMembers));
|
||||
}
|
||||
@ -127,7 +135,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
@NotNull
|
||||
protected static UpdateTeamsPacket removeTeam(@NotNull Velocitab plugin, @NotNull String teamName) {
|
||||
return new UpdateTeamsPacket(plugin)
|
||||
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.teamName(teamName)
|
||||
.mode(UpdateMode.REMOVE_TEAM);
|
||||
}
|
||||
|
||||
@ -144,10 +152,10 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
text = text + "z";
|
||||
|
||||
//serialize & deserialize to downsample rgb to legacy
|
||||
Component component = plugin.getFormatter().emptyFormat(text);
|
||||
final Component component = plugin.getFormatter().deserialize(text);
|
||||
text = LegacyComponentSerializer.legacyAmpersand().serialize(component);
|
||||
|
||||
int lastFormatIndex = text.lastIndexOf("&");
|
||||
final int lastFormatIndex = text.lastIndexOf("&");
|
||||
if (lastFormatIndex == -1 || lastFormatIndex == text.length() - 1) {
|
||||
return 15;
|
||||
}
|
||||
@ -200,16 +208,21 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException("Operation not supported");
|
||||
final Optional<ScoreboardManager> optionalManager = plugin.getScoreboardManager();
|
||||
if (optionalManager.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
optionalManager.get().getPacketAdapter(protocolVersion).decode(byteBuf, this, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
final Optional<ScoreboardManager> optionalManager = plugin.getScoreboardManager();
|
||||
|
||||
if (optionalManager.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
optionalManager.get().getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion);
|
||||
}
|
||||
|
||||
|
@ -19,12 +19,16 @@
|
||||
|
||||
package net.william278.velocitab.player;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Formatter;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.config.Placeholder;
|
||||
import net.william278.velocitab.packet.UpdateTeamsPacket;
|
||||
@ -34,20 +38,29 @@ import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
|
||||
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%(\\w+)%");
|
||||
private static final String PLACEHOLDER_DELIMITER = "<-DELIMITER->";
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final Player player;
|
||||
@Setter
|
||||
private Role role;
|
||||
private int headerIndex = 0;
|
||||
private int footerIndex = 0;
|
||||
private Component lastDisplayName;
|
||||
// Each TabPlayer contains the components for each TabPlayer it's currently viewing this player
|
||||
private final Map<UUID, Component> relationalDisplayNames;
|
||||
private final Map<UUID, Component[]> relationalNametags;
|
||||
private final Map<String, String> cachedPlaceholders;
|
||||
private String lastDisplayName;
|
||||
private Component lastHeader;
|
||||
private Component lastFooter;
|
||||
private String teamName;
|
||||
@ -72,6 +85,9 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
this.player = player;
|
||||
this.role = role;
|
||||
this.group = group;
|
||||
this.relationalDisplayNames = Maps.newConcurrentMap();
|
||||
this.relationalNametags = Maps.newConcurrentMap();
|
||||
this.cachedPlaceholders = Maps.newConcurrentMap();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -115,10 +131,47 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CompletableFuture<Component> getDisplayName(@NotNull Velocitab plugin) {
|
||||
return Placeholder.replace(group.format(), plugin, this)
|
||||
.thenApply(formatted -> plugin.getFormatter().format(formatted, this, plugin))
|
||||
.thenApply(c -> this.lastDisplayName = c);
|
||||
public CompletableFuture<String> getDisplayName(@NotNull Velocitab plugin) {
|
||||
final String format = formatGroup();
|
||||
return Placeholder.replace(format, plugin, this)
|
||||
.thenApply(d -> cacheDisplayName(d, format));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String formatGroup() {
|
||||
final Set<String> placeholders = Sets.newHashSet();
|
||||
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(group.format());
|
||||
while (matcher.find()) {
|
||||
placeholders.add("%" + matcher.group(1) + "%");
|
||||
}
|
||||
|
||||
return String.join(PLACEHOLDER_DELIMITER, placeholders);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String cacheDisplayName(@NotNull String placeholders, @NotNull String keys) {
|
||||
String displayName = group.format();
|
||||
final String[] placeholderArray = placeholders.split(PLACEHOLDER_DELIMITER);
|
||||
final String[] keyArray = keys.split(PLACEHOLDER_DELIMITER);
|
||||
|
||||
for (int i = 0; i < placeholderArray.length; i++) {
|
||||
final String placeholder = keyArray[i];
|
||||
final String value = placeholderArray[i];
|
||||
cachedPlaceholders.put(placeholder, value);
|
||||
displayName = displayName.replace(placeholder, value);
|
||||
}
|
||||
|
||||
displayName = displayName.replace("\n", "");
|
||||
final boolean isMiniMessage = plugin.getFormatter().equals(Formatter.MINIMESSAGE);
|
||||
if (isMiniMessage) {
|
||||
displayName = Formatter.LEGACY.serialize(MiniMessage.miniMessage().deserialize(displayName));
|
||||
}
|
||||
displayName = Placeholder.replaceInternal(displayName, plugin, this);
|
||||
if (isMiniMessage) {
|
||||
displayName = MiniMessage.miniMessage().serialize(Formatter.LEGACY.deserialize(displayName))
|
||||
.replace("\\<", "<");
|
||||
}
|
||||
return lastDisplayName = displayName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -140,18 +193,18 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
return tabList.getHeader(this).thenCompose(header -> tabList.getFooter(this).thenAccept(footer -> {
|
||||
final boolean disabled = plugin.getSettings().isDisableHeaderFooterIfEmpty();
|
||||
if (disabled) {
|
||||
if (!Component.empty().equals(header)) {
|
||||
if ((!Component.empty().equals(header) && !header.equals(lastHeader)) ||
|
||||
(!Component.empty().equals(footer) && !footer.equals(lastFooter))) {
|
||||
lastHeader = header;
|
||||
player.sendPlayerListHeader(header);
|
||||
}
|
||||
if (!Component.empty().equals(footer)) {
|
||||
lastFooter = footer;
|
||||
player.sendPlayerListFooter(footer);
|
||||
player.sendPlayerListHeaderAndFooter(header, footer);
|
||||
}
|
||||
} else {
|
||||
lastHeader = header;
|
||||
lastFooter = footer;
|
||||
player.sendPlayerListHeaderAndFooter(header, footer);
|
||||
if (!header.equals(lastHeader) || !footer.equals(lastFooter)) {
|
||||
lastHeader = header;
|
||||
lastFooter = footer;
|
||||
player.sendPlayerListHeaderAndFooter(header, footer);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
@ -175,6 +228,39 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
}
|
||||
}
|
||||
|
||||
public void setRelationalDisplayName(@NotNull UUID target, @NotNull Component displayName) {
|
||||
relationalDisplayNames.put(target, displayName);
|
||||
}
|
||||
|
||||
public void unsetRelationalDisplayName(@NotNull UUID target) {
|
||||
relationalDisplayNames.remove(target);
|
||||
}
|
||||
|
||||
public Optional<Component> getRelationalDisplayName(@NotNull UUID target) {
|
||||
return Optional.ofNullable(relationalDisplayNames.get(target));
|
||||
}
|
||||
|
||||
public void setRelationalNametag(@NotNull UUID target, @NotNull Component prefix, @NotNull Component suffix) {
|
||||
relationalNametags.put(target, new Component[]{prefix, suffix});
|
||||
}
|
||||
|
||||
public void unsetRelationalNametag(@NotNull UUID target) {
|
||||
relationalNametags.remove(target);
|
||||
}
|
||||
|
||||
public Optional<Component[]> getRelationalNametag(@NotNull UUID target) {
|
||||
return Optional.ofNullable(relationalNametags.get(target));
|
||||
}
|
||||
|
||||
public void clearCachedData() {
|
||||
loaded = false;
|
||||
relationalDisplayNames.clear();
|
||||
relationalNametags.clear();
|
||||
lastHeader = null;
|
||||
lastFooter = null;
|
||||
role = Role.DEFAULT_ROLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom name of the TabPlayer, if it has been set.
|
||||
*
|
||||
@ -197,4 +283,8 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId());
|
||||
}
|
||||
|
||||
public Optional<String> getCachedPlaceholderValue(@NotNull String placeholder) {
|
||||
return Optional.ofNullable(cachedPlaceholders.get(placeholder));
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ public interface LoggerProvider {
|
||||
getLogger().warn(message);
|
||||
}
|
||||
}
|
||||
case DEBUG -> getLogger().debug(message);
|
||||
case INFO -> getLogger().info(message);
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ public interface ScoreboardProvider {
|
||||
*/
|
||||
default void prepareScoreboard() {
|
||||
if (getPlugin().getSettings().isSendScoreboardPackets()) {
|
||||
ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin());
|
||||
final ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin());
|
||||
setScoreboardManager(scoreboardManager);
|
||||
scoreboardManager.registerPacket();
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
package net.william278.velocitab.sorting;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Placeholder;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
@ -49,7 +50,7 @@ public class SortingManager {
|
||||
|
||||
return Placeholder.replace(String.join(DELIMITER, player.getGroup().sortingPlaceholders()), plugin, player)
|
||||
.thenApply(s -> Arrays.asList(s.split(DELIMITER)))
|
||||
.thenApply(v -> v.stream().map(this::adaptValue).collect(Collectors.toList()))
|
||||
.thenApply(v -> v.stream().map(s -> adaptValue(s, player)).collect(Collectors.toList()))
|
||||
.thenApply(v -> handleList(player, v));
|
||||
}
|
||||
|
||||
@ -57,7 +58,7 @@ public class SortingManager {
|
||||
private String handleList(@NotNull TabPlayer player, @NotNull List<String> values) {
|
||||
String result = String.join("", values);
|
||||
|
||||
if (result.length() > 12) {
|
||||
if (result.length() > 12 && isLongTeamNotAllowed(player)) {
|
||||
result = result.substring(0, 12);
|
||||
}
|
||||
|
||||
@ -66,8 +67,13 @@ public class SortingManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isLongTeamNotAllowed(@NotNull TabPlayer player) {
|
||||
return !player.getGroup().getPlayers(plugin, player).stream()
|
||||
.allMatch(t -> t.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_18));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String adaptValue(@NotNull String value) {
|
||||
private String adaptValue(@NotNull String value, @NotNull TabPlayer player) {
|
||||
if (value.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
@ -78,7 +84,7 @@ public class SortingManager {
|
||||
return compressNumber(Integer.MAX_VALUE / 4d - parsed);
|
||||
}
|
||||
|
||||
if (value.length() > 6) {
|
||||
if (value.length() > 6 && isLongTeamNotAllowed(player)) {
|
||||
return value.substring(0, 4);
|
||||
}
|
||||
|
||||
|
@ -30,13 +30,13 @@ import org.jetbrains.annotations.NotNull;
|
||||
public record Nametag(@NotNull String prefix, @NotNull String suffix) {
|
||||
|
||||
@NotNull
|
||||
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
|
||||
return plugin.getFormatter().format(prefix, tabPlayer, plugin);
|
||||
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
|
||||
return plugin.getFormatter().format(prefix, tabPlayer, target, plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
|
||||
return plugin.getFormatter().format(suffix, tabPlayer, plugin);
|
||||
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
|
||||
return plugin.getFormatter().format(suffix, tabPlayer, target, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -26,7 +26,6 @@ import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.player.TabList;
|
||||
import com.velocitypowered.api.proxy.player.TabListEntry;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
@ -45,6 +44,7 @@ import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The main class for tracking the server TAB list
|
||||
@ -55,15 +55,16 @@ public class PlayerTabList {
|
||||
private final VanishTabList vanishTabList;
|
||||
@Getter(value = AccessLevel.PUBLIC)
|
||||
private final Map<UUID, TabPlayer> players;
|
||||
private final Map<Group, GroupTasks> groupTasks;
|
||||
private final TaskManager taskManager;
|
||||
|
||||
public PlayerTabList(@NotNull Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.vanishTabList = new VanishTabList(plugin, this);
|
||||
this.players = Maps.newConcurrentMap();
|
||||
this.groupTasks = Maps.newConcurrentMap();
|
||||
this.taskManager = new TaskManager(plugin);
|
||||
this.reloadUpdate();
|
||||
this.registerListener();
|
||||
this.ensureDisplayNameTask();
|
||||
}
|
||||
|
||||
private void registerListener() {
|
||||
@ -102,16 +103,12 @@ public class PlayerTabList {
|
||||
}
|
||||
|
||||
final String serverName = server.get().getServerInfo().getName();
|
||||
final Group group = getGroup(serverName);
|
||||
final boolean isDefault = group.registeredServers(plugin)
|
||||
.stream()
|
||||
.noneMatch(s -> s.getServerInfo().getName().equals(serverName));
|
||||
|
||||
if (isDefault && !plugin.getSettings().isFallbackEnabled()) {
|
||||
final @NotNull Optional<Group> group = getGroup(serverName);
|
||||
if (group.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
joinPlayer(p, group);
|
||||
joinPlayer(p, group.get());
|
||||
});
|
||||
}
|
||||
|
||||
@ -120,7 +117,7 @@ public class PlayerTabList {
|
||||
* Removes the player's entry from the tab list of all other players on the same group servers.
|
||||
*/
|
||||
public void close() {
|
||||
groupTasks.values().forEach(GroupTasks::cancel);
|
||||
taskManager.cancelAllTasks();
|
||||
plugin.getServer().getAllPlayers().forEach(p -> {
|
||||
final Optional<ServerConnection> server = p.getCurrentServer();
|
||||
if (server.isEmpty()) return;
|
||||
@ -140,88 +137,38 @@ public class PlayerTabList {
|
||||
});
|
||||
}
|
||||
|
||||
protected void clearCachedData(@NotNull Player player) {
|
||||
players.values().forEach(p -> {
|
||||
p.unsetRelationalDisplayName(player.getUniqueId());
|
||||
p.unsetRelationalNametag(player.getUniqueId());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected void joinPlayer(@NotNull Player joined, @NotNull Group group) {
|
||||
// Add the player to the tracking list if they are not already listed
|
||||
final TabPlayer tabPlayer = getTabPlayer(joined).orElseGet(() -> createTabPlayer(joined, group));
|
||||
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
|
||||
tabPlayer.setGroup(group);
|
||||
players.putIfAbsent(joined.getUniqueId(), tabPlayer);
|
||||
|
||||
// Store the player's last server, so it's possible to have the last server on disconnect
|
||||
final Optional<TabPlayer> tabPlayerOptional = getTabPlayer(joined);
|
||||
if (tabPlayerOptional.isPresent()) {
|
||||
tabPlayerOptional.get().clearCachedData();
|
||||
tabPlayerOptional.get().setGroup(group);
|
||||
tabPlayerOptional.get().setRole(plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(joined)).orElse(Role.DEFAULT_ROLE));
|
||||
}
|
||||
final TabPlayer tabPlayer = tabPlayerOptional.orElseGet(() -> createTabPlayer(joined, group));
|
||||
final String serverName = getServerName(joined);
|
||||
// Store last server, so it's possible to have the last server on disconnect
|
||||
tabPlayer.setLastServer(serverName);
|
||||
|
||||
// Send server URLs (1.21 clients)
|
||||
sendPlayerServerLinks(tabPlayer);
|
||||
|
||||
// Determine display name, update TAB for player
|
||||
// Set the player as not loaded until the display name is set
|
||||
tabPlayer.getDisplayName(plugin).thenAccept(d -> {
|
||||
joined.getTabList().getEntry(joined.getUniqueId())
|
||||
.ifPresentOrElse(e -> e.setDisplayName(d),
|
||||
() -> joined.getTabList().addEntry(createEntry(tabPlayer, joined.getTabList(), d)));
|
||||
if (d == null) {
|
||||
plugin.log(Level.ERROR, "Failed to get display name for " + joined.getUsername());
|
||||
return;
|
||||
}
|
||||
|
||||
tabPlayer.sendHeaderAndFooter(this)
|
||||
.thenAccept(v -> tabPlayer.setLoaded(true))
|
||||
.exceptionally(throwable -> {
|
||||
plugin.log(Level.ERROR, String.format("Failed to send header and footer for %s (UUID: %s)",
|
||||
joined.getUsername(), joined.getUniqueId()), throwable);
|
||||
return null;
|
||||
});
|
||||
|
||||
final Set<String> serversInGroup = group.registeredServers(plugin).stream()
|
||||
.map(server -> server.getServerInfo().getName())
|
||||
.collect(HashSet::new, HashSet::add, HashSet::addAll);
|
||||
serversInGroup.remove(serverName);
|
||||
|
||||
// Update lists
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> {
|
||||
final TabList tabList = joined.getTabList();
|
||||
final Set<TabPlayer> tabPlayers = group.getTabPlayers(plugin);
|
||||
for (final TabPlayer player : tabPlayers) {
|
||||
// Skip players on other servers if the setting is enabled
|
||||
if (group.onlyListPlayersInSameServer() && !serverName.equals(getServerName(player.getPlayer()))) {
|
||||
continue;
|
||||
}
|
||||
// check if current player can see the joined player
|
||||
if (!isVanished || plugin.getVanishManager().canSee(player.getPlayer().getUsername(), joined.getUsername())) {
|
||||
addPlayerToTabList(player, tabPlayer, d);
|
||||
} else {
|
||||
player.getPlayer().getTabList().removeEntry(joined.getUniqueId());
|
||||
}
|
||||
// check if joined player can see current player
|
||||
if ((plugin.getVanishManager().isVanished(player.getPlayer().getUsername()) &&
|
||||
!plugin.getVanishManager().canSee(joined.getUsername(), player.getPlayer().getUsername())) &&
|
||||
player.getPlayer() != joined) {
|
||||
tabList.removeEntry(player.getPlayer().getUniqueId());
|
||||
} else {
|
||||
tabList.getEntry(player.getPlayer().getUniqueId()).ifPresentOrElse(
|
||||
entry -> player.getDisplayName(plugin).thenAccept(entry::setDisplayName)
|
||||
.exceptionally(throwable -> {
|
||||
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
|
||||
player.getPlayer().getUsername(), player.getPlayer().getUniqueId()), throwable);
|
||||
return null;
|
||||
}),
|
||||
() -> createEntry(player, tabList).thenAccept(tabList::addEntry)
|
||||
);
|
||||
}
|
||||
|
||||
player.sendHeaderAndFooter(this);
|
||||
}
|
||||
|
||||
plugin.getScoreboardManager().ifPresent(s -> {
|
||||
s.resendAllTeams(tabPlayer);
|
||||
tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(tabPlayer, t, false));
|
||||
});
|
||||
|
||||
fixDuplicateEntries(joined);
|
||||
|
||||
// Fire event without listening for result
|
||||
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group));
|
||||
})
|
||||
.delay(300, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
handleDisplayLoad(tabPlayer);
|
||||
}).exceptionally(throwable -> {
|
||||
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
|
||||
joined.getUsername(), joined.getUniqueId()), throwable);
|
||||
@ -229,6 +176,74 @@ public class PlayerTabList {
|
||||
});
|
||||
}
|
||||
|
||||
private void handleDisplayLoad(@NotNull TabPlayer tabPlayer) {
|
||||
final Player joined = tabPlayer.getPlayer();
|
||||
final Group group = tabPlayer.getGroup();
|
||||
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
|
||||
players.putIfAbsent(joined.getUniqueId(), tabPlayer);
|
||||
tabPlayer.sendHeaderAndFooter(this)
|
||||
.thenAccept(v -> tabPlayer.setLoaded(true))
|
||||
.exceptionally(throwable -> {
|
||||
plugin.log(Level.ERROR, String.format("Failed to send header and footer for %s (UUID: %s)",
|
||||
joined.getUsername(), joined.getUniqueId()), throwable);
|
||||
return null;
|
||||
});
|
||||
|
||||
final Set<TabPlayer> tabPlayers = group.getTabPlayers(plugin, tabPlayer);
|
||||
updateTabListOnJoin(tabPlayer, group, tabPlayers, isVanished);
|
||||
}
|
||||
|
||||
private void updateTabListOnJoin(@NotNull TabPlayer tabPlayer, @NotNull Group group,
|
||||
@NotNull Set<TabPlayer> tabPlayers, boolean isJoinedVanished) {
|
||||
final Player joined = tabPlayer.getPlayer();
|
||||
final String serverName = getServerName(joined);
|
||||
final Set<UUID> uuids = tabPlayers.stream().map(p -> p.getPlayer().getUniqueId()).collect(Collectors.toSet());
|
||||
List.copyOf(tabPlayer.getPlayer().getTabList().getEntries()).forEach(entry -> {
|
||||
if (!uuids.contains(entry.getProfile().getId())) {
|
||||
tabPlayer.getPlayer().getTabList().removeEntry(entry.getProfile().getId());
|
||||
}
|
||||
});
|
||||
for (final TabPlayer iteratedPlayer : tabPlayers) {
|
||||
final Player player = iteratedPlayer.getPlayer();
|
||||
final String username = player.getUsername();
|
||||
final boolean isPlayerVanished = plugin.getVanishManager().isVanished(username);
|
||||
|
||||
if (group.onlyListPlayersInSameServer() && !serverName.equals(getServerName(player))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update lists regarding the joined player
|
||||
checkVisibilityAndUpdateName(iteratedPlayer, tabPlayer, isJoinedVanished);
|
||||
// Update lists regarding the iterated player
|
||||
if (iteratedPlayer != tabPlayer) {
|
||||
checkVisibilityAndUpdateName(tabPlayer, iteratedPlayer, isPlayerVanished);
|
||||
}
|
||||
iteratedPlayer.sendHeaderAndFooter(this);
|
||||
}
|
||||
plugin.getScoreboardManager().ifPresent(s -> {
|
||||
s.resendAllTeams(tabPlayer);
|
||||
tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(tabPlayer, t, false));
|
||||
});
|
||||
fixDuplicateEntries(joined);
|
||||
// Fire event without listening for result
|
||||
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group));
|
||||
}
|
||||
|
||||
private void checkVisibilityAndUpdateName(@NotNull TabPlayer observedPlayer, @NotNull TabPlayer observableTabPlayer,
|
||||
boolean isObservablePlayerVanished) {
|
||||
final UUID observableUUID = observableTabPlayer.getPlayer().getUniqueId();
|
||||
final String observedUsername = observedPlayer.getPlayer().getUsername();
|
||||
final String observableUsername = observableTabPlayer.getPlayer().getUsername();
|
||||
final TabList observableTabPlayerTabList = observableTabPlayer.getPlayer().getTabList();
|
||||
|
||||
if (isObservablePlayerVanished && !plugin.getVanishManager().canSee(observableUsername, observedUsername) &&
|
||||
!observableUUID.equals(observedPlayer.getPlayer().getUniqueId())) {
|
||||
observableTabPlayerTabList.removeEntry(observedPlayer.getPlayer().getUniqueId());
|
||||
} else {
|
||||
updateDisplayName(observedPlayer, observableTabPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getServerName(@NotNull Player player) {
|
||||
return player.getCurrentServer()
|
||||
@ -236,6 +251,21 @@ public class PlayerTabList {
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer,
|
||||
@NotNull Component single, @NotNull String toParse) {
|
||||
if (plugin.getMiniPlaceholdersHook().isEmpty()) {
|
||||
return single;
|
||||
}
|
||||
return plugin.getFormatter().format(toParse, player, viewer, plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull String toParse) {
|
||||
final Component single = plugin.getFormatter().format(toParse, player, viewer, plugin);
|
||||
return getRelationalPlaceholder(player, viewer, single, toParse);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void fixDuplicateEntries(@NotNull Player target) {
|
||||
try {
|
||||
@ -248,7 +278,7 @@ public class PlayerTabList {
|
||||
.filter(entry -> !entry.getKey().equals(target.getUniqueId()))
|
||||
.forEach(entry -> target.getTabList().removeEntry(entry.getKey()));
|
||||
} catch (Throwable error) {
|
||||
plugin.log(Level.ERROR, "Failed to fix duplicate entries", error);
|
||||
plugin.log(Level.ERROR, "Failed to fix duplicate entries for class " + target.getTabList().getClass().getName() , error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,8 +303,9 @@ public class PlayerTabList {
|
||||
.forEach(player -> {
|
||||
player.getPlayer().getTabList().removeEntry(uuid);
|
||||
player.sendHeaderAndFooter(this);
|
||||
updatePlayerDisplayName(player);
|
||||
}))
|
||||
.delay(500, TimeUnit.MILLISECONDS)
|
||||
.delay(250, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
// Delete player team
|
||||
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(target));
|
||||
@ -283,10 +314,6 @@ public class PlayerTabList {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected CompletableFuture<TabListEntry> createEntry(@NotNull TabPlayer player, @NotNull TabList tabList) {
|
||||
return player.getDisplayName(plugin).thenApply(name -> createEntry(player, tabList, name));
|
||||
}
|
||||
|
||||
protected TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull Component displayName) {
|
||||
return TabListEntry.builder()
|
||||
.profile(player.getPlayer().getGameProfile())
|
||||
@ -296,25 +323,43 @@ public class PlayerTabList {
|
||||
.build();
|
||||
}
|
||||
|
||||
private void addPlayerToTabList(@NotNull TabPlayer player, @NotNull TabPlayer newPlayer, @NotNull Component displayName) {
|
||||
if (newPlayer.getPlayer().getUniqueId().equals(player.getPlayer().getUniqueId())) {
|
||||
@NotNull
|
||||
protected TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull TabPlayer viewer) {
|
||||
if (!viewer.getPlayer().getTabList().equals(tabList)) {
|
||||
throw new IllegalArgumentException("TabList of viewer is not the same as the TabList of the entry");
|
||||
}
|
||||
final Component single = plugin.getFormatter().format(player.getLastDisplayName(), player, viewer, plugin);
|
||||
final Component displayName = getRelationalPlaceholder(player, viewer, single, player.getGroup().format());
|
||||
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
|
||||
return TabListEntry.builder()
|
||||
.profile(player.getPlayer().getGameProfile())
|
||||
.displayName(displayName)
|
||||
.latency(Math.max((int) player.getPlayer().getPing(), 0))
|
||||
.tabList(tabList)
|
||||
.build();
|
||||
}
|
||||
|
||||
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer) {
|
||||
final Component displayName = getRelationalPlaceholder(player, viewer, player.getLastDisplayName());
|
||||
updateDisplayName(player, viewer, displayName);
|
||||
}
|
||||
|
||||
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull Component displayName) {
|
||||
final Optional<Component> cached = player.getRelationalDisplayName(viewer.getPlayer().getUniqueId());
|
||||
if (cached.isPresent() && cached.get().equals(displayName) &&
|
||||
viewer.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
|
||||
.flatMap(TabListEntry::getDisplayNameComponent).map(displayName::equals)
|
||||
.orElse(false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getPacketEventManager().getVelocitabEntries().add(newPlayer.getPlayer().getUniqueId());
|
||||
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> plugin.getPacketEventManager().getVelocitabEntries().remove(newPlayer.getPlayer().getUniqueId()))
|
||||
.delay(500, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
|
||||
player.getPlayer()
|
||||
.getTabList().getEntries().stream()
|
||||
.filter(e -> e.getProfile().getId().equals(newPlayer.getPlayer().getUniqueId())).findFirst()
|
||||
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
|
||||
viewer.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
|
||||
.ifPresentOrElse(
|
||||
entry -> entry.setDisplayName(displayName),
|
||||
() -> player.getPlayer().getTabList()
|
||||
.addEntry(createEntry(newPlayer, player.getPlayer().getTabList(), displayName))
|
||||
() -> viewer.getPlayer().getTabList()
|
||||
.addEntry(createEntry(player, viewer.getPlayer().getTabList(), displayName))
|
||||
);
|
||||
}
|
||||
|
||||
@ -353,11 +398,12 @@ public class PlayerTabList {
|
||||
}
|
||||
|
||||
public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) {
|
||||
final Component lastDisplayName = tabPlayer.getLastDisplayName();
|
||||
tabPlayer.getDisplayName(plugin).thenAccept(displayName -> {
|
||||
if (displayName == null || displayName.equals(lastDisplayName)) {
|
||||
if (displayName == null) {
|
||||
plugin.log(Level.ERROR, "Failed to get display name for " + tabPlayer.getPlayer().getUsername());
|
||||
return;
|
||||
}
|
||||
final Component single = plugin.getFormatter().format(displayName, tabPlayer, plugin);
|
||||
|
||||
final boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername());
|
||||
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
|
||||
@ -367,18 +413,51 @@ public class PlayerTabList {
|
||||
return;
|
||||
}
|
||||
|
||||
player.getPlayer().getTabList().getEntries().stream()
|
||||
.filter(e -> e.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId())).findFirst()
|
||||
.ifPresent(entry -> entry.setDisplayName(displayName));
|
||||
final Component relationalPlaceholder = getRelationalPlaceholder(tabPlayer, player, single, displayName);
|
||||
updateDisplayName(tabPlayer, player, relationalPlaceholder);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void checkCorrectDisplayName(@NotNull TabPlayer tabPlayer) {
|
||||
if (!tabPlayer.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
players.values()
|
||||
.stream()
|
||||
.filter(TabPlayer::isLoaded)
|
||||
.forEach(player -> player.getPlayer().getTabList().getEntry(tabPlayer.getPlayer().getUniqueId())
|
||||
.ifPresent(entry -> {
|
||||
final Optional<Component> displayNameOptional = tabPlayer.getRelationalDisplayName(player.getPlayer().getUniqueId());
|
||||
if (displayNameOptional.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Component lastDisplayName = displayNameOptional.get();
|
||||
if (entry.getDisplayNameComponent().isEmpty() || !lastDisplayName.equals(entry.getDisplayNameComponent().get())) {
|
||||
entry.setDisplayName(lastDisplayName);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// Update the display names of all listed players
|
||||
public void updateDisplayNames() {
|
||||
players.values().forEach(this::updatePlayerDisplayName);
|
||||
}
|
||||
|
||||
public void checkCorrectDisplayNames() {
|
||||
players.values().forEach(this::checkCorrectDisplayName);
|
||||
}
|
||||
|
||||
public void ensureDisplayNameTask() {
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, this::checkCorrectDisplayNames)
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(2, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
// Get the component for the TAB list header
|
||||
public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) {
|
||||
final String header = player.getGroup().getHeader(player.getHeaderIndex());
|
||||
@ -395,96 +474,11 @@ public class PlayerTabList {
|
||||
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
|
||||
}
|
||||
|
||||
// Update the tab list periodically
|
||||
private void updatePeriodically(@NotNull Group group) {
|
||||
cancelTasks(group);
|
||||
|
||||
ScheduledTask headerFooterTask = null;
|
||||
ScheduledTask updateTask = null;
|
||||
ScheduledTask latencyTask;
|
||||
|
||||
if (group.headerFooterUpdateRate() > 0) {
|
||||
headerFooterTask = plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> updateGroupPlayers(group, false, true))
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(Math.max(200, group.headerFooterUpdateRate()), TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
if (group.placeholderUpdateRate() > 0) {
|
||||
updateTask = plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> updateGroupPlayers(group, true, false))
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(Math.max(200, group.placeholderUpdateRate()), TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
latencyTask = plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> updateLatency(group))
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(3, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
|
||||
groupTasks.put(group, new GroupTasks(headerFooterTask, updateTask, latencyTask));
|
||||
}
|
||||
|
||||
private void updateLatency(@NotNull Group group) {
|
||||
final Set<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
|
||||
if (groupPlayers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
groupPlayers.stream()
|
||||
.filter(player -> player.getPlayer().isActive())
|
||||
.forEach(player -> {
|
||||
final int latency = (int) player.getPlayer().getPing();
|
||||
final Set<TabPlayer> players = group.getTabPlayers(plugin, player);
|
||||
players.forEach(p -> p.getPlayer().getTabList().getEntries().stream()
|
||||
.filter(e -> e.getProfile().getId().equals(player.getPlayer().getUniqueId())).findFirst()
|
||||
.ifPresent(entry -> entry.setLatency(Math.max(latency, 0))));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the players in the given group.
|
||||
*
|
||||
* @param group The group whose players should be updated.
|
||||
* @param all Whether to update all player properties, or just the header and footer.
|
||||
* @param incrementIndexes Whether to increment the header and footer indexes.
|
||||
*/
|
||||
private void updateGroupPlayers(@NotNull Group group, boolean all, boolean incrementIndexes) {
|
||||
final Set<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
|
||||
if (groupPlayers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
groupPlayers.stream()
|
||||
.filter(player -> player.getPlayer().isActive())
|
||||
.forEach(player -> {
|
||||
if (incrementIndexes) {
|
||||
player.incrementIndexes();
|
||||
}
|
||||
if (all) {
|
||||
this.updatePlayer(player, false);
|
||||
}
|
||||
player.sendHeaderAndFooter(this);
|
||||
});
|
||||
if (all) {
|
||||
updateDisplayNames();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelTasks(@NotNull Group group) {
|
||||
final GroupTasks tasks = groupTasks.get(group);
|
||||
if (tasks != null) {
|
||||
tasks.cancel();
|
||||
groupTasks.remove(group);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the TAB list for all players when a plugin or proxy reload is performed
|
||||
*/
|
||||
public void reloadUpdate() {
|
||||
plugin.getTabGroups().getGroups().forEach(this::updatePeriodically);
|
||||
plugin.getTabGroups().getGroups().forEach(taskManager::updatePeriodically);
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@ -496,8 +490,11 @@ public class PlayerTabList {
|
||||
return;
|
||||
}
|
||||
final String serverName = server.get().getServerInfo().getName();
|
||||
final Group group = getGroup(serverName);
|
||||
player.setGroup(group);
|
||||
final Optional<Group> group = getGroup(serverName);
|
||||
if (group.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
player.setGroup(group.get());
|
||||
this.sendPlayerServerLinks(player);
|
||||
this.updatePlayer(player, true);
|
||||
player.sendHeaderAndFooter(this);
|
||||
@ -506,7 +503,7 @@ public class PlayerTabList {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Group getGroup(@NotNull String serverName) {
|
||||
public Optional<Group> getGroup(@NotNull String serverName) {
|
||||
return plugin.getTabGroups().getGroupFromServer(serverName, plugin);
|
||||
}
|
||||
|
||||
|
@ -68,8 +68,12 @@ public class TabListListener {
|
||||
tabList.removePlayer(event.getPlayer());
|
||||
} else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer redirectPlayer) {
|
||||
tabList.removePlayer(event.getPlayer(), redirectPlayer.getServer());
|
||||
} else if (event.getResult() instanceof KickedFromServerEvent.Notify notify) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.getPlayer().getTabList().removeEntry(event.getPlayer().getUniqueId());
|
||||
event.getPlayer().getTabList().clearHeaderAndFooter();
|
||||
justQuit.add(event.getPlayer().getUniqueId());
|
||||
|
||||
plugin.getServer().getScheduler().buildTask(plugin,
|
||||
@ -88,14 +92,15 @@ public class TabListListener {
|
||||
.orElse("");
|
||||
|
||||
// Get the group the player should now be in
|
||||
final Group group = tabList.getGroup(serverName);
|
||||
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group));
|
||||
final boolean isDefault = group.registeredServers(plugin).stream()
|
||||
.noneMatch(server -> server.getServerInfo().getName().equalsIgnoreCase(serverName));
|
||||
final @NotNull Optional<Group> groupOptional = tabList.getGroup(serverName);
|
||||
final boolean isDefault = groupOptional.map(g -> g.isDefault(plugin)).orElse(false);
|
||||
|
||||
// Removes cached relational data of the joined player from all other players
|
||||
plugin.getTabList().clearCachedData(joined);
|
||||
|
||||
// If the server is not in a group, use fallback.
|
||||
// If fallback is disabled, permit the player to switch excluded servers without a header or footer override
|
||||
if (isDefault && !plugin.getSettings().isFallbackEnabled()) {
|
||||
if (isDefault && !plugin.getSettings().isFallbackEnabled() && !groupOptional.map(g -> g.containsServer(plugin, serverName)).orElse(false)) {
|
||||
final Optional<TabPlayer> tabPlayer = tabList.getTabPlayer(joined);
|
||||
if (tabPlayer.isEmpty()) {
|
||||
return;
|
||||
@ -107,7 +112,6 @@ public class TabListListener {
|
||||
|
||||
final Component header = tabPlayer.get().getLastHeader();
|
||||
final Component footer = tabPlayer.get().getLastFooter();
|
||||
final Component displayName = tabPlayer.get().getLastDisplayName();
|
||||
|
||||
plugin.getServer().getScheduler().buildTask(plugin, () -> {
|
||||
final Component currentHeader = joined.getPlayerListHeader();
|
||||
@ -126,6 +130,12 @@ public class TabListListener {
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupOptional.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Group group = groupOptional.get();
|
||||
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group));
|
||||
if (justQuit.contains(joined.getUniqueId())) {
|
||||
plugin.getServer().getScheduler().buildTask(plugin,
|
||||
() -> tabList.joinPlayer(joined, group))
|
||||
@ -159,7 +169,6 @@ public class TabListListener {
|
||||
return;
|
||||
}
|
||||
tabList.removePlayer(player);
|
||||
plugin.log("Player " + player.getUsername() + " was not removed from the tab list, removing now.");
|
||||
}).delay(500, TimeUnit.MILLISECONDS).schedule();
|
||||
}
|
||||
|
||||
|
132
src/main/java/net/william278/velocitab/tab/TaskManager.java
Normal file
132
src/main/java/net/william278/velocitab/tab/TaskManager.java
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -20,6 +20,8 @@
|
||||
package net.william278.velocitab.tab;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.player.TabListEntry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -30,17 +32,12 @@ import java.util.UUID;
|
||||
/**
|
||||
* The VanishTabList handles the tab list for vanished players
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class VanishTabList {
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final PlayerTabList tabList;
|
||||
|
||||
public VanishTabList(Velocitab plugin, PlayerTabList tabList) {
|
||||
this.plugin = plugin;
|
||||
this.tabList = tabList;
|
||||
}
|
||||
|
||||
|
||||
public void vanishPlayer(@NotNull TabPlayer tabPlayer) {
|
||||
tabList.getPlayers().values().forEach(p -> {
|
||||
if (p.getPlayer().equals(tabPlayer.getPlayer())) {
|
||||
@ -56,17 +53,17 @@ public class VanishTabList {
|
||||
public void unVanishPlayer(@NotNull TabPlayer tabPlayer) {
|
||||
final UUID uuid = tabPlayer.getPlayer().getUniqueId();
|
||||
|
||||
tabPlayer.getDisplayName(plugin).thenAccept(c -> tabList.getPlayers().values().forEach(p -> {
|
||||
tabList.getPlayers().values().forEach(p -> {
|
||||
if (p.getPlayer().equals(tabPlayer.getPlayer())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!p.getPlayer().getTabList().containsEntry(uuid)) {
|
||||
p.getPlayer().getTabList().addEntry(tabList.createEntry(tabPlayer, p.getPlayer().getTabList(), c));
|
||||
tabList.createEntry(tabPlayer, p.getPlayer().getTabList(), p);
|
||||
} else {
|
||||
p.getPlayer().getTabList().getEntry(uuid).ifPresent(entry -> entry.setDisplayName(c));
|
||||
tabList.updateDisplayName(tabPlayer, p);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@ -104,10 +101,9 @@ public class VanishTabList {
|
||||
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, false));
|
||||
} else {
|
||||
if (!player.getTabList().containsEntry(p.getUniqueId())) {
|
||||
tabList.createEntry(target, player.getTabList()).thenAccept(e -> {
|
||||
player.getTabList().addEntry(e);
|
||||
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, true));
|
||||
});
|
||||
final TabListEntry tabListEntry = tabList.createEntry(target, player.getTabList(), tabPlayer);
|
||||
player.getTabList().addEntry(tabListEntry);
|
||||
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, true));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user