Compare commits

...
38 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
NEZNAMY a748a6b2c0 6.2.1 2026-10-18 17:03:58 +02:00
NEZNAMY 5f8eb14c92 [Placeholders] Apply dynamic placeholders to existing placeholders to override them on API use (#1733) 2026-10-17 14:12:43 +02:00
NEZNAMY 1fc5034a77 [Dump Command] Fix NPE when config was not converted correctly 2026-10-15 18:49:37 +02:00
NEZNAMY 5130d1659e [Placeholders] Fix NPE (#1733) 2026-10-13 19:28:08 +02:00
NEZNAMY f68cba21f0 [Bukkit] Fix Canvas detection (once again) 2026-10-13 17:25:34 +02:00
NEZNAMY 6c7d1eba32 [Configuration] Properly detect groups with "." in their name 2026-10-11 09:20:50 +02:00
NEZNAMY 9713db862a [Wiki] Monthly wiki backup for changes made in September 2026 2026-10-07 17:50:39 +02:00
NEZNAMY fbee22d785 Merge pull request #1731 from freddycor/master
Fixed 1.8+ clients not seeing 1.7 players correct names format
2026-10-02 18:51:55 +02:00
freddycor 0ee39728e5 Fixed 1.8+ clients not seeing 1.7 players correct names. 2026-10-02 17:40:49 +02:00
NEZNAMY 37aa69b64e [Teams] Cache teams by entries for O(1) lookup complexity 2026-09-30 19:54:30 +02:00
NEZNAMY 22ddad85ab [Belowname / Playerlist objectives] Calculate value once instead of for every viewer 2026-09-30 17:37:06 +02:00
NEZNAMY 261a81586f [Placeholders] Use correct "continue" operator instead of "return" 2026-09-30 17:19:17 +02:00
NEZNAMY f0abc99b61 [Layout] Properly format / unformat original tablist entries when layout is set / reset (#1728) 2026-09-28 19:27:52 +02:00
NEZNAMY b669871ff1 [Forge] Update to 26.3 2026-09-22 19:38:53 +02:00
NEZNAMY 174c58b642 Update Gradle to 9.7.1 2026-09-21 19:41:44 +02:00
NEZNAMY 28431ef136 6.2.1-SNAPSHOT 2026-09-21 17:59:13 +02:00
NEZNAMY 5f25bf97ba [Description] Update badges 2026-09-17 17:48:28 +02:00
50 changed files with 513 additions and 233 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.1.3"><title>Release: 6.1.3</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.1.3</text><text x="715" y="150" fill-opacity=".3" textLength="290">6.1.3</text></g><text x="715" y="140" textLength="290">6.1.3</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.1.3"
release = "6.2.1"
download_image(f"https://img.shields.io/badge/Release-{release}-blue.svg", "Badge_Release.svg")
java = "8+"
+12 -3
View File
@@ -13,7 +13,7 @@ TAB does not depend on any other plugins. The experience can however be enhanced
With the introduction of new Minecraft versions, it is becoming harder and harder to support many versions in the same jar, eventually making it impossible since Minecraft 26.1.
**As a general rule, if you want the latest TAB version for your server, please use [Modrinth](https://modrinth.com/plugin/tab-was-taken)'s download filter, where you can select your server software and version and you'll get the latest jar.**
Currently, the latest Minecraft version supported by TAB is **26.2**. When new versions come out, TAB is always updated soon after to support them.
Currently, the latest Minecraft version supported by TAB is **26.3**. When new versions come out, TAB is always updated soon after to support them.
If your server software and version is reasonably popular and TAB hasn't been backported to that version in a very long time, you may [request a backport](https://github.com/NEZNAMY/TAB/issues/new/choose) (unless backports to that version are no longer offered, see below).
Since TAB 6.0.0, Minecraft 1.x started taking hits with each subsequent TAB versions - Bukkit versions started disappearing and **modded platforms no longer offer backports of TAB 6.0.0+ to MC 1.x** (this is because the build scripts had to be massively changed for MC 26+).
@@ -21,8 +21,17 @@ Since TAB 6.0.0, Minecraft 1.x started taking hits with each subsequent TAB vers
The sections below provide **additional** information for each platform.
## Bukkit
TAB 5.5.0 supports all 1.x versions starting with 1.7. For 1.5.2 - 1.6.4, use TAB v5.2.5.
Since TAB 6.0.0, support for unused 1.x versions will continue to disappear with each subsequent Minecraft release.
Since TAB 6.0.0, support for unused 1.x versions started disappearing and will continue to disappear with each subsequent Minecraft release.
Currently, the latest release supports the following versions:
* 1.7.10
* 1.8.8
* 1.12.2
* 1.16.5
* 1.17.1
* 1.18.2
* 1.19.4 - 26.3
If you want a jar with support for a 1.x version that is no longer supported, you can use TAB v5.5.0, which supports all MC 1.x versions. For 1.5.2 - 1.6.4, use TAB v5.2.5.
## BungeeCord / Velocity
For proxies, the plugin is made to work with the latest build.
+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.
@@ -15,8 +15,8 @@
* ~Examples~
# About
This feature allows you to see players from other servers, instead of only seeing players on the same server.
If TAB is installed on a backend server, you'll need to set up [Multi server support](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Multi-server-support) for proper linking of servers. On proxy installation, this is not required.
This feature allows you to see players from different servers, instead of only seeing players on the same server. The typical use is to show players under the whole proxy network.
If TAB is installed on a backend server, you'll need to set up [Multi server support](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Multi-server-support) to properly make the servers communicate. On proxy installation, this is not needed, unless you have multiple proxies.
# Configuration
The feature can be configured in **config.yml** under **global-playerlist** section.
@@ -118,6 +118,8 @@ global-playerlist:
- isolatedServer2
```
This is not needed when `isolate-unlisted-servers` is set to `true`, in which case all unlisted servers are isolated by default.
## Seeing all players on the network from some server
If, despite configuring server groups,
you want players to see everyone on the network if they're connected to specific server(s),
+17 -1
View File
@@ -19,7 +19,7 @@
* [Additional note 2 - [1.8 - 1.21.3] Second layer of skin missing](#additional-note-2---18---1213-second-layer-of-skin-missing)
* [Additional note 3 - Entry overlap](#additional-note-3---entry-overlap)
* ~Troubleshooting~
* ~API~
* [API](#api)
* [Examples](#examples)
* [Example 1 - Per-server columns](#example-1---per-server-columns)
* [Example 2 - Per-world playerlist](#example-2---per-world-playerlist)
@@ -284,6 +284,22 @@ Here are a few tips how to do that:
* Remove unnecessary brackets from prefix
# API
*To get started with the API, see [Developer API](https://github.com/NEZNAMY/TAB/wiki/Developer-API) page.*
To access this feature, you'll need to obtain `LayoutManager` instance. Get it using `TabAPI.getInstance().getLayoutManager()`. If this feature is disabled, the method will return `null`.
The `LayoutManager` has the following methods:
* `Layout createNewLayout(String name)` - Creates new layout with given internal name and 80 slots.
* `Layout createNewLayout(String name, int slotCount)` - Creates new layout with given internal name and slot count (for 1.19.3+, <1.19.3 players will still see 80 slots).
* `Layout getLayout(String name)` - Returns layout by given internal name. Works for layouts created in both configuration and through the API.
* `sendLayout(TabPlayer player, Layout layout)` - Sends given layout to the player, overriding configuration. Sending `null` as layout will restore layout from configuration.
After creating a `Layout` using the API or getting it from configuration, you can use the following methods:
* `String getName()` - Returns layout's name
* `addFixedSlot(int slot, String text)` / `addFixedSlot(int slot, String text, String skin)` / `addFixedSlot(int slot, String text, int ping)` / `addFixedSlot(int slot, String text, String skin, int ping)` / `addFixedSlot(int slot, String text, String skin, String ping)` - Adds a fixed slot with gieven properties - slot, text, skin (optional) and ping (optional)
* `addGroup(String condition, int[] slots)` - Adds a new player group with given condition (nullable for no condition) and slots. Condition can be either a conditional expression, or a condition created in config.
# Examples
## Example 1 - Per-server columns
```
+1 -1
View File
@@ -1,5 +1,5 @@
# About the wiki
The wiki explains the functionality of the plugin as of version **6.1.2**.
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.0"
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") {
@@ -14,14 +14,16 @@ import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Implementation provider using direct Mojang-mapped NMS code for Paper 26.2.
* Implementation provider using direct Mojang-mapped NMS code for Paper 26.2 - 26.3.
*/
@Getter
public class NMSImplementationProvider implements ImplementationProvider {
/** Flag tracking if this server is Canvas 26.1.2+ with new scoreboard checks */
private final boolean isCanvas = ReflectionUtils.classExists("io.canvasmc.canvas.world.scores.TeamData") ||
ReflectionUtils.classExists("io.canvasmc.canvas.threadedregions.scores.TeamData");
private final boolean isCanvas =
ReflectionUtils.classExists("io.canvasmc.canvas.world.scores.TeamData") ||
ReflectionUtils.classExists("io.canvasmc.canvas.threadedregions.scores.TeamData") ||
ReflectionUtils.classExists("io.canvasmc.canvas.threadedregions.world.scores.TeamData");
@NotNull
private final ComponentConverter<?> componentConverter = new NMSComponentConverter();
@@ -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")
+4 -3
View File
@@ -15,13 +15,14 @@ repositories {
maven("https://repo.papermc.io/repository/maven-public/") // Adventure
}
val minecraftVersion = "26.2"
val minecraftVersion = "26.3"
// Forge API versions for each Minecraft version for easier backporting
// Official website (for updating in the future): https://files.minecraftforge.net/net/minecraftforge/forge/
val forgeApiVersions = mapOf(
"26.2" to "26.2-65.0.0",
"26.1.2" to "26.1.2-64.0.4"
"26.3" to "26.3-66.0.2",
"26.2" to "26.2-65.1.3",
"26.1.2" to "26.1.2-64.1.3"
)
dependencies {
@@ -4,7 +4,6 @@ import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.minecraft.network.chat.numbers.FixedFormat;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.*;
@@ -15,9 +14,7 @@ import net.minecraft.world.scores.criteria.ObjectiveCriteria;
import net.minecraft.world.scores.criteria.ObjectiveCriteria.RenderType;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
/**
@@ -29,7 +26,6 @@ public class ForgeScoreboard extends SafeScoreboard<ForgeTabPlayer> {
private static final net.minecraft.world.scores.Team.CollisionRule[] collisions = net.minecraft.world.scores.Team.CollisionRule.values();
private static final net.minecraft.world.scores.Team.Visibility[] visibilities = net.minecraft.world.scores.Team.Visibility.values();
private static final Scoreboard dummyScoreboard = new Scoreboard();
private static final Field players = ReflectionUtils.getOnlyField(ClientboundSetPlayerTeamPacket.class, Collection.class);
/**
* Constructs new instance with given player.
@@ -139,7 +135,12 @@ public class ForgeScoreboard extends SafeScoreboard<ForgeTabPlayer> {
if (packet instanceof ClientboundSetPlayerTeamPacket team) {
int method = getMethod(team);
if (method != TeamAction.UPDATE) {
players.set(team, onTeamPacket(method, team.getName(), team.getPlayers()));
return new ClientboundSetPlayerTeamPacket(
team.getName(),
method,
team.getParameters(),
onTeamPacket(method, team.getName(), team.getPlayers())
);
}
}
return packet;
@@ -5,12 +5,10 @@ import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
@@ -20,7 +18,6 @@ import net.minecraft.world.level.GameType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.*;
/**
@@ -36,8 +33,6 @@ public class ForgeTabList extends TrackedTabList<ForgeTabPlayer> {
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> updateListOrder = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> updateHat = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT);
private static final Field entries = ReflectionUtils.getOnlyField(ClientboundPlayerInfoUpdatePacket.class, List.class);
/**
* Constructs new instance.
*
@@ -104,7 +99,6 @@ public class ForgeTabList extends TrackedTabList<ForgeTabPlayer> {
}
@Override
@SneakyThrows
@NotNull
public Object onPacketSend(@NonNull Object packet) {
if (packet instanceof ClientboundTabListPacket tablist) {
@@ -158,18 +152,17 @@ public class ForgeTabList extends TrackedTabList<ForgeTabPlayer> {
}
if (rewritePacket) {
ClientboundPlayerInfoUpdatePacket newPacket = new ClientboundPlayerInfoUpdatePacket(actions, Collections.emptyList());
entries.set(newPacket, updatedList);
newPacket.entries = updatedList;
return newPacket;
}
}
return packet;
}
@SneakyThrows
private void sendPacket(@NonNull EnumSet<ClientboundPlayerInfoUpdatePacket.Action> action, @NonNull UUID id, @NonNull String name, @Nullable Skin skin,
boolean listed, int latency, int gameMode, @Nullable TabComponent displayName, int listOrder, boolean showHat) {
ClientboundPlayerInfoUpdatePacket packet = new ClientboundPlayerInfoUpdatePacket(action, Collections.emptyList());
entries.set(packet, Collections.singletonList(new ClientboundPlayerInfoUpdatePacket.Entry(
packet.entries = Collections.singletonList(new ClientboundPlayerInfoUpdatePacket.Entry(
id,
action.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER) ? createProfile(id, name, skin) : null,
listed,
@@ -179,7 +172,7 @@ public class ForgeTabList extends TrackedTabList<ForgeTabPlayer> {
showHat,
listOrder,
null
)));
));
sendPacket(packet);
}
@@ -0,0 +1,5 @@
# Scoreboard team
public net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket <init>(Ljava/lang/String;ILjava/util/Optional;Ljava/util/Collection;)V
# Player info
public-f net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket entries
+1 -1
View File
@@ -15,6 +15,6 @@ displayURL = "@website@"
[[dependencies.tab]]
modId = "minecraft"
mandatory = true
versionRange = "[26.2]"
versionRange = "[26.2],[26.3]"
ordering = "NONE"
side = "SERVER"
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
+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";
@@ -149,9 +149,13 @@ public class DumpCommand extends SubCommand {
mysql.put("password", "*CENSORED*");
Map<String, Object> proxySupport = (Map<String, Object>) config.get("proxy-support");
Map<String, Object> redis = (Map<String, Object>) proxySupport.get("redis");
redis.put("url", "*CENSORED*");
if (redis != null) {
redis.put("url", "*CENSORED*");
}
Map<String, Object> rabbitmq = (Map<String, Object>) proxySupport.get("rabbitmq");
rabbitmq.put("url", "*CENSORED*");
if (rabbitmq != null) {
rabbitmq.put("url", "*CENSORED*");
}
}
@NotNull
@@ -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");
}
@@ -25,8 +25,9 @@ public class YamlPropertyConfigurationFile extends YamlConfigurationFile impleme
private final String PER_WORLD = "per-world";
private final String category;
private final Collection<String> worldGroups = new ArrayList<>(this.<String, Object>getMap(PER_WORLD).keySet());
private final Collection<String> serverGroups = new ArrayList<>(this.<String, Object>getMap(PER_SERVER).keySet());
private final ConfigurationSection root = getConfigurationSection("");
private final Collection<String> worldGroups = section(PER_WORLD).getKeys().stream().map(Object::toString).collect(Collectors.toList());
private final Collection<String> serverGroups = section(PER_SERVER).getKeys().stream().map(Object::toString).collect(Collectors.toList());
/**
* Constructs new instance and attempts to load specified configuration file.
@@ -48,26 +49,29 @@ public class YamlPropertyConfigurationFile extends YamlConfigurationFile impleme
public YamlPropertyConfigurationFile(@Nullable InputStream source, @NonNull File destination) throws IOException {
super(source, destination);
category = destination.getName().contains("groups") ? "group" : "user";
for (Map.Entry<Object, Object> entry : getValues().entrySet()) {
if (entry.getKey().equals(PER_SERVER)) {
for (Object key : root.getKeys()) {
String entry = key.toString();
if (entry.equals(PER_SERVER)) {
for (String server : serverGroups) {
for (String name : this.<String, Object>getMap(PER_SERVER + "." + server).keySet()) {
for (String property : this.<String, Object>getMap(PER_SERVER + "." + server + "." + name).keySet()) {
checkProperty(destination.getName(), category, name, property, server, null, true);
ConfigurationSection serverSection = section(PER_SERVER, server);
for (Object name : serverSection.getKeys()) {
for (Object property : serverSection.getConfigurationSection(name.toString()).getKeys()) {
checkProperty(destination.getName(), category, name.toString(), property.toString(), server, null, true);
}
}
}
} else if (entry.getKey().equals(PER_WORLD)) {
} else if (entry.equals(PER_WORLD)) {
for (String world : worldGroups) {
for (String name : this.<String, Object>getMap(PER_WORLD + "." + world).keySet()) {
for (String property : this.<String, Object>getMap(PER_WORLD + "." + world + "." + name).keySet()) {
checkProperty(destination.getName(), category, name, property, null, world, true);
ConfigurationSection worldSection = section(PER_WORLD, world);
for (Object name : worldSection.getKeys()) {
for (Object property : worldSection.getConfigurationSection(name.toString()).getKeys()) {
checkProperty(destination.getName(), category, name.toString(), property.toString(), null, world, true);
}
}
}
} else {
for (String property : this.<String, Object>getMap(entry.getKey().toString()).keySet()) {
checkProperty(destination.getName(), category, entry.getKey().toString(), property, null, null, true);
for (Object property : root.getConfigurationSection(entry).getKeys()) {
checkProperty(destination.getName(), category, entry, property.toString(), null, null, true);
}
}
}
@@ -75,13 +79,16 @@ public class YamlPropertyConfigurationFile extends YamlConfigurationFile impleme
@Override
public void setProperty(@NonNull String name, @NonNull String property, @Nullable Server server, @Nullable World world, @Nullable String value) {
ConfigurationSection target;
if (world != null) {
set(String.format("%s.%s.%s.%s", PER_WORLD, world.getName(), name, property), fromString(value));
target = getOrCreateSection(PER_WORLD, world.getName(), name);
} else if (server != null) {
set(String.format("%s.%s.%s.%s", PER_SERVER, server.getName(), name, property), fromString(value));
target = getOrCreateSection(PER_SERVER, server.getName(), name);
} else {
set(String.format("%s.%s", name, property), fromString(value));
target = getOrCreateSection(name);
}
target.put(property, fromString(value));
save();
}
@Override
@@ -89,22 +96,22 @@ public class YamlPropertyConfigurationFile extends YamlConfigurationFile impleme
String worldName = world == null ? null : world.getName();
String serverName = server == null ? null : server.getName();
Object value;
if ((value = getObject(new String[] {PER_WORLD, TAB.getInstance().getConfiguration().getGroup(worldGroups, worldName), name, property})) != null) {
if ((value = section(PER_WORLD, TAB.getInstance().getConfiguration().getGroup(worldGroups, worldName), name).getObject(property)) != null) {
return new String[] {toString(value), category + "=" + name + ", world=" + worldName};
}
if ((value = getObject(new String[] {PER_WORLD, TAB.getInstance().getConfiguration().getGroup(worldGroups, worldName), TabConstants.DEFAULT_GROUP, property})) != null) {
if ((value = section(PER_WORLD, TAB.getInstance().getConfiguration().getGroup(worldGroups, worldName), TabConstants.DEFAULT_GROUP).getObject(property)) != null) {
return new String[] {toString(value), category + "=" + TabConstants.DEFAULT_GROUP + ", world=" + worldName};
}
if ((value = getObject(new String[] {PER_SERVER, TAB.getInstance().getConfiguration().getServerGroup(serverGroups, server), name, property})) != null) {
if ((value = section(PER_SERVER, TAB.getInstance().getConfiguration().getServerGroup(serverGroups, server), name).getObject(property)) != null) {
return new String[] {toString(value), category + "=" + name + ", server=" + serverName};
}
if ((value = getObject(new String[] {PER_SERVER, TAB.getInstance().getConfiguration().getServerGroup(serverGroups, server), TabConstants.DEFAULT_GROUP, property})) != null) {
if ((value = section(PER_SERVER, TAB.getInstance().getConfiguration().getServerGroup(serverGroups, server), TabConstants.DEFAULT_GROUP).getObject(property)) != null) {
return new String[] {toString(value), category + "=" + TabConstants.DEFAULT_GROUP + ", server=" + serverName};
}
if ((value = getObject(new String[] {name, property})) != null) {
if ((value = section(name).getObject(property)) != null) {
return new String[] {toString(value), category + "=" + name};
}
if ((value = getObject(new String[] {TabConstants.DEFAULT_GROUP, property})) != null) {
if ((value = section(TabConstants.DEFAULT_GROUP).getObject(property)) != null) {
return new String[] {toString(value), category + "=" + TabConstants.DEFAULT_GROUP};
}
return new String[0];
@@ -112,39 +119,81 @@ public class YamlPropertyConfigurationFile extends YamlConfigurationFile impleme
@Override
public void remove(@NonNull String name) {
set(name, null);
getMap(PER_WORLD).keySet().forEach(world -> set(PER_WORLD + "." + world + "." + name, null));
getMap(PER_SERVER).keySet().forEach(server -> set(PER_SERVER + "." + server + "." + name, null));
root.remove(name);
for (Object world : section(PER_WORLD).getKeys()) {
section(PER_WORLD, world.toString()).remove(name);
}
for (Object server : section(PER_SERVER).getKeys()) {
section(PER_SERVER, server.toString()).remove(name);
}
save();
}
@Override
@NotNull
public Map<String, Object> getGlobalSettings(@NonNull String name) {
return getMap(name);
Map<String, Object> settings = root.getMap(name);
return settings == null ? new LinkedHashMap<>() : settings;
}
@Override
@NotNull
public Map<String, Map<String, Object>> getPerWorldSettings(@NonNull String name) {
return convertMap(getMap(PER_WORLD), name);
return getSettingsByGroup(PER_WORLD, name);
}
@Override
@NotNull
public Map<String, Map<String, Object>> getPerServerSettings(@NonNull String name) {
return convertMap(getMap(PER_SERVER), name);
return getSettingsByGroup(PER_SERVER, name);
}
@Override
@NotNull
public Set<String> getAllEntries() {
Set<Object> set = new HashSet<>(values.keySet());
Set<Object> set = new HashSet<>(root.getKeys());
set.remove(PER_WORLD);
set.remove(PER_SERVER);
Map<String, Map<String, Map<String, String>>> perWorld = getMap(PER_WORLD);
perWorld.values().forEach(m -> set.addAll(m.keySet()));
Map<String, Map<String, Map<String, String>>> perServer = getMap(PER_SERVER);
perServer.values().forEach(m -> set.addAll(m.keySet()));
addNestedEntryNames(set, PER_WORLD);
addNestedEntryNames(set, PER_SERVER);
return set.stream().map(Object::toString).collect(Collectors.toSet());
}
@NotNull
private ConfigurationSection section(@NotNull String... path) {
ConfigurationSection section = root;
for (String key : path) {
section = section.getConfigurationSection(key);
}
return section;
}
@NotNull
private ConfigurationSection getOrCreateSection(@NotNull String... path) {
ConfigurationSection section = root;
for (String key : path) {
if (section.getMap(key) == null) {
section.put(key, new LinkedHashMap<>());
}
section = section.getConfigurationSection(key);
}
return section;
}
@NotNull
private Map<String, Map<String, Object>> getSettingsByGroup(@NotNull String sectionName, @NotNull String name) {
Map<String, Map<String, Object>> settings = new HashMap<>();
ConfigurationSection sections = section(sectionName);
for (Object key : sections.getKeys()) {
settings.put(key.toString(), sections.getConfigurationSection(key.toString()).getMap(name));
}
return settings;
}
private void addNestedEntryNames(@NotNull Set<Object> entries, @NotNull String sectionName) {
ConfigurationSection sections = section(sectionName);
for (Object key : sections.getKeys()) {
entries.addAll(sections.getConfigurationSection(key.toString()).getKeys());
}
}
}
@@ -495,6 +495,8 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
public void registerServerPlaceholder(@NonNull Pattern identifier, int refresh, @NonNull Function<Matcher, Supplier<String>> function) {
ensureActive();
registeredDynamicPlaceholders.put(identifier, id -> groups -> new ServerPlaceholderImpl(id, refresh, function.apply(groups)));
replaceMatchingPlaceholders(identifier,
(id, groups) -> new ServerPlaceholderImpl(id, refresh, function.apply(groups)));
}
@Override
@@ -502,6 +504,8 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
@NonNull Function<Matcher, Function<me.neznamy.tab.api.TabPlayer, String>> function) {
ensureActive();
registeredDynamicPlaceholders.put(identifier, id -> groups -> new PlayerPlaceholderImpl(id, refresh, function.apply(groups)));
replaceMatchingPlaceholders(identifier,
(id, groups) -> new PlayerPlaceholderImpl(id, refresh, function.apply(groups)));
}
@Override
@@ -509,6 +513,18 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
@NonNull Function<Matcher, BiFunction<me.neznamy.tab.api.TabPlayer, me.neznamy.tab.api.TabPlayer, String>> function) {
ensureActive();
registeredDynamicPlaceholders.put(identifier, id -> groups -> new RelationalPlaceholderImpl(id, refresh, function.apply(groups)));
replaceMatchingPlaceholders(identifier,
(id, groups) -> new RelationalPlaceholderImpl(id, refresh, function.apply(groups)));
}
private void replaceMatchingPlaceholders(@NotNull Pattern identifier,
@NotNull BiFunction<String, Matcher, ? extends Placeholder> factory) {
for (Entry<String, PlaceholderReference> entry : new ArrayList<>(registeredPlaceholders.entrySet())) {
Matcher matcher = identifier.matcher(entry.getKey());
if (!matcher.matches()) continue;
bridgePlaceholders.remove(entry.getKey());
registerPlaceholder(factory.apply(entry.getKey(), matcher));
}
}
@NotNull
@@ -290,9 +290,10 @@ public class BelowName extends RefreshableFeature implements JoinListener, QuitL
}
private void updatePlayer(@NotNull TabPlayer player) {
int value = getValue(player);
for (TabPlayer all : onlinePlayers.getPlayers()) {
setScore(player, all, getValue(all), all.belowNameData.fancyValue.getFormat(player));
if (all != player) setScore(all, player, getValue(player), player.belowNameData.fancyValue.getFormat(all));
if (all != player) setScore(player, all, getValue(all), all.belowNameData.fancyValue.getFormat(player));
setScore(all, player, value, player.belowNameData.fancyValue.getFormat(all));
}
}
@@ -326,8 +327,9 @@ public class BelowName extends RefreshableFeature implements JoinListener, QuitL
@Override
public void onVanishStatusChange(@NotNull TabPlayer player) {
if (player.isVanished()) return;
int value = getValue(player);
for (TabPlayer viewer : onlinePlayers.getPlayers()) {
setScore(viewer, player, getValue(player), player.belowNameData.fancyValue.getFormat(viewer));
setScore(viewer, player, value, player.belowNameData.fancyValue.getFormat(viewer));
}
}
@@ -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]);
@@ -77,23 +77,16 @@ public class LayoutManagerImpl extends RefreshableFeature implements LayoutManag
p.layoutData.sortingString = p.sortingData.fullTeamName;
sortedPlayers.put(p, p.sortingData.fullTeamName);
LayoutPattern highest = getHighestLayout(p);
if (highest != null) {
sendLayout(p, highest);
}
sendLayout(p, highest);
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
if (all.layoutData.currentLayout != null) all.layoutData.currentLayout.view.onJoin(p);
}
// Unformat original entries for players who can see a layout to avoid spaces due to unparsed placeholders and such
if (highest == null) return;
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
p.getTabList().updateDisplayName(all, null);
}
}
@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();
@@ -112,17 +105,30 @@ public class LayoutManagerImpl extends RefreshableFeature implements LayoutManag
LayoutPattern current = p.layoutData.currentLayout == null ? null : p.layoutData.currentLayout.view.getPattern();
if (highest != current) {
if (current != null) p.layoutData.currentLayout.view.destroy();
p.layoutData.currentLayout = null;
if (highest != null) {
sendLayout(p, highest);
}
sendLayout(p, highest);
}
}
private void sendLayout(@NotNull TabPlayer player, @NotNull LayoutPattern pattern) {
LayoutBase view = new FakeEntryLayout(this, pattern, player);
player.layoutData.currentLayout = new LayoutData(view);
view.send();
private void sendLayout(@NotNull TabPlayer player, @Nullable LayoutPattern pattern) {
if (pattern != null) {
LayoutBase view = new FakeEntryLayout(this, pattern, player);
player.layoutData.currentLayout = new LayoutData(view);
view.send();
if (playerList != null) {
// Unformat original entries for players who can see a layout to avoid spaces due to unparsed placeholders and such
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
player.getTabList().updateDisplayName(all, null);
}
}
} else {
player.layoutData.currentLayout = null;
if (playerList != null) {
// Reformat original entries for players without any layout
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
player.getTabList().updateDisplayName(all, playerList.getTabFormat(all, player));
}
}
}
}
@Override
@@ -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 {
@@ -297,8 +297,9 @@ public class YellowNumber extends RefreshableFeature implements JoinListener, Qu
@Override
public void onVanishStatusChange(@NotNull TabPlayer player) {
if (player.isVanished()) return;
int value = getValue(player);
for (TabPlayer viewer : onlinePlayers.getPlayers()) {
setScore(viewer, player, getValue(player), player.playerlistObjectiveData.fancyValue.getFormat(viewer));
setScore(viewer, player, value, player.playerlistObjectiveData.fancyValue.getFormat(viewer));
}
}
@@ -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()) return; // 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);
@@ -115,7 +115,9 @@ public abstract class TabPlaceholder implements Placeholder {
if (PlaceholderIdentifier.isRelational(s)) continue; // Relational placeholders are handled separately
if ((identifier.startsWith("%sync:") && ("%" + identifier.substring(6)).equals(s))) continue; // Self, but as sync variant
PlaceholderReference nested = TAB.getInstance().getPlaceholderManager().getPlaceholderReference(s);
nested.addParent(reference);
if (reference != null) {
nested.addParent(reference);
}
string = string.replace(s, nested.getHandle().parse(player));
}
return string;
@@ -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
@@ -49,6 +49,9 @@ public abstract class SafeScoreboard<T extends TabPlayer> implements Scoreboard
/** Registered teams */
private final Map<String, Team> teams = new ConcurrentHashMap<>();
/** Teams indexed by entries */
private final Map<String, Team> teamsByEntries = new ConcurrentHashMap<>();
@Override
public synchronized void registerObjective(@NonNull String objectiveName, @NonNull TabComponent title,
@NonNull HealthDisplay display, @Nullable TabComponent numberFormat) {
@@ -143,6 +146,9 @@ public abstract class SafeScoreboard<T extends TabPlayer> implements Scoreboard
}
Team team = new Team(createTeam(name), name, prefix, suffix, visibility, collision, players, options, color);
teams.put(name, team);
for (String entry : players) {
teamsByEntries.put(entry, team);
}
if (frozen) return;
registerTeam(team);
}
@@ -154,6 +160,9 @@ public abstract class SafeScoreboard<T extends TabPlayer> implements Scoreboard
error("Tried to unregister non-existing team %s for player ", teamName);
return;
}
for (String entry : team.getPlayers()) {
teamsByEntries.remove(entry, team);
}
if (frozen) return;
unregisterTeam(team);
}
@@ -331,7 +340,7 @@ public abstract class SafeScoreboard<T extends TabPlayer> implements Scoreboard
List<String> newList = new ArrayList<>();
if (action == TeamAction.CREATE || action == TeamAction.ADD_PLAYER) {
for (String entry : players) {
Team expectedTeam = getExpectedTeam(entry);
Team expectedTeam = teamsByEntries.get(entry);
if (expectedTeam == null) {
blockedTeamAdds.remove(entry);
allowedTeamAdds.put(entry, teamName);
@@ -350,7 +359,7 @@ public abstract class SafeScoreboard<T extends TabPlayer> implements Scoreboard
if (action == TeamAction.REMOVE_PLAYER) {
// TAB does not send remove player, making checks easier
for (String entry : players) {
Team expectedTeam = getExpectedTeam(entry);
Team expectedTeam = teamsByEntries.get(entry);
if (expectedTeam != null) {
allowedTeamAdds.remove(entry);
blockedTeamAdds.remove(entry);
@@ -371,14 +380,6 @@ public abstract class SafeScoreboard<T extends TabPlayer> implements Scoreboard
return newList;
}
@Nullable
private Team getExpectedTeam(@NotNull String player) {
for (Team team : teams.values()) {
if (team.getPlayers().contains(player)) return team;
}
return null;
}
/**
* Logs a message into anti-override log when blocking attempt to add
* a player into a team.
@@ -86,7 +86,7 @@ public abstract class TrackedTabList<P extends TabPlayer> implements TabList {
} else {
forcedDisplayNames.remove(target.getTablistId());
}
if (target.getVersion().getNetworkId() < ProtocolVersion.V1_8.getNetworkId()) {
if (player.getVersion().getNetworkId() < ProtocolVersion.V1_8.getNetworkId()) {
return; // Display names are not supported on 1.7 and below
}
if (containsEntry(target.getTablistId())) {
+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();