Compare commits

..
21 Commits
Author SHA1 Message Date
NEZNAMY 7d6206211c Merge pull request #1741 from TH3N3WN00B/fix/small-bugs
Fix 7 small bugs (bossbar send permission, proxy nametag/vanish sync, Velocity quit, ...) and update dependencies
2026-11-11 17:19:44 +01:00
RigorbertoandClaude Opus 5.5 c8aef50f4b Address review: keep mysql-connector-j 9.6.0, no warning for default skins
- Revert mysql-connector-j to 9.6.0 (including protobuf), since the driver is not relocated and a
  newer version could affect other plugins on the same server
- Silently skip default-skins entries without a skin instead of printing a startup warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 12:00:35 -06:00
NEZNAMY 4745301c18 [Vanish compatibility] Make vanish integration have an absolute say in visibility deciding (#1739) 2026-11-09 18:23:15 +01:00
RigorbertoandClaude Opus 5.5 b9f0abe751 Update dependencies
- snakeyaml 2.0 -> 2.7
- mysql-connector-j 9.6.0 -> 26.10.0, excluding protobuf-java (only needed for X DevAPI / mysqlx, not JDBC)
- bStats 3.1.0 -> 3.2.1 (Bukkit, BungeeCord, Velocity)
- VelocityScoreboardAPI 2.0.0 -> 2.1.1
- LuckPerms API 5.4 -> 5.5, ViaVersion API 5.2.1 -> 5.12.2
- Adventure 4.25.0-SNAPSHOT -> 4.26.1 (release)
- jetbrains annotations 26.0.2 -> 26.1.0
- shadow 9.4.2 -> 9.6.1, lombok plugin 9.5.0 -> 9.8.3, blossom 2.1.0 -> 2.2.0
  (blossom 2.2.0 requires a non-null String property value)

All shaded libraries still target Java 8. delivery4j is kept at 1.1.1 (1.1.5 removed the
URL-based Redis connection) and MiniPlaceholders at 3.1.0 (3.2.0 requires Java 25).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:31:38 -06:00
RigorbertoandClaude Opus 5.5 025e23aedd Fix Velocity players never quitting after being loaded by a reload
Quit was only processed for players in the listener's private map, which was filled only by the
join task and never by loadPlayers(). Players that joined while TAB was disabled (e.g. broken
config at startup, then /btab reload) were never removed on disconnect and stayed in the tablist,
global playerlist and layouts of other players; on reconnect the stale TabPlayer wrapping the dead
connection was treated as a server switch. Map entries also leaked while TAB was disabled.

Quit is now checked inside the CPU task by comparing the loaded player's connection with the
disconnecting one, which still ignores the disconnect of a different session with the same UUID.
A stale player from a previous session found on connect is quit first and the connection is
processed as a join, and a player who already disconnected before the join task ran is not added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:18:19 -06:00
RigorbertoandClaude Opus 5.5 83334b97f2 Fix vanished proxy players becoming visible when data was queued
QueuedData.vanished was a primitive boolean (default false) and was always applied on join.
Whenever any other data (belowname, nametag, tablist format, ...) arrived before the join message,
the queued `false` overwrote `vanished=true` from the join, making the vanished player visible
in the global playerlist and online counts on other proxies. Only apply it if a vanish status
update was actually queued.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:17:48 -06:00
RigorbertoandClaude Opus 5.5 c51d9898ce Fix layout default-skins slots never being applied
Slots of each default-skins entry were read from the root layout section instead of the entry's
own section, so the list was always empty and per-slot default skins were never used.
Entries without a skin are now skipped with a startup warning, as they would otherwise put
null into the skin map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:17:29 -06:00
RigorbertoandClaude Opus 5.5 4b116a80bc Fix server placeholder update stopping at a player that is still loading
`return` inside the loop cancelled the refresh for all remaining players and features as soon as
one online player was not loaded yet, while the new value was already stored, so the change was
never sent to them. Skip only that player.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:17:21 -06:00
RigorbertoandClaude Opus 5.5 a0eb00c3c2 Fix API removeScoreboard not resetting players
getPlayers() returns the live set, which unregister() clears before the loop runs, so players
forced onto the removed scoreboard kept forcedScoreboard pointing to it and got no scoreboard
until reset. Iterate over a copy instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:17:21 -06:00
RigorbertoandClaude Opus 5.5 186d5cebcd Fix proxy player nametag updates targeting the wrong team
When a proxy player's prefix/suffix/visibility changed, the update was sent for oldData.teamName
(the team name on the remote server) instead of oldData.resolvedTeamName, which is the team actually
registered locally. If a local player had a team with that name, they received the proxy player's
prefix, suffix and visibility; otherwise the update was silently dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:17:04 -06:00
RigorbertoandClaude Opus 5.5 d9bfbba565 Fix /tab bossbar send using scoreboard permission
The command was gated by tab.scoreboard.show instead of tab.send.bar declared in plugin.yml,
so anyone allowed to pick their own scoreboard could send any boss bar to any player.
Also report the duration argument (args[2]) as the invalid number instead of the bar name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:16:54 -06:00
NEZNAMY 49afdcc156 Merge pull request #1740 from Ambienceeee/fix-proxy-duplicate-team-name
Fix duplicate team names between proxy players
2026-11-04 18:14:24 +01:00
Ambienceeee 6e3c1e61df Fix duplicate team names between proxy players
checkTeamName only compared candidates against local players' short team
names and other proxy players' raw team names. Two proxy players with the
same sorting value could resolve to the same name, and the second team
registration was dropped, leaving that player with no team.

Also treat other proxy players' resolved names, and those of queued
players, as taken.
2026-11-04 11:05:17 -05:00
NEZNAMY d13b83a6d6 [Wiki] Monthly wiki backup for changes made in October 2026 2026-11-02 17:03:04 +01:00
NEZNAMY f9cc5b79dd [Layout] Clear player's layout on quit to reduce impact caused by memory leaks of other plugins 2026-11-01 09:13:54 +01:00
NEZNAMY 06733e5a12 [Placeholders] Fix relational placeholders not updating correctly 2026-10-27 20:10:30 +01:00
NEZNAMY 31c5fdf387 Merge pull request #1735 from Nattapat2871/feature/nametag-opaque-view
Fix opaque nametag occlusion on Folia
2026-10-26 19:14:11 +01:00
NEZNAMY d440a28164 [Configuration] Fix conversion of proxy-support possibly not running 2026-10-25 09:13:27 +01:00
nattapat2871 5b08bd70c1 Fix opaque nametag occlusion on Folia
Schedule target eye position reads and viewer ray traces on their owning entity regions, then apply visibility results on the TAB thread. Preserve synchronous checks for Bukkit and mod loaders.
2026-10-22 11:31:45 +07:00
NEZNAMY 22a7a8a76a 6.2.2-SNAPSHOT 2026-10-18 17:48:11 +02:00
NEZNAMY a46f75b8be [Description] Update badges 2026-10-18 17:37:46 +02:00
31 changed files with 307 additions and 136 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20" role="img" aria-label="Release: 6.2.0"><title>Release: 6.2.0</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="92" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="53" height="20" fill="#555"/><rect x="53" width="39" height="20" fill="#007ec6"/><rect width="92" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="275" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">Release</text><text x="275" y="150" fill-opacity=".3" textLength="430">Release</text></g><text x="275" y="140" textLength="430">Release</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="715" y="150" fill-opacity=".8" filter="url(#blur)" textLength="290">6.2.0</text><text x="715" y="150" fill-opacity=".3" textLength="290">6.2.0</text></g><text x="715" y="140" textLength="290">6.2.0</text></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20" role="img" aria-label="Release: 6.2.1"><title>Release: 6.2.1</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="92" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="53" height="20" fill="#555"/><rect x="53" width="39" height="20" fill="#007ec6"/><rect width="92" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="275" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">Release</text><text x="275" y="150" fill-opacity=".3" textLength="430">Release</text></g><text x="275" y="140" textLength="430">Release</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="715" y="150" fill-opacity=".8" filter="url(#blur)" textLength="290">6.2.1</text><text x="715" y="150" fill-opacity=".3" textLength="290">6.2.1</text></g><text x="715" y="140" textLength="290">6.2.1</text></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -1
View File
@@ -274,7 +274,7 @@ if __name__ == "__main__":
]
)
release = "6.2.0"
release = "6.2.1"
download_image(f"https://img.shields.io/badge/Release-{release}-blue.svg", "Badge_Release.svg")
java = "8+"
+9 -9
View File
@@ -51,15 +51,15 @@ belowname-objective:
```
All the options are explained in the following table.
| Option name | Default value | Description |
|---------------------|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| enabled | true | Enables / Disables the feature |
| value | %health% | [1.20.2-] An integer from -2147483648 to 2147483647, doesn't support decimal values. The number is always white. Supports placeholders with player-specific output, such as player health. Only visible on 1.20.2 and lower. <br/> **Note**: Even if you only support 1.20.3+, you still need to configure this value to properly evaluate to a number, because the value is still sent to the client (just not displayed). You can set it to `0` for simplicity. |
| fancy-value | &c%health% | [1.20.3+] Any text, supports placeholders with per-player output. Only visible on 1.20.3+, where it completely replaces `value`. When evaluates to an empty string, the entire belowname is hidden for viewers using MC 26.2+ (when it was made possible). |
| fancy-value-default | NPC | [1.20.3 - 26.1.2] Default number format (`fancy-value`) for all player entities. `fancy-value` is displayed on every real player, therefore this default value will only appear on player entities which are not actual players, a.k.a. NPCs. Only visible on since 1.20.3 (the function did not exist prior) until 26.1.2 (player entities without scores no longer display any belowname since 26.2). |
| title | Health | Shared label shown after the score for every player entity. Player sees the same text on everyone (= placeholders are parsed for the viewer). Use the `value`/`fancy-value` fields for per‑player data; use `title` only for static labels like `Health` or placeholders which are supposed to be parsed for the viewing player. |
| disable-condition | %world%=disabledworld | A [condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) (either name of a condition or a conditional expression) that must be met for disabling the feature for players. Set to empty for not disabling the feature ever. <br/> **Note**: Disabling the feature for a player means sending objective unregister packet to them, which results in player not seeing belowname on anyone anymore. It doesn't work the other way around - you cannot disable this feature on target players, only for viewers. |
| view-distance | 10 | [26.1+] How close (in blocks) to a player one must be to see their belowname. This value is configurable since Minecraft 26.1, so this option won't work on older versions. |
| Option name | Default value | Description |
|---------------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| enabled | true | Enables / Disables the feature |
| value | %health% | [1.20.2-] An integer from -2147483648 to 2147483647, doesn't support decimal values. The number is always white. Supports placeholders with player-specific output, such as player health. Only visible on 1.20.2 and lower. <br/> **Note**: Even if you only support 1.20.3+, you still need to configure this value to properly evaluate to a number, because the value is still sent to the client (just not displayed). You can set it to `0` for simplicity. |
| fancy-value | &c%health% | [1.20.3+] Any text, supports placeholders with per-player output. Only visible on 1.20.3+, where it completely replaces `value`. When evaluates to an empty string, the entire belowname is hidden for viewers using MC 26.2+ (when it was made possible). |
| fancy-value-default | NPC | [1.20.3 - 26.1.2] Default number format (`fancy-value`) for all player entities. `fancy-value` is displayed on every real player, therefore this default value will only appear on player entities which are not actual players, a.k.a. NPCs. Only visible on since 1.20.3 (the function did not exist prior) until 26.1.2 (player entities without scores no longer display any belowname since 26.2). |
| title | Health | Shared label shown after the score for every player entity. Player sees the same text on everyone (= placeholders are parsed for the viewer). Use the `value`/`fancy-value` fields for per‑player data; use `title` only for static labels like `Health` or placeholders which are supposed to be parsed for the viewing player. |
| disable-condition | %world%=disabledworld | A [condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) (either name of a condition or a conditional expression) that must be met for disabling the feature for players. Set to empty for not disabling the feature ever. <br/> **Note**: Disabling the feature for a player means sending objective unregister packet to them, which results in player not seeing belowname on anyone anymore. It doesn't work the other way around - you cannot disable this feature on target players, only for viewers (at least not until 26.2+, in which case see `fancy-value` description to see how to do it). |
| view-distance | 10 | [26.1+] How close (in blocks) to a player one must be to see their belowname. This value is configurable since Minecraft 26.1, so this option won't work on older versions. |
# Limitations
* [1.12.2-] Title length is limited to 32 characters (including color codes).
@@ -15,6 +15,7 @@
* [Examples](#examples)
* [Example 1 - Chaining conditional placeholders](#example-1---chaining-conditional-placeholders)
* [Example 2 - Combining AND and OR](#example-2---combining-and-and-or)
* [Example 3 - Showing vanish status only for staff members](#example-3---showing-vanish-status-only-for-staff-members)
# About
Conditions / conditional placeholders allow you
@@ -216,3 +217,26 @@ conditions:
type: AND
```
Then, use condition `main` as the display condition (or as a placeholder - `%condition:main%`). Note that `true`/`false` values were not defined, as such, they default to `true` and `false`, respectively. Therefore, we use the placeholder from the condition and check if the result is `true`. Then, check if player is also in the specified server.
## Example 3 - Showing vanish status only for staff members
This example shows you how to show some text on vanished players, but only available to see to staff members. For that, we will use a relational condition.
We want to create a condition that checks for 2 things - target player is vanished and the viewer has permission. If both of them are met (`AND`), some text should display. Otherwise, nothing should display.
Let's build the individual components of the condition:
* Vanish placeholder - in this example, we will use `%essentials_vanished%`. The placeholder returns `yes` if player is vanished, otherwise `no`. If you configured [placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) for the vanish placeholder, you will either need to use the replaced output, or remove the replacement (since you aren't going to use the placeholder directly anyway). To check what exactly a placeholder returns, you can run `/tab parse <player> <placeholder>`.
* Staff check - for the lack of a better idea, let's name our permission `my.permission`. The important part is that we want to check this permission for the player who is looking at the tablist, not the target player who is vanished. Following the guide on relational placeholders, we will use `%viewer:player_has_permission_my.permission%`. We need to compare it to the "true" value, (typically `yes`), just like with vanish placeholder. See above for more info.
* true / false texts - The `true` value will be set to the value to show if target player is vanished and viewer has the permission. `false` value will be displayed otherwise. This will probably remain empty.
After combining all of this, here is what the condition may look like:
```
conditions:
vanish:
conditions:
- '%viewer:player_has_permission_my.permission%=yes'
- '%essentials_vanished%=yes'
type: AND
true: ' &7&l[V]&r'
false: ''
```
You may replace the leading space in `true` value with a trailing one based on your needs. Afterwards, use the placeholder using `%rel_condition:vanish%` in `tabprefix` / `tabsuffix` for example.
The usefulness of this example is questionable, but the purpose of examples is to teach afterall, not to just be copied over to your config (I know you have been doing that!). This one is, however, useful, because unvanishing will result in TAB instantly formatting the player name before placeholders are periodically updated, which would otherwise result in a small flash of vanished text visible for everyone for a split second after unvanishing.
+1 -1
View File
@@ -1,5 +1,5 @@
# About the wiki
The wiki explains the functionality of the plugin as of version **6.2.0**.
The wiki explains the functionality of the plugin as of version **6.2.1**.
If your config looks different or is missing some features, you are using an old version of the plugin.
To get the latest version, check [releases](https://github.com/NEZNAMY/TAB/releases/).
Wiki for older plugin versions is not available.
+2 -25
View File
@@ -28,31 +28,8 @@ Another option is to configure prefixes per-group in config
instead of using placeholders to take those values from your permission plugin.
## #2 - [All platforms] Placeholder refresh intervals
Some placeholders take a long time to process and result in high cpu usage. However, this can be optimized by configuring refresh interval of placeholders.
Internal placeholders use a value hardcoded into the plugin by default. PlaceholderAPI placeholders use `default-refresh-interval`.
To begin, find this part in your config
```yml
placeholder-refresh-intervals:
default-refresh-interval: 500
"%server_uptime%": 1000
"%server_tps_1_colored%": 1000
"%player_health%": 200
"%player_ping%": 1000
"%vault_prefix%": 1000
"%rel_factionsuuid_relation_color%": 500
```
Refresh interval is in milliseconds. It must be divisible by 50.
`default-refresh-interval` is refresh interval for all unlisted PlaceholderAPI placeholders.
Keep in mind that CPU measurement is reset every 10 seconds.
This means that if you configure a higher refresh interval than 10000,
there will be intervals when placeholder didn't refresh at all,
making it look like the CPU is perfect, while on the next refresh it may be high.
For this reason, if you want to cleanly track usage, keep the interval at 10000 at most.
Using a higher interval will help if needed, but may lead to misleading info in cpu output.
Some placeholders take a long time to process and result in high cpu usage. However, this can be optimized by configuring refresh interval of placeholders.
See [Placeholders - Refreshing](https://github.com/NEZNAMY/TAB/wiki/Placeholders#refreshing) for information about modifying refresh intervals of placeholders.
## #3 - [All platforms] Don't use RGB
Though RGB colors look good, they require the text to be split into several components with `color` field used instead of the whole text pasted into the `text` field. This is a complicated process that takes quite some time. Sticking to legacy colors results in better performance.
+24 -4
View File
@@ -157,13 +157,13 @@ On **Velocity**, TAB integrates with [MiniPlaceholders](https://github.com/MiniP
Unlike classic placeholders which take either 0
(placeholders with the same output for all players) or 1 (per-player placeholders) players,
relational placeholders take in 2 players.
The first usage was to display relation between two players, specifically enemies / allies (red / green) for factions,
The first ever recorded usage was to display relation between two players, specifically enemies / allies (red / green) for factions,
allowing players to see faction names of other players colored based on relation.
While the usage is not limited to this, the name of the 2-player placeholder type has been established as "relational"
and will be called that everywhere.
and will be called that everywhere. These placeholders only make sense in functions that allow 2 players to see each other - chat, nametags (+ belowname), tablist formatting (+ objective). Other places, such as scoreboard, bossbar or header/footer don't make sense and don't support these placeholders, as there is no other player to use.
## Usage
TAB does not offer any internal relational placeholders. However, it has full support for PlaceholderAPI placeholders. You can also register your own relational placeholders using the [API](#relational-placeholders-1). In PlaceholderAPI, all relational placeholder identifiers must start with `%rel_`. This rule is adopted and used in TAB as well (including those registered via API).
TAB offers [relational conditions](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders#relational-conditions) as well as full support for PlaceholderAPI placeholders. You can also register your own relational placeholders using the [API](#relational-placeholders-1). In PlaceholderAPI, all relational placeholder identifiers must start with `%rel_`. This rule is adopted and used in TAB as well (including those registered via API).
When passing players into the function, first player is the player viewing the text, second player is the one text is being displayed on.
Code wise, the PlaceholderAPI request function becomes
@@ -177,7 +177,27 @@ To properly distinguish viewer/target (if needed), your relational placeholder e
# Refreshing
## Refresh intervals
TAB is refreshing placeholders in intervals. For internal placeholders it's using values I found the most optimal and set directly in the plugin. Refresh intervals for PlaceholderAPI placeholders can be [configured](https://github.com/NEZNAMY/TAB/wiki/Optimizing-the-plugin#2---all-platforms-placeholder-refresh-intervals).
Internal placeholders use a value hardcoded into the plugin by default. PlaceholderAPI placeholders use `default-refresh-interval` by default.
To begin, find this part in your config
```yml
placeholder-refresh-intervals:
default-refresh-interval: 500
"%server_uptime%": 1000
"%server_tps_1_colored%": 1000
"%player_health%": 200
"%player_ping%": 1000
"%vault_prefix%": 1000
"%rel_factionsuuid_relation_color%": 500
```
Refresh interval is in milliseconds. It must be divisible by 50.
Keep in mind that CPU measurement is reset every 10 seconds.
This means that if you configure a higher refresh interval than 10000,
there will be intervals when placeholder didn't refresh at all,
making it look like the CPU is perfect, while on the next refresh it may be high.
For this reason, if you want to cleanly track usage, keep the interval at 10000 at most.
Using a higher interval will help if needed, but may lead to misleading info in cpu output.
## Sync refreshing
By default, all placeholders are refreshed asynchronously,
+1 -1
View File
@@ -1,5 +1,5 @@
dependencies {
compileOnlyApi("org.jetbrains:annotations:26.0.2")
compileOnlyApi("org.jetbrains:annotations:26.1.0")
}
tasks.javadoc {
+2 -2
View File
@@ -3,7 +3,7 @@ plugins {
}
dependencies {
implementation("com.gradleup.shadow:shadow-gradle-plugin:9.4.2")
implementation("io.freefair.gradle:lombok-plugin:9.5.0")
implementation("com.gradleup.shadow:shadow-gradle-plugin:9.6.1")
implementation("io.freefair.gradle:lombok-plugin:9.8.3")
implementation("dev.architectury.loom-no-remap:dev.architectury.loom-no-remap.gradle.plugin:1.17-SNAPSHOT")
}
+1 -1
View File
@@ -4,7 +4,7 @@ plugins {
allprojects {
group = "me.neznamy"
version = "6.2.1"
version = "6.2.2-SNAPSHOT"
description = "An all-in-one solution that works"
ext.set("id", "tab")
+1 -1
View File
@@ -1,6 +1,6 @@
dependencies {
api(projects.shared)
implementation("org.bstats:bstats-bukkit:3.1.0")
implementation("org.bstats:bstats-bukkit:3.2.1")
compileOnly("org.purpurmc.purpur:purpur-api:1.16.5-R0.1-SNAPSHOT")
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnly("com.github.MilkBowl:VaultAPI:1.7") {
@@ -9,8 +9,10 @@ import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.cpu.TimedCaughtTask;
import me.neznamy.tab.shared.data.World;
import me.neznamy.tab.shared.placeholders.types.PlayerPlaceholderImpl;
import me.neznamy.tab.shared.platform.LineOfSightCheck;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerChangedWorldEvent;
@@ -21,8 +23,15 @@ import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Method;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
/**
* Platform override for Folia.
@@ -36,6 +45,11 @@ public class FoliaPlatform extends BukkitPlatform {
/** FoliaGlobalRegionScheduler#execute(Plugin, Runnable) method */
private final Method globalScheduler_execute;
/** Location overload avoids accessing a target entity from the viewer's region. */
private final Method hasLineOfSightLocation;
private final AtomicBoolean lineOfSightErrorLogged = new AtomicBoolean();
/**
* Constructs new instance with given plugin.
*
@@ -48,6 +62,7 @@ public class FoliaPlatform extends BukkitPlatform {
super(plugin);
globalScheduler = Bukkit.class.getMethod("getGlobalRegionScheduler").invoke(null);
globalScheduler_execute = globalScheduler.getClass().getMethod("execute", Plugin.class, Runnable.class);
hasLineOfSightLocation = Player.class.getMethod("hasLineOfSight", Location.class);
}
@Override
@@ -172,8 +187,93 @@ public class FoliaPlatform extends BukkitPlatform {
globalScheduler_execute.invoke(globalScheduler, getPlugin(), task);
}
/**
* Captures target positions on their owning regions, then ray traces from each viewer's
* owning region. No target entity is read while running a viewer's check.
*/
@Override
public boolean hasLineOfSight(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
return true; // Cross-entity line-of-sight checks are not safe from Folia's global scheduler.
public void requestLineOfSight(@NotNull List<LineOfSightCheck> checks, @NotNull Consumer<boolean[]> callback) {
boolean[] visible = new boolean[checks.size()];
if (checks.isEmpty() || !getPlugin().isEnabled()) {
callback.accept(visible);
return;
}
Map<TabPlayer, CompletableFuture<Location>> targetLocations = new IdentityHashMap<>();
for (LineOfSightCheck check : checks) {
targetLocations.computeIfAbsent(check.getTarget(), target -> {
CompletableFuture<Location> location = new CompletableFuture<>();
scheduleEntity((Player) target.getPlayer(), () -> {
try {
Player player = (Player) target.getPlayer();
location.complete(player.isOnline() ? player.getEyeLocation() : null);
} catch (RuntimeException e) {
location.complete(null);
logLineOfSightError(e);
}
}, () -> location.complete(null));
return location;
});
}
CompletableFuture.allOf(targetLocations.values().toArray(new CompletableFuture[0])).whenComplete((ignored, error) -> {
Map<TabPlayer, List<Integer>> checksByViewer = new IdentityHashMap<>();
for (int i = 0; i < checks.size(); i++) {
checksByViewer.computeIfAbsent(checks.get(i).getViewer(), viewer -> new ArrayList<>()).add(i);
}
List<CompletableFuture<Void>> viewerTasks = new ArrayList<>(checksByViewer.size());
for (Map.Entry<TabPlayer, List<Integer>> entry : checksByViewer.entrySet()) {
CompletableFuture<Void> finished = new CompletableFuture<>();
viewerTasks.add(finished);
scheduleEntity((Player) entry.getKey().getPlayer(), () -> {
try {
Player viewer = (Player) entry.getKey().getPlayer();
if (viewer.isOnline()) {
for (int index : entry.getValue()) {
Location target = targetLocations.get(checks.get(index).getTarget()).getNow(null);
if (target == null || target.getWorld() != viewer.getWorld()) continue;
try {
visible[index] = (boolean) hasLineOfSightLocation.invoke(viewer, target);
} catch (ReflectiveOperationException | RuntimeException e) {
visible[index] = false;
logLineOfSightError(e);
}
}
}
} catch (RuntimeException e) {
for (int index : entry.getValue()) visible[index] = false;
logLineOfSightError(e);
} finally {
finished.complete(null);
}
}, () -> finished.complete(null));
}
CompletableFuture.allOf(viewerTasks.toArray(new CompletableFuture[0]))
.whenComplete((done, failure) -> callback.accept(visible));
});
}
private void scheduleEntity(@NotNull Player player, @NotNull Runnable task, @NotNull Runnable retired) {
if (!getPlugin().isEnabled()) {
retired.run();
return;
}
try {
Object scheduler = Entity.class.getMethod("getScheduler").invoke(player);
Consumer<?> consumer = ignored -> task.run();
Object scheduled = scheduler.getClass().getMethod("run", Plugin.class, Consumer.class, Runnable.class)
.invoke(scheduler, getPlugin(), consumer, retired);
if (scheduled == null) retired.run();
} catch (ReflectiveOperationException | RuntimeException e) {
retired.run();
logLineOfSightError(e);
}
}
private void logLineOfSightError(@NotNull Exception error) {
if (lineOfSightErrorLogged.compareAndSet(false, true)) {
getPlugin().getLogger().log(Level.WARNING, "Unable to check opaque nametag line of sight on Folia", error);
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
dependencies {
implementation(projects.shared)
implementation("org.bstats:bstats-bungeecord:3.1.0")
implementation("org.bstats:bstats-bungeecord:3.2.1")
compileOnly("net.md-5:bungeecord-api:26.1-R0.1-SNAPSHOT")
compileOnly("net.md-5:bungeecord-proxy:1.21-R0.4-SNAPSHOT")
compileOnly("com.github.limework.redisbungee:RedisBungee-Bungee:0.11.0")
+7 -7
View File
@@ -1,10 +1,10 @@
plugins {
id("net.kyori.blossom") version "2.1.0"
id("net.kyori.blossom") version "2.2.0"
}
dependencies {
api(projects.api)
api("org.yaml:snakeyaml:2.0")
api("org.yaml:snakeyaml:2.7")
api("com.github.NEZNAMY:yamlassist:1.0.8")
api("com.googlecode.json-simple:json-simple:1.1.1") {
exclude("junit", "junit")
@@ -14,13 +14,13 @@ dependencies {
exclude("org.checkerframework", "checker-qual")
}
api("com.mysql:mysql-connector-j:9.6.0")
compileOnlyApi("com.viaversion:viaversion-api:5.2.1")
compileOnlyApi("com.viaversion:viaversion-api:5.12.2")
compileOnlyApi("io.netty:netty-all:4.1.90.Final")
compileOnlyApi("net.luckperms:api:5.4")
compileOnlyApi("net.luckperms:api:5.5")
compileOnlyApi("com.google.guava:guava:31.1-jre")
compileOnlyApi("org.geysermc.floodgate:api:2.2.0-SNAPSHOT")
compileOnlyApi("net.kyori:adventure-api:4.25.0-SNAPSHOT")
compileOnlyApi("net.kyori:adventure-text-minimessage:4.25.0-SNAPSHOT")
compileOnlyApi("net.kyori:adventure-api:4.26.1")
compileOnlyApi("net.kyori:adventure-text-minimessage:4.26.1")
implementation("com.saicone.delivery4j:delivery4j:1.1.1")
implementation("com.saicone.delivery4j:broker-rabbitmq:1.1.1") {
exclude("org.slf4j", "slf4j-api")
@@ -38,7 +38,7 @@ sourceSets.main {
property("name", rootProject.name)
property("id", rootProject.ext.get("id")!!.toString())
property("version", project.version.toString())
property("description", project.description)
property("description", project.description.orEmpty())
property("website", rootProject.ext.get("website")!!.toString())
property("author", rootProject.ext.get("author")!!.toString())
property("credits", rootProject.ext.get("credits")!!.toString())
@@ -85,6 +85,7 @@ public class TabConstants {
public static final String COMMAND_PROPERTY_CHANGE_PREFIX = "tab.change.";
public static final String COMMAND_BOSSBAR_ANNOUNCE = "tab.announce.bar";
public static final String COMMAND_BOSSBAR_SEND = "tab.send.bar";
public static final String COMMAND_BOSSBAR_TOGGLE = "tab.bossbar.toggle";
public static final String COMMAND_BOSSBAR_TOGGLE_OTHER = "tab.bossbar.toggle.other";
@@ -22,7 +22,7 @@ public class BossBarSendCommand extends SubCommand {
* Constructs new instance
*/
public BossBarSendCommand() {
super("send", TabConstants.Permission.COMMAND_SCOREBOARD_SHOW);
super("send", TabConstants.Permission.COMMAND_BOSSBAR_SEND);
}
@Override
@@ -46,7 +46,7 @@ public class BossBarSendCommand extends SubCommand {
try {
duration = Integer.parseInt(args[2]);
} catch (NumberFormatException e) {
sendMessage(sender, getMessages().getInvalidNumber(args[1]));
sendMessage(sender, getMessages().getInvalidNumber(args[2]));
return;
}
BossBar bar = feature.getBossBar(barName);
@@ -512,15 +512,16 @@ public class LegacyConverter {
* Config file
*/
public void convert507to510(@NonNull ConfigurationFile config) {
if (config.rename("enable-redisbungee-support", "proxy-support.enabled")) {
config.rename("enable-redisbungee-support", "proxy-support.enabled");
config.setIfMissing("proxy-support.enabled", true);
config.setIfMissing("proxy-support.type", "PLUGIN");
config.setIfMissing("proxy-support.plugin.name", "RedisBungee");
config.setIfMissing("proxy-support.redis.url", "redis://:password@localhost:6379/0");
config.setIfMissing("proxy-support.rabbitmq.exchange", "plugin");
config.setIfMissing("proxy-support.rabbitmq.url", "amqp://guest:guest@localhost:5672/%2F");
if (config.rename("placeholderapi-refresh-intervals", "placeholder-refresh-intervals")) {
TAB.getInstance().getPlatform().logInfo(new TabTextComponent("Performing configuration conversion from 5.0.7 to 5.1.0", TabTextColor.YELLOW));
config.set("proxy-support.type", "PLUGIN");
config.set("proxy-support.plugin.name", "RedisBungee");
config.set("proxy-support.redis.url", "redis://:password@localhost:6379/0");
config.set("proxy-support.rabbitmq.exchange", "plugin");
config.set("proxy-support.rabbitmq.url", "amqp://guest:guest@localhost:5672/%2F");
}
config.rename("placeholderapi-refresh-intervals", "placeholder-refresh-intervals");
config.setIfMissing("playerlist-objective.title", "TAB");
config.setIfMissing("playerlist-objective.render-type", Arrays.asList("%health%", "%player_health%", "%player_health_rounded%").contains(config.getString("playerlist-objective.value", "")) ? "HEARTS" : "INTEGER");
}
@@ -64,7 +64,8 @@ public class LayoutConfiguration {
ConfigurationSection groupSection = defaultSkins.getConfigurationSection(asString);
String skin = groupSection.getString("skin");
for (String line : section.getStringList("slots", Collections.emptyList())) {
if (skin == null) continue;
for (String line : groupSection.getStringList("slots", Collections.emptyList())) {
String[] arr = line.split("-");
int from = Integer.parseInt(arr[0]);
int to = arr.length == 1 ? from : Integer.parseInt(arr[1]);
@@ -86,6 +86,7 @@ public class LayoutManagerImpl extends RefreshableFeature implements LayoutManag
@Override
public void onQuit(@NotNull TabPlayer p) {
sortedPlayers.remove(p);
p.layoutData.currentLayout = null;
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
if (all == p) continue;
if (all.layoutData.currentLayout != null) all.layoutData.currentLayout.view.tick();
@@ -122,7 +122,7 @@ public class NameTagProxyPlayerData extends ProxyMessage {
// Property update
if (viewer.teamData.hasTeamRegistered(target)) {
viewer.getScoreboard().updateTeam(
oldData.teamName,
oldData.resolvedTeamName,
prefix,
suffix,
nameVisibility,
@@ -165,7 +165,18 @@ public class NameTagProxyPlayerData extends ProxyMessage {
if (!nameTaken && feature.getProxy() != null) {
for (ProxyPlayer all : feature.getProxy().getProxyPlayers().values()) {
if (all == player) continue;
if (all.getNametag() != null && potentialTeamName.equals(all.getNametag().teamName)) {
NameTagProxyPlayerData nametag = all.getNametag();
if (nametag != null && (potentialTeamName.equals(nametag.teamName) || potentialTeamName.equals(nametag.resolvedTeamName))) {
nameTaken = true;
break;
}
}
}
if (!nameTaken && feature.getProxy() != null) {
// Players still queued have a resolved name that will be used once they are known
for (QueuedData queued : feature.getProxy().getQueuedData().values()) {
NameTagProxyPlayerData nametag = queued.getNametag();
if (nametag != null && nametag != this && !nametag.playerId.equals(playerId) && potentialTeamName.equals(nametag.resolvedTeamName)) {
nameTaken = true;
break;
}
@@ -11,6 +11,7 @@ import me.neznamy.tab.shared.features.types.JoinListener;
import me.neznamy.tab.shared.features.types.Loadable;
import me.neznamy.tab.shared.features.types.RefreshableFeature;
import me.neznamy.tab.shared.placeholders.conditions.Condition;
import me.neznamy.tab.shared.platform.LineOfSightCheck;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
@@ -21,6 +22,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Sub-feature for NameTag feature that manages nametag visibility.
@@ -39,6 +41,10 @@ public class VisibilityManager extends RefreshableFeature implements JoinListene
@NotNull
private final Set<TabPlayer> opaqueNameTagViewers = Collections.newSetFromMap(new WeakHashMap<>());
/** Prevents an older asynchronous occlusion check from overwriting a newer result. */
private final AtomicLong opaqueRefreshSequence = new AtomicLong();
private long lastAppliedOpaqueRefresh;
/**
* Constructs new instance.
*
@@ -271,30 +277,41 @@ public class VisibilityManager extends RefreshableFeature implements JoinListene
}
private void requestOpaqueOcclusionRefresh() {
List<OpaqueVisibilityCheck> checks = new ArrayList<>();
List<LineOfSightCheck> checks = new ArrayList<>();
List<OpaqueVisibilityResult> immediateResults = new ArrayList<>();
for (TabPlayer player : nameTags.getOnlinePlayers().getPlayers()) {
if (!hasOpaqueNameTagMode(player) || player.teamData.isDisabled()) continue;
for (TabPlayer viewer : nameTags.getOnlinePlayers().getPlayers()) {
checks.add(new OpaqueVisibilityCheck(player, viewer, !isOpaqueNameTagMode(player, viewer)));
if (!isOpaqueNameTagMode(player, viewer) || viewer == player || !viewer.isOnline() || !player.isOnline()) {
immediateResults.add(new OpaqueVisibilityResult(player, viewer, true));
} else if (!canSeeForOpaque(viewer, player)) {
immediateResults.add(new OpaqueVisibilityResult(player, viewer, false));
} else {
checks.add(new LineOfSightCheck(viewer, player));
}
}
}
if (checks.isEmpty()) return;
TAB.getInstance().getPlatform().runSyncGlobal(() -> {
List<OpaqueVisibilityResult> results = new ArrayList<>(checks.size());
for (OpaqueVisibilityCheck check : checks) {
if (!check.player.isOnline() || !check.viewer.isOnline()) {
results.add(new OpaqueVisibilityResult(check.player, check.viewer, true));
continue;
}
results.add(new OpaqueVisibilityResult(check.player, check.viewer,
check.forceVisible || check.viewer == check.player || hasLineOfSight(check.viewer, check.player)));
if (checks.isEmpty() && immediateResults.isEmpty()) return;
long sequence = opaqueRefreshSequence.incrementAndGet();
if (checks.isEmpty()) {
getCustomThread().execute(new TimedCaughtTask(TAB.getInstance().getCpu(),
() -> applyOpaqueOcclusionResults(immediateResults, sequence), getFeatureName(), "Applying opaque nametag occlusion"));
return;
}
TAB.getInstance().getPlatform().requestLineOfSight(checks, visible -> {
List<OpaqueVisibilityResult> results = new ArrayList<>(immediateResults);
for (int i = 0; i < checks.size(); i++) {
LineOfSightCheck check = checks.get(i);
results.add(new OpaqueVisibilityResult(check.getTarget(), check.getViewer(), visible[i]));
}
getCustomThread().execute(new TimedCaughtTask(TAB.getInstance().getCpu(),
() -> applyOpaqueOcclusionResults(results), getFeatureName(), "Applying opaque nametag occlusion"));
() -> applyOpaqueOcclusionResults(results, sequence), getFeatureName(), "Applying opaque nametag occlusion"));
});
}
private void applyOpaqueOcclusionResults(@NonNull List<OpaqueVisibilityResult> results) {
private void applyOpaqueOcclusionResults(@NonNull List<OpaqueVisibilityResult> results, long sequence) {
if (sequence <= lastAppliedOpaqueRefresh) return;
lastAppliedOpaqueRefresh = sequence;
for (OpaqueVisibilityResult result : results) {
if (!nameTags.getOnlinePlayers().contains(result.player) || !nameTags.getOnlinePlayers().contains(result.viewer)) continue;
boolean visible = result.visible;
@@ -302,6 +319,8 @@ public class VisibilityManager extends RefreshableFeature implements JoinListene
visible = true;
} else if (!isOpaqueNameTagMode(result.player, result.viewer)) {
visible = true;
} else if (!canSeeForOpaque(result.viewer, result.player)) {
visible = false;
}
boolean changed = visible
? result.player.teamData.showNametag(result.viewer, NameTagInvisibilityReason.OPAQUE_OCCLUSION)
@@ -333,24 +352,10 @@ public class VisibilityManager extends RefreshableFeature implements JoinListene
return player.teamData.isOpaqueNameTagMode(viewer) || opaqueNameTagViewers.contains(viewer);
}
private boolean hasLineOfSight(@NonNull TabPlayer viewer, @NonNull TabPlayer target) {
private boolean canSeeForOpaque(@NonNull TabPlayer viewer, @NonNull TabPlayer target) {
if (!viewer.server.canSee(target.server)) return false;
if (viewer.world != target.world) return false;
if (!viewer.canSee(target)) return false;
return TAB.getInstance().getPlatform().hasLineOfSight(viewer, target);
}
private static class OpaqueVisibilityCheck {
@NotNull private final TabPlayer player;
@NotNull private final TabPlayer viewer;
private final boolean forceVisible;
private OpaqueVisibilityCheck(@NotNull TabPlayer player, @NotNull TabPlayer viewer, boolean forceVisible) {
this.player = player;
this.viewer = viewer;
this.forceVisible = forceVisible;
}
return viewer.canSee(target);
}
private static class OpaqueVisibilityResult {
@@ -34,8 +34,9 @@ public class QueuedData {
@Nullable
private NameTagProxyPlayerData nametag;
/** Whether player is vanished or not */
private boolean vanished;
/** Whether player is vanished or not, {@code null} if no vanish status update was received */
@Nullable
private Boolean vanished;
/** Name of server the player is connected to */
@NotNull
@@ -87,7 +87,7 @@ public class PlayerJoin extends ProxyMessage {
decodedPlayer.setTabFormat(data.getTabFormat());
decodedPlayer.setNametag(data.getNametag());
decodedPlayer.setPlayerlist(data.getPlayerlist());
decodedPlayer.setVanished(data.isVanished());
if (data.getVanished() != null) decodedPlayer.setVanished(data.getVanished());
}
if (TAB.getInstance().getPlayer(decodedPlayer.getUniqueId()) == null) {
TAB.getInstance().getFeatureManager().onJoin(decodedPlayer);
@@ -314,7 +314,7 @@ public class ScoreboardManagerImpl extends RefreshableFeature implements Scorebo
throw new IllegalArgumentException("No registered scoreboard found with name " + name);
}
Set<TabPlayer> players = removed.getPlayers();
Set<TabPlayer> players = new HashSet<>(removed.getPlayers()); // Copy, unregister() clears the set
removed.unregister();
for (TabPlayer p : players) {
p.scoreboardData.forcedScoreboard = null;
@@ -328,7 +328,7 @@ public class ScoreboardManagerImpl extends RefreshableFeature implements Scorebo
throw new IllegalArgumentException("This scoreboard (" + scoreboard.getName() + ") is not registered.");
}
Set<TabPlayer> players = ((ScoreboardImpl)scoreboard).getPlayers();
Set<TabPlayer> players = new HashSet<>(((ScoreboardImpl)scoreboard).getPlayers()); // Copy, unregister() clears the set
scoreboard.unregister();
for (TabPlayer p : players) {
p.scoreboardData.forcedScoreboard = null;
@@ -90,26 +90,15 @@ public class RelationalPlaceholderImpl extends TabPlaceholder implements Relatio
}
@Override
public void updateFromNested(@NonNull TabPlayer viewer) {
for (TabPlayer target : TAB.getInstance().getOnlinePlayers()) {
public void updateFromNested(@NonNull TabPlayer target) {
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
String value = request(viewer, target);
String s = replacements.findReplacement(String.valueOf(value));
viewer.lastRelationalValues.computeIfAbsent(this, v -> Collections.synchronizedMap(new WeakHashMap<>())).put(target, s);
if (!target.isLoaded()) continue; // Updated on join
for (RefreshableFeature f : reference.getUsedByFeatures()) {
TimedCaughtTask task = new TimedCaughtTask(TAB.getInstance().getCpu(), () -> f.refresh(target, true),
f.getFeatureName(), f.getRefreshDisplayName());
if (f instanceof CustomThreaded) {
((CustomThreaded) f).getCustomThread().execute(task);
} else {
task.run();
}
}
updateParents(target);
}
if (!viewer.isLoaded()) return; // Updated on join
if (!target.isLoaded()) return; // Updated on join
for (RefreshableFeature f : reference.getUsedByFeatures()) {
TimedCaughtTask task = new TimedCaughtTask(TAB.getInstance().getCpu(), () -> f.refresh(viewer, true),
TimedCaughtTask task = new TimedCaughtTask(TAB.getInstance().getCpu(), () -> f.refresh(target, true),
f.getFeatureName(), f.getRefreshDisplayName());
if (f instanceof CustomThreaded) {
((CustomThreaded) f).getCustomThread().execute(task);
@@ -117,7 +106,7 @@ public class RelationalPlaceholderImpl extends TabPlaceholder implements Relatio
task.run();
}
}
updateParents(viewer);
updateParents(target);
}
/**
@@ -60,7 +60,7 @@ public class ServerPlaceholderImpl extends TabPlaceholder implements ServerPlace
if (hasValueChanged(value)) {
for (RefreshableFeature r : reference.getUsedByFeatures()) {
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
if (!all.isLoaded()) return; // Updated on join
if (!all.isLoaded()) continue; // Updated on join
TimedCaughtTask task = new TimedCaughtTask(TAB.getInstance().getCpu(), () -> r.refresh(all, false), r.getFeatureName(), r.getRefreshDisplayName());
if (r instanceof CustomThreaded) {
((CustomThreaded) r).getCustomThread().execute(task);
@@ -0,0 +1,17 @@
package me.neznamy.tab.shared.platform;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
/**
* A viewer and target whose block line of sight must be checked.
* Results are returned in the same order as the submitted checks.
*/
@Getter
@RequiredArgsConstructor
public class LineOfSightCheck {
@NotNull private final TabPlayer viewer;
@NotNull private final TabPlayer target;
}
@@ -15,6 +15,7 @@ import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* An interface with methods that are called in universal code,
@@ -277,4 +278,21 @@ public interface Platform {
default boolean hasLineOfSight(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
return true;
}
/**
* Checks block line of sight without assuming that all entities share a server thread.
* The callback receives one result per input check, in the same order. Platforms with
* region-owned entities can override this to complete the checks asynchronously.
*/
default void requestLineOfSight(@NotNull List<LineOfSightCheck> checks, @NotNull Consumer<boolean[]> callback) {
runSyncGlobal(() -> {
boolean[] results = new boolean[checks.size()];
for (int i = 0; i < checks.size(); i++) {
LineOfSightCheck check = checks.get(i);
results[i] = !check.getViewer().isOnline() || !check.getTarget().isOnline() ||
hasLineOfSight(check.getViewer(), check.getTarget());
}
callback.accept(results);
});
}
}
@@ -347,7 +347,9 @@ public abstract class TabPlayer implements me.neznamy.tab.api.TabPlayer {
if (!VanishIntegration.getHandlers().isEmpty()) {
try {
for (VanishIntegration i : VanishIntegration.getHandlers()) {
if (!i.canSee(this, target)) return false;
// if (!i.canSee(this, target)) return false;
// #1739 - Vanish integration should have an absolute say
return i.canSee(this, target);
}
} catch (ConcurrentModificationException e) {
// PV error, try again
+2 -2
View File
@@ -1,13 +1,13 @@
dependencies {
implementation(projects.shared)
implementation("org.bstats:bstats-velocity:3.1.0")
implementation("org.bstats:bstats-velocity:3.2.1")
compileOnly("com.github.limework.redisbungee:RedisBungee-Velocity:0.11.0")
compileOnly("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT")
compileOnly("com.velocitypowered:velocity-proxy:3.4.0-SNAPSHOT")
compileOnlyApi("net.kyori:adventure-nbt:4.17.0")
annotationProcessor("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT")
compileOnly("com.github.LeonMangler:PremiumVanishAPI:2.9.0-4")
compileOnly("net.william278:velocityscoreboardapi:2.0.0")
compileOnly("net.william278:velocityscoreboardapi:2.1.1")
compileOnly("io.github.miniplaceholders:miniplaceholders-api:3.1.0")
}
@@ -15,18 +15,11 @@ import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.platform.decorators.SafeBossBar;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* The core for Velocity forwarding events into all enabled features
*/
public class VelocityEventListener implements EventListener<Player> {
/** Map for tracking online players */
private final Map<Player, UUID> players = new ConcurrentHashMap<>();
/**
* Listens to player disconnecting from the server.
*
@@ -35,11 +28,16 @@ public class VelocityEventListener implements EventListener<Player> {
*/
@Subscribe
public void onQuit(@NotNull DisconnectEvent e) {
if (TAB.getInstance().isPluginDisabled()) return;
// Check if the player was actually connected to the server in the first place to avoid processing
// disconnect of an existing player who is still there (because players are mapped by UUID in TAB)
UUID id = players.remove(e.getPlayer());
if (id != null) quit(id);
TAB tab = TAB.getInstance();
if (tab.isPluginDisabled()) return;
tab.getCPUManager().runTask(() -> {
// Only process the disconnect if the loaded player belongs to this exact connection to avoid processing
// disconnect of a different session with the same UUID that is still connected (because players are mapped by UUID in TAB)
TabPlayer player = tab.getPlayer(e.getPlayer().getUniqueId());
if (player != null && player.getPlayer() == e.getPlayer()) {
tab.getFeatureManager().onQuit(player);
}
});
}
/**
@@ -73,8 +71,13 @@ public class VelocityEventListener implements EventListener<Player> {
if (tab.isPluginDisabled()) return;
tab.getCPUManager().runTask(() -> {
TabPlayer player = tab.getPlayer(e.getPlayer().getUniqueId());
if (player != null && player.getPlayer() != e.getPlayer()) {
// Player from a previous session with the same UUID whose disconnect was not processed yet, remove it
tab.getFeatureManager().onQuit(player);
player = null;
}
if (player == null) {
players.put(e.getPlayer(), e.getPlayer().getUniqueId());
if (!e.getPlayer().isActive()) return; // Player disconnected before this task ran, do not create a ghost player
tab.getFeatureManager().onJoin(createPlayer(e.getPlayer()));
} else {
if (!(player.getScoreboard() instanceof VelocityScoreboard)) player.getScoreboard().resend();