refactor: internals refactor, fix logic, new configs, spectator fix (#138)

* Started refactor

* more work

* Bug fixes and more work

* Fixed task problem

* More work on providers + fixed relocation problem

* Added providers + relocated snakeyaml

* Fixed relocation problem + removed org.json

* maps instantiation refactored

* Fixed reload problem

* Fixed logic problem

* More work on refactoring PlayerTabList

* Using lombok for procteded values

* More work

* Fixed cache problem + more work on refactor

* Fix for https://github.com/WiIIiam278/Velocitab/issues/35

* fixed conversations

* Code refactor

* Fixed problem while using minimessage

* Added more javadocs and removed kick handling as velocity fixed that problem

* Added username_lower placeholder and removed useless libraries

* Updated docs

* Added option to remove spectator effect in tablist
This commit is contained in:
AlexDev_ 2024-01-16 22:09:46 +01:00 committed by GitHub
parent 08501e84b8
commit 89a1f7add3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1881 additions and 1003 deletions

View File

@ -21,6 +21,7 @@ ext {
repositories {
mavenCentral()
maven { url = 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://repo.william278.net/releases/' }
maven { url = 'https://repo.william278.net/velocity/' }
@ -31,9 +32,7 @@ repositories {
}
dependencies {
compileOnly ('com.velocitypowered:velocity-api:3.3.0-SNAPSHOT') {
exclude group: 'net.kyori'
}
compileOnly 'com.velocitypowered:velocity-api:3.3.0-SNAPSHOT'
compileOnly 'com.velocitypowered:velocity-proxy:3.3.0-SNAPSHOT'
compileOnly 'io.netty:netty-codec-http:4.1.103.Final'
compileOnly 'org.projectlombok:lombok:1.18.30'
@ -42,18 +41,12 @@ dependencies {
compileOnly 'net.william278:PAPIProxyBridge:1.4.2'
compileOnly 'it.unimi.dsi:fastutil:8.5.12'
compileOnly 'net.kyori:adventure-nbt:4.14.0'
compileOnly 'net.kyori:adventure-api:4.14.0'
compileOnly 'net.kyori:adventure-text-minimessage:4.14.0'
compileOnly 'net.kyori:adventure-text-serializer-legacy:4.14.0'
compileOnly 'net.kyori:adventure-text-serializer-gson:4.14.0'
implementation 'org.apache.commons:commons-text:1.11.0'
implementation 'net.william278:annotaml:2.0.7'
implementation 'net.william278:DesertWell:2.0.4'
implementation 'dev.dejvokep:boosted-yaml:1.3.1'
implementation 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
implementation 'org.bstats:bstats-velocity:3.0.2'
implementation 'com.github.Exlll.ConfigLib:configlib-yaml:v4.3.0'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
}
@ -94,14 +87,14 @@ shadowJar {
relocate 'org.jetbrains', 'net.william278.velocitab.libraries'
relocate 'org.intellij', 'net.william278.velocitab.libraries'
relocate 'de.themoep', 'net.william278.velocitab.libraries'
relocate 'dev.dejvokep.boostedyaml', 'net.william278.velocitab.libraries.boostedyaml'
relocate 'net.william278.annotaml', 'net.william278.velocitab.libraries.annotaml'
relocate 'net.william278.desertwell', 'net.william278.velocitab.libraries.desertwell'
relocate 'org.json', 'net.william278.velocitab.libraries.json'
relocate 'org.bstats', 'net.william278.velocitab.libraries.bstats'
relocate 'de.exlll.configlib', 'net.william278.velocitab.libraries.configlib'
dependencies {
exclude dependency(':slf4j-api')
exclude dependency('org.json:json')
}
destinationDirectory.set(file("$rootDir/target"))

View File

@ -11,17 +11,14 @@ To add additional frames of animation to a header format for a [server group](se
```yaml
headers:
default:
- '&rainbow&Running Velocitab by William278'
- '&rainbow:10&Running Velocitab by William278'
- '&rainbow:20&Running Velocitab by William278'
```
</details>
By default, the plugin will switch between each frame whenever it is updated. To get this to animate, you must configure your `update_rate` setting.
### Setting the frame rate
The `update_rate` setting in your `config.yml` file&mdash;set to `0` by default&mdash;controls the length (in milliseconds&dagger;) between your TAB list being updated. On each update, the header or footer format will use the next frame in the list, looping back to the first after the last one has been displayed.
The `header_footer_update_rate` setting in your `tab_groups.yml` (different for each group) file&mdash;set to `0` by default&mdash;controls the length (in milliseconds&dagger;) between your TAB list being updated. On each update, the header or footer format will use the next frame in the list, looping back to the first after the last one has been displayed.
A good starting value for this could be `1000`, which is equivalent to one second. Once you've changed the value, use `/velocitab reload` to update the TAB menu in-game without restarting your proxy. Note the minimum update rate is `200` to avoid excessive network packet traffic, so values between `1`-`199` will be rounded up to `200`. If this value is set to `0` or below (as it is by default), the TAB menu will only update when a player joins or leaves, permissions are recalculated on LuckPerms, or the proxy is reloaded.
@ -35,10 +32,9 @@ Wondering how to make something like the above example? Here's how! This example
<details>
<summary>Example rainbow fade (config.yml)</summary>
Please note this is not a complete config file; you will need to add the relevant sections to the correct part in your own Velocitab `config.yml`.
Please note this is not a complete tab_groups file; you will need to add the relevant sections to the correct part in your own Velocitab `tab_groups.yml`.
```yaml
headers:
default:
- '&rainbow&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
- '&rainbow:2&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
- '&rainbow:4&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
@ -71,18 +67,15 @@ headers:
- '&rainbow:58&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
- '&rainbow:60&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
footers:
default:
- |
\n&7For Velocity proxy servers:
&#1bd96a-#6cffa9&https://modrinth.com/plugin/velocitab
&#1bd96a-#6cffa9&https://william278.net/project/veloictab'
formats:
default: '&#999-#fff&[%server%] &f%username%'
formatting_type: MINEDOWN
server_groups:
default:
- server
- server2
update_rate: 200
format: '&#999-#fff&[%server%] &f%username%'
header_footer_update_rate: 200
```
In config.yml
```yaml
formatter: MINEDOWN
```
</details>

View File

@ -1,4 +1,5 @@
This page contains the configuration file reference for Velocitab. The config file is located in `/plugins/velocitab/config.yml`
This page contains configuration file references for Velocitab.
The config file is located in `/plugins/velocitab/config.yml` and the tab groups file is located in `/plugins/velocitab/tab_groups.yml`
## Example config
<details>
@ -9,27 +10,15 @@ This page contains the configuration file reference for Velocitab. The config fi
# ┃ Velocitab Config ┃
# ┃ Developed by William278 ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
# ┗╸ Placeholders: %players_online%, %max_players_online%, %local_players_online%, %current_date%, %current_time%, %username%, %server%, %ping%, %prefix%, %suffix%, %role%
# Header(s) to display above the TAB list for each server group.
# List multiple headers and set update_rate to the number of ticks between frames for basic animations
headers:
default:
- '&rainbow&Running Velocitab by William278'
# Footer(s) to display below the TAB list for each server group, same as headers.
footers:
default:
- '[There are currently %players_online%/%max_players_online% players online](gray)'
formats:
default: '&7[%server%] &f%prefix%%username%'
# ┣╸ Information: https://william278.net/project/velocitab
# ┗╸ Documentation: https://william278.net/docs/velocitab
# Check for updates on startup
check_for_updates: true
# Whether to remove nametag from players' heads if the nametag associated with their server group is empty.
remove_nametags: false
remove_nametags: true
# Which text formatter to use (MINEDOWN, MINIMESSAGE, or LEGACY)
formatting_type: MINEDOWN
# The servers in each group of servers
server_groups:
default:
- server
- server2
formatter: MINEDOWN
# All servers which are not in other groups will be put in the fallback group.
# "false" will exclude them from Velocitab.
fallback_enabled: true
@ -44,18 +33,57 @@ server_display_names:
# Whether to enable the PAPIProxyBridge hook for PAPI support
enable_papi_hook: true
# How long in seconds to cache PAPI placeholders for, in milliseconds. (0 to disable)
papi_cache_time: 30000
papi_cache_time: 200
# If you are using MINIMESSAGE formatting, enable this to support MiniPlaceholders in formatting.
enable_miniplaceholders_hook: true
enable_mini_placeholders_hook: true
# Whether to send scoreboard teams packets. Required for player list sorting and nametag formatting.
# Turn this off if you're using scoreboard teams on backend servers.
send_scoreboard_packets: true
# Whether to sort players in the TAB list.
sort_players: true
# Ordered list of elements by which players should be sorted. (Correct values are both internal placeholders and, if enabled, PAPI placeholders)
sorting_placeholders:
- %role_weight%
- %username%
# How often in milliseconds to periodically update the TAB list, including header and footer, for all users.
# If set to 0, TAB will be updated on player join/leave instead. (1s = 1000ms)
update_rate: 0
# Remove gamemode spectator effect for other players in the TAB list.
remove_spectator_effect: false
```
</details>
## Example tab groups
<details>
<summary>tab_groups.yml</summary>
```yaml
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ Velocitab TabGroups ┃
# ┃ Developed by William278 ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
# ┣╸ Information: https://william278.net/project/velocitab
# ┗╸ Documentation: https://william278.net/docs/velocitab
groups:
- name: default
headers:
- '&rainbow&Running Velocitab by William278'
footers:
- '[There are currently %players_online%/%max_players_online% players online](gray)'
format: '&7[%server%] &f%prefix%%username%'
nametag:
prefix: '&f%prefix%'
suffix: '&f%suffix%'
servers:
- lobby
- survival
- creative
- minigames
- skyblock
- prison
- hub
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
header_footer_update_rate: 1000
placeholder_update_rate: 1000
```
</details>

View File

@ -1,14 +1,14 @@
Velocitab supports the full range of modern color formatting, including RGB colors and gradients. Both MineDown (_default_), MiniMessage and Legacy formatting are supported. To change which formatter is being used, change the `formatting_type` value in `config.yml` to `MINEDOWN`, `MINIMESSAGE` or `LEGACY` respectively.
Velocitab supports the full range of modern color formatting, including RGB colors and gradients. Both MineDown (_default_), MiniMessage and Legacy formatting are supported. To change which formatter is being used, change the `formatter` value in `config.yml` to `MINEDOWN`, `MINIMESSAGE` or `LEGACY` respectively.
Formatting is applied on header, footer and player text for each server group, and is applied after [[Placeholders]] have been inserted.
## MineDown syntax reference
MineDown is the default formatter type, enabled by setting `formatting_type` to `MINEDOWN` in `config.yml`. See the [MineDown Syntax Reference](https://github.com/Phoenix616/MineDown) on GitHub for the specification of how to format text with it.
MineDown is the default formatter type, enabled by setting `formatter` to `MINEDOWN` in `config.yml`. See the [MineDown Syntax Reference](https://github.com/Phoenix616/MineDown) on GitHub for the specification of how to format text with it.
## MiniMessage syntax reference
MiniMessage formatting can be enabled by setting `formatting_type` to `MINIMESSAGE` in `config.yml`. See the [MiniMessage Syntax Reference](https://docs.advntr.dev/minimessage/format.html) on the Adventure Docs for how to format text with it. Using MiniMessage as the formatter also allows compatibility for using MiniPlaceholders in text.
MiniMessage formatting can be enabled by setting `formatter` to `MINIMESSAGE` in `config.yml`. See the [MiniMessage Syntax Reference](https://docs.advntr.dev/minimessage/format.html) on the Adventure Docs for how to format text with it. Using MiniMessage as the formatter also allows compatibility for using MiniPlaceholders in text.
## Legacy formatting
> **Warning:** The option for legacy formatting is provided only for backwards compatibility with other plugins. Please consider using the MineDown or MiniMessage options instead!
Legacy formatting can be enabled by setting `formatting_type` to `LEGACY` in `config.yml`. Legacy formatter supports Mojang color and formatting codes (e.g. `&d`, `&l`), Adventure-styled RGB color codes (e.g. `&#a25981`), as well as BungeeCord RGB color codes (e.g. `&x&a&2&5&9&8&1`). See the [LegacyComponentSerializer Syntax Reference](https://docs.advntr.dev/serializer/legacy.html) on the Adventure Docs for more technical details.
Legacy formatting can be enabled by setting `formatter` to `LEGACY` in `config.yml`. Legacy formatter supports Mojang color and formatting codes (e.g. `&d`, `&l`), Adventure-styled RGB color codes (e.g. `&#a25981`), as well as BungeeCord RGB color codes (e.g. `&x&a&2&5&9&8&1`). See the [LegacyComponentSerializer Syntax Reference](https://docs.advntr.dev/serializer/legacy.html) on the Adventure Docs for more technical details.

View File

@ -8,13 +8,14 @@ Velocitab supports formatting the nametags of players (the text displayed above
You can configure nametags per-group using the `nametags` section of the config file. Each group should have one nametag format associated with it, which will be applied to all players on servers in that group.
<details>
<summary>Editing nametags (config.yml)</summary>
<summary>Editing nametags (tab_groups.yml)</summary>
```yaml
# Nametag(s) to display above players' heads for each server group. Set to empty to disable.
# Nametag formats must contain a %username%. Docs: https://william278.net/docs/velocitab/nametags
nametags:
default: '&f%prefix%%username%&f%suffix%'
nametag:
prefix: '&f%prefix%'
suffix: '&f%suffix%'
# (...)
@ -27,11 +28,9 @@ send_scoreboard_packets: true
Only players on servers which are part of groups that specify nametag formats will have their nametag formatted. To disable nametag formatting, remove all groups from the `nametags` section of the config file (leaving it empty).
## Removing name tags
In order to remove nametags, you must remove your nametag format from the config file. If you want to remove nametags for all groups, you can set the `nametags` section to empty `nametags: {}`. After that be sure to set `remove_nametags` to `true` to make sure the nametags are removed from players.
In order to remove nametags, you must set `prefix` and `suffix` to empty. After that be sure to set `remove_nametags` to `true` in the [`config.yml` file](config-file).
## Formatting limitations
Nametags must adhere to the following restrictions:
* A %username% placeholder must be present. This is used for delimiting the scoreboard prefix, name, and suffix to facilitate formatting.
* Only legacy colors can be used in formats. If RGB colors are specified, they will automatically be downsampled to the nearest legacy color. This is a limitation of the scoreboard team system.
* Nametags cannot contain newlines (must be on a single line)
* Gradients are not supported.
* Only legacy colors can be used in username formats. If RGB colors are specified, they will automatically be downsampled to the nearest legacy color. This is a limitation of the scoreboard team system.
* Nametags cannot contain newlines (must be on a single line).

View File

@ -11,6 +11,7 @@ Placeholders can be included in the header, footer and player name format of the
| `%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]` |

View File

@ -1,95 +1,198 @@
Velocitab supports defining multiple server groups, each providing distinct formatting for players in the TAB list, alongside unique headers and footers. This is useful if you wish to display different information in TAB depending on the server a player is on. You can also set formatting to use for [[Nametags]] above players' heads per-group.
Velocitab supports defining multiple server groups, each providing distinct formatting for players in the TAB list,
alongside unique headers and footers. This is useful if you wish to display different information in TAB depending on
the server a player is on. You can also set formatting to use for [[Nametags]] above players' heads per-group.
## Defining groups
Groups are defined in the `server_groups` section of `config.yml`, as a list of servers following the group name (by default, a group `default` will be present, alongside a list of servers on your network.
Groups are defined in `tab_groups.yml`, as a list of TabGroup elements.
Every group must have a unique name, and a list of servers to include in the group. You can also define a list of
sorting placeholders to use when sorting players in the TAB list, and a header/footer update rate and placeholder update
rate to use for the group.
## Headers and footers
<details>
<summary>Example of a default config.yml</summary>
<summary>Example of headers and footers</summary>
```yaml
server_groups:
default:
- lobby1
- lobby2
- lobby3
headers:
- '&rainbow&Running Velocitab by William278'
footers:
- '[There are currently %players_online%/%max_players_online% players online](gray)'
```
</details>
You can define as many groups as you wish in this section by adding more lists of servers.
You can define a list of headers and footers to use for each group. These will be cycled through at the rate defined
by `header_footer_update_rate` in milliseconds. If you only want to use one header/footer, you can define a single
element list. You can also use the `|` character to define a multi-line header/footer. See [[Animations]] for more
information.
## Formats
<details>
<summary>Example of format</summary>
```yaml
format: '&7[%server%] &f%prefix%%username%'
```
</details>
You can define a format to use for each group. This will be used to format the text of each player in the TAB list.
See [[Formatting]] for more information.
Player formats may only utilize one line.
## Nametags
<details>
<summary>Example of nametag</summary>
```yaml
nametag:
prefix: '&f%prefix%'
suffix: '&f%suffix%'
```
</details>
You can define a nametag to use for each group. This will be used to format the text above each player's head.
See [[Nametags]] for more information.
Player nametags may only utilize one line.
## Servers
<details>
<summary>Example of servers</summary>
```yaml
servers:
- lobby
- survival
- creative
- minigames
- skyblock
- prison
- hub
```
</details>
You can define a list of servers to include in each group.
## Sorting placeholders
<details>
<summary>Example of sorting placeholders</summary>
```yaml
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
```
</details>
You can define a list of sorting placeholders to use when sorting players in the TAB list. See [[Sorting]] for more
information.
## Header/footer update rate
<details>
<summary>Example of header/footer update rate</summary>
```yaml
header_footer_update_rate: 1000
```
</details>
You can define a header/footer update rate to use for each group, in milliseconds. This will determine how quickly the
headers and footers will cycle through in the TAB list. The default is 1000 milliseconds (1 second).
## Placeholder update rate
<details>
<summary>Example of placeholder update rate</summary>
```yaml
placeholder_update_rate: 1000
```
</details>
You can define a placeholder update rate to use for each group, in milliseconds. This will determine how quickly the
placeholders in the TAB list will update. The default is 1000 milliseconds (1 second).
## Example tab groups
<details>
<summary>Adding more groups</summary>
```yaml
server_groups:
lobbies:
- lobby1
- lobby2
creative:
- creative_lobby
- creative1
survival:
- survival1
- survival2
```
groups:
- name: lobbies
headers:
- '&rainbow&Running Velocitab by William278 on Lobbies!'
footers:
- '[There are currently %players_online%/%max_players_online% players online](gray)'
format: '&7[%server%] &f%prefix%%username%'
servers:
- lobby
- hub
- minigames
- creative
- survival
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
header_footer_update_rate: 1000
placeholder_update_rate: 1000
- name: creative
headers:
- '&rainbow&Running Velocitab by William278 on Creative!'
footers:
- '[There are currently %players_online%/%max_players_online% players online](gray)'
format: '&7[%server%] &f%prefix%%username%'
servers:
- creative
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
header_footer_update_rate: 1000
placeholder_update_rate: 1000
- name: survival
headers:
- '&rainbow&Running Velocitab by William278 on Survival!'
footers:
- '[There are currently %players_online%/%max_players_online% players online](gray)'
format: '&7[%server%] &f%prefix%%username%'
servers:
- survival
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
header_footer_update_rate: 1000
placeholder_update_rate: 1000
```
</details>
## Mapping headers, footers, user formats, and nametags to groups
Once you've defined your groups, you can modify the `headers`, `footers`, `formats` and [`nametags`](nametags) section of the file with different formats for each group.
<details>
<summary>Per-group formats</summary>
```yaml
headers:
lobbies:
- 'Welcome, %username%! Join a server to start!'
creative:
- '%username% is playing Creative!'
survival:
- '%username% is playing Survival!'
footers:
lobbies:
- 'There are %players_online%players online!'
creative:
- 'Currently connected to a creative server: %server%!'
survival:
- 'Today is %current_date%!'
formats:
lobbies: '&8[Lobby] &7%username%'
creative: '&e[Creative] &7[%server%] &f%prefix%%username%'
survival: '&2[Survival (%server%)] &f%prefix%%username%'
nametags:
lobbies: '&8[Lobby] &7%prefix%%username%&7%suffix%'
creative: '&e%prefix%%username%&7%suffix%'
survival: '&7%prefix%%username%&7%suffix%'
```
</details>
See [[Placeholders]] for how to use placeholders in these formats, and [[Formatting]] for how to format text with colors, and see [[Animations]] for how to create basic animations by adding more headers/footers to each group's list. Note that some formatting limitations apply to nametags &mdash; [[Nametags]] for more information.
### Adding new lines
If you want to add a new line to your header or footer format, you can use `\n` to insert one &mdash; but since this gets messy quickly, there's an easier way using the YAML markup pipe character to declare a multiline string:
<details>
<summary>Multi-line headers/footers</summary>
```yaml
footers:
lobbies:
- |
There are %players_online%players online!
I'm a second line
Third line, woohoo~!
```
</details>
Player name formats and nametags may only utilize one line.
See [[Placeholders]] for how to use placeholders in these formats, and [[Formatting]] for how to format text with
colors, and see [[Animations]] for how to create basic animations by adding more headers/footers to each group's list.
Note that some formatting limitations apply to nametags &mdash; [[Nametags]] for more information.
## Default group
If a player isn't connected to a server on your network, their TAB menu will be formatted as per the formats defined by `fallback_group` set in `config.yml`, provided `fallback_enabled` is set to `true`.
If you don't want them to have their TAB handled at all by Velocitab, you can use this to disable Velocitab formatting on certain servers altogether by disabling the `fallback_enabled` setting and excluding servers you do not wish to format from being part of a group.
If a player isn't connected to a server on your network, their TAB menu will be formatted as per the formats defined
by `fallback_group` set in `config.yml`, provided `fallback_enabled` is set to `true`.
If you don't want them to have their TAB handled at all by Velocitab, you can use this to disable Velocitab formatting
on certain servers altogether by disabling the `fallback_enabled` setting and excluding servers you do not wish to
format from being part of a group.
<details>
<summary>Example in config.yml</summary>
@ -101,4 +204,5 @@ fallback_enabled: true
# The formats to use for the fallback group.
fallback_group: 'lobbies'
```
</details>

View File

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

View File

@ -31,69 +31,75 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.scheduler.ScheduledTask;
import lombok.Getter;
import net.william278.annotaml.Annotaml;
import lombok.Setter;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.desertwell.util.Version;
import net.william278.velocitab.api.VelocitabAPI;
import net.william278.velocitab.commands.VelocitabCommand;
import net.william278.velocitab.config.ConfigProvider;
import net.william278.velocitab.config.Formatter;
import net.william278.velocitab.config.Settings;
import net.william278.velocitab.config.TabGroups;
import net.william278.velocitab.hook.Hook;
import net.william278.velocitab.hook.LuckPermsHook;
import net.william278.velocitab.hook.MiniPlaceholdersHook;
import net.william278.velocitab.hook.PAPIProxyBridgeHook;
import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.packet.PacketEventManager;
import net.william278.velocitab.sorting.SortingManager;
import net.william278.velocitab.tab.PlayerTabList;
import net.william278.velocitab.providers.HookProvider;
import net.william278.velocitab.providers.LoggerProvider;
import net.william278.velocitab.providers.MetricProvider;
import net.william278.velocitab.providers.ScoreboardProvider;
import net.william278.velocitab.vanish.VanishManager;
import org.bstats.charts.SimplePie;
import org.bstats.velocity.Metrics;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.event.Level;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Plugin(id = "velocitab")
public class Velocitab {
private static final int METRICS_ID = 18247;
@SuppressWarnings("unused")
@Getter
public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProvider, HookProvider, MetricProvider {
@Setter
private Settings settings;
@Setter
private TabGroups tabGroups;
private final ProxyServer server;
private final Logger logger;
private final Path dataDirectory;
private final Path configDirectory;
@Inject
private PluginContainer pluginContainer;
@Inject
private Metrics.Factory metricsFactory;
@Setter
private PlayerTabList tabList;
@Setter
private List<Hook> hooks;
@Setter
private ScoreboardManager scoreboardManager;
@Setter
private SortingManager sortingManager;
@Getter
private VanishManager vanishManager;
private PacketEventManager packetEventManager;
@Inject
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path dataDirectory) {
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path configDirectory) {
this.server = server;
this.logger = logger;
this.dataDirectory = dataDirectory;
this.configDirectory = configDirectory;
}
@Subscribe
public void onProxyInitialization(@NotNull ProxyInitializeEvent event) {
loadSettings();
loadConfigs();
loadHooks();
prepareVanishManager();
prepareScoreboardManager();
prepareTabList();
prepareSortingManager();
prepareChannelManager();
prepareScoreboard();
registerCommands();
registerMetrics();
checkForUpdates();
@ -105,99 +111,32 @@ public class Velocitab {
public void onProxyShutdown(@NotNull ProxyShutdownEvent event) {
server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel);
disableScoreboardManager();
disableTabList();
getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent);
VelocitabAPI.unregister();
logger.info("Successfully disabled Velocitab");
}
@NotNull
public ProxyServer getServer() {
return server;
}
@NotNull
public Settings getSettings() {
return settings;
}
@NotNull
public Formatter getFormatter() {
return getSettings().getFormatter();
}
public void loadSettings() {
try {
settings = Annotaml.create(
new File(dataDirectory.toFile(), "config.yml"),
new Settings(this)
).get();
settings.getNametags().values().stream()
.filter(nametag -> !nametag.contains("%username%")).forEach(nametag -> {
logger.warn("Nametag '" + nametag + "' does not contain %username% - removing");
settings.getNametags().remove(nametag);
});
} catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
logger.error("Failed to load config file: " + e.getMessage(), e);
}
}
private <H extends Hook> Optional<H> getHook(@NotNull Class<H> hookType) {
return hooks.stream()
.filter(hook -> hook.getClass().equals(hookType))
.map(hookType::cast)
.findFirst();
}
public Optional<LuckPermsHook> getLuckPermsHook() {
return getHook(LuckPermsHook.class);
}
public Optional<PAPIProxyBridgeHook> getPAPIProxyBridgeHook() {
return getHook(PAPIProxyBridgeHook.class);
}
public Optional<MiniPlaceholdersHook> getMiniPlaceholdersHook() {
return getHook(MiniPlaceholdersHook.class);
}
private void loadHooks() {
this.hooks = new ArrayList<>();
Hook.AVAILABLE.forEach(availableHook -> availableHook.apply(this).ifPresent(hooks::add));
}
private void prepareScoreboardManager() {
if (settings.isSendScoreboardPackets()) {
this.scoreboardManager = new ScoreboardManager(this);
scoreboardManager.registerPacket();
}
}
private void disableScoreboardManager() {
if (scoreboardManager != null && settings.isSendScoreboardPackets()) {
scoreboardManager.close();
scoreboardManager.unregisterPacket();
}
}
private void disableTabList() {
if (tabList != null) {
tabList.close();
}
public void loadConfigs() {
loadSettings();
loadTabGroups();
}
private void prepareVanishManager() {
this.vanishManager = new VanishManager(this);
}
private void prepareSortingManager() {
this.sortingManager = new SortingManager(this);
private void prepareChannelManager() {
this.packetEventManager = new PacketEventManager(this);
}
@NotNull
public SortingManager getSortingManager() {
return sortingManager;
@Override
public Velocitab getPlugin() {
return this;
}
@NotNull
@ -205,18 +144,6 @@ public class Velocitab {
return Optional.ofNullable(scoreboardManager);
}
@NotNull
public PlayerTabList getTabList() {
return tabList;
}
private void prepareTabList() {
this.tabList = new PlayerTabList(this);
server.getEventManager().register(this, tabList);
server.getScheduler().buildTask(this, tabList::load).delay(1, TimeUnit.SECONDS).schedule();
}
private void prepareAPI() {
VelocitabAPI.register(this);
}
@ -239,16 +166,6 @@ public class Velocitab {
return Version.fromString(getDescription().getVersion().orElseThrow(), "-");
}
private void registerMetrics() {
final Metrics metrics = metricsFactory.make(this, METRICS_ID);
metrics.addCustomChart(new SimplePie("sort_players", () -> settings.isSortPlayers() ? "Enabled" : "Disabled"));
metrics.addCustomChart(new SimplePie("formatter_type", () -> settings.getFormatter().getName()));
metrics.addCustomChart(new SimplePie("using_luckperms", () -> getLuckPermsHook().isPresent() ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("using_papiproxybridge", () -> getPAPIProxyBridgeHook().isPresent() ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("using_miniplaceholders", () -> getMiniPlaceholdersHook().isPresent() ? "Yes" : "No"));
}
private void checkForUpdates() {
if (!getSettings().isCheckForUpdates()) {
return;
@ -269,28 +186,4 @@ public class Velocitab {
.build();
}
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... exceptions) {
switch (level) {
case ERROR -> {
if (exceptions.length > 0) {
logger.error(message, exceptions[0]);
} else {
logger.error(message);
}
}
case WARN -> {
if (exceptions.length > 0) {
logger.warn(message, exceptions[0]);
} else {
logger.warn(message);
}
}
case INFO -> logger.info(message);
}
}
public void log(@NotNull String message) {
this.log(Level.INFO, message);
}
}

View File

@ -19,54 +19,11 @@
package net.william278.velocitab.api;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@SuppressWarnings("unused")
public class PlayerAddedToTabEvent {
public record PlayerAddedToTabEvent(@NotNull TabPlayer player, @NotNull Group group) {
private final TabPlayer player;
private final String group;
private final List<String> groupServers;
public PlayerAddedToTabEvent(@NotNull TabPlayer player, @NotNull String group, @NotNull List<String> groupServers) {
this.player = player;
this.group = group;
this.groupServers = groupServers;
}
@NotNull
public TabPlayer getTabPlayer() {
return this.player;
}
@NotNull
public String getGroup() {
return this.group;
}
@NotNull
public List<String> getGroupServers() {
return this.groupServers;
}
@NotNull
@Deprecated(forRemoval = true)
public TabPlayer player() {
return this.player;
}
@NotNull
@Deprecated(forRemoval = true)
public String group() {
return this.group;
}
@NotNull
@Deprecated(forRemoval = true)
public List<String> groupServers() {
return this.groupServers;
}
}

View File

@ -21,6 +21,7 @@ package net.william278.velocitab.api;
import com.velocitypowered.api.proxy.Player;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.PlayerTabList;
import net.william278.velocitab.vanish.VanishIntegration;
@ -168,11 +169,11 @@ public class VelocitabAPI {
*
* @param player the player for whom to retrieve the server group
* @return the name of the server group that the player is connected to,
* or an empty string if the player is not in a group server
* or a null value if the player is not connected to a server group
*/
@NotNull
public String getServerGroup(@NotNull Player player) {
return getUser(player).map(t -> t.getServerGroup(plugin)).orElse("");
@Nullable
public Group getServerGroup(@NotNull Player player) {
return getUser(player).map(TabPlayer::getGroup).orElse(null);
}

View File

@ -101,7 +101,7 @@ public final class VelocitabCommand {
.then(LiteralArgumentBuilder.<CommandSource>literal("reload")
.requires(src -> src.hasPermission("velocitab.command.reload"))
.executes(ctx -> {
plugin.loadSettings();
plugin.loadConfigs();
plugin.getTabList().reloadUpdate();
ctx.getSource().sendMessage(Component.text(
"Velocitab has been reloaded!",

View File

@ -0,0 +1,113 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.config;
import de.exlll.configlib.NameFormatters;
import de.exlll.configlib.YamlConfigurationProperties;
import de.exlll.configlib.YamlConfigurations;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
/**
* Interface for getting and setting data from plugin configuration files
*
* @since 1.0
*/
public interface ConfigProvider {
@NotNull
YamlConfigurationProperties.Builder<?> YAML_CONFIGURATION_PROPERTIES = YamlConfigurationProperties.newBuilder()
.setNameFormatter(NameFormatters.LOWER_UNDERSCORE);
/**
* Get the plugin settings, read from the config file
*
* @return the plugin settings
* @since 1.0
*/
@NotNull
Settings getSettings();
/**
* Set the plugin settings
*
* @param settings The settings to set
* @since 1.0
*/
void setSettings(@NotNull Settings settings);
/**
* Load the plugin settings from the config file
*
* @since 1.0
*/
default void loadSettings() {
setSettings(YamlConfigurations.update(
getConfigDirectory().resolve("config.yml"),
Settings.class,
YAML_CONFIGURATION_PROPERTIES.header(Settings.CONFIG_HEADER).build()
));
getSettings().validateConfig();
}
/**
* Get the tab groups
*
* @return the tab groups
* @since 1.0
*/
@NotNull
TabGroups getTabGroups();
/**
* Set the tab groups
*
* @param tabGroups The tab groups to set
* @since 1.0
*/
void setTabGroups(@NotNull TabGroups tabGroups);
/**
* Load the tab groups from the config file
*
* @since 1.0
*/
default void loadTabGroups() {
setTabGroups(YamlConfigurations.update(
getConfigDirectory().resolve("tab_groups.yml"),
TabGroups.class,
YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
));
getTabGroups().validateConfig();
}
/**
* Get the plugin config directory
*
* @return the plugin config directory
* @since 1.0
*/
@NotNull
Path getConfigDirectory();
}

View File

@ -0,0 +1,30 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.config;
public interface ConfigValidator {
/**
* Validates the configuration settings.
* @throws IllegalStateException if the configuration is invalid
*/
void validateConfig() throws IllegalStateException;
}

View File

@ -38,19 +38,22 @@ public enum Formatter {
MINEDOWN(
(text, player, plugin) -> new MineDown(text).toComponent(),
(text) -> text.replace("__", "_\\_"),
"MineDown"
"MineDown",
(text) -> new MineDown(text).toComponent()
),
MINIMESSAGE(
(text, player, plugin) -> plugin.getMiniPlaceholdersHook()
.map(hook -> hook.format(text, player.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(text)),
(text) -> MiniMessage.miniMessage().escapeTags(text),
"MiniMessage"
"MiniMessage",
(text) -> MiniMessage.miniMessage().deserialize(text)
),
LEGACY(
(text, player, plugin) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text),
Function.identity(),
"Legacy Text"
"Legacy Text",
(text) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text)
);
/**
@ -66,12 +69,14 @@ public enum Formatter {
* Function to escape formatting characters in a string
*/
private final Function<String, String> escaper;
private final Function<String, Component> emptyFormatter;
Formatter(@NotNull TriFunction<String, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper,
@NotNull String name) {
@NotNull String name, @NotNull Function<String, Component> emptyFormatter) {
this.formatter = formatter;
this.escaper = escaper;
this.name = name;
this.emptyFormatter = emptyFormatter;
}
@NotNull
@ -85,6 +90,11 @@ public enum Formatter {
.serialize(format(text, player, plugin));
}
@NotNull
public Component emptyFormat(@NotNull String text) {
return emptyFormatter.apply(text);
}
@NotNull
public String escape(@NotNull String text) {
return escaper.apply(text);

View File

@ -0,0 +1,104 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.config;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import de.exlll.configlib.Comment;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.Nametag;
import org.apache.commons.text.StringEscapeUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public record Group(
String name,
List<String> headers,
List<String> footers,
String format,
Nametag nametag,
List<String> servers,
List<String> sortingPlaceholders,
@Comment("""
How often in milliseconds to periodically update the TAB list, including header and footer, for all users.
If set to 0, TAB will be updated on player join/leave instead. (1s = 1000ms)
The minimal update rate is 200ms, anything lower will automatically be set to 200ms.""")
int headerFooterUpdateRate,
int placeholderUpdateRate
) {
@NotNull
public String getHeader(int index) {
return headers.isEmpty() ? "" : StringEscapeUtils.unescapeJava(headers
.get(Math.max(0, Math.min(index, headers.size() - 1))));
}
@NotNull
public String getFooter(int index) {
return footers.isEmpty() ? "" : StringEscapeUtils.unescapeJava(footers
.get(Math.max(0, Math.min(index, footers.size() - 1))));
}
@NotNull
public List<RegisteredServer> registeredServers(Velocitab plugin) {
if (isDefault() && plugin.getSettings().isFallbackEnabled()) {
return new ArrayList<>(plugin.getServer().getAllServers());
}
return servers.stream()
.map(plugin.getServer()::getServer)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
}
public boolean isDefault() {
return name.equals("default");
}
@NotNull
public List<Player> getPlayers(Velocitab plugin) {
List<Player> players = new ArrayList<>();
for (RegisteredServer server : registeredServers(plugin)) {
players.addAll(server.getPlayersConnected());
}
return players;
}
@NotNull
public List<TabPlayer> getTabPlayers(Velocitab plugin) {
return plugin.getTabList().getPlayers()
.values()
.stream()
.filter(tabPlayer -> tabPlayer.getGroup().equals(this))
.toList();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Group group)) {
return false;
}
return name.equals(group.name);
}
}

View File

@ -23,6 +23,7 @@ import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
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;
@ -47,6 +48,7 @@ public enum Placeholder {
CURRENT_DATE((plugin, player) -> DateTimeFormatter.ofPattern("dd MMM yyyy").format(LocalDateTime.now())),
CURRENT_TIME((plugin, player) -> DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.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)),
PING((plugin, player) -> Long.toString(player.getPlayer().getPing())),
PREFIX((plugin, player) -> player.getRole().getPrefix().orElse("")),
@ -54,7 +56,7 @@ public enum Placeholder {
ROLE((plugin, player) -> player.getRole().getName().orElse("")),
ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName().orElse("")),
ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()),
SERVER_GROUP((plugin, player) -> player.getServerGroup(plugin)),
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()
@ -68,6 +70,7 @@ public enum Placeholder {
private final boolean parameterised;
private final Pattern pattern;
private final static Pattern checkPlaceholders = Pattern.compile("%.*?%");
private final static String DELIMITER = ":::";
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
this.parameterised = false;
@ -81,9 +84,20 @@ public enum Placeholder {
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]+%", Pattern.CASE_INSENSITIVE);
}
public static CompletableFuture<Nametag> replace(@NotNull Nametag nametag, @NotNull Velocitab plugin,
@NotNull TabPlayer player) {
return replace(nametag.prefix() + DELIMITER + nametag.suffix(), plugin, player)
.thenApply(s -> s.split(DELIMITER, 2))
.thenApply(v -> new Nametag(v[0], v.length > 1 ? v[1] : ""));
}
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) {

View File

@ -19,185 +19,72 @@
package net.william278.velocitab.config;
import de.exlll.configlib.Comment;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.Getter;
import net.william278.annotaml.YamlComment;
import net.william278.annotaml.YamlFile;
import net.william278.annotaml.YamlKey;
import net.william278.velocitab.Velocitab;
import org.apache.commons.text.StringEscapeUtils;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@YamlFile(header = """
Velocitab Config
Developed by William278
Information: https://william278.net/project/velocitab
Documentation: https://william278.net/docs/velocitab""")
public class Settings {
@Getter
@YamlKey("check_for_updates")
@YamlComment("Check for updates on startup")
@SuppressWarnings("FieldMayBeFinal")
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Settings implements ConfigValidator{
public static final String CONFIG_HEADER = """
Velocitab Config
Developed by William278
Information: https://william278.net/project/velocitab
Documentation: https://william278.net/docs/velocitab""";
@Comment("Check for updates on startup")
private boolean checkForUpdates = true;
@YamlKey("headers")
@YamlComment("Header(s) to display above the TAB list for each server group."
+ "\nList multiple headers and set update_rate to the number of ticks between frames for basic animations")
private Map<String, List<String>> headers = Map.of(
"default",
List.of("&rainbow&Running Velocitab by William278")
);
@YamlKey("footers")
@YamlComment("Footer(s) to display below the TAB list for each server group, same as headers.")
private Map<String, List<String>> footers = Map.of(
"default",
List.of("[There are currently %players_online%/%max_players_online% players online](gray)")
);
@YamlKey("formats")
private Map<String, String> formats = Map.of("default", "&7[%server%] &f%prefix%%username%");
@Getter
@YamlKey("nametags")
@YamlComment("Nametag(s) to display above players' heads for each server group. Set to empty to disable."
+ "\nNametag formats must contain a %username%. Docs: https://william278.net/docs/velocitab/nametags")
private Map<String, String> nametags = Map.of("default", "&f%prefix%%username%&f%suffix%");
@Getter
@YamlKey("remove_nametags")
@YamlComment("Whether to remove nametag from players' heads if the nametag associated with their server group is empty.")
@Comment("Whether to remove nametag from players' heads if the nametag associated with their server group is empty.")
private boolean removeNametags = false;
@Getter
@YamlComment("Which text formatter to use (MINEDOWN, MINIMESSAGE, or LEGACY)")
@YamlKey("formatting_type")
@Comment("Which text formatter to use (MINEDOWN, MINIMESSAGE, or LEGACY)")
private Formatter formatter = Formatter.MINEDOWN;
@Getter
@YamlKey("server_groups")
@YamlComment("The servers in each group of servers. The order of groups is important when sorting by SERVER_GROUP.")
private LinkedHashMap<String, List<String>> serverGroups = new LinkedHashMap<>(Map.of(
"default",
List.of("lobby1", "lobby2", "lobby3"))
);
@Getter
@YamlKey("fallback_enabled")
@YamlComment("All servers which are not in other groups will be put in the fallback group."
@Comment("All servers which are not in other groups will be put in the fallback group."
+ "\n\"false\" will exclude them from Velocitab.")
private boolean fallbackEnabled = true;
@Getter
@YamlKey("fallback_group")
@YamlComment("The formats to use for the fallback group.")
@Comment("The formats to use for the fallback group.")
private String fallbackGroup = "default";
@Getter
@YamlKey("only_list_players_in_same_group")
@YamlComment("Only show other players on a server that is part of the same server group as the player.")
@Comment("Only show other players on a server that is part of the same server group as the player.")
private boolean onlyListPlayersInSameGroup = true;
@Getter
@YamlKey("server_display_names")
@YamlComment("Define custom names to be shown in the TAB list for specific server names."
@Comment("Define custom names to be shown in the TAB list for specific server names."
+ "\nIf no custom display name is provided for a server, its original name will be used.")
private Map<String, String> serverDisplayNames = Map.of("very-long-server-name", "VLSN");
@Getter
@YamlKey("enable_papi_hook")
@YamlComment("Whether to enable the PAPIProxyBridge hook for PAPI support")
@Comment("Whether to enable the PAPIProxyBridge hook for PAPI support")
private boolean enablePapiHook = true;
@Getter
@YamlKey("papi_cache_time")
@YamlComment("How long in seconds to cache PAPI placeholders for, in milliseconds. (0 to disable)")
@Comment("How long in seconds to cache PAPI placeholders for, in milliseconds. (0 to disable)")
private long papiCacheTime = 30000;
@Getter
@YamlKey("enable_miniplaceholders_hook")
@YamlComment("If you are using MINIMESSAGE formatting, enable this to support MiniPlaceholders in formatting.")
@Comment("If you are using MINIMESSAGE formatting, enable this to support MiniPlaceholders in formatting.")
private boolean enableMiniPlaceholdersHook = true;
@Getter
@YamlKey("send_scoreboard_packets")
@YamlComment("Whether to send scoreboard teams packets. Required for player list sorting and nametag formatting."
@Comment("Whether to send scoreboard teams packets. Required for player list sorting and nametag formatting."
+ "\nTurn this off if you're using scoreboard teams on backend servers.")
private boolean sendScoreboardPackets = true;
@Getter
@YamlKey("sort_players")
@YamlComment("Whether to sort players in the TAB list.")
@Comment("Whether to sort players in the TAB list.")
private boolean sortPlayers = true;
@YamlKey("sorting_placeholders")
@YamlComment("Ordered list of elements by which players should be sorted. " +
"(Correct values are both internal placeholders and, if enabled, PAPI placeholders)")
private List<String> sortingPlaceholders = List.of(
"%role_weight%",
"%username%"
);
@Getter
@YamlKey("update_rate")
@YamlComment("""
How often in milliseconds to periodically update the TAB list, including header and footer, for all users.
If set to 0, TAB will be updated on player join/leave instead. (1s = 1000ms)
The minimal update rate is 200ms, anything lower will automatically be set to 200ms.""")
private int updateRate = 0;
public Settings(@NotNull Velocitab plugin) {
this.serverGroups = new LinkedHashMap<>(Map.of("default",
plugin.getServer().getAllServers().stream().map(server -> server.getServerInfo().getName()).toList()
));
}
@SuppressWarnings("unused")
public Settings() {
}
@NotNull
public String getHeader(@NotNull String serverGroup, int index) {
final List<String> groupHeaders = headers.getOrDefault(serverGroup, List.of(""));
return groupHeaders.isEmpty() ? "" : StringEscapeUtils.unescapeJava(groupHeaders
.get(Math.max(0, Math.min(index, getHeaderListSize(serverGroup) - 1))));
}
@NotNull
public String getFooter(@NotNull String serverGroup, int index) {
final List<String> groupFooters = footers.getOrDefault(serverGroup, List.of(""));
return groupFooters.isEmpty() ? "" : StringEscapeUtils.unescapeJava(groupFooters
.get(Math.max(0, Math.min(index, getFooterListSize(serverGroup) - 1))));
}
public int getHeaderListSize(@NotNull String serverGroup) {
return headers.getOrDefault(serverGroup, List.of("")).size();
}
public int getFooterListSize(@NotNull String serverGroup) {
return footers.getOrDefault(serverGroup, List.of("")).size();
}
@NotNull
public String getFormat(@NotNull String serverGroup) {
return StringEscapeUtils.unescapeJava(
formats.getOrDefault(serverGroup, "%username%"));
}
@NotNull
public String getNametag(@NotNull String serverGroup) {
return StringEscapeUtils.unescapeJava(
nametags.getOrDefault(serverGroup, ""));
}
public boolean doNametags() {
return !nametags.isEmpty();
}
@Comment("Remove gamemode spectator effect for other players in the TAB list.")
private boolean removeSpectatorEffect = true;
/**
* Get display name for the server
@ -210,33 +97,8 @@ public class Settings {
return serverDisplayNames.getOrDefault(serverName, serverName);
}
/**
* Get the ordinal position of the server group
*
* @param serverGroupName The server group name
* @return The ordinal position of the server group
*/
public int getServerGroupPosition(@NotNull String serverGroupName) {
return List.copyOf(serverGroups.keySet()).indexOf(serverGroupName);
}
@Override
public void validateConfig() {
/**
* Get the server group that a server is in
*
* @param serverName The name of the server
* @return The server group that the server is in, or "default" if the server is not in a group
*/
@NotNull
public String getServerGroup(String serverName) {
return serverGroups.entrySet().stream()
.filter(entry -> entry.getValue().contains(serverName)).findFirst()
.map(Map.Entry::getKey)
.orElse(fallbackGroup);
}
@NotNull
public List<String> getSortingElements() {
return sortingPlaceholders;
}
}

View File

@ -0,0 +1,91 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.config;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.william278.velocitab.tab.Nametag;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@SuppressWarnings("FieldMayBeFinal")
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TabGroups implements ConfigValidator {
public static final String CONFIG_HEADER = """
Velocitab TabGroups
Developed by William278
Information: https://william278.net/project/velocitab
Documentation: https://william278.net/docs/velocitab""";
public List<Group> groups = List.of(
new Group(
"default",
List.of("&rainbow&Running Velocitab by William278"),
List.of("[There are currently %players_online%/%max_players_online% players online](gray)"),
"&7[%server%] &f%prefix%%username%",
new Nametag("&f%prefix%", "&f%suffix%"),
List.of("lobby", "survival", "creative", "minigames", "skyblock", "prison", "hub"),
List.of("%role_weight%", "%username_lower%"),
1000,
1000
)
);
@NotNull
public Group getGroupFromName(@NotNull String name) {
return groups.stream()
.filter(group -> group.name().equals(name))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No group with name " + name + " found"));
}
@NotNull
public Group getGroupFromServer(@NotNull String server) {
for (Group group : groups) {
if (group.servers().contains(server)) {
return group;
}
}
return getGroupFromName("default");
}
public int getPosition(@NotNull Group group) {
return groups.indexOf(group) + 1;
}
@Override
public void validateConfig() {
if (groups.isEmpty()) {
throw new IllegalStateException("No tab groups defined in config");
}
if (groups.stream().noneMatch(group -> group.name().equals("default"))) {
throw new IllegalStateException("No default tab group defined in config");
}
}
}

View File

@ -105,7 +105,7 @@ public class LuckPermsHook extends Hook {
final Role oldRole = tabPlayer.getRole();
tabPlayer.setRole(getRoleFromMetadata(event.getData().getMetaData()));
tabList.updatePlayerDisplayName(tabPlayer);
tabList.recalculateVanishForPlayer(tabPlayer);
tabList.getVanishTabList().recalculateVanishForPlayer(tabPlayer);
checkRoleUpdate(tabPlayer, oldRole);
})
.delay(500, TimeUnit.MILLISECONDS)

View File

@ -34,6 +34,7 @@ public class PAPIProxyBridgeHook extends Hook {
super(plugin);
this.api = PlaceholderAPI.createInstance();
this.api.setCacheExpiry(Math.max(0, plugin.getSettings().getPapiCacheTime()));
this.api.setRequestTimeout(1500);
}
public CompletableFuture<String> formatPlaceholders(@NotNull String input, @NotNull Player player) {

View File

@ -0,0 +1,118 @@
/*
* 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.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;
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.UpsertPlayerInfo;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.channel.Channel;
import lombok.Getter;
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();
}
private void loadPlayers() {
plugin.getServer().getAllPlayers().forEach(this::injectPlayer);
}
private void loadListeners() {
plugin.getServer().getEventManager().register(plugin, PostLoginEvent.class,
(AwaitingEventExecutor<PostLoginEvent>) postLoginEvent -> EventTask.withContinuation(continuation -> {
injectPlayer(postLoginEvent.getPlayer());
continuation.resume();
}));
plugin.getServer().getEventManager().register(plugin, DisconnectEvent.class,
(AwaitingEventExecutor<DisconnectEvent>) disconnectEvent ->
disconnectEvent.getLoginStatus() == DisconnectEvent.LoginStatus.CONFLICTING_LOGIN
? null
: EventTask.async(() -> removePlayer(disconnectEvent.getPlayer())));
}
public void injectPlayer(@NotNull Player player) {
final PlayerChannelHandler handler = new PlayerChannelHandler(plugin, player);
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
removePlayer(player);
connectedPlayer.getConnection()
.getChannel()
.pipeline()
.addBefore(Connections.HANDLER, KEY, handler);
}
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);
}
}
protected void handleEntry(@NotNull UpsertPlayerInfo 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)
.toList();
if (toUpdate.isEmpty()) {
return;
}
toUpdate.forEach(tabPlayer -> packet.getEntries().stream()
.filter(entry -> entry.getProfile() != null)
.filter(entry -> entry.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId()))
.findFirst()
.ifPresent(entry -> entry.setDisplayName(
new ComponentHolder(player.getProtocolVersion(), tabPlayer.getLastDisplayname()))));
}
}

View File

@ -0,0 +1,70 @@
/*
* 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.packet;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import lombok.RequiredArgsConstructor;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@RequiredArgsConstructor
public class PlayerChannelHandler extends ChannelDuplexHandler {
private final Velocitab plugin;
private final Player player;
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (!(msg instanceof final UpsertPlayerInfo minecraftPacket)) {
super.write(ctx, msg, promise);
return;
}
if (plugin.getSettings().isRemoveSpectatorEffect() && minecraftPacket.containsAction(UpsertPlayerInfo.Action.UPDATE_GAME_MODE)) {
forceGameMode(minecraftPacket.getEntries());
}
if (!minecraftPacket.containsAction(UpsertPlayerInfo.Action.ADD_PLAYER) && !minecraftPacket.containsAction(UpsertPlayerInfo.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;
}
plugin.getPacketEventManager().handleEntry(minecraftPacket, player);
super.write(ctx, msg, promise);
}
private void forceGameMode(@NotNull List<UpsertPlayerInfo.Entry> entries) {
entries.stream()
.filter(entry -> entry.getProfileId() != null && entry.getGameMode() == 3 && !entry.getProfileId().equals(player.getUniqueId()))
.forEach(entry -> entry.setGameMode(0));
}
}

View File

@ -20,9 +20,12 @@
package net.william278.velocitab.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
@ -40,7 +43,8 @@ public class Protocol765Adapter extends Protocol404Adapter {
}
protected void writeComponent(ByteBuf buf, Component component) {
new ComponentHolder(ProtocolVersion.MINECRAFT_1_20_3, component).write(buf);
final BinaryTag tag = ComponentHolder.serialize(GsonComponentSerializer.gson().serializeToTree(component));
ProtocolUtils.writeBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, tag);
}
}

View File

@ -19,6 +19,8 @@
package net.william278.velocitab.packet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
@ -28,11 +30,11 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.Nametag;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static com.velocitypowered.api.network.ProtocolVersion.*;
@ -42,13 +44,13 @@ public class ScoreboardManager {
private final Velocitab plugin;
private final Set<TeamsPacketAdapter> versions;
private final Map<UUID, String> createdTeams;
private final Map<String, TabPlayer.Nametag> nametags;
private final Map<String, Nametag> nametags;
public ScoreboardManager(@NotNull Velocitab velocitab) {
this.plugin = velocitab;
this.createdTeams = new ConcurrentHashMap<>();
this.nametags = new ConcurrentHashMap<>();
this.versions = new HashSet<>();
this.createdTeams = Maps.newConcurrentMap();
this.nametags = Maps.newConcurrentMap();
this.versions = Sets.newHashSet();
this.registerVersions();
}
@ -78,64 +80,37 @@ public class ScoreboardManager {
public void resetCache(@NotNull Player player) {
final String team = createdTeams.remove(player.getUniqueId());
if (team != null) {
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), player);
final TabPlayer tabPlayer = plugin.getTabList().getTabPlayer(player).orElseThrow();
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), tabPlayer);
}
}
public void vanishPlayer(@NotNull Player player) {
public void vanishPlayer(@NotNull TabPlayer tabPlayer) {
this.handleVanish(tabPlayer, true);
}
public void unVanishPlayer(@NotNull TabPlayer tabPlayer) {
this.handleVanish(tabPlayer, false);
}
private void handleVanish(@NotNull TabPlayer tabPlayer, boolean vanish) {
if (!plugin.getSettings().isSortPlayers()) {
return;
}
final Optional<ServerConnection> optionalServerConnection = player.getCurrentServer();
if (optionalServerConnection.isEmpty()) {
return;
}
final RegisteredServer serverInfo = optionalServerConnection.get().getServer();
final List<RegisteredServer> siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName());
final Player player = tabPlayer.getPlayer();
final String teamName = createdTeams.get(player.getUniqueId());
if (teamName == null) {
return;
}
final List<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
final UpdateTeamsPacket packet = UpdateTeamsPacket.removeTeam(plugin, teamName);
siblings.forEach(server -> server.getPlayersConnected().forEach(connected -> {
final boolean canSee = plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername());
if (canSee) {
return;
}
dispatchPacket(packet, connected);
}));
}
public void unVanishPlayer(@NotNull Player player) {
if (!plugin.getSettings().isSortPlayers()) {
return;
}
final Optional<ServerConnection> optionalServerConnection = player.getCurrentServer();
if (optionalServerConnection.isEmpty()) {
return;
}
final RegisteredServer serverInfo = optionalServerConnection.get().getServer();
final List<RegisteredServer> siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName());
final String teamName = createdTeams.get(player.getUniqueId());
if (teamName == null) {
return;
}
final Optional<TabPlayer.Nametag> cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null));
final Optional<Nametag> cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null));
cachedTag.ifPresent(nametag -> {
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(
plugin, createdTeams.get(player.getUniqueId()),
nametag, player.getUsername()
);
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)));
});
}
@ -143,38 +118,38 @@ public class ScoreboardManager {
/**
* Updates the role of the player in the scoreboard.
*
* @param player The player whose role will be updated. Must not be null.
* @param role The new role of the player. Must not be null.
* @param force Whether to force the update even if the player's nametag is the same.
* @param tabPlayer The TabPlayer object representing the player whose role will be updated.
* @param role The new role of the player. Must not be null.
* @param force Whether to force the update even if the player's nametag is the same.
*/
public void updateRole(@NotNull Player player, @NotNull String role, boolean force) {
public void updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
final Player player = tabPlayer.getPlayer();
if (!player.isActive()) {
plugin.getTabList().removeOfflinePlayer(player);
return;
}
final String name = player.getUsername();
final TabPlayer tabPlayer = plugin.getTabList().getTabPlayer(player).orElseThrow();
tabPlayer.getNametag(plugin).thenAccept(newTag -> {
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
if (createdTeams.containsKey(player.getUniqueId())) {
dispatchGroupPacket(
UpdateTeamsPacket.removeTeam(plugin, createdTeams.get(player.getUniqueId())),
player
tabPlayer
);
}
createdTeams.put(player.getUniqueId(), role);
this.nametags.put(role, newTag);
dispatchGroupPacket(
UpdateTeamsPacket.create(plugin, role, newTag, name),
player
UpdateTeamsPacket.create(plugin, tabPlayer, role, newTag, name),
tabPlayer
);
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) {
this.nametags.put(role, newTag);
dispatchGroupPacket(
UpdateTeamsPacket.changeNametag(plugin, role, newTag),
player
UpdateTeamsPacket.changeNametag(plugin, tabPlayer, role, newTag),
tabPlayer
);
}
}).exceptionally(e -> {
@ -184,18 +159,13 @@ public class ScoreboardManager {
}
public void resendAllTeams(@NotNull Player player) {
public void resendAllTeams(@NotNull TabPlayer tabPlayer) {
if (!plugin.getSettings().isSendScoreboardPackets()) {
return;
}
final Optional<ServerConnection> optionalServerConnection = player.getCurrentServer();
if (optionalServerConnection.isEmpty()) {
return;
}
final RegisteredServer serverInfo = optionalServerConnection.get().getServer();
final List<RegisteredServer> siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName());
final Player player = tabPlayer.getPlayer();
final List<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
final List<Player> players = siblings.stream()
.map(RegisteredServer::getPlayersConnected)
.flatMap(Collection::stream)
@ -223,11 +193,10 @@ public class ScoreboardManager {
roles.add(role);
// Send packet
final TabPlayer.Nametag tag = nametags.get(role);
final Nametag tag = nametags.get(role);
if (tag != null) {
final TabPlayer.Nametag nametag = nametags.get(role);
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(
plugin, role, nametag, p.getUsername()
plugin, tabPlayer, role, tag, p.getUsername()
);
dispatchPacket(packet, player);
}
@ -248,14 +217,14 @@ public class ScoreboardManager {
}
}
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Player player) {
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull TabPlayer tabPlayer) {
final Player player = tabPlayer.getPlayer();
final Optional<ServerConnection> optionalServerConnection = player.getCurrentServer();
if (optionalServerConnection.isEmpty()) {
return;
}
final RegisteredServer serverInfo = optionalServerConnection.get().getServer();
final List<RegisteredServer> siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName());
final List<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
siblings.forEach(server -> server.getPlayersConnected().forEach(connected -> {
try {
final boolean canSee = plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername());
@ -324,10 +293,10 @@ public class ScoreboardManager {
dispatchPacket(removeTeam, player);
if (canSee) {
final TabPlayer.Nametag tag = nametags.get(team);
final Nametag tag = nametags.get(team);
if (tag != null) {
final UpdateTeamsPacket addTeam = UpdateTeamsPacket.create(
plugin, team, tag, target.getPlayer().getUsername()
plugin, tabPlayer, team, tag, target.getPlayer().getUsername()
);
dispatchPacket(addTeam, player);
}

View File

@ -30,6 +30,7 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.Nametag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -64,8 +65,9 @@ public class UpdateTeamsPacket implements MinecraftPacket {
}
@NotNull
protected static UpdateTeamsPacket create(@NotNull Velocitab plugin, @NotNull String teamName,
@NotNull TabPlayer.Nametag nametag,
protected static UpdateTeamsPacket create(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName,
@NotNull Nametag nametag,
@NotNull String... teamMembers) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
@ -74,23 +76,24 @@ public class UpdateTeamsPacket implements MinecraftPacket {
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
.collisionRule(CollisionRule.ALWAYS)
.color(getLastColor(nametag.getPrefix()))
.prefix(nametag.getPrefixComponent(plugin))
.suffix(nametag.getSuffixComponent(plugin))
.color(getLastColor(nametag.prefix(), plugin))
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer))
.entities(Arrays.asList(teamMembers));
}
private static boolean isNametagPresent(@NotNull TabPlayer.Nametag nametag, @NotNull Velocitab plugin) {
private static boolean isNametagPresent(@NotNull Nametag nametag, @NotNull Velocitab plugin) {
if (!plugin.getSettings().isRemoveNametags()) {
return true;
}
return !nametag.getPrefix().isEmpty() || !nametag.getSuffix().isEmpty();
return !nametag.prefix().isEmpty() || !nametag.suffix().isEmpty();
}
@NotNull
protected static UpdateTeamsPacket changeNametag(@NotNull Velocitab plugin, @NotNull String teamName,
@NotNull TabPlayer.Nametag nametag) {
protected static UpdateTeamsPacket changeNametag(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName,
@NotNull Nametag nametag) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
.mode(UpdateMode.UPDATE_INFO)
@ -98,9 +101,9 @@ public class UpdateTeamsPacket implements MinecraftPacket {
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
.collisionRule(CollisionRule.ALWAYS)
.color(getLastColor(nametag.getPrefix()))
.prefix(nametag.getPrefixComponent(plugin))
.suffix(nametag.getSuffixComponent(plugin));
.color(getLastColor(nametag.prefix(), plugin))
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer));
}
@NotNull
@ -128,7 +131,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
.mode(UpdateMode.REMOVE_TEAM);
}
public static int getLastColor(@Nullable String text) {
public static int getLastColor(@Nullable String text, @NotNull Velocitab plugin) {
if (text == null) {
return 15;
}
@ -137,8 +140,8 @@ public class UpdateTeamsPacket implements MinecraftPacket {
text = text + "z";
//serialize & deserialize to downsample rgb to legacy
Component legacyComponent = LegacyComponentSerializer.legacyAmpersand().deserialize(text);
text = LegacyComponentSerializer.legacyAmpersand().serialize(legacyComponent);
Component component = plugin.getFormatter().emptyFormat(text);
text = LegacyComponentSerializer.legacyAmpersand().serialize(component);
int lastFormatIndex = text.lastIndexOf("&");
if (lastFormatIndex == -1 || lastFormatIndex == text.length() - 1) {

View File

@ -73,6 +73,4 @@ public class Role implements Comparable<Role> {
return Integer.toString(weight);
}
}

View File

@ -20,12 +20,13 @@
package net.william278.velocitab.player;
import com.velocitypowered.api.proxy.Player;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.tab.Nametag;
import net.william278.velocitab.tab.PlayerTabList;
import org.apache.commons.lang3.ObjectUtils;
import org.jetbrains.annotations.NotNull;
@ -33,38 +34,33 @@ import org.jetbrains.annotations.Nullable;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
@Getter
public final class TabPlayer implements Comparable<TabPlayer> {
private final Player player;
@Setter
private Role role;
@Getter
private int headerIndex = 0;
@Getter
private int footerIndex = 0;
@Getter
private Component lastDisplayname;
private String teamName;
@Nullable
@Setter
private String customName;
@Nullable
@Setter
private String lastServer;
@NotNull
@Setter
private Group group;
@Setter
private boolean loaded;
public TabPlayer(@NotNull Player player, @NotNull Role role) {
public TabPlayer(@NotNull Player player, @NotNull Role role, @NotNull Group group) {
this.player = player;
this.role = role;
}
@NotNull
public Player getPlayer() {
return player;
}
@NotNull
public Role getRole() {
return role;
this.group = group;
}
@NotNull
@ -85,17 +81,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
.orElse(ObjectUtils.firstNonNull(lastServer, "unknown"));
}
/**
* Get the TAB server group this player is connected to
*
* @param plugin instance of the {@link Velocitab} plugin
* @return the name of the server group the player is on
*/
@NotNull
public String getServerGroup(@NotNull Velocitab plugin) {
return plugin.getSettings().getServerGroup(this.getServerName());
}
/**
* Get the ordinal position of the TAB server group this player is connected to
*
@ -103,7 +88,7 @@ public final class TabPlayer implements Comparable<TabPlayer> {
* @return The ordinal position of the server group
*/
public int getServerGroupPosition(@NotNull Velocitab plugin) {
return plugin.getSettings().getServerGroupPosition(getServerGroup(plugin));
return plugin.getTabGroups().getPosition(group);
}
/**
@ -120,17 +105,14 @@ public final class TabPlayer implements Comparable<TabPlayer> {
@NotNull
public CompletableFuture<Component> getDisplayName(@NotNull Velocitab plugin) {
final String serverGroup = plugin.getSettings().getServerGroup(getServerName());
return Placeholder.replace(plugin.getSettings().getFormat(serverGroup), plugin, this)
return Placeholder.replace(group.format(), plugin, this)
.thenApply(formatted -> plugin.getFormatter().format(formatted, this, plugin))
.thenApply(c -> this.lastDisplayname = c);
}
@NotNull
public CompletableFuture<Nametag> getNametag(@NotNull Velocitab plugin) {
final String serverGroup = plugin.getSettings().getServerGroup(getServerName());
return Placeholder.replace(plugin.getSettings().getNametag(serverGroup), plugin, this)
.thenApply(n -> new Nametag(n, player));
return Placeholder.replace(group.nametag(), plugin, this);
}
@NotNull
@ -143,22 +125,26 @@ public final class TabPlayer implements Comparable<TabPlayer> {
return Optional.ofNullable(teamName);
}
public void sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
tabList.getHeader(this).thenAccept(header -> tabList.getFooter(this)
public CompletableFuture<Void> sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
return tabList.getHeader(this).thenCompose(header -> tabList.getFooter(this)
.thenAccept(footer -> player.sendPlayerListHeaderAndFooter(header, footer)));
}
public void incrementHeaderIndex(@NotNull Velocitab plugin) {
public void incrementIndexes() {
incrementHeaderIndex();
incrementFooterIndex();
}
public void incrementHeaderIndex() {
headerIndex++;
if (headerIndex >= plugin.getSettings().getHeaderListSize(getServerGroup(plugin))) {
if (headerIndex >= group.headers().size()) {
headerIndex = 0;
}
}
public void incrementFooterIndex(@NotNull Velocitab plugin) {
public void incrementFooterIndex() {
footerIndex++;
if (footerIndex >= plugin.getSettings().getFooterListSize(getServerGroup(plugin))) {
if (footerIndex >= group.footers().size()) {
footerIndex = 0;
}
}
@ -172,15 +158,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
return Optional.ofNullable(customName);
}
/**
* Sets the custom name of the TabPlayer.
*
* @param customName The custom name to set
*/
public void setCustomName(@Nullable String customName) {
this.customName = customName;
}
@Override
public int compareTo(@NotNull TabPlayer o) {
final int roleDifference = role.compareTo(o.role);
@ -195,40 +172,18 @@ public final class TabPlayer implements Comparable<TabPlayer> {
return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId());
}
/**
* Represents a nametag to be displayed above a player, with prefix and suffix
*/
@Getter
@AllArgsConstructor
public class Nametag {
@NotNull
private final String prefix;
@NotNull
private final String suffix;
private Nametag(@NotNull String tag, @NotNull Player player) {
final String[] split = tag.split(Pattern.quote(player.getUsername()), 2);
this.prefix = split[0];
this.suffix = split.length > 1 ? split[1] : "";
}
@NotNull
public Component getPrefixComponent(@NotNull Velocitab plugin) {
return plugin.getFormatter().format(prefix, TabPlayer.this, plugin);
}
@NotNull
public Component getSuffixComponent(@NotNull Velocitab plugin) {
return plugin.getFormatter().format(suffix, TabPlayer.this, plugin);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Nametag other)) {
return false;
}
return (prefix.equals(other.prefix)) && (suffix.equals(other.suffix));
}
@Override
public String toString() {
return "TabPlayer{" +
"player=" + player +
", role=" + role +
", headerIndex=" + headerIndex +
", footerIndex=" + footerIndex +
", lastDisplayname=" + lastDisplayname +
", teamName='" + teamName + '\'' +
", lastServer='" + lastServer + '\'' +
", group=" + group.name() +
", loaded=" + loaded +
'}';
}
}

View File

@ -0,0 +1,106 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.providers;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.hook.Hook;
import net.william278.velocitab.hook.LuckPermsHook;
import net.william278.velocitab.hook.MiniPlaceholdersHook;
import net.william278.velocitab.hook.PAPIProxyBridgeHook;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public interface HookProvider {
/**
* Retrieves the list of hooks associated with the HookProvider.
*
* @return The list of hooks associated with the HookProvider.
*/
List<Hook> getHooks();
/**
* Sets the list of hooks associated with the HookProvider.
*
* @param hooks The list of hooks to set.
*/
void setHooks(List<Hook> hooks);
/**
* Retrieves the instance of the Velocitab plugin.
*
* @return The instance of the Velocitab plugin.
*/
Velocitab getPlugin();
/**
* Loads the hooks associated with the HookProvider.
*/
default void loadHooks() {
List<Hook> hooks = new ArrayList<>();
Hook.AVAILABLE.forEach(availableHook -> availableHook.apply(getPlugin()).ifPresent(hooks::add));
setHooks(hooks);
}
/**
* Retrieves a hook of the specified type from the list of hooks associated with the HookProvider.
*
* @param hookType The class object representing the type of the hook to retrieve.
* @param <H> The type of the hook to retrieve.
* @return An Optional containing the hook of the specified type, or an empty Optional if the hook is not found.
*/
private <H extends Hook> Optional<H> getHook(@NotNull Class<H> hookType) {
return getHooks().stream()
.filter(hook -> hook.getClass().equals(hookType))
.map(hookType::cast)
.findFirst();
}
/**
* Retrieves the LuckPermsHook from the list of hooks associated with the HookProvider.
*
* @return An Optional containing the LuckPermsHook, or an empty Optional if it is not found.
*/
default Optional<LuckPermsHook> getLuckPermsHook() {
return getHook(LuckPermsHook.class);
}
/**
* Retrieves the PAPIProxyBridgeHook from the list of hooks associated with the HookProvider.
*
* @return An Optional containing the PAPIProxyBridgeHook, or an empty Optional if it is not found.
*/
default Optional<PAPIProxyBridgeHook> getPAPIProxyBridgeHook() {
return getHook(PAPIProxyBridgeHook.class);
}
/**
* Retrieves the MiniPlaceholdersHook from the list of hooks associated with the HookProvider.
*
* @return An Optional containing the MiniPlaceholdersHook, or an empty Optional if it is not found.
*/
default Optional<MiniPlaceholdersHook> getMiniPlaceholdersHook() {
return getHook(MiniPlaceholdersHook.class);
}
}

View File

@ -0,0 +1,69 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.providers;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.event.Level;
public interface LoggerProvider {
/**
* Retrieves the logger for the corresponding class.
*
* @return the logger for the class
*/
Logger getLogger();
/**
* Logs a message with the specified log level.
*
* @param level the log level
* @param message the log message
* @param exceptions the exceptions associated with the log message (optional)
*/
default void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... exceptions) {
switch (level) {
case ERROR -> {
if (exceptions.length > 0) {
getLogger().error(message, exceptions[0]);
} else {
getLogger().error(message);
}
}
case WARN -> {
if (exceptions.length > 0) {
getLogger().warn(message, exceptions[0]);
} else {
getLogger().warn(message);
}
}
case INFO -> getLogger().info(message);
}
}
/**
* Logs a message with the specified log level.
*/
default void log(@NotNull String message) {
this.log(Level.INFO, message);
}
}

View File

@ -0,0 +1,61 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.providers;
import net.william278.velocitab.Velocitab;
import org.bstats.charts.SimplePie;
import org.bstats.velocity.Metrics;
public interface MetricProvider {
int METRICS_ID = 18247;
/**
* Retrieves the Metrics Factory used by the MetricProvider.
*
* @return The Metrics Factory used by the MetricProvider.
*/
Metrics.Factory getMetricsFactory();
/**
* Retrieves the Velocitab plugin instance.
* @return
*/
Velocitab getPlugin();
/**
* Registers metrics for the Velocitab plugin using the Metrics library.
* This method adds custom charts to the metrics object, which include information such as:
* - Whether player sorting is enabled or disabled
* - The type of formatter being used
* - Whether LuckPerms hook is present
* - Whether PAPIProxyBridge hook is present
* - Whether MiniPlaceholders hook is present
*/
default void registerMetrics() {
final Metrics metrics = getMetricsFactory().make(this, METRICS_ID);
metrics.addCustomChart(new SimplePie("sort_players", () -> getPlugin().getSettings().isSortPlayers() ? "Enabled" : "Disabled"));
metrics.addCustomChart(new SimplePie("formatter_type", () -> getPlugin().getFormatter().getName()));
metrics.addCustomChart(new SimplePie("using_luckperms", () -> getPlugin().getLuckPermsHook().isPresent() ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("using_papiproxybridge", () -> getPlugin().getPAPIProxyBridgeHook().isPresent() ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("using_miniplaceholders", () -> getPlugin().getMiniPlaceholdersHook().isPresent() ? "Yes" : "No"));
}
}

View File

@ -0,0 +1,116 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.providers;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.sorting.SortingManager;
import net.william278.velocitab.tab.PlayerTabList;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public interface ScoreboardProvider {
/**
* Retrieves the Velocitab plugin instance.
*
* @return The Velocitab plugin instance.
*/
Velocitab getPlugin();
/**
* Retrieves the optional scoreboard manager.
*
* @return An {@code Optional} object that may contain a {@code ScoreboardManager} instance.
*/
Optional<ScoreboardManager> getScoreboardManager();
/**
* Sets the scoreboard manager.
*
* @param scoreboardManager The scoreboard manager to be set.
*/
void setScoreboardManager(ScoreboardManager scoreboardManager);
/**
* Retrieves the tab list for the player.
*
* @return The PlayerTabList object representing the tab list for the player.
*/
PlayerTabList getTabList();
/**
* Sets the tab list for the player.
*
* @param tabList The PlayerTabList object representing the tab list to be set for the player.
*/
void setTabList(PlayerTabList tabList);
/**
* Returns the SortingManager instance.
*
* @return The SortingManager instance.
*/
SortingManager getSortingManager();
/**
* Sets the sorting manager for the ScoreboardProvider.
*
* @param sortingManager The sorting manager to be set.
*/
void setSortingManager(SortingManager sortingManager);
/**
* Prepares the scoreboard by initializing the necessary components.
* This method is responsible for setting up the scoreboard manager, player tab list,
* scheduler tasks, and sorting manager.
*
*/
default void prepareScoreboard() {
if (getPlugin().getSettings().isSendScoreboardPackets()) {
ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin());
setScoreboardManager(scoreboardManager);
scoreboardManager.registerPacket();
}
final PlayerTabList tabList = new PlayerTabList(getPlugin());
setTabList(tabList);
getPlugin().getServer().getEventManager().register(this, tabList);
getPlugin().getServer().getScheduler().buildTask(this, tabList::load).delay(1, TimeUnit.SECONDS).schedule();
final SortingManager sortingManager = new SortingManager(getPlugin());
setSortingManager(sortingManager);
}
/**
* Disables the ScoreboardManager and closes the tab list for the player.
*/
default void disableScoreboardManager() {
if (getScoreboardManager().isPresent() && getPlugin().getSettings().isSendScoreboardPackets()) {
getScoreboardManager().get().close();
getScoreboardManager().get().unregisterPacket();
}
getTabList().close();
}
}

View File

@ -22,6 +22,7 @@ package net.william278.velocitab.sorting;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.ArrayList;
@ -35,22 +36,24 @@ public class SortingManager {
private final Velocitab plugin;
private static final String DELIMITER = ":::";
public SortingManager(Velocitab plugin) {
public SortingManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
}
public CompletableFuture<String> getTeamName(TabPlayer player) {
@NotNull
public CompletableFuture<String> getTeamName(@NotNull TabPlayer player) {
if (!plugin.getSettings().isSortPlayers()) {
return CompletableFuture.completedFuture("");
}
return Placeholder.replace(String.join(DELIMITER, plugin.getSettings().getSortingElements()), plugin, player)
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 -> handleList(player, v));
}
private String handleList(TabPlayer player, List<String> values) {
@NotNull
private String handleList(@NotNull TabPlayer player, @NotNull List<String> values) {
String result = String.join("", values);
if (result.length() > 12) {
@ -63,7 +66,8 @@ public class SortingManager {
return result;
}
private String adaptValue(String value) {
@NotNull
private String adaptValue(@NotNull String value) {
if (value.isEmpty()) {
return "";
}
@ -81,6 +85,7 @@ public class SortingManager {
return value;
}
@NotNull
public String compressNumber(double number) {
int wholePart = (int) number;
final char decimalChar = (char) ((number - wholePart) * Character.MAX_VALUE);

View File

@ -0,0 +1,50 @@
/*
* 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 net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
/**
* Represents a nametag to be displayed above a player, with prefix and suffix
*/
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);
}
@NotNull
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
return plugin.getFormatter().format(suffix, tabPlayer, plugin);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Nametag other)) {
return false;
}
return (prefix.equals(other.prefix)) && (suffix.equals(other.suffix));
}
}

View File

@ -19,12 +19,8 @@
package net.william278.velocitab.tab;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.player.KickedFromServerEvent;
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.player.TabList;
@ -32,41 +28,46 @@ import com.velocitypowered.api.proxy.player.TabListEntry;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.scheduler.ScheduledTask;
import lombok.AccessLevel;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.api.PlayerAddedToTabEvent;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* The main class for tracking the server TAB list
*/
public class PlayerTabList {
private final Velocitab plugin;
private final ConcurrentHashMap<UUID, TabPlayer> players;
private final ConcurrentLinkedQueue<String> fallbackServers;
private final List<UUID> justKicked;
private ScheduledTask updateTask;
@Getter
private final VanishTabList vanishTabList;
@Getter(value = AccessLevel.PUBLIC)
private final Map<UUID, TabPlayer> players;
private final Map<Group, ScheduledTask> placeholderTasks;
private final Map<Group, ScheduledTask> headerFooterTasks;
public PlayerTabList(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.players = new ConcurrentHashMap<>();
this.fallbackServers = new ConcurrentLinkedQueue<>();
this.justKicked = new CopyOnWriteArrayList<>();
this.vanishTabList = new VanishTabList(plugin, this);
this.players = Maps.newConcurrentMap();
this.placeholderTasks = Maps.newConcurrentMap();
this.headerFooterTasks = Maps.newConcurrentMap();
this.reloadUpdate();
this.registerListener();
}
// If the update time is set to 0 do not schedule the updater
if (plugin.getSettings().getUpdateRate() > 0) {
this.updatePeriodically(plugin.getSettings().getUpdateRate());
}
private void registerListener() {
plugin.getServer().getEventManager().register(plugin, new TabListListener(plugin, this));
}
/**
@ -79,6 +80,15 @@ public class PlayerTabList {
return Optional.ofNullable(players.get(player.getUniqueId()));
}
/**
* Retrieves a TabPlayer object corresponding to the given UUID.
*
* @param uuid The UUID of the player for which to retrieve the corresponding TabPlayer.
* @return An Optional object containing the TabPlayer if found, or an empty Optional if not found.
*/
public Optional<TabPlayer> getTabPlayer(@NotNull UUID uuid) {
return Optional.ofNullable(players.get(uuid));
}
/**
* Loads the tab list for all players connected to the server.
@ -89,12 +99,15 @@ public class PlayerTabList {
final Optional<ServerConnection> server = p.getCurrentServer();
if (server.isEmpty()) return;
final List<RegisteredServer> serversInGroup = new ArrayList<>(getGroupServers(server.get().getServerInfo().getName()));
if (serversInGroup.isEmpty()) return;
final String serverName = server.get().getServerInfo().getName();
final Group group = getGroup(serverName);
final boolean isDefault = !group.servers().contains(serverName);
serversInGroup.remove(server.get().getServer());
if (isDefault && !plugin.getSettings().isFallbackEnabled()) {
return;
}
joinPlayer(p, serversInGroup.stream().map(s -> s.getServerInfo().getName()).toList());
joinPlayer(p, group);
});
}
@ -103,118 +116,113 @@ public class PlayerTabList {
* Removes the player's entry from the tab list of all other players on the same group servers.
*/
public void close() {
placeholderTasks.values().forEach(ScheduledTask::cancel);
placeholderTasks.clear();
headerFooterTasks.values().forEach(ScheduledTask::cancel);
headerFooterTasks.clear();
plugin.getServer().getAllPlayers().forEach(p -> {
final Optional<ServerConnection> server = p.getCurrentServer();
if (server.isEmpty()) return;
final List<RegisteredServer> serversInGroup = new ArrayList<>(getGroupServers(server.get().getServerInfo().getName()));
if (serversInGroup.isEmpty()) return;
final TabPlayer tabPlayer = players.get(p.getUniqueId());
if (tabPlayer == null) {
return;
}
final List<RegisteredServer> serversInGroup = new ArrayList<>(tabPlayer.getGroup().registeredServers(plugin));
if (serversInGroup.isEmpty()) {
return;
}
serversInGroup.remove(server.get().getServer());
serversInGroup.forEach(s -> s.getPlayersConnected().forEach(t -> t.getTabList().removeEntry(p.getUniqueId())));
});
}
@Subscribe
public void onKick(KickedFromServerEvent event) {
event.getPlayer().getTabList().clearAll();
event.getPlayer().getTabList().clearHeaderAndFooter();
justKicked.add(event.getPlayer().getUniqueId());
}
@SuppressWarnings("UnstableApiUsage")
@Subscribe
public void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
final Player joined = event.getPlayer();
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined));
final RegisteredServer previousServer = event.getPreviousServer();
// Get the servers in the group from the joined server name
// If the server is not in a group, use fallback
final Optional<List<String>> serversInGroup = getGroupNames(joined.getCurrentServer()
.map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("?"));
// 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 (serversInGroup.isEmpty() &&
(previousServer != null && !this.fallbackServers.contains(previousServer.getServerInfo().getName()))) {
event.getPlayer().sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
players.remove(event.getPlayer().getUniqueId());
return;
}
joinPlayer(joined, serversInGroup.orElseGet(ArrayList::new));
}
private void joinPlayer(@NotNull Player joined, @NotNull List<String> serversInGroup) {
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));
final TabPlayer tabPlayer = getTabPlayer(joined).orElseGet(() -> createTabPlayer(joined, group));
tabPlayer.setGroup(group);
players.putIfAbsent(joined.getUniqueId(), tabPlayer);
int delay = 500;
if (justKicked.contains(joined.getUniqueId())) {
delay = 1000;
justKicked.remove(joined.getUniqueId());
}
//store last server so it's possible to have the last server on disconnect
//store last server, so it's possible to have the last server on disconnect
tabPlayer.setLastServer(joined.getCurrentServer().map(ServerConnection::getServerInfo).map(ServerInfo::getName).orElse(""));
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
// Update lists
plugin.getServer().getScheduler()
.buildTask(plugin, () -> {
final TabList tabList = joined.getTabList();
for (final TabPlayer player : players.values()) {
// Skip players on other servers if the setting is enabled
if (plugin.getSettings().isOnlyListPlayersInSameGroup()
&& !serversInGroup.contains(player.getServerName())) {
continue;
}
// check if current player can see the joined player
if (!isVanished || plugin.getVanishManager().canSee(player.getPlayer().getUsername(), joined.getUsername())) {
addPlayerToTabList(player, tabPlayer);
} 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)
);
}
final boolean isDefault = group.isDefault();
final boolean isFallback = isDefault && plugin.getSettings().isFallbackEnabled();
player.sendHeaderAndFooter(this);
}
tabPlayer.getDisplayName(plugin).thenAccept(d -> {
plugin.getScoreboardManager().ifPresent(s -> {
s.resendAllTeams(joined);
tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(joined, t, false));
joined.getTabList().getEntry(joined.getUniqueId())
.ifPresentOrElse(e -> e.setDisplayName(d),
() -> joined.getTabList().addEntry(createEntry(tabPlayer, joined.getTabList(), d)));
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;
});
// Fire event without listening for result
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, tabPlayer.getServerGroup(plugin), serversInGroup));
})
.delay(delay, TimeUnit.MILLISECONDS)
.schedule();
// Update lists
plugin.getServer().getScheduler()
.buildTask(plugin, () -> {
final TabList tabList = joined.getTabList();
for (final TabPlayer player : players.values()) {
// Skip players on other servers if the setting is enabled
if (plugin.getSettings().isOnlyListPlayersInSameGroup()
&& !isFallback &&
!group.servers().contains(player.getServerName())
) {
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));
});
// Fire event without listening for result
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group));
})
.delay(300, TimeUnit.MILLISECONDS)
.schedule();
}).exceptionally(throwable -> {
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
joined.getUsername(), joined.getUniqueId()), throwable);
return null;
});
}
@NotNull
private CompletableFuture<TabListEntry> createEntry(@NotNull TabPlayer player, @NotNull TabList tabList) {
CompletableFuture<TabListEntry> createEntry(@NotNull TabPlayer player, @NotNull TabList tabList) {
return player.getDisplayName(plugin).thenApply(name -> TabListEntry.builder()
.profile(player.getPlayer().getGameProfile())
.displayName(name)
@ -223,7 +231,7 @@ public class PlayerTabList {
.build());
}
private TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull Component displayName) {
protected TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull Component displayName) {
return TabListEntry.builder()
.profile(player.getPlayer().getGameProfile())
.displayName(displayName)
@ -232,57 +240,34 @@ public class PlayerTabList {
.build();
}
private void addPlayerToTabList(@NotNull TabPlayer player, @NotNull TabPlayer newPlayer) {
private void addPlayerToTabList(@NotNull TabPlayer player, @NotNull TabPlayer newPlayer, @NotNull Component displayName) {
if (newPlayer.getPlayer().getUniqueId().equals(player.getPlayer().getUniqueId())) {
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()
.ifPresentOrElse(
entry -> newPlayer.getDisplayName(plugin).thenAccept(entry::setDisplayName),
() -> createEntry(newPlayer, player.getPlayer().getTabList())
.thenAccept(entry -> player.getPlayer().getTabList().addEntry(entry))
entry -> entry.setDisplayName(displayName),
() -> player.getPlayer().getTabList()
.addEntry(createEntry(newPlayer, player.getPlayer().getTabList(), displayName))
);
}
@Subscribe(order = PostOrder.LAST)
public void onPlayerQuit(@NotNull DisconnectEvent event) {
if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) {
return;
}
// Remove the player from the tracking list, Print warning if player was not removed
final UUID uuid = event.getPlayer().getUniqueId();
final TabPlayer tabPlayer = players.get(uuid);
if (tabPlayer == null) {
plugin.log(String.format("Failed to remove disconnecting player %s (UUID: %s)",
event.getPlayer().getUsername(), uuid));
}
// Remove the player from the tab list of all other players
plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(uuid));
// Update the tab list of all players
plugin.getServer().getScheduler()
.buildTask(plugin, () -> players.values().forEach(player -> {
player.getPlayer().getTabList().removeEntry(uuid);
player.sendHeaderAndFooter(this);
}))
.delay(500, TimeUnit.MILLISECONDS)
.schedule();
// Delete player team
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(event.getPlayer()));
//remove player from tab list cache
players.remove(uuid);
}
@NotNull
public TabPlayer createTabPlayer(@NotNull Player player) {
public TabPlayer createTabPlayer(@NotNull Player player, @NotNull Group group) {
return new TabPlayer(player,
plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE)
plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE),
group
);
}
@ -298,7 +283,7 @@ public class PlayerTabList {
return;
}
plugin.getScoreboardManager().ifPresent(manager -> manager.updateRole(
tabPlayer.getPlayer(), teamName, force
tabPlayer, teamName, force
));
});
}
@ -331,8 +316,7 @@ public class PlayerTabList {
// Get the component for the TAB list header
public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) {
final String header = plugin.getSettings().getHeader(player.getServerGroup(plugin), player.getHeaderIndex());
player.incrementHeaderIndex(plugin);
final String header = player.getGroup().getHeader(player.getHeaderIndex());
return Placeholder.replace(header, plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
@ -340,104 +324,116 @@ public class PlayerTabList {
// Get the component for the TAB list footer
public CompletableFuture<Component> getFooter(@NotNull TabPlayer player) {
final String footer = plugin.getSettings().getFooter(player.getServerGroup(plugin), player.getFooterIndex());
player.incrementFooterIndex(plugin);
final String footer = player.getGroup().getFooter(player.getFooterIndex());
return Placeholder.replace(footer, plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
}
// Update the tab list periodically
private void updatePeriodically(int updateRate) {
updateTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> {
if (players.isEmpty()) {
return;
private void updatePeriodically(Group group) {
cancelTasks(group);
if (group.headerFooterUpdateRate() > 0) {
final ScheduledTask headerFooterTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateGroupPlayers(group, false, true))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.headerFooterUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
headerFooterTasks.put(group, headerFooterTask);
}
if (group.placeholderUpdateRate() > 0) {
final ScheduledTask updateTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateGroupPlayers(group, true, false))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.placeholderUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
placeholderTasks.put(group, updateTask);
}
}
/**
* 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) {
List<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
if (groupPlayers.isEmpty()) {
return;
}
groupPlayers.stream()
.filter(player -> player.getPlayer().isActive())
.forEach(player -> {
if (incrementIndexes) {
player.incrementIndexes();
}
players.values().forEach(player -> {
if (all) {
this.updatePlayer(player, false);
player.sendHeaderAndFooter(this);
});
updateDisplayNames();
})
.repeat(Math.max(200, updateRate), TimeUnit.MILLISECONDS)
.schedule();
}
player.sendHeaderAndFooter(this);
});
if (all) {
updateDisplayNames();
}
}
private void cancelTasks(Group group) {
ScheduledTask task = placeholderTasks.entrySet().stream()
.filter(entry -> entry.getKey().equals(group))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
if (task != null) {
task.cancel();
placeholderTasks.remove(group);
}
task = headerFooterTasks.entrySet().stream()
.filter(entry -> entry.getKey().equals(group))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
if (task != null) {
task.cancel();
headerFooterTasks.remove(group);
}
}
/**
* Update the TAB list for all players when a plugin or proxy reload is performed
*/
public void reloadUpdate() {
placeholderTasks.values().forEach(ScheduledTask::cancel);
placeholderTasks.clear();
plugin.getTabGroups().getGroups().forEach(this::updatePeriodically);
if (players.isEmpty()) {
return;
}
if (updateTask != null) {
updateTask.cancel();
}
// If the update time is set to 0 do not schedule the updater
if (plugin.getSettings().getUpdateRate() > 0) {
this.updatePeriodically(plugin.getSettings().getUpdateRate());
} else {
players.values().forEach(player -> {
this.updatePlayer(player, true);
player.sendHeaderAndFooter(this);
});
updateDisplayNames();
}
players.values().forEach(player -> {
final Optional<ServerConnection> server = player.getPlayer().getCurrentServer();
if (server.isEmpty()) {
return;
}
final String serverName = server.get().getServerInfo().getName();
final Group group = getGroup(serverName);
player.setGroup(group);
this.updatePlayer(player, true);
player.sendHeaderAndFooter(this);
});
updateDisplayNames();
}
/**
* Get the servers in the same group as the given server, as an optional
* <p>
* If the server is not in a group, use the fallback group
* If the fallback is disabled, return an empty optional
*
* @param serverName The server name
* @return The servers in the same group as the given server, empty if the server is not in a group and fallback is disabled
*/
@NotNull
public Optional<List<String>> getGroupNames(@NotNull String serverName) {
return plugin.getSettings().getServerGroups().values().stream()
.filter(servers -> servers.contains(serverName))
.findFirst()
.or(() -> {
if (!plugin.getSettings().isFallbackEnabled()) {
return Optional.empty();
}
if (!this.fallbackServers.contains(serverName)) {
this.fallbackServers.add(serverName);
}
return Optional.of(this.fallbackServers.stream().toList());
});
public Group getGroup(@NotNull String serverName) {
return plugin.getTabGroups().getGroupFromServer(serverName);
}
/**
* Get the servers in the same group as the given server, as an optional list of {@link ServerInfo}
* <p>
* If the server is not in a group, use the fallback group
* If the fallback is disabled, return an empty optional
*
* @param serverName The server name
* @return The servers in the same group as the given server, empty if the server is not in a group and fallback is disabled
*/
@NotNull
public List<RegisteredServer> getGroupServers(@NotNull String serverName) {
return plugin.getServer().getAllServers().stream()
.filter(server -> plugin.getSettings().getServerGroups().values().stream()
.filter(servers -> servers.contains(serverName))
.anyMatch(servers -> servers.contains(server.getServerInfo().getName())))
.toList();
}
@Subscribe
public void proxyReload(@NotNull ProxyReloadEvent event) {
plugin.loadSettings();
reloadUpdate();
plugin.log("Velocitab has been reloaded!");
}
/**
* Remove an offline player from the list of tracked TAB players
@ -448,81 +444,4 @@ public class PlayerTabList {
players.remove(player.getUniqueId());
}
public void vanishPlayer(@NotNull TabPlayer tabPlayer) {
players.values().forEach(p -> {
if (p.getPlayer().equals(tabPlayer.getPlayer())) {
return;
}
if (!plugin.getVanishManager().canSee(p.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) {
p.getPlayer().getTabList().removeEntry(tabPlayer.getPlayer().getUniqueId());
}
});
}
public void unVanishPlayer(@NotNull TabPlayer tabPlayer) {
final UUID uuid = tabPlayer.getPlayer().getUniqueId();
tabPlayer.getDisplayName(plugin).thenAccept(c -> players.values().forEach(p -> {
if (p.getPlayer().equals(tabPlayer.getPlayer())) {
return;
}
if (!p.getPlayer().getTabList().containsEntry(uuid)) {
p.getPlayer().getTabList().addEntry(createEntry(tabPlayer, p.getPlayer().getTabList(), c));
} else {
p.getPlayer().getTabList().getEntry(uuid).ifPresent(entry -> entry.setDisplayName(c));
}
}));
}
/**
* Recalculates the visibility of players in the tab list for the given player.
* If tabPlayer can see the player, the player will be added to the tab list.
*
* @param tabPlayer The TabPlayer object representing the player for whom to recalculate the tab list visibility.
*/
public void recalculateVanishForPlayer(@NotNull TabPlayer tabPlayer) {
final Player player = tabPlayer.getPlayer();
final Optional<List<String>> serversInGroupOptional = getGroupNames(player.getCurrentServer()
.map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("?"));
final List<String> serversInGroup = serversInGroupOptional.orElseGet(ArrayList::new);
plugin.getServer().getAllPlayers().forEach(p -> {
if (p.equals(player)) {
return;
}
final Optional<TabPlayer> targetOptional = getTabPlayer(p);
if (targetOptional.isEmpty()) {
return;
}
final TabPlayer target = targetOptional.get();
final String serverName = target.getServerName();
if (plugin.getSettings().isOnlyListPlayersInSameGroup()
&& !serversInGroup.contains(serverName)) {
return;
}
final boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) ||
plugin.getVanishManager().canSee(player.getUsername(), p.getUsername());
if (!canSee) {
player.getTabList().removeEntry(p.getUniqueId());
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, false));
} else {
if (!player.getTabList().containsEntry(p.getUniqueId())) {
createEntry(target, player.getTabList()).thenAccept(e -> {
player.getTabList().addEntry(e);
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, true));
});
}
}
});
}
}

View File

@ -0,0 +1,122 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.tab;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.player.KickedFromServerEvent;
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.ServerInfo;
import net.kyori.adventure.text.Component;
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.UUID;
import java.util.concurrent.TimeUnit;
/**
* The TabListListener class is responsible for handling events related to the player tab list.
*/
@SuppressWarnings("unused")
public class TabListListener {
private final Velocitab plugin;
private final PlayerTabList tabList;
public TabListListener(@NotNull Velocitab plugin, @NotNull PlayerTabList tabList) {
this.plugin = plugin;
this.tabList = tabList;
}
@Subscribe
public void onKick(KickedFromServerEvent event) {
event.getPlayer().getTabList().clearAll();
event.getPlayer().getTabList().clearHeaderAndFooter();
}
@SuppressWarnings("UnstableApiUsage")
@Subscribe
public void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
final Player joined = event.getPlayer();
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined));
final String serverName = joined.getCurrentServer()
.map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("");
final Group group = tabList.getGroup(serverName);
final boolean isDefault = !group.servers().contains(serverName);
// 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()) {
event.getPlayer().sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
tabList.getPlayers().remove(event.getPlayer().getUniqueId());
return;
}
tabList.joinPlayer(joined, group);
}
@Subscribe(order = PostOrder.LAST)
public void onPlayerQuit(@NotNull DisconnectEvent event) {
if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) {
return;
}
// Remove the player from the tracking list, Print warning if player was not removed
final UUID uuid = event.getPlayer().getUniqueId();
final TabPlayer tabPlayer = tabList.getPlayers().get(uuid);
if (tabPlayer == null) {
plugin.log(String.format("Failed to remove disconnecting player %s (UUID: %s)",
event.getPlayer().getUsername(), uuid));
}
// Remove the player from the tab list of all other players
plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(uuid));
// Update the tab list of all players
plugin.getServer().getScheduler()
.buildTask(plugin, () -> tabList.getPlayers().values().forEach(player -> {
player.getPlayer().getTabList().removeEntry(uuid);
player.sendHeaderAndFooter(tabList);
}))
.delay(500, TimeUnit.MILLISECONDS)
.schedule();
// Delete player team
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(event.getPlayer()));
//remove player from tab list cache
tabList.getPlayers().remove(uuid);
}
@Subscribe
public void proxyReload(@NotNull ProxyReloadEvent event) {
plugin.loadConfigs();
tabList.reloadUpdate();
plugin.log("Velocitab has been reloaded!");
}
}

View File

@ -0,0 +1,119 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.tab;
import com.velocitypowered.api.proxy.Player;
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.UUID;
/**
* The VanishTabList handles the tab list for vanished players
*/
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())) {
return;
}
if (!plugin.getVanishManager().canSee(p.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) {
p.getPlayer().getTabList().removeEntry(tabPlayer.getPlayer().getUniqueId());
}
});
}
public void unVanishPlayer(@NotNull TabPlayer tabPlayer) {
final UUID uuid = tabPlayer.getPlayer().getUniqueId();
tabPlayer.getDisplayName(plugin).thenAccept(c -> 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));
} else {
p.getPlayer().getTabList().getEntry(uuid).ifPresent(entry -> entry.setDisplayName(c));
}
}));
}
/**
* Recalculates the visibility of players in the tab list for the given player.
* If tabPlayer can see the player, the player will be added to the tab list.
*
* @param tabPlayer The TabPlayer object representing the player for whom to recalculate the tab list visibility.
*/
public void recalculateVanishForPlayer(@NotNull TabPlayer tabPlayer) {
final Player player = tabPlayer.getPlayer();
final List<String> serversInGroup = tabPlayer.getGroup().servers();
plugin.getServer().getAllPlayers().forEach(p -> {
if (p.equals(player)) {
return;
}
final Optional<TabPlayer> targetOptional = tabList.getTabPlayer(p);
if (targetOptional.isEmpty()) {
return;
}
final TabPlayer target = targetOptional.get();
final String serverName = target.getServerName();
if (plugin.getSettings().isOnlyListPlayersInSameGroup()
&& !serversInGroup.contains(serverName)) {
return;
}
final boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) ||
plugin.getVanishManager().canSee(player.getUsername(), p.getUsername());
if (!canSee) {
player.getTabList().removeEntry(p.getUniqueId());
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));
});
}
}
});
}
}

View File

@ -60,8 +60,8 @@ public class VanishManager {
return;
}
plugin.getTabList().vanishPlayer(tabPlayer.get());
plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.vanishPlayer(player));
plugin.getTabList().getVanishTabList().vanishPlayer(tabPlayer.get());
plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.vanishPlayer(tabPlayer.get()));
}
public void unVanishPlayer(@NotNull Player player) {
@ -71,7 +71,7 @@ public class VanishManager {
return;
}
plugin.getTabList().unVanishPlayer(tabPlayer.get());
plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.unVanishPlayer(player));
plugin.getTabList().getVanishTabList().unVanishPlayer(tabPlayer.get());
plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.unVanishPlayer(tabPlayer.get()));
}
}