forked from Upstream/Velocitab
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e398306cd7 | ||
|
9cb20be6e0 | ||
|
a14c8eb2ea | ||
|
d47ee75d5b | ||
|
e17d36deb5 | ||
|
6796f4402f | ||
|
5a46053117 | ||
|
94760f7794 | ||
|
ce88b480a6 | ||
|
a55bd56364 | ||
|
7b8d55ba77 | ||
|
24c29e632f | ||
|
2ba208002a | ||
|
6c558fac3a | ||
|
67931d8d41 | ||
|
da946ac75a | ||
|
d9dcf54a91 | ||
|
724a9b2851 | ||
|
848c681cbf | ||
|
6f140e4708 | ||
|
73de08eea9 | ||
|
cf7c92e28f | ||
|
cc548b19fa | ||
|
4f2fe1ef3f | ||
|
3865387b80 |
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -65,7 +65,7 @@ jobs:
|
||||
hangar-token: ${{ secrets.HANGAR_API_KEY }}
|
||||
hangar-version-type: Alpha
|
||||
hangar-game-versions: |
|
||||
3.3
|
||||
3.4
|
||||
files: target/Velocitab-*.jar
|
||||
name: Velocitab v${{ env.version_name }}
|
||||
version: ${{ env.version_name }}
|
||||
@ -124,4 +124,7 @@ jobs:
|
||||
1.20.6
|
||||
1.21
|
||||
1.21.1
|
||||
1.21.2
|
||||
1.21.3
|
||||
1.21.4
|
||||
java: 17
|
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -54,7 +54,7 @@ jobs:
|
||||
hangar-token: ${{ secrets.HANGAR_API_KEY }}
|
||||
hangar-version-type: Release
|
||||
hangar-game-versions: |
|
||||
3.3
|
||||
3.4
|
||||
files: target/Velocitab-*.jar
|
||||
name: Velocitab v${{ github.event.release.tag_name }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
@ -113,4 +113,7 @@ jobs:
|
||||
1.20.6
|
||||
1.21
|
||||
1.21.1
|
||||
1.21.2
|
||||
1.21.3
|
||||
1.21.4
|
||||
java: 17
|
27
build.gradle
27
build.gradle
@ -2,9 +2,9 @@ import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
plugins {
|
||||
id 'xyz.jpenilla.run-velocity' version '2.3.1'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'com.gradleup.shadow' version '8.3.5'
|
||||
id 'org.cadixdev.licenser' version '0.6.1'
|
||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||
id 'org.ajoberstar.grgit' version '5.3.0'
|
||||
id 'maven-publish'
|
||||
id 'java'
|
||||
}
|
||||
@ -20,39 +20,40 @@ ext {
|
||||
|
||||
set 'velocity_api_version', velocity_api_version.toString()
|
||||
set 'velocity_minimum_build', velocity_minimum_build.toString()
|
||||
set 'papi_proxy_bridge_minimum_version', papi_proxy_bridge_minimum_version.toString()
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://repo.william278.net/velocity/' }
|
||||
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||
maven { url = 'https://repo.william278.net/releases/' }
|
||||
maven { url = 'https://repo.william278.net/snapshots/' }
|
||||
maven { url = 'https://jitpack.io/' }
|
||||
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||
maven { url = 'https://repo.minebench.de/' }
|
||||
maven { url = 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT"
|
||||
compileOnly "com.velocitypowered:velocity-proxy:${velocity_api_version}-SNAPSHOT"
|
||||
compileOnly "net.william278:papiproxybridge:${papi_proxy_bridge_minimum_version}"
|
||||
|
||||
compileOnly 'io.netty:netty-codec-http:4.1.112.Final'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.34'
|
||||
compileOnly 'io.netty:netty-codec-http:4.1.115.Final'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||
compileOnly 'net.luckperms:api:5.4'
|
||||
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.2.3'
|
||||
compileOnly 'net.william278:PAPIProxyBridge:1.5'
|
||||
compileOnly 'it.unimi.dsi:fastutil:8.5.14'
|
||||
compileOnly 'it.unimi.dsi:fastutil:8.5.15'
|
||||
compileOnly 'net.kyori:adventure-nbt:4.17.0'
|
||||
|
||||
implementation 'org.apache.commons:commons-text:1.12.0'
|
||||
implementation 'net.william278:DesertWell:2.0.4'
|
||||
implementation 'net.william278:desertwell:2.0.4'
|
||||
implementation 'net.william278:minedown:1.8.2'
|
||||
implementation 'org.bstats:bstats-velocity:3.0.3'
|
||||
implementation 'org.bstats:bstats-velocity:3.1.0'
|
||||
implementation 'de.exlll:configlib-yaml:4.5.0'
|
||||
implementation 'org.apache.commons:commons-jexl3:3.4.0'
|
||||
implementation 'net.jodah:expiringmap:0.5.11'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.34'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||
}
|
||||
|
||||
processResources {
|
||||
@ -71,7 +72,6 @@ license {
|
||||
}
|
||||
|
||||
logger.lifecycle("Building Velocitab ${version} by William278")
|
||||
|
||||
version rootProject.version
|
||||
archivesBaseName = "${rootProject.name}"
|
||||
|
||||
@ -160,11 +160,12 @@ publishing {
|
||||
}
|
||||
|
||||
tasks {
|
||||
var papi = papi_proxy_bridge_minimum_version
|
||||
runVelocity {
|
||||
velocityVersion("${velocity_api_version}-SNAPSHOT")
|
||||
|
||||
downloadPlugins {
|
||||
modrinth ("papiproxybridge", "1.6.1")
|
||||
github ("WiIIiam278", "PAPIProxyBridge", "1.7.1", "PAPIProxyBridge-Velocity-1.7.1.jar")
|
||||
modrinth ("miniplaceholders", "2.2.4")
|
||||
}
|
||||
}
|
||||
|
@ -28,10 +28,6 @@ fallback_enabled: true
|
||||
fallback_group: default
|
||||
# Whether to show all players from all groups in the TAB list.
|
||||
show_all_players_from_all_groups: false
|
||||
# Define custom names to be shown in the TAB list for specific server names.
|
||||
# If no custom display name is provided for a server, its original name will be used.
|
||||
server_display_names:
|
||||
very-long-server-name: VLSN
|
||||
# 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)
|
||||
@ -50,6 +46,8 @@ sort_players: true
|
||||
remove_spectator_effect: false
|
||||
# Whether to enable the Plugin Message API (allows backend plugins to perform certain operations)
|
||||
enable_plugin_message_api: true
|
||||
# Whether to force sending tab list packets to all players, even if a packet for that action has already been sent. This could fix issues with some mods.
|
||||
force_sending_tab_list_packets: false
|
||||
# A list of URLs that will be sent to display on player pause menus (Minecraft 1.21+ clients only).
|
||||
# • Labels can be fully custom or built-in (one of 'bug_report', 'community_guidelines', 'support', 'status',
|
||||
# 'feedback', 'community', 'website', 'forums', 'news', or 'announcements').
|
||||
@ -99,6 +97,14 @@ groups:
|
||||
sorting_placeholders:
|
||||
- '%role_weight%'
|
||||
- '%username_lower%'
|
||||
placeholder_replacements:
|
||||
'%current_date_weekday_en-US%':
|
||||
- placeholder: Monday
|
||||
replacement: <red>Monday</red>
|
||||
- placeholder: Tuesday
|
||||
replacement: <gold>Tuesday</gold>
|
||||
- placeholder: Else
|
||||
replacement: <green>Other day</green>
|
||||
collisions: false
|
||||
header_footer_update_rate: 1000
|
||||
placeholder_update_rate: 1000
|
||||
@ -124,4 +130,7 @@ Velocitab supports basic header and footer animations by adding multiple frames
|
||||
You can use various placeholders that will be replaced with values (for example, `%username%`) in your config. Support for PlaceholderAPI is also available through [a bridge library plugin](https://modrinth.com/plugin/papiproxybridge), as is the component-based MiniPlaceholders for users of that plugin with the MiniMessage formatter. See [[Placeholders]] for more information.
|
||||
|
||||
### Server Links
|
||||
For Minecraft 1.21+ clients, Velocitab supports specifying a list of URLs that will be sent to display in the player pause menu. See [[Server Links]] for more information.
|
||||
For Minecraft 1.21+ clients, Velocitab supports specifying a list of URLs that will be sent to display in the player pause menu. See [[Server Links]] for more information.
|
||||
|
||||
### Placeholder Replacements
|
||||
Velocitab supports replacing values of placeholders with other values. See [[Placeholders Replacements]] for more information.
|
@ -46,4 +46,35 @@ is equivalent to
|
||||
|
||||
```yaml
|
||||
foo: "bar 1\nbar 2\nbar 3\n"
|
||||
```
|
||||
```
|
||||
|
||||
## List of multi lines strings
|
||||
|
||||
> **Note:** The examples above are generic examples on how yaml works in multi line. If you want to use multi line in headers & footers you need to provide a list of multi line strings like in the example below.
|
||||
|
||||
```yaml
|
||||
headers:
|
||||
- |
|
||||
<rainbow:!2>Running Velocitab by William278 & AlexDev_</rainbow>
|
||||
<gray>Second line of the first element</gray>
|
||||
<yellow>Third line of the first element</yellow>
|
||||
- |
|
||||
<rainbow:!4>Running Velocitab by William278 & AlexDev_</rainbow>
|
||||
<gray>Second line of the second element</gray>
|
||||
<yellow>Third line of the second element</yellow>
|
||||
footers:
|
||||
- <gray>There are currently %players_online%/%max_players_online% players online</gray>
|
||||
- |
|
||||
<gray> Test 1 </gray>
|
||||
<yellow> Test 2 </yellow>
|
||||
```
|
||||
|
||||
In this example the header will switch between the 2 elements, but it will always display all the 3 lines.
|
||||
|
||||
The footer in this example will switch between 2 elements, the first one is just a simple string, the second element will display 2 lines since it's a multi line string
|
||||
|
||||
<figure style="text-align: center;">
|
||||
<img src="https://i.imgur.com/YKu1RWi.gif" />
|
||||
<figcaption>Example of a header and footer with multi line strings</figcaption>
|
||||
</figure>
|
||||
|
||||
|
88
docs/Placeholders-Replacements.md
Normal file
88
docs/Placeholders-Replacements.md
Normal file
@ -0,0 +1,88 @@
|
||||
|
||||
Velocitab supports placeholder replacements, which allow you to replace a placeholder with a different value. This is useful for things like changing the text of a date placeholder to a localized version, changing the text of a biome placeholder to a color or you can use a vanish placeholder to show a player's vanish status if the placeholder returns just a boolean (true/false).
|
||||
|
||||
|
||||
## Configuring
|
||||
|
||||
Placeholder replacements are configured in the `placeholder_replacements` section of the every Tab Group.
|
||||
You can specify a list of replacements for a placeholder, and the replacements will be applied in the order they are listed.
|
||||
|
||||
The replacements are specified as a list of objects with two properties: `placeholder` and `replacement`.
|
||||
`placeholder` is the placeholder to replace, and `replacement` is the replacement text.
|
||||
|
||||
### Example section
|
||||
```yaml
|
||||
placeholder_replacements:
|
||||
'%current_date_weekday_en-US%':
|
||||
- placeholder: Monday
|
||||
replacement: <red>Monday</red>
|
||||
- placeholder: Tuesday
|
||||
replacement: <gold>Tuesday</gold>
|
||||
- placeholder: Else
|
||||
replacement: <green>Other day</green>
|
||||
'%player_world_type%':
|
||||
- placeholder: Overworld
|
||||
replacement: '<aqua>Overworld</aqua>'
|
||||
- placeholder: Nether
|
||||
replacement: '<red>Nether</red>'
|
||||
- placeholder: End
|
||||
replacement: '<yellow>End</yellow>'
|
||||
'%player_biome%':
|
||||
- placeholder: PLAINS
|
||||
replacement: <red>Plains</red>
|
||||
- placeholder: DESERT
|
||||
replacement: <yellow>Desert</yellow>
|
||||
- placeholder: RIVER
|
||||
replacement: <aqua>River</aqua>
|
||||
```
|
||||
|
||||
## Specified cases
|
||||
|
||||
### Vanish status
|
||||
If you want to show a player's vanish status, for example, you can use the `%advancedvanish_is_vanished%` placeholder.
|
||||
This placeholder returns a boolean value, so you can use it to show a player's vanish status.
|
||||
|
||||
For example, if you wanted to show a player's vanish status as a color, you could use the following replacements:
|
||||
```yaml
|
||||
placeholder_replacements:
|
||||
'%advancedvanish_is_vanished%':
|
||||
- placeholder: Yes
|
||||
replacement: <red>Vanished</red>
|
||||
- placeholder: No
|
||||
replacement: <green>Not vanished</green>
|
||||
```
|
||||
|
||||
### Else clause
|
||||
If you don't want to specify every possible value for a placeholder, you can use the `ELSE` placeholder.
|
||||
This placeholder will be replaced with the replacement text of the first replacement that doesn't have a placeholder.
|
||||
|
||||
For example, if you wanted to show the current date as a color, you could use the following replacements:
|
||||
```yaml
|
||||
placeholder_replacements:
|
||||
'%current_date_weekday_en-US%':
|
||||
- placeholder: Monday
|
||||
replacement: <red>Monday</red>
|
||||
- placeholder: Tuesday
|
||||
replacement: <gold>Tuesday</gold>
|
||||
- placeholder: ELSE
|
||||
replacement: <green>Other day</green>
|
||||
```
|
||||
|
||||
### Placeholder not present in a server
|
||||
If you have a group with multiple servers, and you have a placeholder that is not present in one of the servers, you can use the `%<placeholder>%` as a placeholder it will handle the case where the placeholder is not present in the server.
|
||||
|
||||
```yaml
|
||||
placeholder_replacements:
|
||||
'%huskhomes_homes_count%':
|
||||
- placeholder: '%huskhomes_homes_count%'
|
||||
replacement: <red>No homes in this server</red>
|
||||
```
|
||||
|
||||
If you want you can also set the replacement as an empty string, which will be replaced with the empty string.
|
||||
|
||||
```yaml
|
||||
placeholder_replacements:
|
||||
'%huskhomes_homes_count%':
|
||||
- placeholder: '%huskhomes_homes_count%'
|
||||
replacement: ''
|
||||
```
|
@ -40,20 +40,6 @@ Placeholders can be included in the header, footer and player name format of the
|
||||
You can find a list of common primary language subtags [here](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags).
|
||||
|
||||
|
||||
### Customising server display names
|
||||
You can make use of the `server_display_names` feature in `config.yml` to customise how server display name appear when using the `%server%` placeholder. In the below example, if a user is connected to a server with the name "`very-long-server-`name" and the player name format for the group that server belongs to includes a `%server%` placeholder, the placeholder would be replaced with "`VSLN`" instead of the full server name.
|
||||
|
||||
<details>
|
||||
<summary>Server display names (config.yml)</summary>
|
||||
|
||||
```yaml
|
||||
# Define custom names to be shown in the TAB list for specific server names.
|
||||
# If no custom display name is provided for a server, its original name will be used.
|
||||
server_display_names:
|
||||
very-long-server-name: VLSN
|
||||
```
|
||||
</details>
|
||||
|
||||
## PlaceholderAPI support
|
||||
To use PlaceholderAPI placeholders in Velocitab, install the [PAPIProxyBridge](https://modrinth.com/plugin/papiproxybridge) library plugin on your Velocity proxy and all Minecraft spigot servers on your network, and ensure the PAPI hook option is enabled in your Velocitab [[Config File]]. You can then include PAPI placeholders in your formats as you would any of the default placeholders.
|
||||
|
||||
|
@ -1,10 +1,17 @@
|
||||
Velocitab provides a plugin message API.
|
||||
Velocitab provides a plugin message API, to let you do things with Velocitab from your backend servers.
|
||||
|
||||
> **Note:** This feature requires sending Update Teams packets. `send_scoreboard_packets` must be enabled in the [`config.yml` file](config-file) for this to work. [More details...](sorting#compatibility-issues)
|
||||
>
|
||||
## Prerequisites
|
||||
To use the Velocitab plugin message API, you must first turn it on and ensure the following:
|
||||
|
||||
* That `enable_plugin_message_api` and `send_scoreboard_packets` is set to `true` in your Velocitab [[config file]]
|
||||
* That `bungee-plugin-message-channel` is set to `true` in your **Velocity proxy config** TOML (see [Velocity config reference](https://docs.papermc.io/velocity/configuration)).
|
||||
|
||||
## API Requests from Backend Plugins
|
||||
|
||||
### 1 Changing player's username in the TAB List
|
||||
To change a player's username in the tablist, you can send a plugin message with the channel `velocitab:update_custom_name` and as data `customName`.
|
||||
Remember to replace `customName` with the desired name.
|
||||
### 1 Changing player's username in the TAB list
|
||||
To change a player's username in the TAB list, you can send a plugin message on the channel `velocitab:update_custom_name` with a `customName` string, where `customName` is the new desired display name.
|
||||
<details>
|
||||
<summary>Example — Changing player's username in the TAB List</summary>
|
||||
|
||||
@ -13,10 +20,10 @@ player.sendPluginMessage(plugin, "velocitab:update_custom_name", "Steve".getByte
|
||||
```
|
||||
</details>
|
||||
|
||||
### 2 Update team color
|
||||
To change a player's team color in the TAB List, you can send a plugin message with the channel `velocitab:update_team_color` and as data `teamColor`.
|
||||
You can only use legacy color codes, for example `a` for green, `b` for aqua, etc.
|
||||
This option overrides the glow effect if set
|
||||
### 2 Update color of player's nametag
|
||||
To change player's [nametag](nametags) color, you can send a plugin message on the channel `velocitab:update_team_color` with `teamColor` string, where `teamColor` is the new desired name tag color.
|
||||
|
||||
You can only use legacy color codes, for example `a` for green, `b` for aqua, etc. Please note this option overrides the color of the glow potion effect if set. [Check here](https://wiki.vg/index.php?title=Text_formatting&oldid=18983#Colors) for a list of supported colors (The value under the "Code" header on the table is what you need).
|
||||
|
||||
<details>
|
||||
<summary>Example — Changing player's team color</summary>
|
||||
|
@ -1,7 +1,7 @@
|
||||
This page will walk you through installing Velocitab on a Velocity proxy server.
|
||||
|
||||
## Requirements
|
||||
* A Velocity proxy server (running Velocity 3.3.0 or newer)
|
||||
* A Velocity proxy server (running Velocity 3.4.0 or newer)
|
||||
* Backend Minecraft servers. The following Minecraft server versions are fully supported:
|
||||
- Minecraft 1.8—1.8.9
|
||||
- Minecraft 1.12.2—latest
|
||||
|
@ -11,6 +11,7 @@
|
||||
* ✍️ [[Placeholders]]
|
||||
* 🔗 [[Relational Placeholders]]
|
||||
* 🔀 [[Conditional Placeholders]]
|
||||
* 📝 [[Placeholders Replacements]]
|
||||
* ✨ [[Animations]]
|
||||
* 🖼️ [[Custom Logos]]
|
||||
* 🔗 [[Server Links]]
|
||||
|
@ -3,9 +3,10 @@ javaVersion=17
|
||||
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.daemon=true
|
||||
|
||||
plugin_version=1.7.1
|
||||
plugin_version=1.7.3
|
||||
plugin_archive=velocitab
|
||||
plugin_description=A beautiful and versatile TAB list plugin for Velocity proxies
|
||||
|
||||
velocity_api_version=3.3.0
|
||||
velocity_minimum_build=400
|
||||
velocity_api_version=3.4.0
|
||||
velocity_minimum_build=453
|
||||
papi_proxy_bridge_minimum_version=1.7
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
20
gradlew.bat
vendored
20
gradlew.bat
vendored
@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
@ -29,7 +29,6 @@ import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
@ -60,7 +59,6 @@ import org.slf4j.event.Level;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Plugin(id = "velocitab")
|
||||
@Getter
|
||||
@ -145,9 +143,9 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
return this;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Optional<ScoreboardManager> getScoreboardManager() {
|
||||
return Optional.ofNullable(scoreboardManager);
|
||||
@Override
|
||||
public ScoreboardManager getScoreboardManager() {
|
||||
return scoreboardManager;
|
||||
}
|
||||
|
||||
private void prepareAPI() {
|
||||
|
@ -19,6 +19,8 @@
|
||||
|
||||
package net.william278.velocitab.config;
|
||||
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import de.exlll.configlib.NameFormatters;
|
||||
import de.exlll.configlib.YamlConfigurationProperties;
|
||||
import de.exlll.configlib.YamlConfigurations;
|
||||
@ -129,16 +131,24 @@ public interface ConfigProvider {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalIsPresent")
|
||||
default void checkCompatibility() {
|
||||
if (getSkipCompatibilityCheck().orElse(false)) {
|
||||
getPlugin().getLogger().warn("Skipping compatibility check");
|
||||
getPlugin().getLogger().warn("Skipping compatibility checks");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate Velocity platform version
|
||||
final Metadata metadata = getMetadata();
|
||||
final Version proxyVersion = getVelocityVersion();
|
||||
metadata.validateApiVersion(proxyVersion);
|
||||
metadata.validateBuild(proxyVersion);
|
||||
|
||||
// Validate PAPIProxyBridge hook version
|
||||
final Optional<Version> papiProxyBridgeVersion = getPapiProxyBridgeVersion();
|
||||
if (papiProxyBridgeVersion.isPresent()) {
|
||||
metadata.validatePapiProxyBridgeVersion(papiProxyBridgeVersion.get());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -151,6 +161,12 @@ public interface ConfigProvider {
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
default Optional<Version> getPapiProxyBridgeVersion() {
|
||||
return getPlugin().getServer().getPluginManager()
|
||||
.getPlugin("papiproxybridge").map(PluginContainer::getDescription)
|
||||
.flatMap(PluginDescription::getVersion).map(Version::fromString);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Version getVelocityVersion();
|
||||
|
||||
|
@ -26,7 +26,7 @@ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.util.QuadFunction;
|
||||
import net.william278.velocitab.util.SerializerUtil;
|
||||
import net.william278.velocitab.util.SerializationUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@ -57,11 +57,11 @@ public enum Formatter {
|
||||
MiniMessage.miniMessage()::serialize
|
||||
),
|
||||
LEGACY(
|
||||
(text, player, viewer, plugin) -> SerializerUtil.LEGACY_SERIALIZER.deserialize(text),
|
||||
(text, player, viewer, plugin) -> SerializationUtil.LEGACY_SERIALIZER.deserialize(text),
|
||||
Function.identity(),
|
||||
"Legacy Text",
|
||||
SerializerUtil.LEGACY_SERIALIZER::deserialize,
|
||||
SerializerUtil.LEGACY_SERIALIZER::serialize
|
||||
SerializationUtil.LEGACY_SERIALIZER::deserialize,
|
||||
SerializationUtil.LEGACY_SERIALIZER::serialize
|
||||
);
|
||||
|
||||
|
||||
|
@ -31,6 +31,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@ -46,6 +47,7 @@ public record Group(
|
||||
Nametag nametag,
|
||||
Set<String> servers,
|
||||
List<String> sortingPlaceholders,
|
||||
Map<String, List<PlaceholderReplacement>> placeholderReplacements,
|
||||
boolean collisions,
|
||||
int headerFooterUpdateRate,
|
||||
int placeholderUpdateRate,
|
||||
|
@ -37,6 +37,7 @@ public class Metadata {
|
||||
|
||||
private String velocityApiVersion;
|
||||
private int velocityMinimumBuild;
|
||||
private String papiProxyBridgeMinimumVersion;
|
||||
|
||||
public void validateApiVersion(@NotNull Version version) {
|
||||
if (version.compareTo(Version.fromString(velocityApiVersion)) < 0) {
|
||||
@ -56,6 +57,14 @@ public class Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
public void validatePapiProxyBridgeVersion(@NotNull Version version) {
|
||||
if (version.compareTo(Version.fromString(papiProxyBridgeMinimumVersion)) < 0) {
|
||||
final String serverVersion = version.toStringWithoutMetadata();
|
||||
throw new IllegalStateException("Your PAPIProxyBridge version (" + serverVersion + ") is not supported! " +
|
||||
"Disabling Velocitab. Please update to at least PAPIProxyBridge v" + papiProxyBridgeMinimumVersion + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private int getBuildNumber(@NotNull String proxyVersion) {
|
||||
final Matcher matcher = Pattern.compile(".*-b(\\d+).*").matcher(proxyVersion);
|
||||
if (matcher.find(1)) {
|
||||
|
@ -19,6 +19,8 @@
|
||||
|
||||
package net.william278.velocitab.config;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
@ -36,8 +38,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
@ -95,7 +96,7 @@ public enum Placeholder {
|
||||
}),
|
||||
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)),
|
||||
SERVER((plugin, player) -> player.getServerName()),
|
||||
PING((plugin, player) -> Long.toString(player.getPlayer().getPing())),
|
||||
PREFIX((plugin, player) -> player.getRole().getPrefix()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_prefix%"))),
|
||||
@ -105,7 +106,8 @@ public enum Placeholder {
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
|
||||
ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
|
||||
ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()),
|
||||
ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_weight%"))),
|
||||
SERVER_GROUP((plugin, player) -> player.getGroup().name()),
|
||||
SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))),
|
||||
DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse(""))),
|
||||
@ -116,7 +118,7 @@ public enum Placeholder {
|
||||
private final static Pattern VELOCITAB_PATTERN = Pattern.compile("<velocitab_.*?>");
|
||||
private final static Pattern TEST = Pattern.compile("<.*?>");
|
||||
private final static Pattern CONDITION_REPLACER = Pattern.compile("<velocitab_rel_condition:[^:]*:");
|
||||
private final static Pattern PLACEHOLDER_PATTERN = Pattern.compile("%.*?%");
|
||||
private final static Pattern PLACEHOLDER_PATTERN = Pattern.compile("%.*?%", Pattern.DOTALL);
|
||||
private final static String DELIMITER = ":::";
|
||||
private final static Map<String, String> SYMBOL_SUBSTITUTES = Map.of(
|
||||
"<", "*LESS*",
|
||||
@ -126,6 +128,9 @@ public enum Placeholder {
|
||||
"*LESS*", "*LESS2*",
|
||||
"*GREATER*", "*GREATER2*"
|
||||
);
|
||||
private final static String VEL_PLACEHOLDER = "<vel";
|
||||
private final static String VELOCITAB_PLACEHOLDER = "<velocitab_rel";
|
||||
private final static String ELSE_PLACEHOLDER = "ELSE";
|
||||
|
||||
/**
|
||||
* Function to replace placeholders with a real value
|
||||
@ -162,17 +167,13 @@ public enum Placeholder {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String replaceInternal(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
|
||||
final Pair<Boolean, String> result = processRelationalPlaceholders(format, plugin);
|
||||
format = result.right();
|
||||
format = replacePlaceholders(format, plugin, player);
|
||||
|
||||
return format;
|
||||
public static Pair<String, Map<String, String>> replaceInternal(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
|
||||
format = processRelationalPlaceholders(format, plugin);
|
||||
return replacePlaceholders(format, plugin, player);
|
||||
}
|
||||
|
||||
private static Pair<Boolean, String> processRelationalPlaceholders(@NotNull String format, @NotNull Velocitab plugin) {
|
||||
boolean foundRelational = false;
|
||||
if (plugin.getFormatter().equals(Formatter.MINIMESSAGE) && format.contains("<vel")) {
|
||||
private static String processRelationalPlaceholders(@NotNull String format, @NotNull Velocitab plugin) {
|
||||
if (plugin.getFormatter().equals(Formatter.MINIMESSAGE) && format.contains(VEL_PLACEHOLDER)) {
|
||||
final Matcher conditionReplacer = CONDITION_REPLACER.matcher(format);
|
||||
while (conditionReplacer.find()) {
|
||||
|
||||
@ -189,14 +190,14 @@ public enum Placeholder {
|
||||
|
||||
final Matcher testMatcher = TEST.matcher(format);
|
||||
while (testMatcher.find()) {
|
||||
if(testMatcher.group().startsWith("<velocitab_rel")) {
|
||||
if (testMatcher.group().startsWith(VELOCITAB_PLACEHOLDER)) {
|
||||
final Matcher second = TEST.matcher(testMatcher.group().substring(1));
|
||||
while (second.find()) {
|
||||
String s = second.group();
|
||||
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
|
||||
s = s.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
format = format.replace(second.group(), s);
|
||||
format = format.replace(second.group(), s);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@ -209,7 +210,6 @@ public enum Placeholder {
|
||||
|
||||
final Matcher velocitabRelationalMatcher = VELOCITAB_PATTERN.matcher(format);
|
||||
while (velocitabRelationalMatcher.find()) {
|
||||
foundRelational = true;
|
||||
final String relationalPlaceholder = velocitabRelationalMatcher.group().substring(1, velocitabRelationalMatcher.group().length() - 1);
|
||||
String fixedString = relationalPlaceholder;
|
||||
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES_2.entrySet()) {
|
||||
@ -223,25 +223,58 @@ public enum Placeholder {
|
||||
}
|
||||
|
||||
}
|
||||
return Pair.of(foundRelational, format);
|
||||
return format;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String replacePlaceholders(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
|
||||
private static Pair<String, Map<String, String>> replacePlaceholders(@NotNull String format, @NotNull Velocitab plugin,
|
||||
@Nullable TabPlayer player) {
|
||||
final Map<String, String> replacedPlaceholders = Maps.newHashMap();
|
||||
for (Placeholder placeholder : values()) {
|
||||
Matcher matcher = placeholder.pattern.matcher(format);
|
||||
final Matcher matcher = placeholder.pattern.matcher(format);
|
||||
if (placeholder.parameterised) {
|
||||
format = matcher.replaceAll(matchResult ->
|
||||
Matcher.quoteReplacement(
|
||||
placeholder.replacer.apply(StringUtils.chop(matchResult.group().replace("%" + placeholder.name().toLowerCase(), "")
|
||||
.replaceFirst("_", ""))
|
||||
, plugin, player)
|
||||
));
|
||||
format = matcher.replaceAll(matchResult -> {
|
||||
final String replacement = placeholder.replacer.apply(StringUtils.chop(matchResult.group().replace("%" + placeholder.name().toLowerCase(), "")
|
||||
.replaceFirst("_", "")), plugin, player);
|
||||
replacedPlaceholders.put(matchResult.group(), replacement);
|
||||
return Matcher.quoteReplacement(replacement);
|
||||
});
|
||||
} else {
|
||||
format = matcher.replaceAll(matchResult -> Matcher.quoteReplacement(placeholder.replacer.apply(null, plugin, player)));
|
||||
format = matcher.replaceAll(matchResult -> {
|
||||
final String replacement = placeholder.replacer.apply(null, plugin, player);
|
||||
replacedPlaceholders.put(matchResult.group(), replacement);
|
||||
return Matcher.quoteReplacement(replacement);
|
||||
});
|
||||
}
|
||||
}
|
||||
return format;
|
||||
return Pair.of(format, replacedPlaceholders);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String applyPlaceholderReplacements(@NotNull String text, @NotNull TabPlayer player,
|
||||
@NotNull Map<String, String> parsed) {
|
||||
for (final Map.Entry<String, List<PlaceholderReplacement>> entry : player.getGroup().placeholderReplacements().entrySet()) {
|
||||
if (!parsed.containsKey(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String replaced = parsed.get(entry.getKey());
|
||||
final Optional<PlaceholderReplacement> replacement = entry.getValue().stream()
|
||||
.filter(r -> r.placeholder().equalsIgnoreCase(replaced))
|
||||
.findFirst();
|
||||
|
||||
if (replacement.isPresent()) {
|
||||
text = text.replace(entry.getKey(), replacement.get().replacement());
|
||||
} else {
|
||||
final Optional<PlaceholderReplacement> elseReplacement = entry.getValue().stream()
|
||||
.filter(r -> r.placeholder().equalsIgnoreCase(ELSE_PLACEHOLDER))
|
||||
.findFirst();
|
||||
if (elseReplacement.isPresent()) {
|
||||
text = text.replace(entry.getKey(), elseReplacement.get().replacement());
|
||||
}
|
||||
}
|
||||
}
|
||||
return applyPlaceholders(text, parsed);
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin,
|
||||
@ -251,22 +284,48 @@ public enum Placeholder {
|
||||
return CompletableFuture.completedFuture("");
|
||||
}
|
||||
|
||||
final String replaced = replaceInternal(format, plugin, player);
|
||||
|
||||
if (!PLACEHOLDER_PATTERN.matcher(replaced).find()) {
|
||||
return CompletableFuture.completedFuture(replaced);
|
||||
final Pair<String, Map<String, String>> replaced = replaceInternal(format, plugin, player);
|
||||
if (!PLACEHOLDER_PATTERN.matcher(replaced.first()).find()) {
|
||||
return CompletableFuture.completedFuture(applyPlaceholderReplacements(format, player, replaced.second()));
|
||||
}
|
||||
|
||||
final List<String> placeholders = extractPlaceholders(replaced.first());
|
||||
return plugin.getPAPIProxyBridgeHook()
|
||||
.map(hook -> hook.formatPlaceholders(replaced, player.getPlayer())
|
||||
.map(hook -> hook.parsePlaceholders(placeholders, player.getPlayer())
|
||||
.exceptionally(e -> {
|
||||
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
|
||||
return replaced;
|
||||
return Map.of();
|
||||
})
|
||||
)
|
||||
.orElse(CompletableFuture.completedFuture(replaced)).exceptionally(e -> {
|
||||
.orElse(CompletableFuture.completedFuture(Maps.newHashMap()))
|
||||
.exceptionally(e -> {
|
||||
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
|
||||
return replaced;
|
||||
});
|
||||
return Map.of();
|
||||
})
|
||||
.thenApply(m -> applyPlaceholderReplacements(format, player, mergeMaps(m, replaced.second())));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String applyPlaceholders(@NotNull String text, @NotNull Map<String, String> replacements) {
|
||||
for (Map.Entry<String, String> entry : replacements.entrySet()) {
|
||||
text = text.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, String> mergeMaps(@NotNull Map<String, String> map1, @NotNull Map<String, String> map2) {
|
||||
map1.putAll(map2);
|
||||
return map1;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<String> extractPlaceholders(@NotNull String text) {
|
||||
final List<String> placeholders = Lists.newArrayList();
|
||||
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
|
||||
while (matcher.find()) {
|
||||
placeholders.add(matcher.group());
|
||||
}
|
||||
return placeholders;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record PlaceholderReplacement(@NotNull String placeholder, @NotNull String replacement) {
|
||||
}
|
@ -45,7 +45,7 @@ public record ServerUrl(
|
||||
(type) -> CompletableFuture.completedFuture(ServerLink.serverLink(type, url()))
|
||||
).orElseGet(
|
||||
() -> Placeholder.replace(label(), plugin, player)
|
||||
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
|
||||
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
|
||||
.thenApply(formatted -> ServerLink.serverLink(formatted, url()))
|
||||
);
|
||||
}
|
||||
|
@ -67,10 +67,6 @@ public class Settings implements ConfigValidator {
|
||||
@Comment("Whether to show all players from all groups in the TAB list.")
|
||||
private boolean showAllPlayersFromAllGroups = false;
|
||||
|
||||
@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");
|
||||
|
||||
@Comment("Whether to enable the PAPIProxyBridge hook for PAPI support")
|
||||
private boolean enablePapiHook = true;
|
||||
|
||||
@ -112,17 +108,6 @@ public class Settings implements ConfigValidator {
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get display name for the server
|
||||
*
|
||||
* @param serverName The server name
|
||||
* @return The display name, or the server name if no display name is defined
|
||||
*/
|
||||
@NotNull
|
||||
public String getServerDisplayName(@NotNull String serverName) {
|
||||
return serverDisplayNames.getOrDefault(serverName, serverName);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<ServerUrl> getUrlsForGroup(@NotNull Group group) {
|
||||
return serverLinks.stream()
|
||||
|
@ -51,9 +51,16 @@ public class TabGroups implements ConfigValidator {
|
||||
List.of("<rainbow:!2>Running Velocitab by William278 & AlexDev_</rainbow>"),
|
||||
List.of("<gray>There are currently %players_online%/%max_players_online% players online</gray>"),
|
||||
"<gray>[%server%] %prefix%%username%</gray>",
|
||||
new Nametag("<white>%prefix%</white>", "<white>%suffix%</white>"),
|
||||
new Nametag("", ""),
|
||||
Set.of("lobby", "survival", "creative", "minigames", "skyblock", "prison", "hub"),
|
||||
List.of("%role_weight%", "%username_lower%"),
|
||||
new LinkedHashMap<>() {{
|
||||
put("%current_date_weekday_en-US%", List.of(
|
||||
new PlaceholderReplacement("Monday", "<red>Monday</red>"),
|
||||
new PlaceholderReplacement("Tuesday", "<gold>Tuesday</gold>"),
|
||||
new PlaceholderReplacement("Else", "<green>Other day</green>")
|
||||
));
|
||||
}},
|
||||
false,
|
||||
1000,
|
||||
1000,
|
||||
@ -63,11 +70,12 @@ public class TabGroups implements ConfigValidator {
|
||||
public List<Group> groups = List.of(DEFAULT_GROUP);
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
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"));
|
||||
.orElseThrow(() -> new IllegalStateException("No group with name %s found".formatted(name)));
|
||||
}
|
||||
|
||||
public Optional<Group> getGroup(@NotNull String name) {
|
||||
@ -139,6 +147,10 @@ public class TabGroups implements ConfigValidator {
|
||||
if (group.sortingPlaceholders() == null) {
|
||||
missingKeys.put(group, "sortingPlaceholders");
|
||||
}
|
||||
|
||||
if (group.placeholderReplacements() == null) {
|
||||
missingKeys.put(group, "placeholderReplacements");
|
||||
}
|
||||
}
|
||||
|
||||
return missingKeys;
|
||||
@ -159,6 +171,7 @@ public class TabGroups implements ConfigValidator {
|
||||
group.nametag() == null ? DEFAULT_GROUP.nametag() : group.nametag(),
|
||||
group.servers() == null ? DEFAULT_GROUP.servers() : group.servers(),
|
||||
group.sortingPlaceholders() == null ? DEFAULT_GROUP.sortingPlaceholders() : group.sortingPlaceholders(),
|
||||
group.placeholderReplacements() == null ? DEFAULT_GROUP.placeholderReplacements() : group.placeholderReplacements(),
|
||||
group.collisions(),
|
||||
group.headerFooterUpdateRate(),
|
||||
group.placeholderUpdateRate(),
|
||||
|
@ -19,11 +19,15 @@
|
||||
|
||||
package net.william278.velocitab.hook;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import net.william278.papiproxybridge.api.PlaceholderAPI;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class PAPIProxyBridgeHook extends Hook {
|
||||
@ -41,4 +45,17 @@ public class PAPIProxyBridgeHook extends Hook {
|
||||
return api.formatPlaceholders(input, player.getUniqueId());
|
||||
}
|
||||
|
||||
public CompletableFuture<Map<String, String>> parsePlaceholders(@NotNull List<String> input, @NotNull Player player) {
|
||||
final Map<String, String> map = Maps.newConcurrentMap();
|
||||
final List<CompletableFuture<String>> futures = Lists.newArrayList();
|
||||
|
||||
for (String s : input) {
|
||||
final CompletableFuture<String> future = formatPlaceholders(s, player);
|
||||
futures.add(future);
|
||||
future.thenAccept(r -> map.put(s, r));
|
||||
}
|
||||
|
||||
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).thenApply(v -> map);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ public class VelocitabMiniExpansion {
|
||||
}
|
||||
|
||||
final String value = fixValue(popAll(queue));
|
||||
String replaced = Placeholder.replaceInternal(value, plugin, targetPlayer);
|
||||
final String replaced = Placeholder.replaceInternal(value, plugin, targetPlayer).first();
|
||||
|
||||
return Tag.selfClosingInserting(MiniMessage.miniMessage().deserialize(replaced, MiniPlaceholders.getAudienceGlobalPlaceholders(audience)));
|
||||
}));
|
||||
|
@ -49,10 +49,10 @@ public class MiniConditionManager {
|
||||
|
||||
public final static Map<String, String> REPLACE_2 = Map.of(
|
||||
"*LESS3*", "<",
|
||||
"*GREATER3*",">",
|
||||
"*GREATER3*", ">",
|
||||
"*LESS2*", "<",
|
||||
"*GREATER2*", ">"
|
||||
);
|
||||
);
|
||||
|
||||
private final static Map<String, String> REPLACE_3 = Map.of(
|
||||
"?dp?", ":"
|
||||
@ -110,7 +110,7 @@ public class MiniConditionManager {
|
||||
}
|
||||
|
||||
|
||||
condition = Placeholder.replaceInternal(condition, plugin, tabPlayer.get());
|
||||
condition = Placeholder.replaceInternal(condition, plugin, tabPlayer.get()).first();
|
||||
final String falseValue = processFalseValue(parameters.get(2));
|
||||
final String expression = buildExpression(condition);
|
||||
return evaluateAndFormatCondition(expression, target, audience, parameters.get(1), falseValue);
|
||||
|
@ -138,20 +138,31 @@ public final class PacketRegistration<P extends MinecraftPacket> {
|
||||
final MethodHandles.Lookup lookup = MethodHandles.lookup();
|
||||
try {
|
||||
final MethodHandles.Lookup stateRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.class, lookup);
|
||||
STATE_REGISTRY$clientBound = stateRegistryLookup.findGetter(StateRegistry.class, "clientbound", StateRegistry.PacketRegistry.class);
|
||||
STATE_REGISTRY$serverBound = stateRegistryLookup.findGetter(StateRegistry.class, "serverbound", StateRegistry.PacketRegistry.class);
|
||||
STATE_REGISTRY$clientBound = stateRegistryLookup.findGetter(
|
||||
StateRegistry.class, "clientbound", StateRegistry.PacketRegistry.class);
|
||||
STATE_REGISTRY$serverBound = stateRegistryLookup.findGetter(
|
||||
StateRegistry.class, "serverbound", StateRegistry.PacketRegistry.class);
|
||||
|
||||
final MethodType mapType = MethodType.methodType(StateRegistry.PacketMapping.class, Integer.TYPE, ProtocolVersion.class, Boolean.TYPE);
|
||||
PACKET_MAPPING$map = stateRegistryLookup.findStatic(StateRegistry.class, "map", mapType);
|
||||
final MethodType mapType = MethodType.methodType(
|
||||
StateRegistry.PacketMapping.class, Integer.TYPE, ProtocolVersion.class, Boolean.TYPE);
|
||||
PACKET_MAPPING$map = stateRegistryLookup.findStatic(
|
||||
StateRegistry.class, "map", mapType);
|
||||
|
||||
final MethodHandles.Lookup packetRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.class, lookup);
|
||||
final MethodType registerType = MethodType.methodType(void.class, Class.class, Supplier.class, StateRegistry.PacketMapping[].class);
|
||||
PACKET_REGISTRY$register = packetRegistryLookup.findVirtual(StateRegistry.PacketRegistry.class, "register", registerType);
|
||||
PACKET_REGISTRY$versions = packetRegistryLookup.findGetter(StateRegistry.PacketRegistry.class, "versions", Map.class);
|
||||
final MethodHandles.Lookup packetRegistryLookup = MethodHandles.privateLookupIn(
|
||||
StateRegistry.PacketRegistry.class, lookup);
|
||||
final MethodType registerType = MethodType.methodType(
|
||||
void.class, Class.class, Supplier.class, StateRegistry.PacketMapping[].class);
|
||||
PACKET_REGISTRY$register = packetRegistryLookup.findVirtual(
|
||||
StateRegistry.PacketRegistry.class, "register", registerType);
|
||||
PACKET_REGISTRY$versions = packetRegistryLookup.findGetter(
|
||||
StateRegistry.PacketRegistry.class, "versions", Map.class);
|
||||
|
||||
final MethodHandles.Lookup protocolRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.ProtocolRegistry.class, lookup);
|
||||
PACKET_REGISTRY$packetIdToSupplier = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetIdToSupplier", IntObjectMap.class);
|
||||
PACKET_REGISTRY$packetClassToId = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetClassToId", Object2IntMap.class);
|
||||
final MethodHandles.Lookup protocolRegistryLookup = MethodHandles.privateLookupIn(
|
||||
StateRegistry.PacketRegistry.ProtocolRegistry.class, lookup);
|
||||
PACKET_REGISTRY$packetIdToSupplier = protocolRegistryLookup.findGetter(
|
||||
StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetIdToSupplier", IntObjectMap.class);
|
||||
PACKET_REGISTRY$packetClassToId = protocolRegistryLookup.findGetter(
|
||||
StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetClassToId", Object2IntMap.class);
|
||||
|
||||
|
||||
} catch (Throwable e) {
|
||||
|
@ -20,12 +20,15 @@
|
||||
package net.william278.velocitab.packet;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -42,8 +45,8 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
|
||||
@Override
|
||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
||||
if (msg instanceof final UpdateTeamsPacket updateTeamsPacket && plugin.getSettings().isSendScoreboardPackets()) {
|
||||
final Optional<ScoreboardManager> scoreboardManager = plugin.getScoreboardManager();
|
||||
if (scoreboardManager.isEmpty()) {
|
||||
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
|
||||
if (!scoreboardManager.handleTeams()) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
@ -53,7 +56,7 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scoreboardManager.get().isInternalTeam(updateTeamsPacket.teamName())) {
|
||||
if (scoreboardManager.isInternalTeam(updateTeamsPacket.teamName())) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
@ -71,8 +74,8 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
|
||||
// Cancel packet if the backend is trying to send a team packet with an online player.
|
||||
// This is to prevent conflicts with Velocitab teams.
|
||||
plugin.getLogger().warn("Cancelled team \"{}\" packet from backend for player {}. " +
|
||||
"We suggest disabling \"send_scoreboard_packets\" in Velocitab's config.yml file, " +
|
||||
"but note this will disable TAB sorting",
|
||||
"We suggest disabling \"send_scoreboard_packets\" in Velocitab's config.yml file, " +
|
||||
"but note this will disable TAB sorting",
|
||||
updateTeamsPacket.teamName(), player.getUsername());
|
||||
return;
|
||||
}
|
||||
@ -83,7 +86,7 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
|
||||
|
||||
try {
|
||||
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
|
||||
if (tabPlayer.isEmpty()) {
|
||||
if (tabPlayer.isEmpty() && !isFutureTabPlayer()) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
@ -121,4 +124,14 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
|
||||
.filter(entry -> entry.getProfileId() != null && entry.getGameMode() == 3 && !entry.getProfileId().equals(player.getUniqueId()))
|
||||
.forEach(entry -> entry.setGameMode(0));
|
||||
}
|
||||
|
||||
private boolean isFutureTabPlayer() {
|
||||
final String serverName = player.getCurrentServer()
|
||||
.map(ServerConnection::getServerInfo)
|
||||
.map(ServerInfo::getName)
|
||||
.orElse("");
|
||||
|
||||
final Optional<Group> groupOptional = plugin.getTabList().getGroup(serverName);
|
||||
return groupOptional.isPresent();
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,10 @@ public class Protocol404Adapter extends TeamsPacketAdapter {
|
||||
private final GsonComponentSerializer serializer;
|
||||
|
||||
public Protocol404Adapter(@NotNull Velocitab plugin) {
|
||||
super(plugin, Set.of(ProtocolVersion.MINECRAFT_1_13_2,
|
||||
super(plugin, Set.of(
|
||||
ProtocolVersion.MINECRAFT_1_13,
|
||||
ProtocolVersion.MINECRAFT_1_13_1,
|
||||
ProtocolVersion.MINECRAFT_1_13_2,
|
||||
ProtocolVersion.MINECRAFT_1_14,
|
||||
ProtocolVersion.MINECRAFT_1_14_1,
|
||||
ProtocolVersion.MINECRAFT_1_14_2,
|
||||
|
@ -104,6 +104,7 @@ public class Protocol48Adapter extends TeamsPacketAdapter {
|
||||
/**
|
||||
* Returns a shortened version of the given string, with a maximum length of 16 characters.
|
||||
* This is used to ensure that the team name, display name, prefix and suffix are not too long for the client.
|
||||
*
|
||||
* @param string the string to be shortened
|
||||
* @return the shortened string
|
||||
*/
|
||||
|
@ -32,7 +32,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Adapter for handling the UpdateTeamsPacket for Minecraft 1.20.3-1.20.5
|
||||
* Adapter for handling the UpdateTeamsPacket for Minecraft 1.20.3-1.21.2
|
||||
*/
|
||||
public class Protocol765Adapter extends Protocol404Adapter {
|
||||
|
||||
@ -40,7 +40,9 @@ public class Protocol765Adapter extends Protocol404Adapter {
|
||||
super(plugin, Set.of(
|
||||
ProtocolVersion.MINECRAFT_1_20_3,
|
||||
ProtocolVersion.MINECRAFT_1_20_5,
|
||||
ProtocolVersion.MINECRAFT_1_21
|
||||
ProtocolVersion.MINECRAFT_1_21,
|
||||
ProtocolVersion.MINECRAFT_1_21_2,
|
||||
ProtocolVersion.MINECRAFT_1_21_4
|
||||
));
|
||||
}
|
||||
|
||||
@ -51,7 +53,9 @@ public class Protocol765Adapter extends Protocol404Adapter {
|
||||
|
||||
@NotNull
|
||||
protected Component readComponent(@NotNull ByteBuf buf) {
|
||||
return GsonComponentSerializer.gson().deserializeFromTree(ComponentHolder.deserialize(ProtocolUtils.readBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, null)));
|
||||
return GsonComponentSerializer.gson().deserializeFromTree(ComponentHolder.deserialize(
|
||||
ProtocolUtils.readBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, null)
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,18 +30,18 @@ import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.sorting.SortedSet;
|
||||
import net.william278.velocitab.tab.Nametag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.*;
|
||||
|
||||
@ -49,29 +49,43 @@ public class ScoreboardManager {
|
||||
|
||||
private PacketRegistration<UpdateTeamsPacket> packetRegistration;
|
||||
private final Velocitab plugin;
|
||||
private final Set<TeamsPacketAdapter> versions;
|
||||
private final boolean teams;
|
||||
private final Map<ProtocolVersion, TeamsPacketAdapter> versions;
|
||||
@Getter
|
||||
private final Map<UUID, String> createdTeams;
|
||||
private final Map<String, Nametag> nametags;
|
||||
private final Multimap<UUID, String> trackedTeams;
|
||||
@Getter
|
||||
private final SortedSet sortedTeams;
|
||||
|
||||
public ScoreboardManager(@NotNull Velocitab velocitab) {
|
||||
public ScoreboardManager(@NotNull Velocitab velocitab, boolean teams) {
|
||||
this.plugin = velocitab;
|
||||
this.teams = teams;
|
||||
this.createdTeams = Maps.newConcurrentMap();
|
||||
this.nametags = Maps.newConcurrentMap();
|
||||
this.versions = Sets.newHashSet();
|
||||
this.versions = Maps.newHashMap();
|
||||
this.trackedTeams = Multimaps.synchronizedMultimap(Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet));
|
||||
this.sortedTeams = new SortedSet(Comparator.reverseOrder());
|
||||
this.registerVersions();
|
||||
}
|
||||
|
||||
public boolean handleTeams() {
|
||||
return teams;
|
||||
}
|
||||
|
||||
private void registerVersions() {
|
||||
try {
|
||||
versions.add(new Protocol765Adapter(plugin));
|
||||
versions.add(new Protocol735Adapter(plugin));
|
||||
versions.add(new Protocol404Adapter(plugin));
|
||||
versions.add(new Protocol48Adapter(plugin));
|
||||
final Protocol765Adapter protocol765Adapter = new Protocol765Adapter(plugin);
|
||||
protocol765Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol765Adapter));
|
||||
final Protocol735Adapter protocol735Adapter = new Protocol735Adapter(plugin);
|
||||
protocol735Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol735Adapter));
|
||||
final Protocol404Adapter protocol404Adapter = new Protocol404Adapter(plugin);
|
||||
protocol404Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol404Adapter));
|
||||
final Protocol48Adapter protocol48Adapter = new Protocol48Adapter(plugin);
|
||||
protocol48Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol48Adapter));
|
||||
} catch (NoSuchFieldError e) {
|
||||
throw new IllegalStateException("Failed to register Scoreboard Teams packets." +
|
||||
" Velocitab probably does not (yet) support your Proxy version.", e);
|
||||
" Velocitab probably does not (yet) support your Proxy version.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,11 +93,13 @@ public class ScoreboardManager {
|
||||
return nametags.containsKey(teamName);
|
||||
}
|
||||
|
||||
public int getPosition(@NotNull String teamName) {
|
||||
return sortedTeams.getPosition(teamName);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public TeamsPacketAdapter getPacketAdapter(@NotNull ProtocolVersion version) {
|
||||
return versions.stream()
|
||||
.filter(adapter -> adapter.getProtocolVersions().contains(version))
|
||||
.findFirst()
|
||||
return Optional.ofNullable(versions.get(version))
|
||||
.orElseThrow(() -> new IllegalArgumentException("No adapter found for protocol version " + version));
|
||||
}
|
||||
|
||||
@ -94,6 +110,7 @@ public class ScoreboardManager {
|
||||
public void resetCache(@NotNull Player player) {
|
||||
final String team = createdTeams.remove(player.getUniqueId());
|
||||
if (team != null) {
|
||||
removeSortedTeam(team);
|
||||
plugin.getTabList().getTabPlayer(player).ifPresent(tabPlayer ->
|
||||
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), tabPlayer)
|
||||
);
|
||||
@ -104,10 +121,18 @@ public class ScoreboardManager {
|
||||
public void resetCache(@NotNull Player player, @NotNull Group group) {
|
||||
final String team = createdTeams.remove(player.getUniqueId());
|
||||
if (team != null) {
|
||||
removeSortedTeam(team);
|
||||
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), group);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeSortedTeam(@NotNull String teamName) {
|
||||
final boolean result = sortedTeams.removeTeam(teamName);
|
||||
if (!result) {
|
||||
plugin.log(Level.ERROR, "Failed to remove team " + teamName + " from sortedTeams");
|
||||
}
|
||||
}
|
||||
|
||||
public void vanishPlayer(@NotNull TabPlayer tabPlayer) {
|
||||
this.handleVanish(tabPlayer, true);
|
||||
}
|
||||
@ -127,12 +152,13 @@ public class ScoreboardManager {
|
||||
return;
|
||||
}
|
||||
final Set<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
|
||||
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
|
||||
|
||||
final Optional<Nametag> cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null));
|
||||
cachedTag.ifPresent(nametag -> siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player)
|
||||
.forEach(connected -> {
|
||||
if (vanish && !plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername())) {
|
||||
dispatchPacket(UpdateTeamsPacket.removeTeam(plugin, teamName), connected);
|
||||
sendPacket(connected, UpdateTeamsPacket.removeTeam(plugin, teamName), isNameTagEmpty);
|
||||
trackedTeams.remove(connected.getUniqueId(), teamName);
|
||||
} else {
|
||||
dispatchGroupCreatePacket(plugin, tabPlayer, teamName, nametag, player.getUsername());
|
||||
@ -147,14 +173,14 @@ public class ScoreboardManager {
|
||||
* @param role The new role of the player. Must not be null.
|
||||
* @param force Whether to force the update even if the player's nametag is the same.
|
||||
*/
|
||||
public void updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
|
||||
public CompletableFuture<Void> updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
|
||||
final Player player = tabPlayer.getPlayer();
|
||||
if (!player.isActive()) {
|
||||
plugin.getTabList().removeOfflinePlayer(player);
|
||||
return;
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
final String name = player.getUsername();
|
||||
final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
tabPlayer.getNametag(plugin).thenAccept(newTag -> {
|
||||
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
|
||||
if (createdTeams.containsKey(player.getUniqueId())) {
|
||||
@ -163,8 +189,15 @@ public class ScoreboardManager {
|
||||
tabPlayer
|
||||
);
|
||||
}
|
||||
|
||||
final String oldRole = createdTeams.remove(player.getUniqueId());
|
||||
if (oldRole != null) {
|
||||
removeSortedTeam(oldRole);
|
||||
}
|
||||
createdTeams.put(player.getUniqueId(), role);
|
||||
final boolean a = sortedTeams.addTeam(role);
|
||||
if (!a) {
|
||||
plugin.log(Level.ERROR, "Failed to add team " + role + " to sortedTeams");
|
||||
}
|
||||
this.nametags.put(role, newTag);
|
||||
dispatchGroupCreatePacket(plugin, tabPlayer, role, newTag, name);
|
||||
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) {
|
||||
@ -173,10 +206,13 @@ public class ScoreboardManager {
|
||||
} else {
|
||||
updatePlaceholders(tabPlayer);
|
||||
}
|
||||
future.complete(null);
|
||||
}).exceptionally(e -> {
|
||||
plugin.log(Level.ERROR, "Failed to update role for " + player.getUsername(), e);
|
||||
return null;
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public void updatePlaceholders(@NotNull TabPlayer tabPlayer) {
|
||||
@ -192,6 +228,9 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
public void resendAllTeams(@NotNull TabPlayer tabPlayer) {
|
||||
if (!teams) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.getSettings().isSendScoreboardPackets()) {
|
||||
return;
|
||||
}
|
||||
@ -238,6 +277,9 @@ public class ScoreboardManager {
|
||||
private void dispatchGroupCreatePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
|
||||
@NotNull String teamName, @NotNull Nametag nametag,
|
||||
@NotNull String... teamMembers) {
|
||||
if (!teams) {
|
||||
return;
|
||||
}
|
||||
tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> {
|
||||
if (!viewer.getPlayer().isActive()) {
|
||||
return;
|
||||
@ -252,6 +294,9 @@ public class ScoreboardManager {
|
||||
@NotNull String teamName, @NotNull Nametag nametag,
|
||||
@NotNull TabPlayer viewer,
|
||||
@NotNull String... teamMembers) {
|
||||
if (!teams) {
|
||||
return;
|
||||
}
|
||||
final boolean canSee = plugin.getVanishManager().canSee(viewer.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername());
|
||||
if (!canSee) {
|
||||
return;
|
||||
@ -259,12 +304,17 @@ public class ScoreboardManager {
|
||||
|
||||
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(plugin, tabPlayer, teamName, nametag, viewer, teamMembers);
|
||||
trackedTeams.put(viewer.getPlayer().getUniqueId(), teamName);
|
||||
dispatchPacket(packet, viewer.getPlayer());
|
||||
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
|
||||
sendPacket(viewer.getPlayer(), packet, isNameTagEmpty);
|
||||
}
|
||||
|
||||
private void dispatchGroupChangePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
|
||||
@NotNull String teamName,
|
||||
@NotNull Nametag nametag) {
|
||||
if (!teams) {
|
||||
return;
|
||||
}
|
||||
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
|
||||
tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> {
|
||||
if (viewer == tabPlayer || !viewer.getPlayer().isActive()) {
|
||||
return;
|
||||
@ -289,30 +339,20 @@ public class ScoreboardManager {
|
||||
return;
|
||||
}
|
||||
tabPlayer.setRelationalNametag(viewer.getPlayer().getUniqueId(), prefix, suffix);
|
||||
dispatchPacket(packet, viewer.getPlayer());
|
||||
sendPacket(viewer.getPlayer(), packet, isNameTagEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchPacket(@NotNull UpdateTeamsPacket packet, @NotNull Player player) {
|
||||
if (!player.isActive()) {
|
||||
plugin.getTabList().removeOfflinePlayer(player);
|
||||
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Group group) {
|
||||
if (!teams) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
|
||||
connectedPlayer.getConnection().write(packet);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Group group) {
|
||||
final boolean isRemove = packet.isRemoveTeam();
|
||||
final boolean isNameTagEmpty = group.nametag().isEmpty();
|
||||
group.registeredServers(plugin).forEach(server -> server.getPlayersConnected().forEach(connected -> {
|
||||
try {
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected;
|
||||
connectedPlayer.getConnection().write(packet);
|
||||
sendPacket(connected, packet, isNameTagEmpty);
|
||||
if (isRemove) {
|
||||
trackedTeams.remove(connected.getUniqueId(), packet.teamName());
|
||||
}
|
||||
@ -323,6 +363,9 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull TabPlayer tabPlayer) {
|
||||
if (!teams) {
|
||||
return;
|
||||
}
|
||||
final Player player = tabPlayer.getPlayer();
|
||||
final Optional<ServerConnection> optionalServerConnection = player.getCurrentServer();
|
||||
if (optionalServerConnection.isEmpty()) {
|
||||
@ -330,6 +373,7 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
final Set<Player> players = tabPlayer.getGroup().getPlayers(plugin);
|
||||
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
|
||||
players.forEach(connected -> {
|
||||
try {
|
||||
final boolean canSee = plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername());
|
||||
@ -337,15 +381,30 @@ public class ScoreboardManager {
|
||||
return;
|
||||
}
|
||||
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected;
|
||||
connectedPlayer.getConnection().write(packet);
|
||||
sendPacket(connected, packet, isNameTagEmpty);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendPacket(@NotNull Player player, @NotNull UpdateTeamsPacket packet, boolean isNameTagEmpty) {
|
||||
if (!player.isActive()) {
|
||||
plugin.getTabList().removeOfflinePlayer(player);
|
||||
return;
|
||||
}
|
||||
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2) && isNameTagEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
|
||||
connectedPlayer.getConnection().write(packet);
|
||||
}
|
||||
|
||||
public void registerPacket() {
|
||||
if (!teams) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
packetRegistration = PacketRegistration.of(UpdateTeamsPacket.class)
|
||||
.direction(ProtocolUtils.Direction.CLIENTBOUND)
|
||||
@ -362,7 +421,8 @@ public class ScoreboardManager {
|
||||
.mapping(0x5A, MINECRAFT_1_19_4, false)
|
||||
.mapping(0x5C, MINECRAFT_1_20_2, false)
|
||||
.mapping(0x5E, MINECRAFT_1_20_3, false)
|
||||
.mapping(0x60, MINECRAFT_1_20_5, false);
|
||||
.mapping(0x60, MINECRAFT_1_20_5, false)
|
||||
.mapping(0x67, MINECRAFT_1_21_2, false);
|
||||
packetRegistration.register();
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.ERROR, "Failed to register UpdateTeamsPacket", e);
|
||||
@ -389,6 +449,9 @@ public class ScoreboardManager {
|
||||
* @param canSee A boolean indicating whether the player can see the target player.
|
||||
*/
|
||||
public void recalculateVanishForPlayer(TabPlayer tabPlayer, TabPlayer target, boolean canSee) {
|
||||
if (!teams) {
|
||||
return;
|
||||
}
|
||||
final Player player = tabPlayer.getPlayer();
|
||||
final String team = createdTeams.get(target.getPlayer().getUniqueId());
|
||||
if (team == null) {
|
||||
@ -396,7 +459,8 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
final UpdateTeamsPacket removeTeam = UpdateTeamsPacket.removeTeam(plugin, team);
|
||||
dispatchPacket(removeTeam, player);
|
||||
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
|
||||
sendPacket(player, removeTeam, isNameTagEmpty);
|
||||
trackedTeams.remove(player.getUniqueId(), team);
|
||||
|
||||
if (canSee) {
|
||||
|
@ -36,7 +36,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@ -208,22 +207,14 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
final Optional<ScoreboardManager> optionalManager = plugin.getScoreboardManager();
|
||||
if (optionalManager.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
optionalManager.get().getPacketAdapter(protocolVersion).decode(byteBuf, this, protocolVersion);
|
||||
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
|
||||
scoreboardManager.getPacketAdapter(protocolVersion).decode(byteBuf, this, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
final Optional<ScoreboardManager> optionalManager = plugin.getScoreboardManager();
|
||||
if (optionalManager.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
optionalManager.get().getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion);
|
||||
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
|
||||
scoreboardManager.getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -30,7 +30,7 @@ import java.util.Optional;
|
||||
@RequiredArgsConstructor
|
||||
public class Role implements Comparable<Role> {
|
||||
|
||||
public static final int DEFAULT_WEIGHT = 0;
|
||||
public static final int DEFAULT_WEIGHT = -1;
|
||||
public static final Role DEFAULT_ROLE = new Role(DEFAULT_WEIGHT, null, null, null, null);
|
||||
@Getter
|
||||
private final int weight;
|
||||
@ -65,8 +65,11 @@ public class Role implements Comparable<Role> {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected String getWeightString() {
|
||||
return Integer.toString(weight);
|
||||
protected Optional<String> getWeightString() {
|
||||
if (weight == -1) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(Integer.toString(weight));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -36,7 +36,10 @@ import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@ -58,10 +61,13 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
private final Map<UUID, Component> relationalDisplayNames;
|
||||
private final Map<UUID, Component[]> relationalNametags;
|
||||
private final Map<String, String> cachedPlaceholders;
|
||||
private final Map<UUID, Integer> cachedListOrders;
|
||||
private String lastDisplayName;
|
||||
private Component lastHeader;
|
||||
private Component lastFooter;
|
||||
private String teamName;
|
||||
@Setter
|
||||
private int listOrder = -1;
|
||||
@Nullable
|
||||
@Setter
|
||||
private UpdateTeamsPacket.TeamColor teamColor;
|
||||
@ -86,10 +92,11 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
this.relationalDisplayNames = Maps.newConcurrentMap();
|
||||
this.relationalNametags = Maps.newConcurrentMap();
|
||||
this.cachedPlaceholders = Maps.newConcurrentMap();
|
||||
this.cachedListOrders = Maps.newConcurrentMap();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getRoleWeightString() {
|
||||
public Optional<String> getRoleWeightString() {
|
||||
return getRole().getWeightString();
|
||||
}
|
||||
|
||||
@ -116,18 +123,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
return plugin.getTabGroups().getPosition(group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name of the server the player is currently on.
|
||||
* Affected by server aliases defined in the config.
|
||||
*
|
||||
* @param plugin The plugin instance
|
||||
* @return The display name of the server
|
||||
*/
|
||||
@NotNull
|
||||
public String getServerDisplayName(@NotNull Velocitab plugin) {
|
||||
return plugin.getSettings().getServerDisplayName(getServerName());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CompletableFuture<String> getDisplayName(@NotNull Velocitab plugin) {
|
||||
final String format = formatGroup();
|
||||
@ -159,7 +154,7 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
displayName = displayName.replace(placeholder, value);
|
||||
}
|
||||
|
||||
displayName = Placeholder.replaceInternal(displayName, plugin, this);
|
||||
displayName = Placeholder.replaceInternal(displayName, plugin, this).first();
|
||||
return lastDisplayName = displayName;
|
||||
}
|
||||
|
||||
@ -237,6 +232,10 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
relationalNametags.remove(target);
|
||||
}
|
||||
|
||||
public void unsetTabListOrder(@NotNull UUID target) {
|
||||
cachedListOrders.remove(target);
|
||||
}
|
||||
|
||||
public Optional<Component[]> getRelationalNametag(@NotNull UUID target) {
|
||||
return Optional.ofNullable(relationalNametags.get(target));
|
||||
}
|
||||
@ -248,6 +247,8 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
lastHeader = null;
|
||||
lastFooter = null;
|
||||
role = Role.DEFAULT_ROLE;
|
||||
teamName = null;
|
||||
cachedListOrders.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,7 +263,7 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
@Override
|
||||
public int compareTo(@NotNull TabPlayer o) {
|
||||
final int roleDifference = role.compareTo(o.role);
|
||||
if (roleDifference == 0) {
|
||||
if (roleDifference <= 0) {
|
||||
return player.getUsername().compareTo(o.player.getUsername());
|
||||
}
|
||||
return roleDifference;
|
||||
|
@ -30,6 +30,7 @@ public interface LoggerProvider {
|
||||
*
|
||||
* @return the logger for the class
|
||||
*/
|
||||
@NotNull
|
||||
Logger getLogger();
|
||||
|
||||
/**
|
||||
|
@ -36,7 +36,8 @@ public interface MetricProvider {
|
||||
|
||||
/**
|
||||
* Retrieves the Velocitab plugin instance.
|
||||
* @return
|
||||
*
|
||||
* @return The Velocitab plugin instance.
|
||||
*/
|
||||
Velocitab getPlugin();
|
||||
|
||||
|
@ -24,7 +24,6 @@ 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 {
|
||||
@ -37,11 +36,10 @@ public interface ScoreboardProvider {
|
||||
Velocitab getPlugin();
|
||||
|
||||
/**
|
||||
* Retrieves the optional scoreboard manager.
|
||||
*
|
||||
* @return An {@code Optional} object that may contain a {@code ScoreboardManager} instance.
|
||||
* Retrieves the scoreboard manager
|
||||
* @return The scoreboard manager
|
||||
*/
|
||||
Optional<ScoreboardManager> getScoreboardManager();
|
||||
ScoreboardManager getScoreboardManager();
|
||||
|
||||
/**
|
||||
* Sets the scoreboard manager.
|
||||
@ -85,11 +83,9 @@ public interface ScoreboardProvider {
|
||||
*
|
||||
*/
|
||||
default void prepareScoreboard() {
|
||||
if (getPlugin().getSettings().isSendScoreboardPackets()) {
|
||||
final ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin());
|
||||
setScoreboardManager(scoreboardManager);
|
||||
scoreboardManager.registerPacket();
|
||||
}
|
||||
final ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin(), getPlugin().getSettings().isSendScoreboardPackets());
|
||||
setScoreboardManager(scoreboardManager);
|
||||
scoreboardManager.registerPacket();
|
||||
|
||||
final PlayerTabList tabList = new PlayerTabList(getPlugin());
|
||||
setTabList(tabList);
|
||||
@ -105,10 +101,8 @@ public interface ScoreboardProvider {
|
||||
* 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();
|
||||
}
|
||||
getScoreboardManager().close();
|
||||
getScoreboardManager().unregisterPacket();
|
||||
|
||||
getTabList().close();
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.sorting;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
|
||||
public class SortedSet {
|
||||
|
||||
private final ConcurrentSkipListSet<String> sortedTeams;
|
||||
private final Map<String, Integer> positionMap;
|
||||
|
||||
public SortedSet(@NotNull Comparator<String> comparator) {
|
||||
sortedTeams = new ConcurrentSkipListSet<>(comparator);
|
||||
positionMap = Maps.newConcurrentMap();
|
||||
}
|
||||
|
||||
public synchronized boolean addTeam(@NotNull String teamName) {
|
||||
final boolean result = sortedTeams.add(teamName);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
updatePositions();
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean removeTeam(@NotNull String teamName) {
|
||||
final boolean result = sortedTeams.remove(teamName);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
updatePositions();
|
||||
return true;
|
||||
}
|
||||
|
||||
private synchronized void updatePositions() {
|
||||
int index = 0;
|
||||
positionMap.clear();
|
||||
for (final String team : sortedTeams) {
|
||||
positionMap.put(team, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized int getPosition(@NotNull String teamName) {
|
||||
return positionMap.getOrDefault(teamName, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return sortedTeams.toString();
|
||||
}
|
||||
}
|
@ -22,7 +22,8 @@ package net.william278.velocitab.tab;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public record GroupTasks(@Nullable ScheduledTask updateTask, @Nullable ScheduledTask headerFooterTask, @Nullable ScheduledTask latencyTask) {
|
||||
public record GroupTasks(@Nullable ScheduledTask updateTask, @Nullable ScheduledTask headerFooterTask,
|
||||
@Nullable ScheduledTask latencyTask) {
|
||||
|
||||
public void cancel() {
|
||||
if (updateTask != null) {
|
||||
|
@ -32,13 +32,13 @@ public record Nametag(@NotNull String prefix, @NotNull String suffix) {
|
||||
|
||||
@NotNull
|
||||
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
|
||||
final String formatted = Placeholder.replaceInternal(prefix, plugin, tabPlayer);
|
||||
final String formatted = Placeholder.replaceInternal(prefix, plugin, tabPlayer).first();
|
||||
return plugin.getFormatter().format(formatted, tabPlayer, target, plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
|
||||
final String formatted = Placeholder.replaceInternal(suffix, plugin, tabPlayer);
|
||||
final String formatted = Placeholder.replaceInternal(suffix, plugin, tabPlayer).first();
|
||||
return plugin.getFormatter().format(formatted, tabPlayer, target, plugin);
|
||||
}
|
||||
|
||||
@ -50,4 +50,8 @@ public record Nametag(@NotNull String prefix, @NotNull String suffix) {
|
||||
return (prefix.equals(other.prefix)) && (suffix.equals(other.suffix));
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return prefix.isEmpty() && suffix.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.player.TabList;
|
||||
import com.velocitypowered.api.proxy.player.TabListEntry;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
|
||||
import com.velocitypowered.proxy.tablist.KeyedVelocityTabList;
|
||||
import com.velocitypowered.proxy.tablist.VelocityTabList;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
@ -34,6 +39,7 @@ import net.william278.velocitab.api.PlayerAddedToTabEvent;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.config.Placeholder;
|
||||
import net.william278.velocitab.config.ServerUrl;
|
||||
import net.william278.velocitab.packet.ScoreboardManager;
|
||||
import net.william278.velocitab.player.Role;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -46,25 +52,51 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER;
|
||||
|
||||
/**
|
||||
* The main class for tracking the server TAB list
|
||||
* The main class for tracking the server TAB list for a map of {@link TabPlayer}s
|
||||
*/
|
||||
public class PlayerTabList {
|
||||
|
||||
private final Velocitab plugin;
|
||||
@Getter
|
||||
private final VanishTabList vanishTabList;
|
||||
@Getter(value = AccessLevel.PUBLIC)
|
||||
private final Map<UUID, TabPlayer> players;
|
||||
private final TaskManager taskManager;
|
||||
private final Map<Class<?>, Field> entriesFields;
|
||||
|
||||
public PlayerTabList(@NotNull Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.vanishTabList = new VanishTabList(plugin, this);
|
||||
this.players = Maps.newConcurrentMap();
|
||||
this.taskManager = new TaskManager(plugin);
|
||||
this.entriesFields = Maps.newHashMap();
|
||||
this.reloadUpdate();
|
||||
this.registerListener();
|
||||
this.ensureDisplayNameTask();
|
||||
this.registerFields();
|
||||
}
|
||||
|
||||
// VelocityTabListLegacy is not supported
|
||||
private void registerFields() {
|
||||
final Class<KeyedVelocityTabList> keyedVelocityTabListClass = KeyedVelocityTabList.class;
|
||||
final Class<VelocityTabList> velocityTabListClass = VelocityTabList.class;
|
||||
try {
|
||||
final Field entriesField = keyedVelocityTabListClass.getDeclaredField("entries");
|
||||
entriesField.setAccessible(true);
|
||||
this.entriesFields.put(keyedVelocityTabListClass, entriesField);
|
||||
} catch (NoSuchFieldException e) {
|
||||
plugin.log(Level.ERROR, "Failed to register KeyedVelocityTabList field", e);
|
||||
}
|
||||
try {
|
||||
final Field entriesField = velocityTabListClass.getDeclaredField("entries");
|
||||
entriesField.setAccessible(true);
|
||||
this.entriesFields.put(velocityTabListClass, entriesField);
|
||||
} catch (NoSuchFieldException e) {
|
||||
plugin.log(Level.ERROR, "Failed to register VelocityTabList field", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerListener() {
|
||||
@ -141,6 +173,7 @@ public class PlayerTabList {
|
||||
players.values().forEach(p -> {
|
||||
p.unsetRelationalDisplayName(player.getUniqueId());
|
||||
p.unsetRelationalNametag(player.getUniqueId());
|
||||
p.unsetTabListOrder(player.getUniqueId());
|
||||
});
|
||||
}
|
||||
|
||||
@ -220,10 +253,9 @@ public class PlayerTabList {
|
||||
}
|
||||
iteratedPlayer.sendHeaderAndFooter(this);
|
||||
}
|
||||
plugin.getScoreboardManager().ifPresent(s -> {
|
||||
s.resendAllTeams(tabPlayer);
|
||||
tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(tabPlayer, t, false));
|
||||
});
|
||||
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
|
||||
scoreboardManager.resendAllTeams(tabPlayer);
|
||||
updateSorting(tabPlayer, false);
|
||||
fixDuplicateEntries(joined);
|
||||
// Fire event without listening for result
|
||||
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group));
|
||||
@ -269,8 +301,11 @@ public class PlayerTabList {
|
||||
@SuppressWarnings("unchecked")
|
||||
private void fixDuplicateEntries(@NotNull Player target) {
|
||||
try {
|
||||
final Field entriesField = target.getTabList().getClass().getDeclaredField("entries");
|
||||
entriesField.setAccessible(true);
|
||||
final Optional<Field> optionalField = Optional.ofNullable(this.entriesFields.get(target.getTabList().getClass()));
|
||||
if (optionalField.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final Field entriesField = optionalField.get();
|
||||
final Map<UUID, TabListEntry> entries = (Map<UUID, TabListEntry>) entriesField.get(target.getTabList());
|
||||
entries.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getProfile() != null)
|
||||
@ -278,7 +313,7 @@ public class PlayerTabList {
|
||||
.filter(entry -> !entry.getKey().equals(target.getUniqueId()))
|
||||
.forEach(entry -> target.getTabList().removeEntry(entry.getKey()));
|
||||
} catch (Throwable error) {
|
||||
plugin.log(Level.ERROR, "Failed to fix duplicate entries for class " + target.getTabList().getClass().getName() , error);
|
||||
plugin.log(Level.ERROR, "Failed to fix duplicate entries for class " + target.getTabList().getClass().getName(), error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,12 +323,13 @@ public class PlayerTabList {
|
||||
|
||||
/**
|
||||
* Remove a player from the tab list
|
||||
* @param uuid
|
||||
*
|
||||
* @param uuid {@link UUID} of the {@link TabPlayer player} to remove
|
||||
*/
|
||||
protected void removeTablistUUID(@NotNull UUID uuid) {
|
||||
getPlayers().forEach((key, value) -> {
|
||||
value.getPlayer().getTabList().getEntry(uuid).ifPresent(entry -> value.getPlayer().getTabList().removeEntry(uuid));
|
||||
});
|
||||
protected void removeTabListUUID(@NotNull UUID uuid) {
|
||||
getPlayers().forEach((key, value) -> value.getPlayer().getTabList().getEntry(uuid).ifPresent(
|
||||
entry -> value.getPlayer().getTabList().removeEntry(uuid)
|
||||
));
|
||||
}
|
||||
|
||||
protected void removePlayer(@NotNull Player target, @Nullable RegisteredServer server) {
|
||||
@ -318,7 +354,7 @@ public class PlayerTabList {
|
||||
.delay(250, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
// Delete player team
|
||||
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(target));
|
||||
plugin.getScoreboardManager().resetCache(target);
|
||||
//remove player from tab list cache
|
||||
getPlayers().remove(uuid);
|
||||
}
|
||||
@ -389,13 +425,25 @@ public class PlayerTabList {
|
||||
return;
|
||||
}
|
||||
|
||||
updateSorting(tabPlayer, force);
|
||||
}
|
||||
|
||||
private void updateSorting(@NotNull TabPlayer tabPlayer, boolean force) {
|
||||
tabPlayer.getTeamName(plugin).thenAccept(teamName -> {
|
||||
if (teamName.isBlank()) {
|
||||
return;
|
||||
}
|
||||
plugin.getScoreboardManager().ifPresent(manager -> manager.updateRole(
|
||||
tabPlayer, teamName, force
|
||||
));
|
||||
plugin.getScoreboardManager().updateRole(tabPlayer, teamName, force).thenAccept(v -> {
|
||||
final int order = plugin.getScoreboardManager().getPosition(teamName);
|
||||
if (order == -1) {
|
||||
plugin.log(Level.ERROR, "Failed to get position for " + tabPlayer.getPlayer().getUsername());
|
||||
return;
|
||||
}
|
||||
|
||||
tabPlayer.setListOrder(order);
|
||||
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
|
||||
players.forEach(p -> recalculateSortingForPlayer(p, players));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -519,6 +567,10 @@ public class PlayerTabList {
|
||||
return plugin.getTabGroups().getGroupFromServer(serverName, plugin);
|
||||
}
|
||||
|
||||
public void removeOldEntry(@NotNull Group group, @NotNull UUID uuid) {
|
||||
final Set<TabPlayer> players = group.getTabPlayers(plugin);
|
||||
players.forEach(player -> player.getPlayer().getTabList().removeEntry(uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an offline player from the list of tracked TAB players
|
||||
@ -529,4 +581,44 @@ public class PlayerTabList {
|
||||
players.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the player can use server-side specified TAB list ordering (Minecraft 1.21.2+)
|
||||
*
|
||||
* @param tabPlayer player to check
|
||||
* @return {@code true} if the user is on Minecraft 1.21.2+; {@code false}
|
||||
*/
|
||||
private boolean hasListOrder(@NotNull TabPlayer tabPlayer) {
|
||||
return tabPlayer.getPlayer().getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2);
|
||||
}
|
||||
|
||||
private void updateSorting(@NotNull TabPlayer tabPlayer, @NotNull UUID uuid, int position) {
|
||||
if (!tabPlayer.getPlayer().getTabList().containsEntry(uuid)) {
|
||||
return;
|
||||
}
|
||||
if (tabPlayer.getCachedListOrders().containsKey(uuid) && tabPlayer.getCachedListOrders().get(uuid) == position) {
|
||||
return;
|
||||
}
|
||||
tabPlayer.getCachedListOrders().put(uuid, position);
|
||||
final UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(UPDATE_LIST_ORDER);
|
||||
final UpsertPlayerInfoPacket.Entry entry = new UpsertPlayerInfoPacket.Entry(uuid);
|
||||
entry.setListOrder(position);
|
||||
packet.addEntry(entry);
|
||||
((ConnectedPlayer) tabPlayer.getPlayer()).getConnection().write(packet);
|
||||
}
|
||||
|
||||
private String getPlayerName(UUID uuid) {
|
||||
return plugin.getServer().getPlayer(uuid).map(Player::getUsername).orElse("Unknown");
|
||||
}
|
||||
|
||||
public synchronized void recalculateSortingForPlayer(@NotNull TabPlayer tabPlayer, @NotNull Set<TabPlayer> players) {
|
||||
if (!hasListOrder(tabPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
players.forEach(p -> {
|
||||
final int order = p.getListOrder();
|
||||
updateSorting(tabPlayer, p.getPlayer().getUniqueId(), order);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -48,7 +48,8 @@ public class TabListListener {
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final PlayerTabList tabList;
|
||||
// In 1.8 there is a packet delay problem
|
||||
|
||||
// Set of UUIDs of users who just left the game - fixes packet delay problem on Minecraft 1.8.x
|
||||
private final Set<UUID> justQuit;
|
||||
|
||||
public TabListListener(@NotNull Velocitab plugin, @NotNull PlayerTabList tabList) {
|
||||
@ -91,13 +92,22 @@ public class TabListListener {
|
||||
.map(ServerInfo::getName)
|
||||
.orElse("");
|
||||
|
||||
final Optional<Group> previousGroup = tabList.getTabPlayer(joined)
|
||||
.map(TabPlayer::getGroup);
|
||||
|
||||
// Get the group the player should now be in
|
||||
final @NotNull Optional<Group> groupOptional = tabList.getGroup(serverName);
|
||||
final boolean isDefault = groupOptional.map(g -> g.isDefault(plugin)).orElse(false);
|
||||
final boolean isDefault = groupOptional.map(g -> g.isDefault(plugin)).orElse(true);
|
||||
|
||||
// Removes cached relational data of the joined player from all other players
|
||||
plugin.getTabList().clearCachedData(joined);
|
||||
|
||||
if (!plugin.getSettings().isShowAllPlayersFromAllGroups() && previousGroup.isPresent()
|
||||
&& (groupOptional.isPresent() && !previousGroup.get().equals(groupOptional.get())
|
||||
|| groupOptional.isEmpty())) {
|
||||
tabList.removeOldEntry(previousGroup.get(), joined.getUniqueId());
|
||||
}
|
||||
|
||||
// If the server is not in a group, use fallback.
|
||||
// If fallback is disabled, permit the player to switch excluded servers without a header or footer override
|
||||
if (isDefault && !plugin.getSettings().isFallbackEnabled() && !groupOptional.map(g -> g.containsServer(plugin, serverName)).orElse(false)) {
|
||||
@ -135,20 +145,21 @@ public class TabListListener {
|
||||
}
|
||||
|
||||
final Group group = groupOptional.get();
|
||||
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group));
|
||||
if (justQuit.contains(joined.getUniqueId())) {
|
||||
plugin.getServer().getScheduler().buildTask(plugin,
|
||||
() -> tabList.joinPlayer(joined, group))
|
||||
.delay(250, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
return;
|
||||
}
|
||||
plugin.getScoreboardManager().resetCache(joined, group);
|
||||
|
||||
tabList.joinPlayer(joined, group);
|
||||
final int delay = justQuit.contains(joined.getUniqueId()) ? 100 : 250;
|
||||
plugin.getServer().getScheduler().buildTask(plugin,
|
||||
() -> tabList.joinPlayer(joined, group))
|
||||
.delay(delay, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
@SuppressWarnings("deprecation")
|
||||
@Subscribe(order = PostOrder.CUSTOM, priority = Short.MIN_VALUE)
|
||||
public void onPlayerQuit(@NotNull DisconnectEvent event) {
|
||||
if (event.getLoginStatus() == DisconnectEvent.LoginStatus.CONFLICTING_LOGIN) {
|
||||
return;
|
||||
}
|
||||
if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) {
|
||||
checkDelayedDisconnect(event);
|
||||
return;
|
||||
@ -169,7 +180,8 @@ public class TabListListener {
|
||||
return;
|
||||
}
|
||||
|
||||
tabList.removeTablistUUID(event.getPlayer().getUniqueId());
|
||||
tabList.removeOfflinePlayer(player);
|
||||
tabList.removeTabListUUID(event.getPlayer().getUniqueId());
|
||||
}).delay(750, TimeUnit.MILLISECONDS).schedule();
|
||||
}
|
||||
|
||||
|
@ -89,21 +89,21 @@ public class VanishTabList {
|
||||
final String serverName = target.getServerName();
|
||||
|
||||
if (tabPlayer.getGroup().onlyListPlayersInSameServer()
|
||||
&& !tabPlayer.getServerName().equals(serverName)) {
|
||||
&& !tabPlayer.getServerName().equals(serverName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) ||
|
||||
plugin.getVanishManager().canSee(player.getUsername(), 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));
|
||||
plugin.getScoreboardManager().recalculateVanishForPlayer(tabPlayer, target, false);
|
||||
} else {
|
||||
if (!player.getTabList().containsEntry(p.getUniqueId())) {
|
||||
final TabListEntry tabListEntry = tabList.createEntry(target, player.getTabList(), tabPlayer);
|
||||
player.getTabList().addEntry(tabListEntry);
|
||||
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, true));
|
||||
plugin.getScoreboardManager().recalculateVanishForPlayer(tabPlayer, target, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -32,4 +32,5 @@ public interface QuadFunction<T, U, V, S, R> {
|
||||
* @return the function result
|
||||
*/
|
||||
R apply(T t, U u, V v, S s);
|
||||
|
||||
}
|
@ -21,7 +21,7 @@ package net.william278.velocitab.util;
|
||||
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
|
||||
public final class SerializerUtil {
|
||||
public final class SerializationUtil {
|
||||
|
||||
public final static LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder()
|
||||
.hexCharacter('#')
|
@ -61,7 +61,7 @@ public class VanishManager {
|
||||
}
|
||||
|
||||
plugin.getTabList().getVanishTabList().vanishPlayer(tabPlayer.get());
|
||||
plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.vanishPlayer(tabPlayer.get()));
|
||||
plugin.getScoreboardManager().vanishPlayer(tabPlayer.get());
|
||||
}
|
||||
|
||||
public void unVanishPlayer(@NotNull Player player) {
|
||||
@ -72,6 +72,6 @@ public class VanishManager {
|
||||
}
|
||||
|
||||
plugin.getTabList().getVanishTabList().unVanishPlayer(tabPlayer.get());
|
||||
plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.unVanishPlayer(tabPlayer.get()));
|
||||
plugin.getScoreboardManager().unVanishPlayer(tabPlayer.get());
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
velocity_api_version: '${velocity_api_version}'
|
||||
velocity_minimum_build: ${velocity_minimum_build}
|
||||
velocity_minimum_build: ${velocity_minimum_build}
|
||||
papi_proxy_bridge_minimum_version: '${papi_proxy_bridge_minimum_version}'
|
Loading…
Reference in New Issue
Block a user