Compare commits

...
42 Commits
Author SHA1 Message Date
NEZNAMY f6359d2f77 6.2.0 2026-09-17 16:26:18 +02:00
NEZNAMY 727c500b17 [Bukkit] Update supported versions message 2026-09-16 20:40:47 +02:00
NEZNAMY bebf2988fb [Bukkit] Mark Spigot 26.3 as compatible 2026-09-16 20:37:36 +02:00
NEZNAMY e3cde1968b [NeoForge] Update to support 26.3 as well 2026-09-16 20:26:49 +02:00
NEZNAMY ef6b668c70 [Fabric] Change "players" field access to support both 26.2 and 26.3 2026-09-16 20:04:56 +02:00
NEZNAMY fef62e796b [Bukkit] Drop support for 1.19.1 - 1.19.3 2026-09-16 18:04:28 +02:00
NEZNAMY 0d4b1dbce2 [Bukkit] Mark Paper 26.3 as compatible 2026-09-16 18:01:42 +02:00
NEZNAMY 43d210dbf8 [Bukkit] Update to new bStats chart 2026-09-15 19:03:33 +02:00
NEZNAMY e0d2209e97 [API] Mark redundant method as deprecated 2026-09-15 18:30:02 +02:00
NEZNAMY 720cae3e94 [Fabric] Update to 26.3 2026-09-15 17:56:00 +02:00
NEZNAMY f2ea34a717 [Conditions] Fix NPE during config conversion 2026-09-15 17:51:27 +02:00
NEZNAMY 2c33747e1b [Placeholders] Make method synchronized to avoid concurrency issues (#1724, #1725) 2026-09-12 11:32:36 +02:00
NEZNAMY 5f9d72eb90 Include forgotten change 2026-09-12 10:47:42 +02:00
NEZNAMY 3abcc1d96f [Fabric] Update to 26.3-rc1 2026-09-12 10:35:59 +02:00
NEZNAMY 7d458422c7 [Belowname / Playerlist objective] Only create the features if they are actually enabled (got broken by a PR) 2026-09-12 10:28:24 +02:00
NEZNAMY 0feee14bfb 6.2.0-SNAPSHOT 2026-09-10 18:53:28 +02:00
NEZNAMY 0023cfcafb [Description] Update badges 2026-09-06 17:57:35 +02:00
NEZNAMY 08f0d7f635 6.1.3 2026-09-05 17:33:02 +02:00
NEZNAMY 73a5b14084 [Wiki] Monthly wiki backup for changes made in August 2026 2026-09-01 17:17:14 +02:00
NEZNAMY da8befca86 [Layout] Change layout entry UUIDs back to previous value due to a bug in old MC versions (#1716) 2026-08-23 09:45:54 +02:00
NEZNAMY 33cc64a441 Merge pull request #1714 from EUBedwars/feature/objective-api
[API] Add BelowName and PlayerListObjective managers
2026-08-20 16:34:00 +02:00
crackma a13fb02c75 [API] Add requested changed to BelowName and PlayerListObjective managers 2026-08-20 00:30:05 +03:00
NEZNAMY c9d9a8f04a [GitHub] Create a new issue template for "Problem" type 2026-08-19 17:08:46 +02:00
crackma 5de6c850ed [API] Add BelowName and PlayerListObjective managers 2026-08-17 22:06:38 +03:00
NEZNAMY d98044c51c [Scoreboard] Fix issues with "Long|" lines from previous commit 2026-08-16 19:42:39 +02:00
NEZNAMY 0575bf0239 [Scoreboard] Rework line management and improve empty text handling (#1685) 2026-08-16 18:06:57 +02:00
NEZNAMY 992c96dd52 [Placeholders] Properly call update function (#1708) 2026-08-16 14:13:03 +02:00
NEZNAMY 4d969ab320 Fix 2 NPEs and move more towards PlaceholderReference usage (#1713) 2026-08-14 18:52:47 +02:00
NEZNAMY 283713ff8a [Placeholders] Rework nested placeholder management (#1708) 2026-08-13 19:46:32 +02:00
NEZNAMY 231aa89686 Merge pull request #1712 from juinwei7/fix/velocity-scoreboard-api-sidebar-yield
Fix VSA downstream sidebar yielding race
2026-08-13 17:09:30 +02:00
weiwei d67372b72f Fix yielding sidebar to downstream objectives with VSA 2026-08-12 23:40:40 +08:00
NEZNAMY 93046154a9 [Conditions] Reorder priorities to give "=" operators higher priority than others (#1709) 2026-08-10 17:13:29 +02:00
NEZNAMY 2966f57d42 [Commands] Allow "-s" argument to be used without specifying player in bossbar and scoreboard commands 2026-08-09 13:24:28 +02:00
NEZNAMY 951cb260c5 6.1.3 2026-08-09 08:25:10 +02:00
NEZNAMY 2256e9ef77 [Description] Update badges 2026-08-09 08:25:00 +02:00
NEZNAMY 188e0ea932 Merge pull request #1705 from flennium/agent/scoreboard-fix-number-format-line-hiding
[Scoreboard] Fix line hiding with number format
2026-08-09 08:21:25 +02:00
NEZNAMY 6e6848e007 6.1.2 2026-08-09 07:58:23 +02:00
NEZNAMY fc83f0fec6 [Bukkit] Catch everything to fix error on 1.8.8 and lower (#1706) 2026-08-09 07:47:31 +02:00
flennium 034a4b6b7c Fix scoreboard line hiding with number format 2026-08-08 21:58:05 +01:00
NEZNAMY 30593c1bfd [Bukkit] Attempt to reduce false bug reports by 75% (#1701 #1702 #1704) 2026-08-08 09:42:39 +02:00
NEZNAMY 89e69591e0 6.1.2-SNAPSHOT 2026-08-08 09:20:26 +02:00
NEZNAMY 147279a8ca [Description] Update badges 2026-08-04 17:48:22 +02:00
88 changed files with 1692 additions and 2287 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
name: Problem
description: A problem with the plugin that is not a bug
labels: ["Type: Problem"]
body:
- type: textarea
attributes:
label: Description
description: Describe the problem
placeholder: |
Describe the problem here. This is not limited to the plugin jar itself. You may report issues with the wiki, plugin description on marketplaces, or anything else related to TAB.
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
description: Let's make sure this report is valid
options:
- label: This is not an error, bug report, compatibility issue or anything of that kind
required: true
+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.0"><title>Release: 6.1.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.1.0</text><text x="715" y="150" fill-opacity=".3" textLength="290">6.1.0</text></g><text x="715" y="140" textLength="290">6.1.0</text></g></g></svg>
<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>

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.0"
release = "6.1.3"
download_image(f"https://img.shields.io/badge/Release-{release}-blue.svg", "Badge_Release.svg")
java = "8+"
+2 -1
View File
@@ -8,9 +8,10 @@ For TAB to properly detect player as a bedrock player, floodgate and TAB must be
If TAB is installed on backend and floodgate is on both proxy and backend,
make sure [they are connected](https://geysermc.org/wiki/floodgate/setup/?platform=proxy-servers).
To make sure floodgate is configured correctly, you have two options:
To make sure floodgate is configured correctly, you can do any of the following:
* Set `debug: true` in TAB config and check console output on join/reload. It should say `Floodgate returned bedrock status TRUE for player <player>`.
* Run `/tab parse <player> %bedrock%`. It should return `true`.
* Run `/tab dump <player>`, open the link and see ` bedrockPlayer: '<true|false> (floodgate is installed: <true|false>)'`
If any of them say `false` for an actual bedrock player, you didn't connect floodgate correctly.
+3 -4
View File
@@ -11,12 +11,12 @@ TAB does not depend on any other plugins. The experience can however be enhanced
# Supported server software and versions
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.**
**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.
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 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+).
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+).
The sections below provide **additional** information for each platform.
@@ -38,8 +38,7 @@ Every TAB update only supports the latest Fabric version(s) at the time of relea
Minecraft 1.x - TAB 5.5.0 was backported to 1.20 - 1.21.11 and the jars were uploaded to Modrinth. TAB 5.0.7 supports all Fabric versions from 1.14 to 1.19.4. Backports to 1.x are no longer offered.
## Forge
Every TAB update only supports the latest Forge version(s) at the time of release.
**Currently, Forge 26.x is not supported because the modding tool is broken**.
Every TAB update only supports the latest Forge version(s) at the time of release.
Minecraft 1.x - TAB 5.5.0 was backported to 1.20 - 1.21.11 and the jars were uploaded to Modrinth, along with a few other random versions people requested. If your version is not available, you may request a backport.
+4 -4
View File
@@ -17,7 +17,7 @@
# About
This page explains how to import TAB's API into your project and together with other pages everything you can do with it.
Before starting please seriously reconsider if you really need this and cannot already achieve it using existing features (such as registering placeholders into PlaceholderAPI instead of TAB, using display conditions for scoreboards, ...). If not, consider making a request.
Before starting, please seriously reconsider if you really need this and cannot already achieve it using existing features (such as registering placeholders into PlaceholderAPI instead of TAB, using display conditions for scoreboards, ...). If not, consider making a request.
# Adding the dependency
@@ -35,7 +35,7 @@ For Maven users, you can add the API dependency like this:
<dependency>
<groupId>com.github.NEZNAMY</groupId>
<artifactId>TAB-API</artifactId>
<version>6.0.0</version>
<version>6.1.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
@@ -50,7 +50,7 @@ repositories {
}
dependencies {
compileOnly 'com.github.NEZNAMY:TAB-API:6.0.0'
compileOnly 'com.github.NEZNAMY:TAB-API:6.1.2'
}
```
@@ -61,7 +61,7 @@ repositories {
}
dependencies {
compileOnly("com.github.NEZNAMY", "TAB-API", "6.0.0")
compileOnly("com.github.NEZNAMY", "TAB-API", "6.1.2")
}
```
+3 -2
View File
@@ -55,10 +55,11 @@ All the options are explained in the following table.
|---------------------|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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). |
| 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. |
# Limitations
* [1.12.2-] Title length is limited to 32 characters (including color codes).
+6 -8
View File
@@ -52,7 +52,7 @@ Most commonly used plugins for this are [FastLogin](https://www.spigotmc.org/res
which change connections of premium players who enabled it to online connections.
The same goes for disabling heads - you cannot disable them if you have online mode enabled.
Minecraft 1.21.9 has added [object components](https://github.com/NEZNAMY/TAB/wiki/How-to-use-Minecraft-components#object-components-1219), which allow you to display player heads. You can put for example `<head:name:%player%>` at the beginning of tabprefix to simulate this.
Minecraft 1.21.9 has added [object components](https://github.com/NEZNAMY/TAB/wiki/How-to-use-Minecraft-components#object-components-1219), which allow you to display player heads. You can put for example `<head:%player%>` at the beginning of tabprefix to simulate this (you will need a plugin like SkinsRestorer for the players to properly be able to recognize skins by player name).
## #7 - Where can I find current default config files?
You can do any of the following:
@@ -118,16 +118,14 @@ Using `''` will result in the text being display literally, not as a code.
**Note:** Minecraft does not support every single UTF symbol and displays unsupported symbols as a box. You can try sending your symbol into chat and see if it works or not. If not, it's not supported by MC.
## #14 - Is there a way to remove all players from tablist?
In Minecraft, for a player to be visible in-game, they also need to appear in the tablist.
Remember the issue with NPCs showing up in the tablist? It's the same underlying mechanic.
Here are various ways to achieve this goal:
If you're okay with players not being visible in the world, you can use a plugin that hides all players completely.
* **Option 1: Hide players entirely** - In Minecraft, for a player to be visible in-game, they also need to added into the tablist. If you don't mind hiding players from game as well, there are plugins that offer this (TAB is, however, not one of them).
In 1.19.3, the player info packet received a new field that allows us to explicitly hide players from tablist while all the necessary data is still sent to players.
Since Minecraft 1.19.3, this limitation has been lifted — it's now possible to have entities in the game without showing them in the tablist.
However, the TAB plugin does **not** support removing all players from the tablist in any version of Minecraft.
* **Option 2: Layout with 0 slots** - Since Minecraft 1.19.3, the limitation explained above has been lifted — it's now possible to have entities in the game without showing them in the tablist. TAB does not have an explicit feature for doing this. However, since 1.19.3, the [Layout](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Layout) feature uses this mechanic to allow less than 80 slots. This feature can be "abused" by enabling the feature, removing fixed-slots and adding `slot-count: 0` to the displayed layout. The final effect will be a playerless tablist.
An alternative solution using TAB is putting a lot of empty lines into header,
which will push all players out of the screen.
* **Option 3: Pushing players out of the screen with header** - You can add a ton of empty lines into the header. As a result, players will be pushed down out of the screen and effectively not visible.
## #15 - How to add images to tablist?
You can check out [this Reddit post](https://www.reddit.com/r/admincraft/comments/llrgty/comment/gnswdcz/?utm_source=share&utm_medium=web2x&context=3).
+1 -1
View File
@@ -1,5 +1,5 @@
# About the wiki
The wiki explains the functionality of the plugin as of version **6.1.0**.
The wiki explains the functionality of the plugin as of version **6.1.2**.
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.
+13 -23
View File
@@ -12,7 +12,7 @@
* [Object components (1.21.9+)](#object-components-1219)
* [Atlas sprite](#atlas-sprite)
* [Player sprite](#player-sprite)
* [Compatibility with < 1.21.9](#compatibility-with--1219)
* [Compatibility with <1.21.9](#compatibility-with-1219)
* [MiniMessage](#minimessage)
* [Config options](#config-options)
* [Tips & Tricks](#tips--tricks)
@@ -121,13 +121,6 @@ Example:
## Player sprite
> [!WARNING]
> In order to use these, you will need to disable MiniMessage support, because head components were not added to MiniMessage yet. Even when they are added, they will have a different syntax, so TAB will be updated to match the format to avoid complications. For now, use the existing syntax for prototyping and testing. To disable MiniMessage support, set:
> ```
> components:
> minimessage-support: false
> ```
> in **config.yml**.
This type allows you to display Minecraft heads. There are 3 ways to define a head:
| Type | Functionality |
@@ -139,27 +132,25 @@ This type allows you to display Minecraft heads. There are 3 ways to define a he
Here are all the ways you can display player skins using TAB and their implementation (using `ALL CAPS` for "placeholder" text instead of `<>` to avoid confusion as `<>` is actual part of the syntax):
| Syntax | Explanation | Implementation |
|-----|-------------|------------|
| `<head:name:NAME>` | Shows head defined by player name | name |
| `<head:id:UUID>` | Shows head defined by player UUID | uuid |
| `<head:signed_texture:VALUE;SIGNATURE>` | Shows head defined by value and signature | value and signature |
| `<head:player:NAME` | Connects to Mojang and retrieves value and signature of defined player | value and signature |
| `<head:mineskin:ID>` | Connects to Mineskin and retrieves value and signature of specified upload ID | value and signature |
| `<head:texture:TEXTURE>` | Gets the texture from `http://textures.minecraft.net/texture/TEXTURE` | value and signature |
| `<head:NAME>` | Shows head defined by player name | name |
| `<head:UUID>` | Shows head defined by player UUID | uuid |
| `<mineskin:ID>` | Connects to Mineskin and retrieves value and signature of specified upload ID | value and signature |
| `<head_texture:TEXTURE>` | Gets the texture from `http://textures.minecraft.net/texture/TEXTURE` | value and signature |
All these syntaxes are identical to MiniMessage syntax for convenience. MiniMessage only supports the first two types by default, but TAB adds tag resolvers for `mineskin` and `head_texture` as well.
Here is an example for all types:
```
footer:
- 'Head by id: <head:id:%uuid%>' # This will obviously not work in offline mode
- 'Head by name: <head:name:%player%>' # May or may not work in offline mode (see above for full explanation)
- 'Head by raw texture: <head:signed_texture:ewogICJ0aW1lc3RhbXAiIDogMTc1NjMxNjc0OTk4MywKICAicHJvZmlsZUlkIiA6ICIyMzdkOGI1NTNmOTc0NzQ5YWE2MGU5ZmU5N2I0NTA2MiIsCiAgInByb2ZpbGVOYW1lIiA6ICJfTkVaTkFNWV8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZmY1ZTNhNWRmODkxYzEzMTdhZjUzMmMzNmY4NDgxNDBhNDBmMGZhMGNlZGRmZmEwNWU5NGU1NzU1OGQxY2Q4YyIKICAgIH0KICB9Cn0=;YFVoqA0CqWgnZhme5s/M7xtadURyLkyLWVsh0vN8BqdfwzN32nqlBJb2pRjkxIs0V21EOeKbGFjgdKQKzPRQdf6sTFQ/x0KOjUg8A/neAiwCvDSDrsTQ2Yf7yoVP6M2PpuB7te19N+I139mGY5psxU100x6GiV/uFfpUfG3XBJog43JtXzRJ9fRtoSeIEzqLrkpCmt6o5Mzo6GZFZc4CtI76OU90Mg9ZvTZTYelvtjFtllxTtkNZCcglzvh5R19+qtzLOzEr+N8m4Ed+5yZyezEb3LeeSgmbSLIjuOKuUupE+2F6yVYP3eKhgGLZ2G+cg9TZZjTCsNMzHqewM/1+qStzTQCdNmggXCGcIfC8HcYsBfdv4SicxBq8ff+BDyveFZMFyREpQNqX/fLmlz16cmxHvBQ9qqA+IzIsBJ7i/mrG78jBhkJsvtcHMHvTviXascCSQP1TCn58D6HJ/Agww6HFTJe/B6sX0Utzm0URE4jZK7wnhrx0q2H8OkCXc5ZwuXALqtvk0uWmZo2RMnIGQNi4nb5AUsGV8pNirhS16MfqZeJ4q0HGaIVscRp4jazab6kMVKusbuqQB1cZNbaao7mP1HAUVCd5geWQL4CQQIc6gv0q3KT2E45d0YeYqpy/RppKMWmg1+aQ5wVGQW4/p2mqXos71FKy6vP0ur6txd8=>'
- 'Head by player: <head:player:%player%>'
- 'Head by mineskin: <head:mineskin:37e93c8e12cd426cb28fce31969e0674>'
- 'Head by texture: <head:texture:ff9bb9e56125c8227b94bbda9f6e0f862931c229255ba8f1205d13c44c1bb561>'
- 'Head by id: <head:%uuid%>' # This will obviously not work in offline mode
- 'Head by name: <head:%player%>' # May or may not work in offline mode (see above for full explanation)
- 'Head by mineskin: <mineskin:37e93c8e12cd426cb28fce31969e0674>'
- 'Head by texture: <head_texture:ff9bb9e56125c8227b94bbda9f6e0f862931c229255ba8f1205d13c44c1bb561>'
```
<img width="331" height="111" alt="image" src="https://github.com/user-attachments/assets/5912975a-a892-42c3-b9ab-7eab33ee42b6" />
## Compatibility with < 1.21.9
## Compatibility with <1.21.9
You may be wondering what happens when you try to use these and the client is below 1.21.9.
See table below for behavior based on your setup.
@@ -177,8 +168,7 @@ TAB has [MiniMessage](https://docs.advntr.dev/minimessage/format.html) hook, how
When MiniMessage is detected on your server, it is automatically used unless support is disabled. All codes are translated to MiniMessage syntax and then parsed by MiniMessage.
> [!WARNING]
> TAB's gradient syntax (`<#RRGGBB>Text</#RRGGBB>`) is a compatibility layer that TAB rewrites before parsing, it is more fragile than native [MiniMessage](https://docs.advntr.dev/minimessage/format.html) gradients (`<gradient:#RRGGBB:#RRGGBB>Text</gradient>`). On **Velocity**, this is especially problematic because [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) (see [TAB integration PR #1677](https://github.com/NEZNAMY/TAB/pull/1677)) parses MiniMessage *before* TAB can rewrite its gradient syntax. For example, placeholder output like `<#E0B11E>MyServer</#FF0000>` is interpreted by MiniPlaceholders as a hex color tag (`<#E0B11E>`), but `</#FF0000>` is not recognized as a gradient closing tag, so the formatting breaks. Prefer native MiniMessage syntax in placeholder output (e.g. `<gradient:#E0B11E:#FF0000>MyServer</gradient>`). See [Placeholders - MiniPlaceholders](https://github.com/NEZNAMY/TAB/wiki/Placeholders#miniplaceholders) for more information.
TAB creates a modified MiniMessage parser that also accepts `<head_texture:TEXTURE>` and `<mineskin:ID>` head syntaxes just like TAB's own component parser.
# Config options
| Option name | Default value | Description |
@@ -4,9 +4,11 @@ import java.util.UUID;
import lombok.NonNull;
import lombok.Setter;
import me.neznamy.tab.api.belowname.BelowNameManager;
import me.neznamy.tab.api.bossbar.BossBarManager;
import me.neznamy.tab.api.event.EventBus;
import me.neznamy.tab.api.placeholder.PlaceholderManager;
import me.neznamy.tab.api.playerlistobjective.PlayerListObjectiveManager;
import me.neznamy.tab.api.scoreboard.ScoreboardManager;
import me.neznamy.tab.api.tablist.SortingManager;
import me.neznamy.tab.api.tablist.HeaderFooterManager;
@@ -83,6 +85,22 @@ public abstract class TabAPI {
*/
public abstract @Nullable ScoreboardManager getScoreboardManager();
/**
* Returns belowname manager instance. Unlike other features, this one is returned even if
* it is disabled in config, which allows enabling it for specific players using the API.
*
* @return belowname manager
*/
public abstract @Nullable BelowNameManager getBelowNameManager();
/**
* Returns playerlist objective manager instance. Unlike other features, this one is returned
* even if it is disabled in config, which allows enabling it for specific players using the API.
*
* @return playerlist objective manager
*/
public abstract @Nullable PlayerListObjectiveManager getPlayerListObjectiveManager();
/**
* Returns team manager instance if the feature is enabled. If not, returns {@code null}.
*
@@ -0,0 +1,118 @@
package me.neznamy.tab.api.belowname;
import lombok.NonNull;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabPlayer;
import org.jetbrains.annotations.Nullable;
/**
* Interface for manipulating the belowname objective of individual players.
* <p>
* Instance can be obtained using {@link TabAPI#getBelowNameManager()}.
* Unlike other features, this one is available even if it is disabled in config,
* which allows enabling it for specific players using {@link #setEnabled(TabPlayer, Boolean)}.
*/
@SuppressWarnings("unused") // API class
public interface BelowNameManager {
/**
* Forces the objective to be visible or hidden for specified player, overriding
* both the config option and the disable condition. This only affects what the
* player sees, not how the player is displayed to others. Use {@code null} to
* give the control back to configuration.
*
* @param player
* Player to set visibility for
* @param enabled
* Forced visibility or {@code null} to reset back to configuration
* @see #getCustomEnabled(TabPlayer)
*/
void setEnabled(@NonNull TabPlayer player, @Nullable Boolean enabled);
/**
* Returns forced visibility assigned using {@link #setEnabled(TabPlayer, Boolean)}.
* If no visibility is forced, returns {@code null}.
*
* @param player
* Player to get forced visibility of
* @return Forced visibility or {@code null} if not forced
* @see #setEnabled(TabPlayer, Boolean)
*/
@Nullable Boolean getCustomEnabled(@NonNull TabPlayer player);
/**
* Changes score value of specified player to provided value. Supports placeholders,
* however, the value must evaluate to a number. Only 1.20.2 and lower clients display
* this value, newer clients display the number format instead. Use {@code null} to
* reset value back to the configured one.
*
* @param player
* Player to change score value of
* @param value
* New score value or {@code null} to reset back to configuration
* @see #getCustomValue(TabPlayer)
* @see #setFancyValue(TabPlayer, String)
*/
void setValue(@NonNull TabPlayer player, @Nullable String value);
/**
* Returns score value assigned using {@link #setValue(TabPlayer, String)}.
* If no value is assigned, returns {@code null}.
*
* @param player
* Player to get custom score value of
* @return Custom score value assigned using the API
* @see #setValue(TabPlayer, String)
*/
@Nullable String getCustomValue(@NonNull TabPlayer player);
/**
* Changes number format of specified player to provided value. Supports placeholders,
* as well as any supported RGB formats. Only 1.20.3+ clients display the number format,
* older clients display the score value instead. If it evaluates to an empty string, the
* score is not displayed at all. Use {@code null} to reset value back to the configured one.
*
* @param player
* Player to change number format of
* @param fancyValue
* New number format or {@code null} to reset back to configuration
* @see #getCustomFancyValue(TabPlayer)
* @see #setValue(TabPlayer, String)
*/
void setFancyValue(@NonNull TabPlayer player, @Nullable String fancyValue);
/**
* Returns number format assigned using {@link #setFancyValue(TabPlayer, String)}.
* If no number format is assigned, returns {@code null}.
*
* @param player
* Player to get custom number format of
* @return Custom number format assigned using the API
* @see #setFancyValue(TabPlayer, String)
*/
@Nullable String getCustomFancyValue(@NonNull TabPlayer player);
/**
* Changes objective title of specified player to provided value. Supports placeholders,
* as well as any supported RGB formats. Use {@code null} to reset value back to the
* configured one.
*
* @param player
* Player to change objective title of
* @param title
* New objective title or {@code null} to reset back to configuration
* @see #getCustomTitle(TabPlayer)
*/
void setTitle(@NonNull TabPlayer player, @Nullable String title);
/**
* Returns objective title assigned using {@link #setTitle(TabPlayer, String)}.
* If no title is assigned, returns {@code null}.
*
* @param player
* Player to get custom objective title of
* @return Custom objective title assigned using the API
* @see #setTitle(TabPlayer, String)
*/
@Nullable String getCustomTitle(@NonNull TabPlayer player);
}
@@ -0,0 +1,153 @@
package me.neznamy.tab.api.playerlistobjective;
import lombok.NonNull;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabPlayer;
import org.jetbrains.annotations.Nullable;
/**
* Interface for manipulating the playerlist objective of individual players.
* <p>
* Instance can be obtained using {@link TabAPI#getPlayerListObjectiveManager()}.
* Unlike other features, this one is available even if it is disabled in config,
* which allows enabling it for specific players using {@link #setEnabled(TabPlayer, Boolean)}.
*/
@SuppressWarnings("unused") // API class
public interface PlayerListObjectiveManager {
/**
* Forces the objective to be visible or hidden for specified player, overriding
* both the config option and the disable condition. This only affects what the
* player sees, not how the player is displayed to others. Use {@code null} to
* give the control back to configuration.
*
* @param player
* Player to set visibility for
* @param enabled
* Forced visibility or {@code null} to reset back to configuration
* @see #getCustomEnabled(TabPlayer)
*/
void setEnabled(@NonNull TabPlayer player, @Nullable Boolean enabled);
/**
* Returns forced visibility assigned using {@link #setEnabled(TabPlayer, Boolean)}.
* If no visibility is forced, returns {@code null}.
*
* @param player
* Player to get forced visibility of
* @return Forced visibility or {@code null} if not forced
* @see #setEnabled(TabPlayer, Boolean)
*/
@Nullable Boolean getCustomEnabled(@NonNull TabPlayer player);
/**
* Changes score value of specified player to provided value. Supports placeholders,
* however, the value must evaluate to a number. Only 1.20.2 and lower clients display
* this value, newer clients display the number format instead. Use {@code null} to
* reset value back to the configured one.
*
* @param player
* Player to change score value of
* @param value
* New score value or {@code null} to reset back to configuration
* @see #getCustomValue(TabPlayer)
* @see #setFancyValue(TabPlayer, String)
*/
void setValue(@NonNull TabPlayer player, @Nullable String value);
/**
* Returns score value assigned using {@link #setValue(TabPlayer, String)}.
* If no value is assigned, returns {@code null}.
*
* @param player
* Player to get custom score value of
* @return Custom score value assigned using the API
* @see #setValue(TabPlayer, String)
*/
@Nullable String getCustomValue(@NonNull TabPlayer player);
/**
* Changes number format of specified player to provided value. Supports placeholders,
* as well as any supported RGB formats. Only 1.20.3+ clients display the number format,
* older clients display the score value instead. Use {@code null} to reset value back
* to the configured one.
*
* @param player
* Player to change number format of
* @param fancyValue
* New number format or {@code null} to reset back to configuration
* @see #getCustomFancyValue(TabPlayer)
* @see #setValue(TabPlayer, String)
*/
void setFancyValue(@NonNull TabPlayer player, @Nullable String fancyValue);
/**
* Returns number format assigned using {@link #setFancyValue(TabPlayer, String)}.
* If no number format is assigned, returns {@code null}.
*
* @param player
* Player to get custom number format of
* @return Custom number format assigned using the API
* @see #setFancyValue(TabPlayer, String)
*/
@Nullable String getCustomFancyValue(@NonNull TabPlayer player);
/**
* Changes objective title of specified player to provided value. Supports placeholders,
* as well as any supported RGB formats. The title is only visible on Bedrock Edition.
* Use {@code null} to reset value back to the configured one.
*
* @param player
* Player to change objective title of
* @param title
* New objective title or {@code null} to reset back to configuration
* @see #getCustomTitle(TabPlayer)
*/
void setTitle(@NonNull TabPlayer player, @Nullable String title);
/**
* Returns objective title assigned using {@link #setTitle(TabPlayer, String)}.
* If no title is assigned, returns {@code null}.
*
* @param player
* Player to get custom objective title of
* @return Custom objective title assigned using the API
* @see #setTitle(TabPlayer, String)
*/
@Nullable String getCustomTitle(@NonNull TabPlayer player);
/**
* Changes render type of the objective for specified player. Use {@code null} to reset
* value back to the configured one.
*
* @param player
* Player to change render type of
* @param renderType
* New render type or {@code null} to reset back to configuration
* @see #getCustomRenderType(TabPlayer)
*/
void setRenderType(@NonNull TabPlayer player, @Nullable RenderType renderType);
/**
* Returns render type assigned using {@link #setRenderType(TabPlayer, RenderType)}.
* If no render type is assigned, returns {@code null}.
*
* @param player
* Player to get custom render type of
* @return Custom render type assigned using the API
* @see #setRenderType(TabPlayer, RenderType)
*/
@Nullable RenderType getCustomRenderType(@NonNull TabPlayer player);
/**
* Render type of the objective, defining how the score is displayed.
*/
enum RenderType {
/** Score is displayed as a number */
INTEGER,
/** Score is displayed as hearts */
HEARTS
}
}
@@ -48,14 +48,12 @@ public interface LayoutManager {
Layout getLayout(@NonNull String name);
/**
* Sends layout to player. Set to {@code null} to make player not see
* any layout, but default TabList instead.
* Sends layout to player. Set to {@code null} to reset layout back to original one based on configuration.
*
* @param player
* Player to send layout to
* @param layout
* Layout to send
* @see #resetLayout(TabPlayer)
*/
void sendLayout(@NonNull TabPlayer player, @Nullable Layout layout);
@@ -65,6 +63,8 @@ public interface LayoutManager {
* @param player
* Player to reset layout for
* @see #sendLayout(TabPlayer, Layout)
* @deprecated Use {@link #sendLayout(TabPlayer, Layout)} with {@code null} instead
*/
@Deprecated
void resetLayout(@NonNull TabPlayer player);
}
+1 -3
View File
@@ -4,7 +4,7 @@ plugins {
allprojects {
group = "me.neznamy"
version = "6.1.1"
version = "6.2.0"
description = "An all-in-one solution that works"
ext.set("id", "tab")
@@ -27,8 +27,6 @@ val platformPaths = setOf(
":bukkit:v1_16_R3",
":bukkit:v1_17_R1",
":bukkit:v1_18_R2",
":bukkit:v1_19_R1",
":bukkit:v1_19_R2",
":bukkit:v1_19_R3",
":bukkit:v1_20_R1",
":bukkit:v1_20_R2",
@@ -2,10 +2,12 @@ package me.neznamy.tab.platforms.bukkit;
import me.neznamy.tab.platforms.bukkit.platform.BukkitPlatform;
import me.neznamy.tab.platforms.bukkit.platform.FoliaPlatform;
import me.neznamy.tab.shared.ProjectVariables;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.util.ReflectionUtils;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
/**
* Main class for Bukkit.
@@ -19,15 +21,32 @@ public class BukkitTAB extends JavaPlugin {
TAB.create(folia ? new FoliaPlatform(this) : new BukkitPlatform(this));
} catch (IllegalStateException e) {
Bukkit.getConsoleSender().sendMessage("§c[TAB] ================================================================================");
Bukkit.getConsoleSender().sendMessage("§c[TAB] Your server version (" + Bukkit.getBukkitVersion() + ") is not supported.");
Bukkit.getConsoleSender().sendMessage("§c[TAB] This jar only supports 1.7.10, 1.8.8, 1.12.2, 1.16.5, 1.17.1 and 1.18.2 - 26.2");
Bukkit.getConsoleSender().sendMessage("§c[TAB] If you just updated to a new Minecraft version, check for TAB updates.");
Bukkit.getConsoleSender().sendMessage("§c[TAB] If you are using an unsupported 1.x version, use an older version of TAB (latest TAB 5.x supports all MC 1.x versions).");
Bukkit.getConsoleSender().sendMessage("§c[TAB] Thrown error message: " + e.getMessage());
if (isSpecialJar()) {
Bukkit.getConsoleSender().sendMessage("§c[TAB] The " + ProjectVariables.PLUGIN_VERSION + " release consists of 2 jars due to Java version problems.");
Bukkit.getConsoleSender().sendMessage("§c[TAB] One jar is for Paper 1.20.5 - 1.21.4, the other jar is for everything else.");
Bukkit.getConsoleSender().sendMessage("§c[TAB] You have installed the jar that explicitly says \"Paper 1.20.5 - 1.21.4\", which does not support your server version (" + Bukkit.getBukkitVersion() + ").");
Bukkit.getConsoleSender().sendMessage("§c[TAB] Use the other jar in the release instead.");
} else {
Bukkit.getConsoleSender().sendMessage("§c[TAB] Your server version (" + Bukkit.getBukkitVersion() + ") is not supported.");
Bukkit.getConsoleSender().sendMessage("§c[TAB] This jar only supports 1.7.10, 1.8.8, 1.12.2, 1.16.5, 1.17.1, 1.18.2 and 1.19.4 - 26.3");
Bukkit.getConsoleSender().sendMessage("§c[TAB] If you just updated to a new Minecraft version, check for TAB updates.");
Bukkit.getConsoleSender().sendMessage("§c[TAB] If you are using an unsupported 1.x version, use an older version of TAB (latest TAB 5.x supports all MC 1.x versions).");
Bukkit.getConsoleSender().sendMessage("§c[TAB] Thrown error message: " + e.getMessage());
}
Bukkit.getConsoleSender().sendMessage("§c[TAB] ================================================================================");
}
}
private boolean isSpecialJar() {
return !moduleExists("v1_8_R3") &&
moduleExists("paper_1_20_5") && moduleExists("paper_1_21_2") && moduleExists("paper_1_21_4") &&
!moduleExists("paper_1_21_9");
}
private boolean moduleExists(@NotNull String module) {
return ReflectionUtils.classExists("me.neznamy.tab.platforms.bukkit." + module + ".NMSImplementationProvider");
}
@Override
public void onDisable() {
if (TAB.getInstance() == null) return;
@@ -25,7 +25,7 @@ public class BukkitTabPlayer extends BackendTabPlayer {
static {
try {
BELOW_NAME_DISTANCE = (Attribute) Attribute.class.getDeclaredField("BELOW_NAME_DISTANCE").get(null);
} catch (ReflectiveOperationException e) {
} catch (Throwable e) {
// 1.21.11 and lower
}
}
@@ -96,6 +96,7 @@ public class ServerVersionInfo {
spigotVersions.put(ProtocolVersion.V26_1_1, "v26_1");
spigotVersions.put(ProtocolVersion.V26_1_2, "v26_1");
spigotVersions.put(ProtocolVersion.V26_2, "v26_2");
spigotVersions.put(ProtocolVersion.V26_3, "v26_2");
Map<ProtocolVersion, String> paperVersions = new LinkedHashMap<>();
paperVersions.put(ProtocolVersion.V1_20_5, "paper_1_20_5");
@@ -116,6 +117,7 @@ public class ServerVersionInfo {
paperVersions.put(ProtocolVersion.V26_1_1, "paper_1_21_11"); // v26_1 works too
paperVersions.put(ProtocolVersion.V26_1_2, "paper_1_21_11"); // v26_1 works too
paperVersions.put(ProtocolVersion.V26_2, "paper_26_2"); // v26_2 works too
paperVersions.put(ProtocolVersion.V26_3, "paper_26_2");
if (serverVersion == ProtocolVersion.UNKNOWN) {
throw new IllegalStateException(String.format(
@@ -4,10 +4,10 @@ import lombok.Getter;
import me.clip.placeholderapi.PlaceholderAPI;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import me.clip.placeholderapi.expansion.Relational;
import me.neznamy.tab.api.placeholder.Placeholder;
import me.neznamy.tab.shared.ProjectVariables;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.features.PlaceholderManagerImpl;
import me.neznamy.tab.shared.placeholders.PlaceholderReference;
import me.neznamy.tab.shared.placeholders.expansion.TabExpansion;
import me.neznamy.tab.shared.placeholders.types.RelationalPlaceholderImpl;
import me.neznamy.tab.shared.platform.TabPlayer;
@@ -98,7 +98,7 @@ public class BukkitTabExpansion extends PlaceholderExpansion implements TabExpan
String requestedPlaceholder = "%" + identifier.substring("placeholder_".length()) + "%";
PlaceholderManagerImpl pm = TAB.getInstance().getPlaceholderManager();
pm.addUsedPlaceholder(requestedPlaceholder, pm);
return pm.getPlaceholder(requestedPlaceholder).parse(p);
return pm.getPlaceholderReference(requestedPlaceholder).getHandle().parse(p);
}
return p.expansionData.getValue(identifier);
}
@@ -121,9 +121,9 @@ public class BukkitTabExpansion extends PlaceholderExpansion implements TabExpan
String requestedPlaceholder = "%" + identifier.substring("placeholder_".length()) + "%";
PlaceholderManagerImpl pm = TAB.getInstance().getPlaceholderManager();
pm.addUsedPlaceholder(requestedPlaceholder, pm);
Placeholder placeholder = pm.getPlaceholder(requestedPlaceholder);
if (placeholder instanceof RelationalPlaceholderImpl) {
RelationalPlaceholderImpl rel = (RelationalPlaceholderImpl) placeholder;
PlaceholderReference placeholder = pm.getPlaceholderReference(requestedPlaceholder);
if (placeholder.getHandle() instanceof RelationalPlaceholderImpl) {
RelationalPlaceholderImpl rel = (RelationalPlaceholderImpl) placeholder.getHandle();
TabPlayer v = TAB.getInstance().getPlayer(viewer.getUniqueId());
TabPlayer t = TAB.getInstance().getPlayer(target.getUniqueId());
if (v == null || t == null) return "<Player is not loaded>";
@@ -238,9 +238,9 @@ public class BukkitPlatform implements BackendPlatform {
Metrics metrics = new Metrics(plugin, TabConstants.BSTATS_PLUGIN_ID_BUKKIT);
metrics.addCustomChart(new SimplePie(TabConstants.MetricsChart.PERMISSION_SYSTEM,
() -> TAB.getInstance().getGroupManager().getPermissionPlugin()));
metrics.addCustomChart(new SimplePie("tab_6_1_0_servers",
metrics.addCustomChart(new SimplePie("tab_6_2_x_servers",
() -> serverVersionInfo.getServerName() + " " + serverVersionInfo.getServerVersion().getFriendlyName()));
metrics.addCustomChart(new SimplePie("tab_6_1_0_package", serverVersionInfo::getImplementationPackage));
metrics.addCustomChart(new SimplePie("tab_6_2_x_package", serverVersionInfo::getImplementationPackage));
}
@Override
-10
View File
@@ -1,10 +0,0 @@
dependencies {
implementation(projects.bukkit)
compileOnly("org.spigotmc:spigot:1.19.2-R0.1-SNAPSHOT") {
// Broken dependencies
exclude("com.mojang", "authlib")
exclude("com.mojang", "logging")
exclude("com.mojang", "datafixerupper")
}
compileOnly("com.mojang:authlib:1.5.25")
}
@@ -1,64 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R1;
import me.neznamy.tab.shared.chat.TabStyle;
import me.neznamy.tab.shared.chat.component.object.TabAtlasSprite;
import me.neznamy.tab.shared.chat.component.object.TabObjectComponent;
import me.neznamy.tab.platforms.bukkit.provider.ComponentConverter;
import me.neznamy.tab.shared.chat.component.object.TabPlayerSprite;
import net.minecraft.network.chat.*;
import net.minecraft.resources.MinecraftKey;
import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct NMS code.
*/
public class NMSComponentConverter extends ComponentConverter<IChatBaseComponent> {
@Override
@NotNull
public IChatBaseComponent newTextComponent(@NotNull String text) {
return IChatBaseComponent.b(text);
}
@Override
@NotNull
public IChatBaseComponent newTranslatableComponent(@NotNull String key) {
return IChatBaseComponent.c(key);
}
@Override
@NotNull
public IChatBaseComponent newKeybindComponent(@NotNull String keybind) {
return IChatBaseComponent.d(keybind);
}
@Override
@NotNull
public IChatBaseComponent newObjectComponent(@NotNull TabAtlasSprite sprite) {
return IChatBaseComponent.b(TabObjectComponent.ERROR_MESSAGE);
}
@Override
@NotNull
public IChatBaseComponent newObjectComponent(@NotNull TabPlayerSprite sprite) {
return IChatBaseComponent.b(TabObjectComponent.ERROR_MESSAGE);
}
@Override
public void applyStyle(@NotNull IChatBaseComponent nmsComponent, @NotNull TabStyle modifier) {
((IChatMutableComponent)nmsComponent).c(ChatModifier.a
.a(modifier.getColor() == null ? null : ChatHexColor.a(modifier.getColor().getRgb()))
.a(modifier.getBold())
.b(modifier.getItalic())
.c(modifier.getUnderlined())
.d(modifier.getStrikethrough())
.e(modifier.getObfuscated())
.a(modifier.getFont() == null ? null : MinecraftKey.a(modifier.getFont()))
);
}
@Override
public void addSibling(@NotNull IChatBaseComponent parent, @NotNull IChatBaseComponent child) {
((IChatMutableComponent)parent).b(child);
}
}
@@ -1,52 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R1;
import io.netty.channel.Channel;
import lombok.Getter;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.provider.ComponentConverter;
import me.neznamy.tab.platforms.bukkit.provider.ImplementationProvider;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.TabListEntryTracker;
import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Implementation provider using direct NMS code for 1.19.1 - 1.19.2.
*/
@Getter
public class NMSImplementationProvider implements ImplementationProvider {
@NotNull
private final ComponentConverter<?> componentConverter = new NMSComponentConverter();
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
return new NMSPacketScoreboard(player);
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return new NMSPacketTabList(player);
}
@Override
@NotNull
public Channel getChannel(@NotNull Player player) {
return ((CraftPlayer)player).getHandle().b.b.m;
}
@Override
@NotNull
public TabListEntryTracker newTabListEntryTracker(@NotNull Player player) {
return new NMSTabListEntryTracker(getChannel(player));
}
@Override
public int getPing(@NotNull BukkitTabPlayer player) {
return player.getPlayer().getPing();
}
}
@@ -1,207 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R1;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import net.minecraft.EnumChatFormat;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutScoreboardDisplayObjective;
import net.minecraft.network.protocol.game.PacketPlayOutScoreboardObjective;
import net.minecraft.network.protocol.game.PacketPlayOutScoreboardScore;
import net.minecraft.network.protocol.game.PacketPlayOutScoreboardTeam;
import net.minecraft.server.ScoreboardServer;
import net.minecraft.world.scores.Scoreboard;
import net.minecraft.world.scores.ScoreboardObjective;
import net.minecraft.world.scores.ScoreboardTeam;
import net.minecraft.world.scores.ScoreboardTeamBase;
import net.minecraft.world.scores.criteria.IScoreboardCriteria;
import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Optional;
/**
* Scoreboard implementation using direct NMS code.
*/
public class NMSPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
private static final ScoreboardTeamBase.EnumNameTagVisibility[] visibilities = ScoreboardTeamBase.EnumNameTagVisibility.values();
private static final ScoreboardTeamBase.EnumTeamPush[] collisions = ScoreboardTeamBase.EnumTeamPush.values();
private static final Scoreboard dummyScoreboard = new Scoreboard();
private static final Constructor<PacketPlayOutScoreboardTeam> teamConstructor;
static {
try {
teamConstructor = PacketPlayOutScoreboardTeam.class.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class);
teamConstructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public NMSPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
@Override
public void registerObjective(@NonNull Objective objective) {
ScoreboardObjective obj = new ScoreboardObjective(
dummyScoreboard,
objective.getName(),
IScoreboardCriteria.a,
objective.getTitle().convert(),
IScoreboardCriteria.EnumScoreboardHealthDisplay.values()[objective.getHealthDisplay().ordinal()]
);
objective.setPlatformObjective(obj);
sendPacket(new PacketPlayOutScoreboardObjective(obj, ObjectiveAction.REGISTER));
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
sendPacket(new PacketPlayOutScoreboardDisplayObjective(
objective.getDisplaySlot().ordinal(),
(ScoreboardObjective) objective.getPlatformObjective()
));
}
@Override
public void unregisterObjective(@NonNull Objective objective) {
sendPacket(new PacketPlayOutScoreboardObjective((ScoreboardObjective) objective.getPlatformObjective(), ObjectiveAction.UNREGISTER));
}
@Override
public void updateObjective(@NonNull Objective objective) {
ScoreboardObjective obj = (ScoreboardObjective) objective.getPlatformObjective();
obj.a((IChatBaseComponent) objective.getTitle().convert());
obj.a(IScoreboardCriteria.EnumScoreboardHealthDisplay.valueOf(objective.getHealthDisplay().name()));
sendPacket(new PacketPlayOutScoreboardObjective(obj, ObjectiveAction.UPDATE));
}
@Override
public void setScore(@NonNull Score score) {
sendPacket(new PacketPlayOutScoreboardScore(
ScoreboardServer.Action.a,
score.getObjective().getName(),
score.getHolder(),
score.getValue()
));
}
@Override
public void removeScore(@NonNull Score score) {
sendPacket(new PacketPlayOutScoreboardScore(
ScoreboardServer.Action.b,
score.getObjective().getName(),
score.getHolder(),
score.getValue()
));
}
@Override
@NotNull
public Object createTeam(@NonNull String name) {
return new ScoreboardTeam(dummyScoreboard, name);
}
@Override
public void registerTeam(@NonNull Team team) {
updateTeamProperties(team);
ScoreboardTeam t = (ScoreboardTeam) team.getPlatformTeam();
t.g().addAll(team.getPlayers());
sendPacket(PacketPlayOutScoreboardTeam.a(t, true));
}
@Override
public void unregisterTeam(@NonNull Team team) {
sendPacket(PacketPlayOutScoreboardTeam.a((ScoreboardTeam) team.getPlatformTeam()));
}
@Override
public void updateTeam(@NonNull Team team) {
updateTeamProperties(team);
sendPacket(PacketPlayOutScoreboardTeam.a((ScoreboardTeam) team.getPlatformTeam(), false));
}
private void updateTeamProperties(@NonNull Team team) {
ScoreboardTeam t = (ScoreboardTeam) team.getPlatformTeam();
t.a((team.getOptions() & 0x01) != 0);
t.b((team.getOptions() & 0x02) != 0);
t.a(visibilities[team.getVisibility().ordinal()]);
t.a(collisions[team.getCollision().ordinal()]);
t.b((IChatBaseComponent) team.getPrefix().convert());
t.c(team.getSuffix().convert());
t.a(EnumChatFormat.valueOf(team.getColor().name()));
}
@Override
@SneakyThrows
@NotNull
public Object onPacketSend(@NonNull Object packet) {
if (packet instanceof PacketPlayOutScoreboardDisplayObjective) {
TAB.getInstance().getFeatureManager().onDisplayObjective(
player,
((PacketPlayOutScoreboardDisplayObjective)packet).b(),
((PacketPlayOutScoreboardDisplayObjective)packet).c()
);
}
if (packet instanceof PacketPlayOutScoreboardObjective) {
TAB.getInstance().getFeatureManager().onObjective(
player,
((PacketPlayOutScoreboardObjective)packet).d(),
((PacketPlayOutScoreboardObjective)packet).b()
);
}
if (packet instanceof PacketPlayOutScoreboardTeam) {
int action = getMethod((PacketPlayOutScoreboardTeam) packet);
if (action != TeamAction.UPDATE) {
Collection<String> players = ((PacketPlayOutScoreboardTeam) packet).e();
if (players != null) {
String name = ((PacketPlayOutScoreboardTeam) packet).d();
return teamConstructor.newInstance(
name,
action,
((PacketPlayOutScoreboardTeam) packet).f(),
onTeamPacket(action, name, players)
);
}
}
}
return packet;
}
private int getMethod(@NonNull PacketPlayOutScoreboardTeam team) {
if (team.c() == PacketPlayOutScoreboardTeam.a.a) {
return 0;
} else if (team.c() == PacketPlayOutScoreboardTeam.a.b) {
return 1;
} else if (team.b() == PacketPlayOutScoreboardTeam.a.a) {
return 3;
} else if (team.b() == PacketPlayOutScoreboardTeam.a.b) {
return 4;
} else {
return 2;
}
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
private void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().b.a(packet);
}
}
@@ -1,210 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R1;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo;
import net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo.EnumPlayerInfoAction;
import net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo.PlayerInfoData;
import net.minecraft.network.protocol.game.PacketPlayOutPlayerListHeaderFooter;
import net.minecraft.world.entity.player.ProfilePublicKey;
import net.minecraft.world.level.EnumGamemode;
import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* TabList implementation using direct NMS code.
*/
public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
private static final EnumPlayerInfoAction ADD_PLAYER = EnumPlayerInfoAction.a;
private static final EnumPlayerInfoAction UPDATE_GAME_MODE = EnumPlayerInfoAction.b;
private static final EnumPlayerInfoAction UPDATE_LATENCY = EnumPlayerInfoAction.c;
private static final EnumPlayerInfoAction UPDATE_DISPLAY_NAME = EnumPlayerInfoAction.d;
private static final EnumPlayerInfoAction REMOVE_PLAYER = EnumPlayerInfoAction.e;
/**
* Constructs new instance.
*
* @param player
* Player this tablist will belong to
*/
public NMSPacketTabList(@NotNull BukkitTabPlayer player) {
super(player);
}
@Override
@SneakyThrows
public void removeEntry(@NonNull UUID entry) {
sendPacket(EnumPlayerInfoAction.e, entry, "", null, 0, 0, null);
}
@Override
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
sendPacket(EnumPlayerInfoAction.d, entry, "", null, 0, 0, displayName);
}
@Override
public void updateLatency(@NonNull UUID entry, int latency) {
sendPacket(EnumPlayerInfoAction.c, entry, "", null, latency, 0, null);
}
@Override
public void updateGameMode(@NonNull UUID entry, int gameMode) {
sendPacket(EnumPlayerInfoAction.b, entry, "", null, 0, gameMode, null);
}
@Override
public void updateListed(@NonNull UUID entry, boolean listed) {
// Added in 1.19.3
}
@Override
public void updateListOrder(@NonNull UUID entry, int listOrder) {
// Added in 1.21.2
}
@Override
public void updateHat(@NonNull UUID entry, boolean showHat) {
// Added in 1.21.4
}
@Override
public void addEntry0(@NonNull Entry entry) {
sendPacket(EnumPlayerInfoAction.a, entry.getUniqueId(), entry.getName(), entry.getSkin(), entry.getLatency(),
entry.getGameMode(), entry.getDisplayName());
}
@Override
@SneakyThrows
public void setPlayerListHeaderFooter0(@NonNull TabComponent header, @NonNull TabComponent footer) {
sendPacket(new PacketPlayOutPlayerListHeaderFooter(header.convert(), footer.convert()));
}
@Override
@Nullable
public Skin getSkin() {
Collection<Property> properties = ((CraftPlayer)player.getPlayer()).getProfile().getProperties().get(TEXTURES_PROPERTY);
if (properties.isEmpty()) return null; // Offline mode
Property property = properties.iterator().next();
return new Skin(property.getValue(), property.getSignature());
}
@Override
@SneakyThrows
@NotNull
public Object onPacketSend(@NonNull Object packet) {
if (packet instanceof PacketPlayOutPlayerListHeaderFooter) {
PacketPlayOutPlayerListHeaderFooter tablist = (PacketPlayOutPlayerListHeaderFooter) packet;
if (header == null || footer == null) return packet;
if (tablist.a != header.convert() || tablist.b != footer.convert()) {
return new PacketPlayOutPlayerListHeaderFooter(header.convert(), footer.convert());
}
}
if (!(packet instanceof PacketPlayOutPlayerInfo)) return packet;
PacketPlayOutPlayerInfo info = (PacketPlayOutPlayerInfo) packet;
EnumPlayerInfoAction action = info.c();
List<PlayerInfoData> updatedList = new ArrayList<>();
boolean rewritePacket = false;
for (PlayerInfoData nmsData : info.b()) {
boolean rewriteEntry = false;
GameProfile profile = nmsData.a();
UUID id = profile.getId();
IChatBaseComponent displayName = nmsData.d();
int latency = nmsData.b();
int gameMode = nmsData.c().a();
ProfilePublicKey.a publicKey = nmsData.e();
if (action == UPDATE_DISPLAY_NAME || action == ADD_PLAYER) {
TabComponent forcedDisplayName = getForcedDisplayNames().get(id);
if (forcedDisplayName != null && forcedDisplayName.convert() != displayName) {
displayName = forcedDisplayName.convert();
rewriteEntry = rewritePacket = true;
}
}
if (action == UPDATE_GAME_MODE || action == ADD_PLAYER) {
if (getBlockedSpectators().contains(id) && gameMode == 3) {
gameMode = 0;
rewriteEntry = rewritePacket = true;
}
}
if (action == UPDATE_LATENCY || action == ADD_PLAYER) {
if (getForcedLatency() != null) {
latency = getForcedLatency();
rewriteEntry = rewritePacket = true;
}
}
if (action == ADD_PLAYER) {
TAB.getInstance().getFeatureManager().onEntryAdd(player, id, profile.getName());
}
updatedList.add(rewriteEntry ? new PlayerInfoData(
profile,
latency,
EnumGamemode.a(gameMode),
displayName,
publicKey
) : nmsData);
}
if (rewritePacket) {
PacketPlayOutPlayerInfo newPacket = new PacketPlayOutPlayerInfo(action, Collections.emptyList());
newPacket.b().clear();
newPacket.b().addAll(updatedList);
return newPacket;
}
return packet;
}
private void sendPacket(@NonNull EnumPlayerInfoAction action, @NonNull UUID id, @NonNull String name,
@Nullable Skin skin, int latency, int gameMode, @Nullable TabComponent displayName) {
PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo(action);
packet.b().add(new PlayerInfoData(
createProfile(id, name, skin),
latency,
EnumGamemode.a(gameMode),
displayName == null ? null : displayName.convert(),
null
));
sendPacket(packet);
}
/**
* Creates GameProfile from given parameters.
*
* @param id
* Profile ID
* @param name
* Profile name
* @param skin
* Player skin
* @return GameProfile from given parameters
*/
@NotNull
private GameProfile createProfile(@NonNull UUID id, @NonNull String name, @Nullable Skin skin) {
GameProfile profile = new GameProfile(id, name);
if (skin != null) {
profile.getProperties().put(TabList.TEXTURES_PROPERTY,
new Property(TabList.TEXTURES_PROPERTY, skin.getValue(), skin.getSignature()));
}
return profile;
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
private void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().b.a(packet);
}
}
@@ -1,35 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R1;
import io.netty.channel.Channel;
import me.neznamy.tab.shared.platform.NettyTabListEntryTracker;
import net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo;
import net.minecraft.network.protocol.game.PacketPlayOutPlayerInfo.EnumPlayerInfoAction;
import org.jetbrains.annotations.NotNull;
/**
* Implementation of TabListEntryTracker.
*/
public class NMSTabListEntryTracker extends NettyTabListEntryTracker {
private static final EnumPlayerInfoAction ADD_PLAYER = EnumPlayerInfoAction.valueOf("ADD_PLAYER");
private static final EnumPlayerInfoAction REMOVE_PLAYER = EnumPlayerInfoAction.valueOf("REMOVE_PLAYER");
public NMSTabListEntryTracker(@NotNull Channel channel) {
super(channel);
}
@Override
public void onPacketSend(@NotNull Object packet) {
if (packet instanceof PacketPlayOutPlayerInfo) {
PacketPlayOutPlayerInfo info = (PacketPlayOutPlayerInfo) packet;
for (PacketPlayOutPlayerInfo.PlayerInfoData nmsData : info.b()) {
if (info.c() == ADD_PLAYER) {
tablistEntries.add(nmsData.a().getId());
}
if (info.c() == REMOVE_PLAYER) {
tablistEntries.remove(nmsData.a().getId());
}
}
}
}
}
-14
View File
@@ -1,14 +0,0 @@
dependencies {
implementation(projects.bukkit)
compileOnly("org.spigotmc:spigot:1.19.3-R0.1-SNAPSHOT") {
// Broken dependencies
exclude("com.mojang", "authlib")
exclude("com.mojang", "logging")
exclude("com.mojang", "datafixerupper")
}
compileOnly("com.mojang:authlib:1.5.25")
}
tasks.compileJava {
options.release.set(14) // Records
}
@@ -1,64 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R2;
import me.neznamy.tab.shared.chat.TabStyle;
import me.neznamy.tab.shared.chat.component.object.TabAtlasSprite;
import me.neznamy.tab.shared.chat.component.object.TabObjectComponent;
import me.neznamy.tab.platforms.bukkit.provider.ComponentConverter;
import me.neznamy.tab.shared.chat.component.object.TabPlayerSprite;
import net.minecraft.network.chat.*;
import net.minecraft.resources.MinecraftKey;
import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct NMS code.
*/
public class NMSComponentConverter extends ComponentConverter<IChatBaseComponent> {
@Override
@NotNull
public IChatBaseComponent newTextComponent(@NotNull String text) {
return IChatBaseComponent.b(text);
}
@Override
@NotNull
public IChatBaseComponent newTranslatableComponent(@NotNull String key) {
return IChatBaseComponent.c(key);
}
@Override
@NotNull
public IChatBaseComponent newKeybindComponent(@NotNull String keybind) {
return IChatBaseComponent.d(keybind);
}
@Override
@NotNull
public IChatBaseComponent newObjectComponent(@NotNull TabAtlasSprite sprite) {
return IChatBaseComponent.b(TabObjectComponent.ERROR_MESSAGE);
}
@Override
@NotNull
public IChatBaseComponent newObjectComponent(@NotNull TabPlayerSprite sprite) {
return IChatBaseComponent.b(TabObjectComponent.ERROR_MESSAGE);
}
@Override
public void applyStyle(@NotNull IChatBaseComponent nmsComponent, @NotNull TabStyle modifier) {
((IChatMutableComponent)nmsComponent).c(ChatModifier.a
.a(modifier.getColor() == null ? null : ChatHexColor.a(modifier.getColor().getRgb()))
.a(modifier.getBold())
.b(modifier.getItalic())
.c(modifier.getUnderlined())
.d(modifier.getStrikethrough())
.e(modifier.getObfuscated())
.a(modifier.getFont() == null ? null : MinecraftKey.a(modifier.getFont()))
);
}
@Override
public void addSibling(@NotNull IChatBaseComponent parent, @NotNull IChatBaseComponent child) {
((IChatMutableComponent)parent).b(child);
}
}
@@ -1,52 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R2;
import io.netty.channel.Channel;
import lombok.Getter;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.provider.ComponentConverter;
import me.neznamy.tab.platforms.bukkit.provider.ImplementationProvider;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.TabListEntryTracker;
import org.bukkit.craftbukkit.v1_19_R2.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Implementation provider using direct NMS code for 1.19.3.
*/
@Getter
public class NMSImplementationProvider implements ImplementationProvider {
@NotNull
private final ComponentConverter<?> componentConverter = new NMSComponentConverter();
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
return new NMSPacketScoreboard(player);
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return new NMSPacketTabList(player);
}
@Override
@NotNull
public Channel getChannel(@NotNull Player player) {
return ((CraftPlayer)player).getHandle().b.b.m;
}
@Override
@NotNull
public TabListEntryTracker newTabListEntryTracker(@NotNull Player player) {
return new NMSTabListEntryTracker(getChannel(player));
}
@Override
public int getPing(@NotNull BukkitTabPlayer player) {
return player.getPlayer().getPing();
}
}
@@ -1,206 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R2;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import net.minecraft.EnumChatFormat;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.PacketPlayOutScoreboardDisplayObjective;
import net.minecraft.network.protocol.game.PacketPlayOutScoreboardObjective;
import net.minecraft.network.protocol.game.PacketPlayOutScoreboardScore;
import net.minecraft.network.protocol.game.PacketPlayOutScoreboardTeam;
import net.minecraft.server.ScoreboardServer;
import net.minecraft.world.scores.Scoreboard;
import net.minecraft.world.scores.ScoreboardObjective;
import net.minecraft.world.scores.ScoreboardTeam;
import net.minecraft.world.scores.ScoreboardTeamBase;
import net.minecraft.world.scores.criteria.IScoreboardCriteria;
import org.bukkit.craftbukkit.v1_19_R2.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Optional;
/**
* Scoreboard implementation using direct NMS code.
*/
public class NMSPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
private static final ScoreboardTeamBase.EnumNameTagVisibility[] visibilities = ScoreboardTeamBase.EnumNameTagVisibility.values();
private static final ScoreboardTeamBase.EnumTeamPush[] collisions = ScoreboardTeamBase.EnumTeamPush.values();
private static final Scoreboard dummyScoreboard = new Scoreboard();
private static final Constructor<PacketPlayOutScoreboardTeam> teamConstructor;
static {
try {
teamConstructor = PacketPlayOutScoreboardTeam.class.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class);
teamConstructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public NMSPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
@Override
public void registerObjective(@NonNull Objective objective) {
ScoreboardObjective obj = new ScoreboardObjective(
dummyScoreboard,
objective.getName(),
IScoreboardCriteria.a,
objective.getTitle().convert(),
IScoreboardCriteria.EnumScoreboardHealthDisplay.values()[objective.getHealthDisplay().ordinal()]
);
objective.setPlatformObjective(obj);
sendPacket(new PacketPlayOutScoreboardObjective(obj, ObjectiveAction.REGISTER));
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
sendPacket(new PacketPlayOutScoreboardDisplayObjective(
objective.getDisplaySlot().ordinal(),
(ScoreboardObjective) objective.getPlatformObjective()
));
}
@Override
public void unregisterObjective(@NonNull Objective objective) {
sendPacket(new PacketPlayOutScoreboardObjective((ScoreboardObjective) objective.getPlatformObjective(), ObjectiveAction.UNREGISTER));
}
@Override
public void updateObjective(@NonNull Objective objective) {
ScoreboardObjective obj = (ScoreboardObjective) objective.getPlatformObjective();
obj.a((IChatBaseComponent) objective.getTitle().convert());
obj.a(IScoreboardCriteria.EnumScoreboardHealthDisplay.valueOf(objective.getHealthDisplay().name()));
sendPacket(new PacketPlayOutScoreboardObjective(obj, ObjectiveAction.UPDATE));
}
@Override
public void setScore(@NonNull Score score) {
sendPacket(new PacketPlayOutScoreboardScore(
ScoreboardServer.Action.a,
score.getObjective().getName(),
score.getHolder(),
score.getValue()
));
}
@Override
public void removeScore(@NonNull Score score) {
sendPacket(new PacketPlayOutScoreboardScore(
ScoreboardServer.Action.b,
score.getObjective().getName(),
score.getHolder(),
score.getValue()
));
}
@Override
@NotNull
public Object createTeam(@NonNull String name) {
return new ScoreboardTeam(dummyScoreboard, name);
}
@Override
public void registerTeam(@NonNull Team team) {
updateTeamProperties(team);
ScoreboardTeam t = (ScoreboardTeam) team.getPlatformTeam();
t.g().addAll(team.getPlayers());
sendPacket(PacketPlayOutScoreboardTeam.a(t, true));
}
@Override
public void unregisterTeam(@NonNull Team team) {
sendPacket(PacketPlayOutScoreboardTeam.a((ScoreboardTeam) team.getPlatformTeam()));
}
@Override
public void updateTeam(@NonNull Team team) {
updateTeamProperties(team);
sendPacket(PacketPlayOutScoreboardTeam.a((ScoreboardTeam) team.getPlatformTeam(), false));
}
private void updateTeamProperties(@NonNull Team team) {
ScoreboardTeam t = (ScoreboardTeam) team.getPlatformTeam();
t.a((team.getOptions() & 0x01) != 0);
t.b((team.getOptions() & 0x02) != 0);
t.a(visibilities[team.getVisibility().ordinal()]);
t.a(collisions[team.getCollision().ordinal()]);
t.b((IChatBaseComponent) team.getPrefix().convert());
t.c(team.getSuffix().convert());
t.a(EnumChatFormat.valueOf(team.getColor().name()));
}
@Override
@SneakyThrows
@NotNull
public Object onPacketSend(@NonNull Object packet) {
if (packet instanceof PacketPlayOutScoreboardDisplayObjective) {
TAB.getInstance().getFeatureManager().onDisplayObjective(
player,
((PacketPlayOutScoreboardDisplayObjective)packet).b(),
((PacketPlayOutScoreboardDisplayObjective)packet).c()
);
}
if (packet instanceof PacketPlayOutScoreboardObjective) {
TAB.getInstance().getFeatureManager().onObjective(
player,
((PacketPlayOutScoreboardObjective)packet).d(),
((PacketPlayOutScoreboardObjective)packet).b()
);
}
if (packet instanceof PacketPlayOutScoreboardTeam) {
int action = getMethod((PacketPlayOutScoreboardTeam) packet);
if (action != TeamAction.UPDATE) {
Collection<String> players = ((PacketPlayOutScoreboardTeam) packet).e();
if (players != null) {
String name = ((PacketPlayOutScoreboardTeam) packet).d();
return teamConstructor.newInstance(
name,
action,
((PacketPlayOutScoreboardTeam) packet).f(),
onTeamPacket(action, name, players)
);
}
}
}
return packet;
}
private int getMethod(@NonNull PacketPlayOutScoreboardTeam team) {
if (team.c() == PacketPlayOutScoreboardTeam.a.a) {
return 0;
} else if (team.c() == PacketPlayOutScoreboardTeam.a.b) {
return 1;
} else if (team.b() == PacketPlayOutScoreboardTeam.a.a) {
return 3;
} else if (team.b() == PacketPlayOutScoreboardTeam.a.b) {
return 4;
} else {
return 2;
}
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
private void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().b.a(packet);
}
}
@@ -1,223 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R2;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
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.TabList;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import net.minecraft.network.protocol.game.PacketPlayOutPlayerListHeaderFooter;
import net.minecraft.world.level.EnumGamemode;
import org.bukkit.craftbukkit.v1_19_R2.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.*;
/**
* TabList implementation using direct NMS code.
*/
public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
private static final Field PLAYERS = ReflectionUtils.getOnlyField(ClientboundPlayerInfoUpdatePacket.class, List.class);
private static final ClientboundPlayerInfoUpdatePacket.a ADD_PLAYER = ClientboundPlayerInfoUpdatePacket.a.valueOf("ADD_PLAYER");
private static final ClientboundPlayerInfoUpdatePacket.a UPDATE_GAME_MODE = ClientboundPlayerInfoUpdatePacket.a.valueOf("UPDATE_GAME_MODE");
private static final ClientboundPlayerInfoUpdatePacket.a UPDATE_LATENCY = ClientboundPlayerInfoUpdatePacket.a.valueOf("UPDATE_LATENCY");
private static final ClientboundPlayerInfoUpdatePacket.a UPDATE_DISPLAY_NAME = ClientboundPlayerInfoUpdatePacket.a.valueOf("UPDATE_DISPLAY_NAME");
private static final ClientboundPlayerInfoUpdatePacket.a UPDATE_LISTED = ClientboundPlayerInfoUpdatePacket.a.valueOf("UPDATE_LISTED");
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.a> ADD_PLAYER_SET = EnumSet.allOf(ClientboundPlayerInfoUpdatePacket.a.class);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.a> UPDATE_GAME_MODE_SET = EnumSet.of(UPDATE_GAME_MODE);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.a> UPDATE_DISPLAY_NAME_SET = EnumSet.of(UPDATE_DISPLAY_NAME);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.a> UPDATE_LATENCY_SET = EnumSet.of(UPDATE_LATENCY);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.a> UPDATE_LISTED_SET = EnumSet.of(UPDATE_LISTED);
/**
* Constructs new instance.
*
* @param player
* Player this tablist will belong to
*/
public NMSPacketTabList(@NotNull BukkitTabPlayer player) {
super(player);
}
@Override
@SneakyThrows
public void removeEntry(@NonNull UUID entry) {
sendPacket(new ClientboundPlayerInfoRemovePacket(Collections.singletonList(entry)));
}
@Override
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
sendPacket(UPDATE_DISPLAY_NAME_SET, entry, "", null, false, 0, 0, displayName);
}
@Override
public void updateLatency(@NonNull UUID entry, int latency) {
sendPacket(UPDATE_LATENCY_SET, entry, "", null, false, latency, 0, null);
}
@Override
public void updateGameMode(@NonNull UUID entry, int gameMode) {
sendPacket(UPDATE_GAME_MODE_SET, entry, "", null, false, 0, gameMode, null);
}
@Override
public void updateListed(@NonNull UUID entry, boolean listed) {
sendPacket(UPDATE_LISTED_SET, entry, "", null, listed, 0, 0, null);
}
@Override
public void updateListOrder(@NonNull UUID entry, int listOrder) {
// Added in 1.21.2
}
@Override
public void updateHat(@NonNull UUID entry, boolean showHat) {
// Added in 1.21.4
}
@Override
public void addEntry0(@NonNull Entry entry) {
sendPacket(ADD_PLAYER_SET, entry.getUniqueId(), entry.getName(), entry.getSkin(), entry.isListed(), entry.getLatency(),
entry.getGameMode(), entry.getDisplayName());
}
@Override
@SneakyThrows
public void setPlayerListHeaderFooter0(@NonNull TabComponent header, @NonNull TabComponent footer) {
sendPacket(new PacketPlayOutPlayerListHeaderFooter(header.convert(), footer.convert()));
}
@Override
@Nullable
public Skin getSkin() {
Collection<Property> properties = ((CraftPlayer)player.getPlayer()).getProfile().getProperties().get(TEXTURES_PROPERTY);
if (properties.isEmpty()) return null; // Offline mode
Property property = properties.iterator().next();
return new Skin(property.getValue(), property.getSignature());
}
@SneakyThrows
@Override
@NotNull
public Object onPacketSend(@NonNull Object packet) {
if (packet instanceof PacketPlayOutPlayerListHeaderFooter) {
PacketPlayOutPlayerListHeaderFooter tablist = (PacketPlayOutPlayerListHeaderFooter) packet;
if (header == null || footer == null) return packet;
if (tablist.a != header.convert() || tablist.b != footer.convert()) {
return new PacketPlayOutPlayerListHeaderFooter(header.convert(), footer.convert());
}
}
if (!(packet instanceof ClientboundPlayerInfoUpdatePacket)) return packet;
ClientboundPlayerInfoUpdatePacket info = (ClientboundPlayerInfoUpdatePacket) packet;
EnumSet<ClientboundPlayerInfoUpdatePacket.a> actions = info.b();
List<ClientboundPlayerInfoUpdatePacket.b> updatedList = new ArrayList<>();
boolean rewritePacket = false;
for (ClientboundPlayerInfoUpdatePacket.b nmsData : info.c()) {
UUID profileId = nmsData.a();
boolean rewriteEntry = false;
IChatBaseComponent displayName = nmsData.f();
int latency = nmsData.d();
int gameMode = nmsData.e().a();
boolean listed = nmsData.c();
if (actions.contains(UPDATE_DISPLAY_NAME)) {
TabComponent forcedDisplayName = getForcedDisplayNames().get(profileId);
if (forcedDisplayName != null && forcedDisplayName.convert() != displayName) {
displayName = forcedDisplayName.convert();
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(UPDATE_GAME_MODE)) {
if (getBlockedSpectators().contains(profileId) && gameMode == 3) {
gameMode = 0;
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(UPDATE_LATENCY)) {
if (getForcedLatency() != null) {
latency = getForcedLatency();
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(UPDATE_LISTED)) {
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
listed = false;
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(ADD_PLAYER)) {
TAB.getInstance().getFeatureManager().onEntryAdd(player, profileId, nmsData.b().getName());
}
updatedList.add(rewriteEntry ? new ClientboundPlayerInfoUpdatePacket.b(
profileId, nmsData.b(), listed, latency, EnumGamemode.a(gameMode), displayName,
nmsData.g()
) : nmsData);
}
if (rewritePacket) {
ClientboundPlayerInfoUpdatePacket newPacket = new ClientboundPlayerInfoUpdatePacket(actions, Collections.emptyList());
PLAYERS.set(newPacket, updatedList);
return newPacket;
}
return packet;
}
@SneakyThrows
private void sendPacket(@NonNull EnumSet<ClientboundPlayerInfoUpdatePacket.a> action, @NonNull UUID id, @NonNull String name,
@Nullable Skin skin, boolean listed, int latency, int gameMode, @Nullable TabComponent displayName) {
ClientboundPlayerInfoUpdatePacket packet = new ClientboundPlayerInfoUpdatePacket(action, Collections.emptyList());
PLAYERS.set(packet, Collections.singletonList(new ClientboundPlayerInfoUpdatePacket.b(
id,
createProfile(id, name, skin),
listed,
latency,
EnumGamemode.a(gameMode),
displayName == null ? null : displayName.convert(),
null
)));
sendPacket(packet);
}
/**
* Creates GameProfile from given parameters.
*
* @param id
* Profile ID
* @param name
* Profile name
* @param skin
* Player skin
* @return GameProfile from given parameters
*/
@NotNull
private GameProfile createProfile(@NonNull UUID id, @NonNull String name, @Nullable Skin skin) {
GameProfile profile = new GameProfile(id, name);
if (skin != null) {
profile.getProperties().put(TabList.TEXTURES_PROPERTY,
new Property(TabList.TEXTURES_PROPERTY, skin.getValue(), skin.getSignature()));
}
return profile;
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
private void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().b.a(packet);
}
}
@@ -1,40 +0,0 @@
package me.neznamy.tab.platforms.bukkit.v1_19_R2;
import io.netty.channel.Channel;
import me.neznamy.tab.shared.platform.NettyTabListEntryTracker;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.a;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* Implementation of TabListEntryTracker.
*/
public class NMSTabListEntryTracker extends NettyTabListEntryTracker {
private static final a ADD_PLAYER = a.valueOf("ADD_PLAYER");
public NMSTabListEntryTracker(@NotNull Channel channel) {
super(channel);
}
@Override
public void onPacketSend(@NotNull Object packet) {
if (packet instanceof ClientboundPlayerInfoRemovePacket) {
ClientboundPlayerInfoRemovePacket remove = (ClientboundPlayerInfoRemovePacket) packet;
for (UUID id : remove.b()) {
tablistEntries.remove(id);
}
}
if (packet instanceof ClientboundPlayerInfoUpdatePacket) {
ClientboundPlayerInfoUpdatePacket update = (ClientboundPlayerInfoUpdatePacket) packet;
if (update.b().contains(ADD_PLAYER)) {
for (ClientboundPlayerInfoUpdatePacket.b nmsData : update.c()) {
tablistEntries.add(nmsData.a());
}
}
}
}
}
+2 -1
View File
@@ -14,11 +14,12 @@ repositories {
maven("https://repo.papermc.io/repository/maven-public/") // Adventure
}
val minecraftVersion = "26.2"
val minecraftVersion = "26.3"
// Fabric API versions for each Minecraft version for easier backporting
// Official website (for updating in the future): https://fabricmc.net/develop/
val fabricApiVersions = mapOf(
"26.3" to "0.160.5+26.3",
"26.2" to "0.150.3+26.2",
"26.1.2" to "0.150.0+26.1.2"
)
@@ -133,7 +133,12 @@ public class FabricScoreboard extends SafeScoreboard<FabricTabPlayer> {
if (packet instanceof ClientboundSetPlayerTeamPacket team) {
int method = getMethod(team);
if (method != TeamAction.UPDATE) {
team.players = onTeamPacket(method, team.getName(), team.getPlayers());
return new ClientboundSetPlayerTeamPacket(
team.getName(),
method,
team.getParameters(),
onTeamPacket(method, team.getName(), team.getPlayers())
);
}
}
return packet;
@@ -74,7 +74,7 @@ public class FabricTabExpansion implements TabExpansion {
String placeholder = "%"+arg+"%";
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
manager.addUsedPlaceholder(placeholder, manager);
return PlaceholderResult.value(manager.getPlaceholder(placeholder).getLastValue(player));
return PlaceholderResult.value(manager.getPlaceholderReference(placeholder).getHandle().getLastValue(player));
});
}
+2 -1
View File
@@ -14,7 +14,8 @@
"fabric-api-base": "*",
"java": ">=25",
"minecraft": [
"26.2"
"26.2",
"26.3"
]
},
"recommends": {
@@ -5,8 +5,7 @@ accessible field net/minecraft/server/network/ServerCommonPacketListenerImpl con
accessible field net/minecraft/network/Connection channel Lio/netty/channel/Channel;
# Scoreboard team
accessible field net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket players Ljava/util/Collection;
mutable field net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket players Ljava/util/Collection;
accessible method net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket <init> (Ljava/lang/String;ILjava/util/Optional;Ljava/util/Collection;)V
# Player info
accessible field net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket entries Ljava/util/List;
@@ -5,9 +5,6 @@ import io.fand.api.placeholder.PlaceholderContext;
import io.fand.api.placeholder.PlaceholderProvider;
import io.fand.api.placeholder.PlaceholderRegistration;
import io.fand.api.plugin.PluginContext;
import java.util.HashSet;
import java.util.Set;
import java.util.function.UnaryOperator;
import me.neznamy.tab.api.placeholder.Placeholder;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.features.PlaceholderManagerImpl;
@@ -17,6 +14,10 @@ import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
import java.util.function.UnaryOperator;
/** Exposes TAB's placeholder expansion through Fand's placeholder service. */
public final class FandTabExpansion implements TabExpansion {
@@ -64,7 +65,7 @@ public final class FandTabExpansion implements TabExpansion {
String requestedPlaceholder = "%" + expansionIdentifier.substring("placeholder_".length()) + "%";
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
manager.addUsedPlaceholder(requestedPlaceholder, manager);
return manager.getPlaceholder(requestedPlaceholder).parse(player);
return manager.getPlaceholderReference(requestedPlaceholder).getHandle().parse(player);
}
return player.expansionData.getValue(expansionIdentifier);
}
-2
View File
@@ -27,8 +27,6 @@ val platformPaths = setOf(
":bukkit:v1_16_R3",
":bukkit:v1_17_R1",
":bukkit:v1_18_R2",
":bukkit:v1_19_R1",
":bukkit:v1_19_R2",
":bukkit:v1_19_R3",
":bukkit:v1_20_R1",
":bukkit:v1_20_R2",
+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"
// NeoForge API versions for each Minecraft version for easier backporting
// Official website (for updating in the future): https://projects.neoforged.net/neoforged/neoforge
val neoforgeApiVersions = mapOf(
"26.2" to "26.2.0.1-beta",
"26.1.2" to "26.1.2.17-beta"
"26.3" to "26.3.0.1-beta",
"26.2" to "26.2.0.88",
"26.1.2" to "26.1.2.109"
)
dependencies {
@@ -133,7 +133,12 @@ public class NeoForgeScoreboard extends SafeScoreboard<NeoForgeTabPlayer> {
if (packet instanceof ClientboundSetPlayerTeamPacket team) {
int method = getMethod(team);
if (method != TeamAction.UPDATE) {
team.players = onTeamPacket(method, team.getName(), team.getPlayers());
return new ClientboundSetPlayerTeamPacket(
team.getName(),
method,
team.getParameters(),
onTeamPacket(method, team.getName(), team.getPlayers())
);
}
}
return packet;
@@ -1,5 +1,5 @@
# Scoreboard team
public-f net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket players
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
@@ -15,6 +15,6 @@ description = '''@description@'''
[[dependencies.tab]]
modId = "minecraft"
type = "required"
versionRange = "[26.2]"
versionRange = "[26.2],[26.3]"
ordering = "NONE"
side = "SERVER"
-2
View File
@@ -52,8 +52,6 @@ include(":bukkit:v1_12_R1")
include(":bukkit:v1_16_R3")
include(":bukkit:v1_17_R1")
include(":bukkit:v1_18_R2")
include(":bukkit:v1_19_R1")
include(":bukkit:v1_19_R2")
include(":bukkit:v1_19_R3")
include(":bukkit:v1_20_R1")
include(":bukkit:v1_20_R2")
@@ -11,6 +11,7 @@ import org.jetbrains.annotations.NotNull;
public enum ProtocolVersion {
UNKNOWN,
V26_3 (777),
V26_2 (776),
V26_1_2 (775),
V26_1_1 (775),
@@ -3,7 +3,9 @@ package me.neznamy.tab.shared;
import lombok.Getter;
import lombok.Setter;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.belowname.BelowNameManager;
import me.neznamy.tab.api.bossbar.BossBarManager;
import me.neznamy.tab.api.playerlistobjective.PlayerListObjectiveManager;
import me.neznamy.tab.api.scoreboard.ScoreboardManager;
import me.neznamy.tab.api.tablist.HeaderFooterManager;
import me.neznamy.tab.api.tablist.SortingManager;
@@ -315,6 +317,16 @@ public class TAB extends TabAPI {
return featureManager.getFeature(TabConstants.Feature.SCOREBOARD);
}
@Override
public @Nullable BelowNameManager getBelowNameManager() {
return featureManager.getFeature(TabConstants.Feature.BELOW_NAME);
}
@Override
public @Nullable PlayerListObjectiveManager getPlayerListObjectiveManager() {
return featureManager.getFeature(TabConstants.Feature.YELLOW_NUMBER);
}
@Override
public @Nullable NameTag getNameTagManager() {
return featureManager.getFeature(TabConstants.Feature.NAME_TAGS);
@@ -26,17 +26,10 @@ public class BossBarCommand extends SubCommand {
@Override
public void execute(@Nullable TabPlayer sender, @NotNull String[] args) {
SubCommand command;
if (args.length == 0) {
command = getSubcommands().get("toggle");
if (command.hasPermission(sender)) {
command.execute(sender, new String[0]);
} else {
sendMessage(sender, getMessages().getNoPermission());
}
return;
args = new String[]{"toggle"};
}
command = getSubcommands().get(args[0].toLowerCase());
SubCommand command = getSubcommands().get(args[0].toLowerCase());
if (command != null) {
if (command.hasPermission(sender)) {
command.execute(sender, Arrays.copyOfRange(args, 1, args.length));
@@ -8,6 +8,7 @@ import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -31,11 +32,14 @@ public class BossBarOffCommand extends SubCommand {
return;
}
TabPlayer target = sender;
if (args.length > 0) {
boolean silent = args.length > 0 && args[args.length-1].equals("-s");
int providedArgs = args.length - (silent ? 1 : 0);
if (providedArgs > 0) {
if (hasPermission(sender, TabConstants.Permission.COMMAND_BOSSBAR_TOGGLE_OTHER)) {
target = TAB.getInstance().getPlayer(args[0]);
String playerName = args[0];
target = TAB.getInstance().getPlayer(playerName);
if (target == null) {
sendMessage(sender, getMessages().getPlayerNotFound(args[0]));
sendMessage(sender, getMessages().getPlayerNotFound(playerName));
return;
}
} else {
@@ -46,13 +50,17 @@ public class BossBarOffCommand extends SubCommand {
sendMessage(null, getMessages().getCommandOnlyFromGame());
return;
}
boolean silent = args.length == 2 && args[1].equals("-s");
feature.setBossBarVisible(target, false, !silent);
}
@Override
public @NotNull List<String> complete(@Nullable TabPlayer sender, @NotNull String[] arguments) {
if (arguments.length == 1) return getOnlinePlayers(arguments[0]);
if (arguments.length == 1) {
List<String> suggestions = new ArrayList<>();
suggestions.addAll(getOnlinePlayers(arguments[0]));
suggestions.addAll(getStartingArgument(Collections.singletonList("-s"), arguments[0]));
return suggestions;
}
if (arguments.length == 2) return getStartingArgument(Collections.singletonList("-s"), arguments[1]);
return Collections.emptyList();
}
@@ -8,6 +8,7 @@ import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -31,11 +32,14 @@ public class BossBarOnCommand extends SubCommand {
return;
}
TabPlayer target = sender;
if (args.length > 0) {
boolean silent = args.length > 0 && args[args.length-1].equals("-s");
int providedArgs = args.length - (silent ? 1 : 0);
if (providedArgs > 0) {
if (hasPermission(sender, TabConstants.Permission.COMMAND_BOSSBAR_TOGGLE_OTHER)) {
target = TAB.getInstance().getPlayer(args[0]);
String playerName = args[0];
target = TAB.getInstance().getPlayer(playerName);
if (target == null) {
sendMessage(sender, getMessages().getPlayerNotFound(args[0]));
sendMessage(sender, getMessages().getPlayerNotFound(playerName));
return;
}
} else {
@@ -46,13 +50,17 @@ public class BossBarOnCommand extends SubCommand {
sendMessage(null, getMessages().getCommandOnlyFromGame());
return;
}
boolean silent = args.length == 2 && args[1].equals("-s");
feature.setBossBarVisible(target, true, !silent);
}
@Override
public @NotNull List<String> complete(@Nullable TabPlayer sender, @NotNull String[] arguments) {
if (arguments.length == 1) return getOnlinePlayers(arguments[0]);
if (arguments.length == 1) {
List<String> suggestions = new ArrayList<>();
suggestions.addAll(getOnlinePlayers(arguments[0]));
suggestions.addAll(getStartingArgument(Collections.singletonList("-s"), arguments[0]));
return suggestions;
}
if (arguments.length == 2) return getStartingArgument(Collections.singletonList("-s"), arguments[1]);
return Collections.emptyList();
}
@@ -8,6 +8,7 @@ import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -31,11 +32,14 @@ public class BossBarToggleCommand extends SubCommand {
return;
}
TabPlayer target = sender;
if (args.length > 0) {
boolean silent = args.length > 0 && args[args.length-1].equals("-s");
int providedArgs = args.length - (silent ? 1 : 0);
if (providedArgs > 0) {
if (hasPermission(sender, TabConstants.Permission.COMMAND_BOSSBAR_TOGGLE_OTHER)) {
target = TAB.getInstance().getPlayer(args[0]);
String playerName = args[0];
target = TAB.getInstance().getPlayer(playerName);
if (target == null) {
sendMessage(sender, getMessages().getPlayerNotFound(args[0]));
sendMessage(sender, getMessages().getPlayerNotFound(playerName));
return;
}
} else {
@@ -46,13 +50,17 @@ public class BossBarToggleCommand extends SubCommand {
sendMessage(null, getMessages().getCommandOnlyFromGame());
return;
}
boolean silent = args.length == 2 && args[1].equals("-s");
feature.toggleBossBar(target, !silent);
}
@Override
public @NotNull List<String> complete(@Nullable TabPlayer sender, @NotNull String[] arguments) {
if (arguments.length == 1) return getOnlinePlayers(arguments[0]);
if (arguments.length == 1) {
List<String> suggestions = new ArrayList<>();
suggestions.addAll(getOnlinePlayers(arguments[0]));
suggestions.addAll(getStartingArgument(Collections.singletonList("-s"), arguments[0]));
return suggestions;
}
if (arguments.length == 2) return getStartingArgument(Collections.singletonList("-s"), arguments[1]);
return Collections.emptyList();
}
@@ -26,17 +26,10 @@ public class ScoreboardCommand extends SubCommand {
@Override
public void execute(@Nullable TabPlayer sender, @NotNull String[] args) {
SubCommand command;
if (args.length == 0) {
command = getSubcommands().get("toggle");
if (command.hasPermission(sender)) {
command.execute(sender, new String[0]);
} else {
sendMessage(sender, getMessages().getNoPermission());
}
return;
args = new String[]{"toggle"};
}
command = getSubcommands().get(args[0].toLowerCase());
SubCommand command = getSubcommands().get(args[0].toLowerCase());
if (command != null) {
if (command.hasPermission(sender)) {
command.execute(sender, Arrays.copyOfRange(args, 1, args.length));
@@ -8,6 +8,7 @@ import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -31,11 +32,14 @@ public class ScoreboardOffCommand extends SubCommand {
return;
}
TabPlayer target = sender;
if (args.length > 0) {
boolean silent = args.length > 0 && args[args.length-1].equals("-s");
int providedArgs = args.length - (silent ? 1 : 0);
if (providedArgs > 0) {
if (hasPermission(sender, TabConstants.Permission.COMMAND_SCOREBOARD_TOGGLE_OTHER)) {
target = TAB.getInstance().getPlayer(args[0]);
String playerName = args[0];
target = TAB.getInstance().getPlayer(playerName);
if (target == null) {
sendMessage(sender, getMessages().getPlayerNotFound(args[0]));
sendMessage(sender, getMessages().getPlayerNotFound(playerName));
return;
}
} else {
@@ -46,13 +50,17 @@ public class ScoreboardOffCommand extends SubCommand {
sendMessage(null, getMessages().getCommandOnlyFromGame());
return;
}
boolean silent = args.length == 2 && args[1].equals("-s");
scoreboard.setScoreboardVisible(target, false, !silent);
}
@Override
public @NotNull List<String> complete(@Nullable TabPlayer sender, @NotNull String[] arguments) {
if (arguments.length == 1) return getOnlinePlayers(arguments[0]);
if (arguments.length == 1) {
List<String> suggestions = new ArrayList<>();
suggestions.addAll(getOnlinePlayers(arguments[0]));
suggestions.addAll(getStartingArgument(Collections.singletonList("-s"), arguments[0]));
return suggestions;
}
if (arguments.length == 2) return getStartingArgument(Collections.singletonList("-s"), arguments[1]);
return Collections.emptyList();
}
@@ -8,6 +8,7 @@ import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -31,11 +32,14 @@ public class ScoreboardOnCommand extends SubCommand {
return;
}
TabPlayer target = sender;
if (args.length > 0) {
boolean silent = args.length > 0 && args[args.length-1].equals("-s");
int providedArgs = args.length - (silent ? 1 : 0);
if (providedArgs > 0) {
if (hasPermission(sender, TabConstants.Permission.COMMAND_SCOREBOARD_TOGGLE_OTHER)) {
target = TAB.getInstance().getPlayer(args[0]);
String playerName = args[0];
target = TAB.getInstance().getPlayer(playerName);
if (target == null) {
sendMessage(sender, getMessages().getPlayerNotFound(args[0]));
sendMessage(sender, getMessages().getPlayerNotFound(playerName));
return;
}
} else {
@@ -46,13 +50,17 @@ public class ScoreboardOnCommand extends SubCommand {
sendMessage(null, getMessages().getCommandOnlyFromGame());
return;
}
boolean silent = args.length == 2 && args[1].equals("-s");
scoreboard.setScoreboardVisible(target, true, !silent);
}
@Override
public @NotNull List<String> complete(@Nullable TabPlayer sender, @NotNull String[] arguments) {
if (arguments.length == 1) return getOnlinePlayers(arguments[0]);
if (arguments.length == 1) {
List<String> suggestions = new ArrayList<>();
suggestions.addAll(getOnlinePlayers(arguments[0]));
suggestions.addAll(getStartingArgument(Collections.singletonList("-s"), arguments[0]));
return suggestions;
}
if (arguments.length == 2) return getStartingArgument(Collections.singletonList("-s"), arguments[1]);
return Collections.emptyList();
}
@@ -8,6 +8,7 @@ import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -31,11 +32,14 @@ public class ScoreboardToggleCommand extends SubCommand {
return;
}
TabPlayer target = sender;
if (args.length > 0) {
boolean silent = args.length > 0 && args[args.length-1].equals("-s");
int providedArgs = args.length - (silent ? 1 : 0);
if (providedArgs > 0) {
if (hasPermission(sender, TabConstants.Permission.COMMAND_SCOREBOARD_TOGGLE_OTHER)) {
target = TAB.getInstance().getPlayer(args[0]);
String playerName = args[0];
target = TAB.getInstance().getPlayer(playerName);
if (target == null) {
sendMessage(sender, getMessages().getPlayerNotFound(args[0]));
sendMessage(sender, getMessages().getPlayerNotFound(playerName));
return;
}
} else {
@@ -46,13 +50,17 @@ public class ScoreboardToggleCommand extends SubCommand {
sendMessage(null, getMessages().getCommandOnlyFromGame());
return;
}
boolean silent = args.length == 2 && args[1].equals("-s");
scoreboard.toggleScoreboard(target, !silent);
}
@Override
public @NotNull List<String> complete(@Nullable TabPlayer sender, @NotNull String[] arguments) {
if (arguments.length == 1) return getOnlinePlayers(arguments[0]);
if (arguments.length == 1) {
List<String> suggestions = new ArrayList<>();
suggestions.addAll(getOnlinePlayers(arguments[0]));
suggestions.addAll(getStartingArgument(Collections.singletonList("-s"), arguments[0]));
return suggestions;
}
if (arguments.length == 2) return getStartingArgument(Collections.singletonList("-s"), arguments[1]);
return Collections.emptyList();
}
@@ -100,7 +100,7 @@ public class ModernConverter {
defaultDesign.put("display-condition", "%condition:" + namedCondition.getName() + "%=" + namedCondition.getNo());
} else {
// Short format
defaultDesign.put("display-condition", new Condition(disableCondition).invert().toShortFormat());
defaultDesign.put("display-condition", Condition.fromShortFormat(disableCondition).invert().toShortFormat());
}
}
defaultDesign.put("header", defaultHeader);
@@ -96,15 +96,15 @@ public class Config {
placeholders = PlaceholdersConfiguration.fromSection(config.getConfigurationSection("placeholders"));
components = ComponentConfiguration.fromSection(config.getConfigurationSection("components"));
if (config.getBoolean("belowname-objective.enabled", false)) belowname = BelowNameConfiguration.fromSection(config.getConfigurationSection("belowname-objective"));
if (config.getBoolean("bossbar.enabled", false)) bossbar = BossBarConfiguration.fromSection(config.getConfigurationSection("bossbar"));
if (config.getBoolean("belowname-objective.enabled", false)) belowname = BelowNameConfiguration.fromSection(config.getConfigurationSection("belowname-objective"));
if (config.getBoolean("playerlist-objective.enabled", true)) playerlistObjective = PlayerListObjectiveConfiguration.fromSection(config.getConfigurationSection("playerlist-objective"));
if (config.getBoolean("global-playerlist.enabled", false)) globalPlayerList = GlobalPlayerListConfiguration.fromSection(config.getConfigurationSection("global-playerlist"));
if (config.getBoolean("header-footer.enabled", true)) headerFooter = HeaderFooterConfiguration.fromSection(config.getConfigurationSection("header-footer"));
if (config.getBoolean("layout.enabled", false)) layout = LayoutConfiguration.fromSection(config.getConfigurationSection("layout"));
if (config.getBoolean("mysql.enabled", false)) mysql = MySQLConfiguration.fromSection(config.getConfigurationSection("mysql"));
if (config.getBoolean("per-world-playerlist.enabled", false)) perWorldPlayerList = PerWorldPlayerListConfiguration.fromSection(config.getConfigurationSection("per-world-playerlist"));
if (config.getBoolean("ping-spoof.enabled", false)) pingSpoof = PingSpoofConfiguration.fromSection(config.getConfigurationSection("ping-spoof"));
if (config.getBoolean("playerlist-objective.enabled", true)) playerlistObjective = PlayerListObjectiveConfiguration.fromSection(config.getConfigurationSection("playerlist-objective"));
if (config.getBoolean("scoreboard.enabled", false)) scoreboard = ScoreboardConfiguration.fromSection(config.getConfigurationSection("scoreboard"));
if (config.getBoolean("scoreboard-teams.enabled", true) || config.getBoolean("layout.enabled", false)) sorting = SortingConfiguration.fromSection(config.getConfigurationSection("scoreboard-teams"));
if (config.getBoolean("tablist-name-formatting.enabled", false)) tablistFormatting = TablistFormattingConfiguration.fromSection(config.getConfigurationSection("tablist-name-formatting"));
@@ -152,7 +152,7 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
if (!target.isOnline()) continue; // Player disconnected in the meantime while refreshing in another thread
if (placeholder.hasValueChanged(viewer, target, targetResult.getValue())) {
placeholder.updateParents(target);
for (RefreshableFeature f : placeholder.getUsedByFeatures()) {
for (RefreshableFeature f : placeholder.getReference().getUsedByFeatures()) {
update.computeIfAbsent(f, c -> new HashSet<>()).add(target);
}
}
@@ -172,7 +172,7 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
if (!player.isOnline()) continue; // Player disconnected in the meantime while refreshing in another thread
if (placeholder.hasValueChanged(player, playerResult.getValue(), true)) {
placeholder.updateParents(player);
for (RefreshableFeature f : placeholder.getUsedByFeatures()) {
for (RefreshableFeature f : placeholder.getReference().getUsedByFeatures()) {
update.computeIfAbsent(f, c -> new HashSet<>()).add(player);
}
if (placeholder.getIdentifier().equals(TabConstants.Placeholder.VANISHED)) {
@@ -192,7 +192,7 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
for (Entry<ServerPlaceholderImpl, String> entry : results.entrySet()) {
ServerPlaceholderImpl placeholder = entry.getKey();
if (placeholder.hasValueChanged(entry.getValue())) {
set.addAll(placeholder.getUsedByFeatures());
set.addAll(placeholder.getReference().getUsedByFeatures());
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
placeholder.updateParents(all);
}
@@ -226,7 +226,7 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
existing.setHandle((TabPlaceholder) placeholder);
for (TabPlayer p : TAB.getInstance().getOnlinePlayers()) {
if (!p.isLoaded()) continue;
for (RefreshableFeature f : ((TabPlaceholder)placeholder).getUsedByFeatures()) {
for (RefreshableFeature f : existing.getUsedByFeatures()) {
TimedCaughtTask task = new TimedCaughtTask(cpu, () -> f.refresh(p, true), f.getFeatureName(), f.getRefreshDisplayName());
if (f instanceof CustomThreaded) {
((CustomThreaded) f).getCustomThread().execute(task);
@@ -235,10 +235,12 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
}
}
}
((TabPlaceholder) placeholder).setReference(existing);
return existing;
} else {
PlaceholderReference reference = new PlaceholderReference(placeholder.getIdentifier(), (TabPlaceholder) placeholder);
registeredPlaceholders.put(placeholder.getIdentifier(), reference);
((TabPlaceholder) placeholder).setReference(reference);
return reference;
}
}
@@ -337,7 +339,7 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
* Feature using the placeholder
*/
public synchronized void addUsedPlaceholder(@NonNull String identifier, @NonNull RefreshableFeature feature) {
if (getPlaceholder(identifier).addUsedFeature(feature)) {
if (getPlaceholder(identifier).getReference().addUsedFeature(feature)) {
recalculateUsedPlaceholders();
TabPlaceholder p = getPlaceholder(identifier);
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
@@ -355,7 +357,7 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
* Feature using the placeholder
*/
public synchronized void addUsedPlaceholder(@NonNull TabPlaceholder placeholder, @NonNull RefreshableFeature feature) {
if (placeholder.addUsedFeature(feature)) {
if (placeholder.getReference().addUsedFeature(feature)) {
recalculateUsedPlaceholders();
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
all.expansionData.setPlaceholderValue(placeholder.getIdentifier(), placeholder.getLastValueSafe(all));
@@ -367,7 +369,7 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
* Updates array of used placeholders.
*/
private void recalculateUsedPlaceholders() {
usedPlaceholders = registeredPlaceholders.values().stream().filter(p -> !p.getHandle().getUsedByFeatures().isEmpty()).toArray(PlaceholderReference[]::new);
usedPlaceholders = registeredPlaceholders.values().stream().filter(p -> !p.getUsedByFeatures().isEmpty()).toArray(PlaceholderReference[]::new);
}
/**
@@ -389,7 +391,7 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
public void onJoin(@NotNull TabPlayer connectedPlayer) {
for (PlaceholderReference p : usedPlaceholders) {
if (p.getHandle() instanceof ServerPlaceholderImpl) { // server placeholders don't update on join
connectedPlayer.expansionData.setPlaceholderValue(p.getIdentifier(), ((ServerPlaceholderImpl) p.getHandle()).getLastValue());
connectedPlayer.expansionData.setPlaceholderValue(p.getIdentifier(), p.getHandle().getLastValue(null));
}
}
// Initialize to avoid onVanishStatusChange being called in the loop after joining because previous value was null
@@ -584,7 +586,7 @@ public class PlaceholderManagerImpl extends RefreshableFeature implements Placeh
TabPlaceholder p = reference.getHandle();
if (p.getIdentifier().contains("AnonymousCondition")) continue; // These are ugly, don't show them
if (p instanceof ServerPlaceholderImpl) {
serverPlaceholders.add(Arrays.asList(p.getIdentifier(), String.valueOf(reference.getRefresh()), ((ServerPlaceholderImpl) p).getLastValue()));
serverPlaceholders.add(Arrays.asList(p.getIdentifier(), String.valueOf(reference.getRefresh()), p.getLastValue(null)));
} else if (p instanceof PlayerPlaceholderImpl) {
playerPlaceholders.add(Arrays.asList(p.getIdentifier(), String.valueOf(reference.getRefresh()), p.getLastValueSafe(player)));
} else if (p instanceof RelationalPlaceholderImpl) {
@@ -1,6 +1,8 @@
package me.neznamy.tab.shared.features.belowname;
import lombok.Getter;
import lombok.NonNull;
import me.neznamy.tab.api.belowname.BelowNameManager;
import me.neznamy.tab.shared.Property;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
@@ -28,7 +30,7 @@ import java.util.stream.Collectors;
*/
@Getter
public class BelowName extends RefreshableFeature implements JoinListener, QuitListener, Loadable, UnLoadable,
WorldSwitchListener, ServerSwitchListener, CustomThreaded, ProxyFeature, VanishListener, Dumpable {
WorldSwitchListener, ServerSwitchListener, CustomThreaded, ProxyFeature, VanishListener, Dumpable, BelowNameManager {
/** Objective name used by this feature */
public static final String OBJECTIVE_NAME = "TAB-BelowName";
@@ -70,7 +72,7 @@ public class BelowName extends RefreshableFeature implements JoinListener, QuitL
for (TabPlayer loaded : onlinePlayers.getPlayers()) {
loaded.setBelowNameDistance(configuration.getViewDistance());
loadProperties(loaded);
if (disableChecker.isDisableConditionMet(loaded)) {
if (configuredDisabled(loaded)) {
loaded.belowNameData.disabled.set(true);
} else {
register(loaded);
@@ -104,7 +106,7 @@ public class BelowName extends RefreshableFeature implements JoinListener, QuitL
onlinePlayers.addPlayer(connectedPlayer);
connectedPlayer.setBelowNameDistance(configuration.getViewDistance());
loadProperties(connectedPlayer);
if (disableChecker.isDisableConditionMet(connectedPlayer)) {
if (configuredDisabled(connectedPlayer)) {
connectedPlayer.belowNameData.disabled.set(true);
} else {
register(connectedPlayer);
@@ -143,6 +145,24 @@ public class BelowName extends RefreshableFeature implements JoinListener, QuitL
* Whether the feature is disabled now or not
*/
public void onDisableConditionChange(TabPlayer p, boolean disabledNow) {
Boolean forced = p.belowNameData.forcedEnabled;
if (forced != null) {
p.belowNameData.disabled.set(!forced);
return;
}
updateVisibility(p, disabledNow);
}
private boolean configuredDisabled(@NotNull TabPlayer p) {
return disableChecker.isDisableConditionMet(p);
}
private void applyDisabledState(@NotNull TabPlayer p, boolean disabled) {
if (p.belowNameData.disabled.getAndSet(disabled) == disabled) return;
updateVisibility(p, disabled);
}
private void updateVisibility(@NotNull TabPlayer p, boolean disabledNow) {
if (disabledNow) {
p.getScoreboard().unregisterObjective(OBJECTIVE_NAME);
} else {
@@ -220,6 +240,15 @@ public class BelowName extends RefreshableFeature implements JoinListener, QuitL
player.getScoreboard().setDisplaySlot(OBJECTIVE_NAME, Scoreboard.DisplaySlot.BELOW_NAME);
}
private void updateObjective(@NotNull TabPlayer player) {
player.getScoreboard().updateObjective(
OBJECTIVE_NAME,
cache.get(player.belowNameData.title.updateAndGet()),
Scoreboard.HealthDisplay.INTEGER,
cache.get(player.belowNameData.defaultNumberFormat.updateAndGet())
);
}
/**
* Updates score of specified entry to player.
*
@@ -302,6 +331,85 @@ public class BelowName extends RefreshableFeature implements JoinListener, QuitL
}
}
// ------------------
// API Implementation
// ------------------
@Override
public void setEnabled(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable Boolean enabled) {
ensureActive();
customThread.execute(new TimedCaughtTask(TAB.getInstance().getCpu(), () -> {
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
p.belowNameData.forcedEnabled = enabled;
applyDisabledState(p, enabled == null ? configuredDisabled(p) : !enabled);
}, getFeatureName(), "Processing API call (setEnabled)"));
}
@Override
@Nullable
public Boolean getCustomEnabled(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
return ((TabPlayer) player).belowNameData.forcedEnabled;
}
@Override
public void setValue(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable String value) {
ensureActive();
customThread.execute(new TimedCaughtTask(TAB.getInstance().getCpu(), () -> {
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
if (Objects.equals(p.belowNameData.value.getTemporaryValue(), value)) return;
p.belowNameData.value.setTemporaryValue(value);
refresh(p, true);
}, getFeatureName(), "Processing API call (setValue)"));
}
@Override
@Nullable
public String getCustomValue(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
return ((TabPlayer) player).belowNameData.value.getTemporaryValue();
}
@Override
public void setFancyValue(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable String fancyValue) {
ensureActive();
customThread.execute(new TimedCaughtTask(TAB.getInstance().getCpu(), () -> {
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
if (Objects.equals(p.belowNameData.fancyValue.getTemporaryValue(), fancyValue)) return;
p.belowNameData.fancyValue.setTemporaryValue(fancyValue);
refresh(p, true);
}, getFeatureName(), "Processing API call (setFancyValue)"));
}
@Override
@Nullable
public String getCustomFancyValue(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
return ((TabPlayer) player).belowNameData.fancyValue.getTemporaryValue();
}
@Override
public void setTitle(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable String title) {
ensureActive();
customThread.execute(new TimedCaughtTask(TAB.getInstance().getCpu(), () -> {
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
if (Objects.equals(p.belowNameData.title.getTemporaryValue(), title)) return;
p.belowNameData.title.setTemporaryValue(title);
if (!p.belowNameData.disabled.get()) updateObjective(p);
}, getFeatureName(), "Processing API call (setTitle)"));
}
@Override
@Nullable
public String getCustomTitle(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
return ((TabPlayer) player).belowNameData.title.getTemporaryValue();
}
@Override
@NotNull
public Object dump(@NotNull TabPlayer analyzed) {
@@ -1,6 +1,7 @@
package me.neznamy.tab.shared.features.belowname;
import me.neznamy.tab.shared.Property;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -23,4 +24,7 @@ public class BelowNamePlayerData {
/** Flag tracking whether this feature is disabled for the player with condition or not */
public final AtomicBoolean disabled = new AtomicBoolean();
/** Visibility forced using the API, {@code null} if not forced */
@Nullable public volatile Boolean forcedEnabled;
}
@@ -50,7 +50,7 @@ public class LayoutManagerImpl extends RefreshableFeature implements LayoutManag
this.configuration = configuration;
skinManager = new LayoutSkinManager(TAB.getInstance().getConfiguration().getSkinManager(), configuration.getDefaultSkin(), configuration.getDefaultSkinHashMap());
for (int slot=1; slot<=80; slot++) {
UUID id = new UUID(1, configuration.getDirection().translateSlot(slot));
UUID id = new UUID(0, configuration.getDirection().translateSlot(slot));
uuids[slot-1] = id;
UUIDS_SET.add(id);
}
@@ -210,11 +210,7 @@ public class LayoutManagerImpl extends RefreshableFeature implements LayoutManag
@Override
public void resetLayout(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
p.layoutData.forcedLayout = null;
refresh(p, false);
sendLayout(player, null);
}
@NotNull
@@ -1,6 +1,8 @@
package me.neznamy.tab.shared.features.playerlistobjective;
import me.neznamy.tab.api.playerlistobjective.PlayerListObjectiveManager.RenderType;
import me.neznamy.tab.shared.Property;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -20,4 +22,10 @@ public class PlayerListObjectivePlayerData {
/** Flag tracking whether this feature is disabled for the player with condition or not */
public final AtomicBoolean disabled = new AtomicBoolean();
/** Visibility forced using the API, {@code null} if not forced */
@Nullable public volatile Boolean forcedEnabled;
/** Render type forced using the API, {@code null} if not forced */
@Nullable public volatile RenderType forcedRenderType;
}
@@ -1,6 +1,8 @@
package me.neznamy.tab.shared.features.playerlistobjective;
import lombok.Getter;
import lombok.NonNull;
import me.neznamy.tab.api.playerlistobjective.PlayerListObjectiveManager;
import me.neznamy.tab.shared.Property;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
@@ -26,7 +28,7 @@ import java.util.stream.Collectors;
*/
@Getter
public class YellowNumber extends RefreshableFeature implements JoinListener, QuitListener, Loadable,
CustomThreaded, ProxyFeature, VanishListener, Dumpable {
CustomThreaded, ProxyFeature, VanishListener, Dumpable, PlayerListObjectiveManager {
/** Objective name used by this feature */
public static final String OBJECTIVE_NAME = "TAB-PlayerList";
@@ -67,7 +69,7 @@ public class YellowNumber extends RefreshableFeature implements JoinListener, Qu
Map<TabPlayer, Integer> values = new HashMap<>();
for (TabPlayer loaded : onlinePlayers.getPlayers()) {
loadProperties(loaded);
if (disableChecker.isDisableConditionMet(loaded)) {
if (configuredDisabled(loaded)) {
loaded.playerlistObjectiveData.disabled.set(true);
} else {
register(loaded);
@@ -92,7 +94,7 @@ public class YellowNumber extends RefreshableFeature implements JoinListener, Qu
public void onJoin(@NotNull TabPlayer connectedPlayer) {
onlinePlayers.addPlayer(connectedPlayer);
loadProperties(connectedPlayer);
if (disableChecker.isDisableConditionMet(connectedPlayer)) {
if (configuredDisabled(connectedPlayer)) {
connectedPlayer.playerlistObjectiveData.disabled.set(true);
} else {
register(connectedPlayer);
@@ -131,6 +133,24 @@ public class YellowNumber extends RefreshableFeature implements JoinListener, Qu
* Whether the feature is disabled now or not
*/
public void onDisableConditionChange(TabPlayer p, boolean disabledNow) {
Boolean forced = p.playerlistObjectiveData.forcedEnabled;
if (forced != null) {
p.playerlistObjectiveData.disabled.set(!forced);
return;
}
updateVisibility(p, disabledNow);
}
private boolean configuredDisabled(@NotNull TabPlayer p) {
return disableChecker.isDisableConditionMet(p);
}
private void applyDisabledState(@NotNull TabPlayer p, boolean disabled) {
if (p.playerlistObjectiveData.disabled.getAndSet(disabled) == disabled) return;
updateVisibility(p, disabled);
}
private void updateVisibility(@NotNull TabPlayer p, boolean disabledNow) {
if (disabledNow) {
p.getScoreboard().unregisterObjective(OBJECTIVE_NAME);
} else {
@@ -202,12 +222,27 @@ public class YellowNumber extends RefreshableFeature implements JoinListener, Qu
player.getScoreboard().registerObjective(
OBJECTIVE_NAME,
cache.get(player.playerlistObjectiveData.title.updateAndGet()),
configuration.getHealthDisplay(),
getHealthDisplay(player),
TabComponent.empty()
);
player.getScoreboard().setDisplaySlot(OBJECTIVE_NAME, Scoreboard.DisplaySlot.PLAYER_LIST);
}
private void updateObjective(@NotNull TabPlayer player) {
player.getScoreboard().updateObjective(
OBJECTIVE_NAME,
cache.get(player.playerlistObjectiveData.title.updateAndGet()),
getHealthDisplay(player),
TabComponent.empty()
);
}
@NotNull
private Scoreboard.HealthDisplay getHealthDisplay(@NotNull TabPlayer player) {
RenderType forced = player.playerlistObjectiveData.forcedRenderType;
return forced == null ? configuration.getHealthDisplay() : Scoreboard.HealthDisplay.valueOf(forced.name());
}
/**
* Updates score of specified entry to player.
*
@@ -267,6 +302,104 @@ public class YellowNumber extends RefreshableFeature implements JoinListener, Qu
}
}
// ------------------
// API Implementation
// ------------------
@Override
public void setEnabled(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable Boolean enabled) {
ensureActive();
customThread.execute(new TimedCaughtTask(TAB.getInstance().getCpu(), () -> {
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
p.playerlistObjectiveData.forcedEnabled = enabled;
applyDisabledState(p, enabled == null ? configuredDisabled(p) : !enabled);
}, getFeatureName(), "Processing API call (setEnabled)"));
}
@Override
@Nullable
public Boolean getCustomEnabled(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
return ((TabPlayer) player).playerlistObjectiveData.forcedEnabled;
}
@Override
public void setValue(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable String value) {
ensureActive();
customThread.execute(new TimedCaughtTask(TAB.getInstance().getCpu(), () -> {
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
if (Objects.equals(p.playerlistObjectiveData.value.getTemporaryValue(), value)) return;
p.playerlistObjectiveData.value.setTemporaryValue(value);
refresh(p, true);
}, getFeatureName(), "Processing API call (setValue)"));
}
@Override
@Nullable
public String getCustomValue(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
return ((TabPlayer) player).playerlistObjectiveData.value.getTemporaryValue();
}
@Override
public void setFancyValue(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable String fancyValue) {
ensureActive();
customThread.execute(new TimedCaughtTask(TAB.getInstance().getCpu(), () -> {
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
if (Objects.equals(p.playerlistObjectiveData.fancyValue.getTemporaryValue(), fancyValue)) return;
p.playerlistObjectiveData.fancyValue.setTemporaryValue(fancyValue);
refresh(p, true);
}, getFeatureName(), "Processing API call (setFancyValue)"));
}
@Override
@Nullable
public String getCustomFancyValue(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
return ((TabPlayer) player).playerlistObjectiveData.fancyValue.getTemporaryValue();
}
@Override
public void setTitle(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable String title) {
ensureActive();
customThread.execute(new TimedCaughtTask(TAB.getInstance().getCpu(), () -> {
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
if (Objects.equals(p.playerlistObjectiveData.title.getTemporaryValue(), title)) return;
p.playerlistObjectiveData.title.setTemporaryValue(title);
if (!p.playerlistObjectiveData.disabled.get()) updateObjective(p);
}, getFeatureName(), "Processing API call (setTitle)"));
}
@Override
@Nullable
public String getCustomTitle(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
return ((TabPlayer) player).playerlistObjectiveData.title.getTemporaryValue();
}
@Override
public void setRenderType(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable RenderType renderType) {
ensureActive();
customThread.execute(new TimedCaughtTask(TAB.getInstance().getCpu(), () -> {
TabPlayer p = (TabPlayer) player;
p.ensureLoaded();
if (p.playerlistObjectiveData.forcedRenderType == renderType) return;
p.playerlistObjectiveData.forcedRenderType = renderType;
if (!p.playerlistObjectiveData.disabled.get()) updateObjective(p);
}, getFeatureName(), "Processing API call (setRenderType)"));
}
@Override
@Nullable
public RenderType getCustomRenderType(@NonNull me.neznamy.tab.api.TabPlayer player) {
ensureActive();
return ((TabPlayer) player).playerlistObjectiveData.forcedRenderType;
}
@Override
@NotNull
public Object dump(@NotNull TabPlayer analyzed) {
@@ -1,123 +0,0 @@
package me.neznamy.tab.shared.features.scoreboard;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.Setter;
import me.neznamy.tab.shared.Property;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.cpu.ThreadExecutor;
import me.neznamy.tab.shared.features.scoreboard.lines.ScoreboardLine;
import me.neznamy.tab.shared.features.types.CustomThreaded;
import me.neznamy.tab.shared.features.types.RefreshableFeature;
import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.util.cache.StringToComponentCache;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Feature refreshing score / NumberFormat in a scoreboard line for players.
*/
@AllArgsConstructor
public class ScoreRefresher extends RefreshableFeature implements CustomThreaded {
private static final StringToComponentCache cache = new StringToComponentCache("Scoreboard NumberFormat", 1000);
/** Line this score belongs to */
@NonNull private final ScoreboardLine line;
/** Line number to use if nothing is configured (1-15) */
@Setter private int lineNumber;
/** Configured score for <1.20.3 (can be null if not configured) */
@Nullable private final String score;
/** Configured number format (can be null if not configured) */
@Nullable private final String numberFormat;
@NotNull
@Override
public String getRefreshDisplayName() {
return "Updating NumberFormat";
}
@Override
public void refresh(@NotNull TabPlayer refreshed, boolean force) {
if (refreshed.scoreboardData.activeScoreboard != line.getParent()) return; //player has different scoreboard displayed
if (refreshed.scoreboardData.numberFormatProperties.get(line) == null) return; // Shrug
refreshed.getScoreboard().setScore(
ScoreboardManagerImpl.OBJECTIVE_NAME,
line.getPlayerName(refreshed),
getScore(refreshed),
null,
getNumberFormat(refreshed)
);
}
/**
* Registers properties for player.
*
* @param player
* Player to register properties for
*/
public void registerProperties(@NotNull TabPlayer player) {
player.scoreboardData.scoreProperties.put(line, new Property(this, player, score == null ? "" : score));
player.scoreboardData.numberFormatProperties.put(line, new Property(this, player, numberFormat == null ? "" : numberFormat));
}
/**
* Returns new score value for specified player.
*
* @param player
* Player to get score for
* @return New score based on current placeholder results
*/
public int getScore(@NotNull TabPlayer player) {
return getScore(player, line.getParent().getLines().size() + 1 - lineNumber);
}
/**
* Returns new score value for specified player.
* If score is not configured, returns provided fallback number.
*
* @param player
* Player to get score for
* @param fallbackNumber
* Fallback number to return if score is not configured
* @return New score based on current placeholder results
*/
public int getScore(@NotNull TabPlayer player, int fallbackNumber) {
// Check if score is configured (for <1.20.3)
if (score != null) {
try {
return Integer.parseInt(player.scoreboardData.scoreProperties.get(line).updateAndGet());
} catch (NumberFormatException e) {
return -1;
}
}
return fallbackNumber;
}
/**
* Returns new number format value for specified player.
*
* @param player
* Player to get number format for
* @return New number format based on current placeholder results
*/
@Nullable
public TabComponent getNumberFormat(@NotNull TabPlayer player) {
return cache.get(player.scoreboardData.numberFormatProperties.get(line).updateAndGet());
}
@Override
@NotNull
public ThreadExecutor getCustomThread() {
return line.getCustomThread();
}
@NotNull
@Override
public String getFeatureName() {
return line.getFeatureName();
}
}
@@ -9,9 +9,7 @@ import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.cpu.ThreadExecutor;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardConfiguration.ScoreboardDefinition;
import me.neznamy.tab.shared.features.scoreboard.lines.LongLine;
import me.neznamy.tab.shared.features.scoreboard.lines.ScoreboardLine;
import me.neznamy.tab.shared.features.scoreboard.lines.StableDynamicLine;
import me.neznamy.tab.shared.features.scoreboard.lines.ScoreboardLineHolder;
import me.neznamy.tab.shared.features.types.Conditional;
import me.neznamy.tab.shared.features.types.CustomThreaded;
import me.neznamy.tab.shared.features.types.RefreshableFeature;
@@ -19,7 +17,6 @@ import me.neznamy.tab.shared.placeholders.conditions.Condition;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
@@ -66,7 +63,7 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
* Scoreboard properties
*/
public ScoreboardImpl(@NonNull ScoreboardManagerImpl manager, @NonNull String name, @NonNull ScoreboardDefinition definition) {
this(manager, name, definition, false, false);
this(manager, name, definition, false);
displayCondition = TAB.getInstance().getPlaceholderManager().getConditionManager().getByNameOrExpression(definition.getDisplayCondition());
if (displayCondition != null) {
manager.addUsedPlaceholder(displayCondition.getPlaceholderIdentifier());
@@ -82,12 +79,10 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
* name of this scoreboard
* @param definition
* Scoreboard properties
* @param dynamicLinesOnly
* Whether this scoreboard should only use dynamic lines or not
* @param api
* Whether this scoreboard was created using API or not
*/
public ScoreboardImpl(@NonNull ScoreboardManagerImpl manager, @NonNull String name, @NonNull ScoreboardDefinition definition, boolean dynamicLinesOnly, boolean api) {
public ScoreboardImpl(@NonNull ScoreboardManagerImpl manager, @NonNull String name, @NonNull ScoreboardDefinition definition, boolean api) {
this.manager = manager;
this.name = name;
this.api = api;
@@ -95,37 +90,12 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
for (int i = 0; i< definition.getLines().size(); i++) {
String line = definition.getLines().get(i);
if (line == null) line = "";
ScoreboardLine score;
if (dynamicLinesOnly) {
score = new StableDynamicLine(this, i+1, line);
} else {
score = registerLine(i+1, line);
}
ScoreboardLineHolder score = new ScoreboardLineHolder(this, line, i+1);
lines.add(score);
TAB.getInstance().getFeatureManager().registerFeature(TabConstants.Feature.scoreboardLine(name, i), score);
}
}
/**
* Registers line with given text and line number
*
* @param lineNumber
* ID of line
* @param text
* text to display
* @return most optimal line from provided text
*/
private @NotNull ScoreboardLine registerLine(int lineNumber, @Nullable String text) {
if (text == null) return new LongLine(this, lineNumber, "");
if (text.startsWith("Long|")) {
return new LongLine(this, lineNumber, text.substring(5));
}
if (text.contains("%")) {
return new StableDynamicLine(this, lineNumber, text);
}
return new LongLine(this, lineNumber, text);
}
/**
* Adds the player into scoreboard. This includes registering properties,
* as well as scoreboard and all lines.
@@ -146,11 +116,10 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
p.getScoreboard().setDisplaySlot(ScoreboardManagerImpl.OBJECTIVE_NAME, Scoreboard.DisplaySlot.SIDEBAR);
}
for (Line s : lines) {
((ScoreboardLine)s).register(p);
((ScoreboardLineHolder)s).register(p);
}
players.add(p);
p.scoreboardData.activeScoreboard = this;
recalculateScores(p);
p.expansionData.setScoreboardName(name);
}
@@ -164,18 +133,12 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
if (p.scoreboardData.activeScoreboard != this) return; // not registered
p.getScoreboard().unregisterObjective(ScoreboardManagerImpl.OBJECTIVE_NAME);
for (Line l : lines) {
ScoreboardLine line = (ScoreboardLine) l;
if (line.isShownTo(p)) {
p.getScoreboard().unregisterTeam(line.getTeamName());
line.removePlayerSilently(p);
}
p.getScoreboard().unregisterTeam(((ScoreboardLineHolder) l).getTeamName());
}
players.remove(p);
p.scoreboardData.activeScoreboard = null;
p.scoreboardData.titleProperty = null;
p.scoreboardData.lineProperties.clear();
p.scoreboardData.lineNameProperties.clear();
p.scoreboardData.numberFormatProperties.clear();
p.expansionData.setScoreboardName("");
}
@@ -196,30 +159,6 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
);
}
/**
* Recalculate scores for each line if using numbers. This takes into
* consideration lines that are not visible.
*
* @param p
* Player to recalculate scores for
*/
public void recalculateScores(@NonNull TabPlayer p) {
int score = 1;
for (Line line : lines) {
Property pr = p.scoreboardData.lineProperties.get((ScoreboardLine) line);
if (pr.getCurrentRawValue().isEmpty() || (!pr.getCurrentRawValue().isEmpty() && !pr.get().isEmpty())) {
((ScoreboardLine)line).getScoreRefresher().setLineNumber(score++);
p.getScoreboard().setScore(
ScoreboardManagerImpl.OBJECTIVE_NAME,
((ScoreboardLine)line).getPlayerName(p),
((ScoreboardLine)line).getScoreRefresher().getScore(p),
null, // Makes no sense for TAB
((ScoreboardLine) line).getScoreRefresher().getNumberFormat(p)
);
}
}
}
/**
* Removes this player from list of players who can see it.
*
@@ -228,9 +167,6 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
*/
public void removePlayerFromSet(@NonNull TabPlayer player) {
players.remove(player);
for (Line line : lines) {
((ScoreboardLine)line).removePlayerSilently(player);
}
}
@NotNull
@@ -261,12 +197,11 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
@Override
public void addLine(@NonNull String text) {
ensureActive();
StableDynamicLine line = new StableDynamicLine(this, lines.size()+1, text);
ScoreboardLineHolder line = new ScoreboardLineHolder(this, text, lines.size()+1);
TAB.getInstance().getFeatureManager().registerFeature(TabConstants.Feature.scoreboardLine(name, lines.size()), line);
lines.add(line);
for (TabPlayer p : players) {
line.register(p);
recalculateScores(p);
}
}
@@ -274,11 +209,10 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
public void removeLine(int index) {
ensureActive();
if (index < 0 || index >= lines.size()) throw new IndexOutOfBoundsException("Index " + index + " is out of range (0 - " + (lines.size()-1) + ")");
ScoreboardLine line = (ScoreboardLine) lines.get(index);
ScoreboardLineHolder line = (ScoreboardLineHolder) lines.get(index);
lines.remove(line);
for (TabPlayer p : players) {
line.unregister(p);
recalculateScores(p);
}
TAB.getInstance().getFeatureManager().unregisterFeature(TabConstants.Feature.scoreboardLine(name, index));
}
@@ -286,13 +220,13 @@ public class ScoreboardImpl extends RefreshableFeature implements me.neznamy.tab
@Override
public void setLines(@NonNull List<String> newLines) {
ensureActive();
int commonSize = Math.min(this.lines.size(), newLines.size());
int commonSize = Math.min(lines.size(), newLines.size());
for (int i = 0; i < commonSize; i++) {
String newText = newLines.get(i) == null ? "" : newLines.get(i);
if (!this.lines.get(i).getText().equals(newText))
this.lines.get(i).setText(newText);
if (!lines.get(i).getText().equals(newText))
lines.get(i).setText(newText);
}
for (int i = this.lines.size() - 1; i >= commonSize; i--)
for (int i = lines.size() - 1; i >= commonSize; i--)
removeLine(i);
for (int i = commonSize; i < newLines.size(); i++)
addLine(newLines.get(i) == null ? "" : newLines.get(i));
@@ -11,7 +11,7 @@ import me.neznamy.tab.shared.cpu.TimedCaughtTask;
import me.neznamy.tab.shared.data.Server;
import me.neznamy.tab.shared.features.ToggleManager;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardConfiguration.ScoreboardDefinition;
import me.neznamy.tab.shared.features.scoreboard.lines.ScoreboardLine;
import me.neznamy.tab.shared.features.scoreboard.lines.ScoreboardLineHolder;
import me.neznamy.tab.shared.features.types.*;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabPlayer;
@@ -34,6 +34,7 @@ public class ScoreboardManagerImpl extends RefreshableFeature implements Scorebo
public static final String OBJECTIVE_NAME = "TAB-Scoreboard";
private final StringToComponentCache cache = new StringToComponentCache("Scoreboard", 1000);
private final StringToComponentCache numberFormatCache = new StringToComponentCache("Scoreboard NumberFormat", 1000);
private final ThreadExecutor customThread = new ThreadExecutor("TAB Scoreboard Thread");
@@ -189,6 +190,34 @@ public class ScoreboardManagerImpl extends RefreshableFeature implements Scorebo
}
}
/**
* Handles a sidebar display sent through VelocityScoreboardAPI.
*
* <p>VelocityScoreboardAPI gives proxy objectives priority over downstream
* objectives. Its display event is therefore the last opportunity to remove
* TAB's proxy objective before the downstream display packet is cancelled.
* This method is intentionally called synchronously by the Velocity hook.</p>
*
* @param receiver
* player receiving the packet
* @param slot
* display slot
* @param objective
* objective assigned to the slot, or {@code null}
*/
public void onVelocityScoreboardDisplay(@NotNull TabPlayer receiver, int slot, @Nullable String objective) {
if (slot != Scoreboard.DisplaySlot.SIDEBAR.ordinal()) {
return;
}
if (objective == null) {
restoreAfterVelocityScoreboard(receiver);
} else if (!objective.equals(OBJECTIVE_NAME)) {
TAB.getInstance().debug("Player " + receiver.getName() + " received scoreboard called " + objective + ", releasing TAB's proxy scoreboard.");
receiver.scoreboardData.otherPluginScoreboard = objective;
unregisterScoreboard(receiver);
}
}
@Override
public void onObjective(@NotNull TabPlayer receiver, int action, @NotNull String objective) {
if (action == Scoreboard.ObjectiveAction.UNREGISTER && objective.equals(receiver.scoreboardData.otherPluginScoreboard)) {
@@ -200,6 +229,30 @@ public class ScoreboardManagerImpl extends RefreshableFeature implements Scorebo
}
}
/**
* Handles removal of a downstream objective reported by VelocityScoreboardAPI.
* This is synchronous for the same reason as {@link #onVelocityScoreboardDisplay(TabPlayer, int, String)}.
*
* @param receiver
* player receiving the packet
* @param objective
* removed objective
*/
public void onVelocityScoreboardUnregister(@NotNull TabPlayer receiver, @NotNull String objective) {
if (objective.equals(receiver.scoreboardData.otherPluginScoreboard)) {
restoreAfterVelocityScoreboard(receiver);
}
}
private void restoreAfterVelocityScoreboard(@NotNull TabPlayer receiver) {
if (receiver.scoreboardData.otherPluginScoreboard == null) {
return;
}
TAB.getInstance().debug("Player " + receiver.getName() + " no longer has another scoreboard, restoring TAB's proxy scoreboard.");
receiver.scoreboardData.otherPluginScoreboard = null;
sendHighestScoreboard(receiver);
}
@Override
public void onQuit(@NotNull TabPlayer disconnectedPlayer) {
ScoreboardImpl sb = disconnectedPlayer.scoreboardData.activeScoreboard;
@@ -230,8 +283,8 @@ public class ScoreboardManagerImpl extends RefreshableFeature implements Scorebo
put("title", player.scoreboardData.titleProperty.get());
List<String> lines = new ArrayList<>();
for (Line line : player.scoreboardData.activeScoreboard.getLines()) {
lines.add(player.scoreboardData.lineProperties.get((ScoreboardLine) line).get() + " || " +
player.scoreboardData.numberFormatProperties.get((ScoreboardLine) line).get());
lines.add(player.scoreboardData.lineProperties.get((ScoreboardLineHolder) line).textProperty.get() + " || " +
player.scoreboardData.lineProperties.get((ScoreboardLineHolder) line).numberFormatProperty.get());
}
put("lines", lines);
}});
@@ -249,7 +302,7 @@ public class ScoreboardManagerImpl extends RefreshableFeature implements Scorebo
@NotNull
public me.neznamy.tab.api.scoreboard.Scoreboard createScoreboard(@NonNull String name, @NonNull String title, @NonNull List<String> lines) {
ensureActive();
ScoreboardImpl sb = new ScoreboardImpl(this, name, new ScoreboardDefinition(null, title, lines), true, true);
ScoreboardImpl sb = new ScoreboardImpl(this, name, new ScoreboardDefinition(null, title, lines), true);
registeredScoreboards.put(name, sb);
return sb;
}
@@ -1,7 +1,8 @@
package me.neznamy.tab.shared.features.scoreboard;
import lombok.AllArgsConstructor;
import me.neznamy.tab.shared.Property;
import me.neznamy.tab.shared.features.scoreboard.lines.ScoreboardLine;
import me.neznamy.tab.shared.features.scoreboard.lines.ScoreboardLineHolder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -35,19 +36,30 @@ public class ScoreboardPlayerData {
@Nullable
public Property titleProperty;
/** Map of line text properties */
/** Properties of scoreboard lines of scoreboard the player can currently see */
@NotNull
public final Map<ScoreboardLine, Property> lineProperties = new IdentityHashMap<>();
public final Map<ScoreboardLineHolder, LineProperties> lineProperties = new IdentityHashMap<>();
/** Map of line player name properties (used in long lines) */
@NotNull
public final Map<ScoreboardLine, Property> lineNameProperties = new IdentityHashMap<>();
/**
* Data class for storing properties of a scoreboard line for a player.
*/
@AllArgsConstructor
public static class LineProperties {
/** Map of line score properties */
@NotNull
public final Map<ScoreboardLine, Property> scoreProperties = new IdentityHashMap<>();
/** Property of line text */
@NotNull
public final Property textProperty;
/** Map of line NumberFormat properties */
@NotNull
public final Map<ScoreboardLine, Property> numberFormatProperties = new IdentityHashMap<>();
/** Name of the score (used in long lines) */
@NotNull
public String scoreName;
/** Property of line NumberFormat */
@NotNull
public final Property numberFormatProperty;
/** Property of line score */
@NotNull
public final Property scoreProperty;
}
}
@@ -0,0 +1,36 @@
package me.neznamy.tab.shared.features.scoreboard.lines;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardManagerImpl;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
/**
* Implementation for empty lines. They are treated as a special case to make them actually
* appear, as refresh logic hides lines that evaluated to an empty string, which we do not want
* if a line is configured to be empty (spacer line).
*/
@RequiredArgsConstructor
public class EmptyLine implements ScoreboardLine {
/** Holder of this line */
@NotNull
private final ScoreboardLineHolder holder;
@Override
public void register(@NonNull TabPlayer player) {
holder.setScore(player, holder.getForcedPlayerNameStart());
}
@Override
public void refresh(@NotNull TabPlayer player) {
player.scoreboardData.lineProperties.get(holder).scoreProperty.update();
holder.setScore(player, holder.getForcedPlayerNameStart());
}
@Override
public void unregister(@NotNull TabPlayer player) {
player.getScoreboard().removeScore(ScoreboardManagerImpl.OBJECTIVE_NAME, holder.getForcedPlayerNameStart());
}
}
@@ -1,14 +1,19 @@
package me.neznamy.tab.shared.features.scoreboard.lines;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.Property;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.TabTextColor;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardManagerImpl;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardPlayerData;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardImpl;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
/**
* Line using all 3 values - prefix, name and suffix. Line may flicker when placeholder changes value.
* Limitations:
@@ -16,81 +21,129 @@ import org.jetbrains.annotations.NotNull;
* 1.8.x - 1.12.x: up to 66 characters
* 1.13+: unlimited
*/
public class LongLine extends ScoreboardLine {
@RequiredArgsConstructor
public class LongLine implements ScoreboardLine {
/**
* Constructs new instance with given parameters
*
* @param parent
* scoreboard this line belongs to
* @param lineNumber
* ID of this line
* @param text
* line text
*/
public LongLine(@NonNull ScoreboardImpl parent, int lineNumber, @NonNull String text) {
super(parent, lineNumber, text);
/** Holder of this line */
@NotNull
private final ScoreboardLineHolder holder;
@Override
public void register(@NotNull TabPlayer player) {
ScoreboardPlayerData.LineProperties properties = player.scoreboardData.lineProperties.get(holder);
if (properties.textProperty.get().isEmpty() && properties.numberFormatProperty.get().isEmpty()) {
return;
}
if (player.getVersion().getMinorVersion() >= 13 && !TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) {
holder.updateTeam(player, properties.textProperty.get(), "");
holder.setScore(player, holder.getForcedPlayerNameStart());
} else {
String[] values = splitText(
holder.getForcedPlayerNameStart(),
holder.getParent().getManager().getCache().get(properties.textProperty.get()).toLegacyText(),
player.getVersion().getNetworkId() >= ProtocolVersion.V1_8.getNetworkId() ? Limitations.SCOREBOARD_SCORE_LENGTH_1_8 : Limitations.SCOREBOARD_SCORE_LENGTH_1_7
);
properties.scoreName = values[1];
updateTeam(player, values);
holder.setScore(player, values[1]);
}
}
@Override
public void refresh(@NotNull TabPlayer refreshed, boolean force) {
if (refreshed.scoreboardData.activeScoreboard != parent) return; //player has different scoreboard displayed
Property lineProperty = refreshed.scoreboardData.lineProperties.get(this);
if (lineProperty.update()) {
if (refreshed.getVersion().getMinorVersion() >= 13 && !TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) {
updateTeam(refreshed, lineProperty.get(), "");
public void refresh(@NotNull TabPlayer player) {
if (player.scoreboardData.activeScoreboard != holder.getParent()) return; // Player has different scoreboard displayed
ScoreboardPlayerData.LineProperties properties = player.scoreboardData.lineProperties.get(holder);
boolean textUpdated = properties.textProperty.update();
boolean numberFormatUpdated = properties.numberFormatProperty.update();
boolean scoreUpdated = properties.scoreProperty.update();
if (properties.textProperty.get().isEmpty() && properties.numberFormatProperty.get().isEmpty()) {
player.getScoreboard().removeScore(ScoreboardManagerImpl.OBJECTIVE_NAME, properties.scoreName);
} else if (textUpdated || numberFormatUpdated || scoreUpdated) {
if (player.getVersion().getMinorVersion() >= 13 && !TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) {
holder.updateTeam(player, properties.textProperty.get(), "");
holder.setScore(player, holder.getForcedPlayerNameStart());
} else {
removeLine(refreshed, refreshed.scoreboardData.lineNameProperties.get(this).get());
player.getScoreboard().removeScore(ScoreboardManagerImpl.OBJECTIVE_NAME, properties.scoreName);
String[] values = splitText(
forcedPlayerNameStart,
parent.getManager().getCache().get(lineProperty.get()).toLegacyText(),
refreshed.getVersion().getNetworkId() >= ProtocolVersion.V1_8.getNetworkId() ? Limitations.SCOREBOARD_SCORE_LENGTH_1_8 : Limitations.SCOREBOARD_SCORE_LENGTH_1_7
holder.getForcedPlayerNameStart(),
holder.getParent().getManager().getCache().get(properties.textProperty.get()).toLegacyText(),
player.getVersion().getNetworkId() >= ProtocolVersion.V1_8.getNetworkId() ? Limitations.SCOREBOARD_SCORE_LENGTH_1_8 : Limitations.SCOREBOARD_SCORE_LENGTH_1_7
);
addLine(refreshed, values[1], values[0], values[2]);
refreshed.scoreboardData.lineNameProperties.get(this).changeRawValue(values[1]);
updateTeam(player, values);
holder.setScore(player, values[1]);
properties.scoreName = values[1];
}
}
}
@Override
public void register(@NonNull TabPlayer p) {
p.scoreboardData.lineProperties.put(this, new Property(this, p, text));
getScoreRefresher().registerProperties(p);
String value = p.scoreboardData.lineProperties.get(this).get();
if (p.getVersion().getMinorVersion() >= 13 && !TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) {
addLine(p, forcedPlayerNameStart, value, "");
p.scoreboardData.lineNameProperties.put(this, new Property(this, p, forcedPlayerNameStart));
} else {
String[] values = splitText(
forcedPlayerNameStart,
parent.getManager().getCache().get(value).toLegacyText(),
p.getVersion().getNetworkId() >= ProtocolVersion.V1_8.getNetworkId() ? Limitations.SCOREBOARD_SCORE_LENGTH_1_8 : Limitations.SCOREBOARD_SCORE_LENGTH_1_7
);
addLine(p, values[1], values[0], values[2]);
p.scoreboardData.lineNameProperties.put(this, new Property(this, p, values[1]));
}
private void updateTeam(@NotNull TabPlayer player, @NotNull String[] values) {
player.getScoreboard().unregisterTeam(holder.getTeamName());
player.getScoreboard().registerTeam(
holder.getTeamName(),
holder.getParent().getManager().getCache().get(values[0]),
holder.getParent().getManager().getCache().get(values[2]),
Scoreboard.NameVisibility.NEVER,
Scoreboard.CollisionRule.NEVER,
Collections.singletonList(values[1]),
0,
TabTextColor.RESET.getLegacyColor()
);
}
@Override
public void unregister(@NonNull TabPlayer p) {
if (p.scoreboardData.activeScoreboard == parent) {
removeLine(p, p.scoreboardData.lineNameProperties.get(this).get());
}
public void unregister(@NotNull TabPlayer player) {
player.getScoreboard().removeScore(
ScoreboardManagerImpl.OBJECTIVE_NAME,
player.scoreboardData.lineProperties.get(holder).scoreName
);
}
@Override
public void setText(@NonNull String text) {
ensureActive();
initializeText(text);
for (TabPlayer p : parent.getPlayers()) {
p.scoreboardData.lineProperties.get(this).changeRawValue(text);
refresh(p, false);
}
}
@Override
/**
* Splits entered text into 3 parts - prefix, name and suffix respecting all limits.
* Returns the values as an array of 3 elements.
*
* @param playerNameStart
* forced start of name field (used to secure unique names and line order)
* @param text
* text to display
* @param maxNameLength
* maximum length of name field
* @return Split text as an array of 3 elements
*/
@NotNull
public String getPlayerName(@NonNull TabPlayer viewer) {
return viewer.scoreboardData.lineNameProperties.get(this).get();
private String[] splitText(@NonNull String playerNameStart, @NonNull String text, int maxNameLength) {
String prefixValue;
String nameValue;
String suffixValue;
if (text.length() <= (maxNameLength - playerNameStart.length())) {
prefixValue = "";
nameValue = playerNameStart + text;
suffixValue = "";
} else {
String[] prefixOther = split(text, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13);
prefixValue = prefixOther[0];
String other = prefixOther[1];
other = playerNameStart + getLastColors(prefixValue) + other;
String[] nameSuffix = split(other, maxNameLength);
nameValue = nameSuffix[0];
suffixValue = nameSuffix[1];
}
return new String[]{prefixValue, nameValue, suffixValue};
}
}
/**
* Splits the text into 2 with given max length of first string
*
* @param string
* string to split
* @param firstElementMaxLength
* max length of first string
* @return array of 2 strings where second one might be empty
*/
private String[] split(@NonNull String string, int firstElementMaxLength) {
if (string.length() <= firstElementMaxLength) return new String[] {string, ""};
int splitIndex = firstElementMaxLength;
if (string.charAt(splitIndex-1) == '§') splitIndex--;
return new String[] {string.substring(0, splitIndex), string.substring(splitIndex)};
}
}
@@ -0,0 +1,94 @@
package me.neznamy.tab.shared.features.scoreboard.lines;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardManagerImpl;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardPlayerData;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
/**
* Line of text that uses only team prefix and suffix to display text.
* This allows for changes without having to remove a score and add a new one, possibly
* causing a flicker on update.
* Limitations:
* 1.12-: Up to 32 characters depending on color / magic codes
* 1.13+: Unlimited
*/
@RequiredArgsConstructor
public class NoFlickerLine implements ScoreboardLine {
/** Holder of this line */
@NotNull
private final ScoreboardLineHolder holder;
@Override
public void register(@NonNull TabPlayer player) {
ScoreboardPlayerData.LineProperties properties = player.scoreboardData.lineProperties.get(holder);
if (properties.textProperty.get().isEmpty() && properties.numberFormatProperty.get().isEmpty()) {
return;
}
String[] prefixSuffix = split(player, properties.textProperty.get());
holder.updateTeam(player, prefixSuffix[0], prefixSuffix[1]);
holder.setScore(player, holder.getForcedPlayerNameStart());
}
@Override
public void refresh(@NotNull TabPlayer player) {
if (player.scoreboardData.activeScoreboard != holder.getParent()) return; // Player has different scoreboard displayed
ScoreboardPlayerData.LineProperties properties = player.scoreboardData.lineProperties.get(holder);
boolean emptyBefore = properties.textProperty.get().isEmpty() && properties.numberFormatProperty.get().isEmpty();
if (properties.textProperty.update()) {
String[] prefixSuffix = split(player, properties.textProperty.get());
holder.updateTeam(player, prefixSuffix[0], prefixSuffix[1]);
}
boolean numberFormatUpdated = properties.numberFormatProperty.update();
boolean scoreUpdated = properties.scoreProperty.update();
if (properties.textProperty.get().isEmpty() && properties.numberFormatProperty.get().isEmpty()) {
player.getScoreboard().removeScore(ScoreboardManagerImpl.OBJECTIVE_NAME, holder.getForcedPlayerNameStart());
} else if (emptyBefore || numberFormatUpdated || scoreUpdated) {
holder.setScore(player, holder.getForcedPlayerNameStart());
}
}
@Override
public void unregister(@NotNull TabPlayer player) {
player.getScoreboard().removeScore(ScoreboardManagerImpl.OBJECTIVE_NAME, holder.getForcedPlayerNameStart());
}
/**
* Splits text into 2 values (prefix/suffix) based on client version and text itself
*
* @param p
* player to split text for
* @param text
* text to split
* @return array of 2 elements for prefix and suffix
*/
@NotNull
private String[] split(@NonNull TabPlayer p, @NonNull String text) {
if (p.getVersion().getNetworkId() < ProtocolVersion.V1_16.getNetworkId()) {
text = holder.getParent().getManager().getCache().get(text).toLegacyText(); //converting RGB to legacy here to avoid splitting in the middle of RGB code
}
if (p.getVersion().getMinorVersion() >= 13 && !TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) return new String[] {text, ""};
int charLimit = Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13;
if (text.length() > charLimit) {
StringBuilder prefix = new StringBuilder(text);
StringBuilder suffix = new StringBuilder(text);
prefix.setLength(charLimit);
suffix.delete(0, charLimit);
if (prefix.charAt(charLimit-1) == '§') {
prefix.setLength(prefix.length()-1);
suffix.insert(0, '§');
}
String prefixString = prefix.toString();
suffix.insert(0, getLastColors(holder.getParent().getManager().getCache().get(prefixString).toLegacyText()));
return new String[] {prefixString, suffix.toString()};
} else {
return new String[] {text, ""};
}
}
}
@@ -1,278 +1,36 @@
package me.neznamy.tab.shared.features.scoreboard.lines;
import lombok.Getter;
import lombok.NonNull;
import me.neznamy.tab.shared.chat.TabTextColor;
import me.neznamy.tab.api.scoreboard.Line;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.cpu.ThreadExecutor;
import me.neznamy.tab.shared.features.scoreboard.ScoreRefresher;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardImpl;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardManagerImpl;
import me.neznamy.tab.shared.features.types.CustomThreaded;
import me.neznamy.tab.shared.features.types.RefreshableFeature;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Abstract class representing a line of scoreboard
* An interface representing a line in the scoreboard.
*/
@Getter
public abstract class ScoreboardLine extends RefreshableFeature implements Line, CustomThreaded {
/** Text to display in this line */
@NotNull
protected String text;
/** Score to use instead of line number (can be null if not configured) */
@Nullable
protected String score;
/** Number format to use (can be null if not configured) */
@Nullable
protected String numberFormat;
//scoreboard this line belongs to
protected final ScoreboardImpl parent;
//scoreboard team name of player in this line
protected final String teamName;
/** Forced player name start to make lines unique & sort them by names */
protected final String forcedPlayerNameStart;
private final ScoreRefresher scoreRefresher;
private final Set<TabPlayer> shownPlayers = Collections.newSetFromMap(new ConcurrentHashMap<>());
/**
* Constructs new instance with given parameters
*
* @param parent
* scoreboard this line belongs to
* @param lineNumber
* ID of this line
*/
protected ScoreboardLine(@NonNull ScoreboardImpl parent, int lineNumber, @NotNull String text) {
initializeText(text);
this.parent = parent;
teamName = "TAB-Sidebar-" + lineNumber;
if (lineNumber > 99) throw new IllegalStateException("Internal code does not support more than 99 lines per scoreboard.");
forcedPlayerNameStart = String.format("§%d§%d§r", (lineNumber / 10) % 10, lineNumber % 10);
scoreRefresher = new ScoreRefresher(this, lineNumber, score, numberFormat);
TAB.getInstance().getFeatureManager().registerFeature(TabConstants.Feature.scoreboardScore(parent.getName(), lineNumber), scoreRefresher);
}
public interface ScoreboardLine {
/**
* Registers this line to the player
*
* @param p
* player to register line to
*/
public abstract void register(@NonNull TabPlayer p);
/**
* Unregisters this line to the player
*
* @param p
* player to unregister line to
*/
public abstract void unregister(@NonNull TabPlayer p);
/**
* Returns player name of this line for specified viewer.
*
* @param viewer
* Player to get line name for
* @return Name of this line for specified viewer
*/
@NotNull
public String getPlayerName(@NonNull TabPlayer viewer) {
return forcedPlayerNameStart;
}
/**
* Splits the text into 2 with given max length of first string
*
* @param string
* string to split
* @param firstElementMaxLength
* max length of first string
* @return array of 2 strings where second one might be empty
*/
protected String[] split(@NonNull String string, int firstElementMaxLength) {
if (string.length() <= firstElementMaxLength) return new String[] {string, ""};
int splitIndex = firstElementMaxLength;
if (string.charAt(splitIndex-1) == '§') splitIndex--;
return new String[] {string.substring(0, splitIndex), string.substring(splitIndex)};
}
/**
* Sends this line to player
*
* @param p
* player to send line to
* @param fakePlayer
* player name
* @param prefix
* prefix
* @param suffix
* suffix
*/
protected void addLine(@NonNull TabPlayer p, @NonNull String fakePlayer, @NonNull String prefix, @NonNull String suffix) {
p.getScoreboard().setScore(
ScoreboardManagerImpl.OBJECTIVE_NAME,
fakePlayer,
scoreRefresher.getScore(p),
null, // Makes no sense for TAB
scoreRefresher.getNumberFormat(p)
);
p.getScoreboard().registerTeam(
teamName,
parent.getManager().getCache().get(prefix),
parent.getManager().getCache().get(suffix),
Scoreboard.NameVisibility.NEVER,
Scoreboard.CollisionRule.NEVER,
Collections.singletonList(fakePlayer),
0,
TabTextColor.RESET.getLegacyColor()
);
shownPlayers.add(p);
}
/**
* Removes this line from player
*
* @param p
* player to remove line from
* @param fakePlayer
* player name
*/
protected void removeLine(@NonNull TabPlayer p, @NonNull String fakePlayer) {
p.getScoreboard().removeScore(ScoreboardManagerImpl.OBJECTIVE_NAME, fakePlayer);
p.getScoreboard().unregisterTeam(teamName);
shownPlayers.remove(p);
}
/**
* Splits entered text into 3 parts - prefix, name and suffix respecting all limits.
* Returns the values as an array of 3 elements.
*
* @param playerNameStart
* forced start of name field (used to secure unique names and line order)
* @param text
* text to display
* @param maxNameLength
* maximum length of name field
* @return Split text as an array of 3 elements
*/
protected String[] splitText(@NonNull String playerNameStart, @NonNull String text, int maxNameLength) {
String prefixValue;
String nameValue;
String suffixValue;
if (text.length() <= (maxNameLength - playerNameStart.length())) {
prefixValue = "";
nameValue = playerNameStart + text;
suffixValue = "";
} else {
String[] prefixOther = split(text, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13);
prefixValue = prefixOther[0];
String other = prefixOther[1];
other = playerNameStart + getLastColors(prefixValue) + other;
String[] nameSuffix = split(other, maxNameLength);
nameValue = nameSuffix[0];
suffixValue = nameSuffix[1];
}
return new String[]{prefixValue, nameValue, suffixValue};
}
/**
* Returns {@code true} if this line is visible to specified player,
* {@code false} if not.
* Registers the line for the specified player.
*
* @param player
* Player to check
* @return {@code true} if shown, {@code false} if not
* the player for whom the line should be registered
*/
public boolean isShownTo(@NonNull TabPlayer player) {
return shownPlayers.contains(player);
}
void register(@NotNull TabPlayer player);
/**
* Splits text using {@code "||"} string, where first part is text to display,
* second part is number format (optional) and third part is score (optional).
*
* @param text
* Inputted text to categorize
*/
protected void initializeText(@NotNull String text) {
numberFormat = null;
score = null;
if (text.contains("||")) {
String[] split = text.split("\\|\\|", -1);
this.text = split.length > 0 ? split[0] : "";
if (split.length > 1) numberFormat = split[1];
if (split.length > 2) score = split[2];
} else {
this.text = text;
}
}
/**
* Updates prefix/suffix of the fake player.
* Refreshes the line for the specified player.
*
* @param player
* Player to send the update to
* @param prefix
* Prefix to use
* @param suffix
* Suffix to use
* the player for whom the line should be refreshed
*/
protected void updateTeam(@NotNull TabPlayer player, @NotNull String prefix, @NotNull String suffix) {
player.getScoreboard().updateTeam(
teamName,
parent.getManager().getCache().get(prefix),
parent.getManager().getCache().get(suffix),
TabTextColor.RESET.getLegacyColor()
);
}
void refresh(@NotNull TabPlayer player);
/**
* Silently removes players from the list of players this line is shown to.
* Unregisters the line for the specified player.
*
* @param player
* Player to remove
* the player for whom the line should be unregistered
*/
public void removePlayerSilently(@NonNull TabPlayer player) {
shownPlayers.remove(player);
}
@Override
@NotNull
public ThreadExecutor getCustomThread() {
return parent.getCustomThread();
}
@NotNull
@Override
public String getFeatureName() {
return parent.getFeatureName();
}
@NotNull
@Override
public String getRefreshDisplayName() {
return "Updating Scoreboard lines";
}
void unregister(@NotNull TabPlayer player);
/**
* Returns last color codes used in provided text.
@@ -282,7 +40,7 @@ public abstract class ScoreboardLine extends RefreshableFeature implements Line,
* @return last colors used in provided text or empty string if nothing was found
*/
@NotNull
protected String getLastColors(@NotNull String input) {
default String getLastColors(@NotNull String input) {
StringBuilder result = new StringBuilder();
int length = input.length();
for (int index = length - 1; index > -1; index--) {
@@ -300,4 +58,4 @@ public abstract class ScoreboardLine extends RefreshableFeature implements Line,
}
return result.toString();
}
}
}
@@ -0,0 +1,242 @@
package me.neznamy.tab.shared.features.scoreboard.lines;
import lombok.Getter;
import lombok.NonNull;
import me.neznamy.tab.api.scoreboard.Line;
import me.neznamy.tab.shared.Property;
import me.neznamy.tab.shared.chat.TabTextColor;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.cpu.ThreadExecutor;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardImpl;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardManagerImpl;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardPlayerData;
import me.neznamy.tab.shared.features.types.CustomThreaded;
import me.neznamy.tab.shared.features.types.RefreshableFeature;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
/**
* A class representing a line in the scoreboard.
* It holds all information about a line and is able to swap between implementations
* if text is changed via API.
*/
@Getter
public class ScoreboardLineHolder extends RefreshableFeature implements Line, CustomThreaded {
/** The scoreboard this line belongs to */
@NotNull
private final ScoreboardImpl parent;
/** The configured text to display in raw form (including || separators) */
@NotNull
private String text;
@NotNull
private String leftText;
@NotNull
private String numberFormat;
@NotNull
private String score;
/** Number of this line (1-15+) */
private final int lineNumber;
@NotNull
private final String forcedPlayerNameStart;
/** The team name for this line */
@NotNull
private final String teamName;
/** The handle for the scoreboard line based on current text */
@NotNull
private ScoreboardLine handle;
/**
* Constructs a new instance with given parameters.
*
* @param parent
* the scoreboard this line belongs to
* @param text
* the configured text to display in raw form (including || separators)
* @param lineNumber
* number of this line (1-15+)
*/
public ScoreboardLineHolder(@NonNull ScoreboardImpl parent, @NonNull String text, int lineNumber) {
if (lineNumber > 99) throw new IllegalStateException("Internal code does not support more than 99 lines per scoreboard.");
this.parent = parent;
this.text = text;
this.lineNumber = lineNumber;
forcedPlayerNameStart = String.format("§%d§%d§r", (lineNumber / 10) % 10, lineNumber % 10);
teamName = "TAB-Sidebar-" + lineNumber;
splitText();
handle = createHandle();
}
@Override
@NotNull
public String getText() {
return text;
}
@Override
public void setText(@NonNull String text) {
if (this.text.equals(text)) return;
for (TabPlayer player : parent.getPlayers()) {
handle.unregister(player);
}
this.text = text;
splitText();
handle = createHandle();
for (TabPlayer player : parent.getPlayers()) {
player.scoreboardData.lineProperties.put(this, new ScoreboardPlayerData.LineProperties(
new Property(this, player, leftText),
forcedPlayerNameStart,
new Property(this, player, numberFormat),
new Property(this, player, score)
));
handle.register(player);
}
}
@Override
@NotNull
public ThreadExecutor getCustomThread() {
return parent.getCustomThread();
}
@Override
@NotNull
public String getRefreshDisplayName() {
return "Updating Scoreboard lines";
}
@Override
public void refresh(@NotNull TabPlayer refreshed, boolean force) {
handle.refresh(refreshed);
}
@Override
@NotNull
public String getFeatureName() {
return parent.getFeatureName();
}
/**
* Registers this line to the player
*
* @param player
* player to register line to
*/
public void register(@NotNull TabPlayer player) {
player.scoreboardData.lineProperties.put(this, new ScoreboardPlayerData.LineProperties(
new Property(this, player, leftText),
forcedPlayerNameStart,
new Property(this, player, numberFormat),
new Property(this, player, score)
));
player.getScoreboard().registerTeam(
teamName,
TabComponent.empty(),
TabComponent.empty(),
Scoreboard.NameVisibility.NEVER,
Scoreboard.CollisionRule.NEVER,
Collections.singletonList(forcedPlayerNameStart),
0,
TabTextColor.RESET.getLegacyColor()
);
handle.register(player);
}
/**
* Updates the team prefix and suffix for the player
*
* @param player
* player to update team for
* @param prefix
* new prefix to set
* @param suffix
* new suffix to set
*/
public void updateTeam(@NotNull TabPlayer player, @NotNull String prefix, @NotNull String suffix) {
player.getScoreboard().updateTeam(
teamName,
parent.getManager().getCache().get(prefix),
parent.getManager().getCache().get(suffix),
TabTextColor.RESET.getLegacyColor()
);
}
/**
* Sets the score for player. Created as a separate method to avoid duplication.
*
* @param player
* player to set score for
* @param scoreHolder
* score holder to set score for
*/
public void setScore(@NotNull TabPlayer player, @NotNull String scoreHolder) {
int score;
if (!this.score.isEmpty()) {
try {
score = Integer.parseInt(player.scoreboardData.lineProperties.get(this).scoreProperty.get());
} catch (NumberFormatException e) {
score = -1;
}
} else {
score = parent.getLines().size() + 1 - lineNumber;
}
player.getScoreboard().setScore(
ScoreboardManagerImpl.OBJECTIVE_NAME,
scoreHolder,
score,
null,
parent.getManager().getNumberFormatCache().get(player.scoreboardData.lineProperties.get(this).numberFormatProperty.get())
);
}
/**
* Unregisters this line to the player
*
* @param player
* player to unregister line to
*/
public void unregister(@NotNull TabPlayer player) {
player.getScoreboard().unregisterTeam(teamName);
handle.unregister(player);
}
private void splitText() {
leftText = "";
numberFormat = "";
score = "";
String realText = text;
if (realText.startsWith("Long|")) {
realText = realText.substring(5);
}
String[] split = realText.split("\\|\\|", -1);
if (split.length > 0) leftText = split[0];
if (split.length > 1) numberFormat = split[1];
if (split.length > 2) score = split[2];
}
@NotNull
private ScoreboardLine createHandle() {
if (leftText.isEmpty() && numberFormat.isEmpty()) {
return new EmptyLine(this);
}
if (text.startsWith("Long|")) {
return new LongLine(this);
}
if (text.contains("%") || text.contains("<")) {
return new NoFlickerLine(this);
}
return new LongLine(this);
}
}
@@ -1,147 +0,0 @@
package me.neznamy.tab.shared.features.scoreboard.lines;
import lombok.NonNull;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.Property;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardImpl;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
/**
* Line of text with placeholder support
* Limitations:
* 1.5.x - 1.12.x: 28 - 32 characters (depending on used magic codes)
* 1.13+: unlimited
*/
public class StableDynamicLine extends ScoreboardLine {
private final String[] EMPTY_ARRAY = new String[0];
/**
* Constructs new instance with given parameters
*
* @param parent
* scoreboard this line belongs to
* @param lineNumber
* ID of this line
* @param text
* text to display
*/
public StableDynamicLine(@NonNull ScoreboardImpl parent, int lineNumber, @NonNull String text) {
super(parent, lineNumber, text);
}
@Override
public void refresh(@NotNull TabPlayer refreshed, boolean force) {
if (refreshed.scoreboardData.activeScoreboard != parent) return; //player has different scoreboard displayed
String[] prefixSuffix = replaceText(refreshed, force, false);
if (prefixSuffix.length == 0) return;
updateTeam(refreshed, prefixSuffix[0], prefixSuffix[1]);
}
@Override
public void register(@NonNull TabPlayer p) {
p.scoreboardData.lineProperties.put(this, new Property(this, p, text));
getScoreRefresher().registerProperties(p);
String[] prefixSuffix = replaceText(p, true, true);
if (prefixSuffix.length == 0) return;
addLine(p, forcedPlayerNameStart, prefixSuffix[0], prefixSuffix[1]);
}
@Override
public void unregister(@NonNull TabPlayer p) {
if (p.scoreboardData.activeScoreboard == parent && !p.scoreboardData.lineProperties.get(this).get().isEmpty()) {
removeLine(p, forcedPlayerNameStart);
}
}
/**
* Applies all placeholders and splits the result into prefix/suffix based on client version
* or hides the line entirely if result is empty (and shows back once it's not)
*
* @param p
* player to replace text for
* @param force
* if action should be done despite update seemingly not needed
* @param suppressToggle
* if line should NOT be removed despite being empty
* @return array of 2 elements for prefix/suffix
*/
private String[] replaceText(TabPlayer p, boolean force, boolean suppressToggle) {
Property scoreProperty = p.scoreboardData.lineProperties.get(this);
if (scoreProperty == null) return EMPTY_ARRAY; //not actually loaded yet (force refresh called from placeholder manager register method)
boolean emptyBefore = scoreProperty.get().isEmpty();
if (!scoreProperty.update() && !force) return EMPTY_ARRAY;
String replaced = scoreProperty.get();
if (p.getVersion().getNetworkId() < ProtocolVersion.V1_16.getNetworkId()) {
replaced = parent.getManager().getCache().get(replaced).toLegacyText(); //converting RGB to legacy here to avoid splitting in the middle of RGB code
}
String[] split = split(p, replaced);
if (!replaced.isEmpty()) {
if (emptyBefore) {
//was "", now it is not
addLine(p, forcedPlayerNameStart, split[0], split[1]);
parent.recalculateScores(p);
return EMPTY_ARRAY;
} else {
return split;
}
} else {
if (!suppressToggle) {
//new string is "", but before it was not
removeLine(p, forcedPlayerNameStart);
parent.recalculateScores(p);
}
return EMPTY_ARRAY;
}
}
/**
* Splits text into 2 values (prefix/suffix) based on client version and text itself
*
* @param p
* player to split text for
* @param text
* text to split
* @return array of 2 elements for prefix and suffix
*/
private String[] split(@NonNull TabPlayer p, @NonNull String text) {
if (p.getVersion().getMinorVersion() >= 13 && !TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) return new String[] {text, ""};
int charLimit = Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13;
if (text.length() > charLimit) {
StringBuilder prefix = new StringBuilder(text);
StringBuilder suffix = new StringBuilder(text);
prefix.setLength(charLimit);
suffix.delete(0, charLimit);
if (prefix.charAt(charLimit-1) == '§') {
prefix.setLength(prefix.length()-1);
suffix.insert(0, '§');
}
String prefixString = prefix.toString();
suffix.insert(0, getLastColors(parent.getManager().getCache().get(prefixString).toLegacyText()));
return new String[] {prefixString, suffix.toString()};
} else {
return new String[] {text, ""};
}
}
@Override
public void setText(@NonNull String text) {
ensureActive();
initializeText(text);
for (TabPlayer p : parent.getPlayers()) {
p.scoreboardData.lineProperties.get(this).changeRawValue(text);
String[] prefixSuffix = replaceText(p, true, true);
if (prefixSuffix.length == 0) {
if (text.isEmpty()) {
prefixSuffix = new String[]{"", ""};
} else {
continue;
}
}
updateTeam(p, prefixSuffix[0], prefixSuffix[1]);
}
}
}
@@ -2,10 +2,14 @@ package me.neznamy.tab.shared.placeholders;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import me.neznamy.tab.shared.features.types.RefreshableFeature;
import me.neznamy.tab.shared.placeholders.types.TabPlaceholder;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* This class holds a reference to a placeholder.
* Placeholder implementations may change at runtime, breaking all existing
@@ -30,6 +34,16 @@ public class PlaceholderReference {
@NotNull
private TabPlaceholder handle;
/**
* List of placeholders using this placeholder as a nested placeholder,
* mutual tracking allows faster parent placeholder changes when a nested
* placeholder changed value.
*/
private final List<PlaceholderReference> parents = new ArrayList<>();
/** Set of features using this placeholder, used to call refresh on them */
private final Set<RefreshableFeature> usedByFeatures = Collections.synchronizedSet(new HashSet<>());
/**
* Returns refresh interval of the placeholder.
*
@@ -38,4 +52,31 @@ public class PlaceholderReference {
public int getRefresh() {
return handle.getRefresh();
}
/**
* Registers a feature that is using this placeholder, to allow
* refreshing it when this placeholder changes.
*
* @param feature
* Feature using this placeholder
* @return true if feature was not already registered
*/
public boolean addUsedFeature(@NonNull RefreshableFeature feature) {
return usedByFeatures.add(feature);
}
/**
* Internal method used to mark placeholders as parents who use this placeholder
* inside their outputs for faster updates.
*
* @param parent
* parent placeholder using this placeholder in output
*/
public void addParent(@NonNull PlaceholderReference parent) {
if (parent == this) return; // ???
if (!parents.contains(parent)) {
usedByFeatures.addAll(parent.usedByFeatures);
parents.add(parent);
}
}
}
@@ -1,11 +1,11 @@
package me.neznamy.tab.shared.placeholders;
import lombok.Getter;
import me.neznamy.tab.shared.chat.EnumChatFormat;
import me.neznamy.tab.shared.features.PlaceholderManagerImpl;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
@@ -30,12 +30,6 @@ public class PlaceholderReplacementPattern {
*/
private final Map<float[], String> numberIntervals = new HashMap<>();
/** Set of all used placeholders in replacement values */
@Getter private final Set<String> nestedPlaceholders = new HashSet<>();
/** Flag tracking if this replacement map is empty */
private final boolean empty;
/**
* Constructs new instance from given replacement map from config
*
@@ -45,15 +39,12 @@ public class PlaceholderReplacementPattern {
* replacement map from config
*/
private PlaceholderReplacementPattern(@NotNull String identifier, @NotNull Map<Object, Object> map) {
empty = map.isEmpty();
for (Entry<Object, Object> entry : map.entrySet()) {
String key = String.valueOf(entry.getKey());
String value = String.valueOf(entry.getValue()).replace(identifier, "%value%");
replacements.put(EnumChatFormat.color(key), EnumChatFormat.color(value));
replacements.put(key, value);
nestedPlaceholders.addAll(PlaceholderManagerImpl.detectPlaceholders(value));
nestedPlaceholders.remove("%value%"); //not a real placeholder
//snakeyaml converts yes & no to booleans, making them not work when used without "
// SnakeYAML converts yes & no to booleans, making them not work when used without "
if ("true".equals(key)) {
replacements.put("yes", value);
replacements.put("Yes", value);
@@ -81,7 +72,7 @@ public class PlaceholderReplacementPattern {
*/
@NotNull
public String findReplacement(@NotNull String output) {
if (empty) return output;
if (this == EMPTY) return output;
String replacement = findReplacement0(output);
if (replacement.contains("%value%")) {
replacement = replacement.replace("%value%", output);
@@ -182,9 +182,8 @@ public class UniversalPlaceholderRegistry {
manager.registerPlayerPlaceholder(TabConstants.Placeholder.animation(a.getName()), a.getRefresh(), p -> a.getMessage());
}
for (Entry<String, ConditionDefinition> condition : TAB.getInstance().getConfiguration().getConfig().getConditions().getConditions().entrySet()) {
Condition c = new Condition(condition.getValue());
Condition c = Condition.fromDefinition(condition.getValue());
TAB.getInstance().getPlaceholderManager().getConditionManager().registerCondition(c);
}
manager.getConditionManager().finishSetups();
}
}
@@ -1,7 +1,6 @@
package me.neznamy.tab.shared.placeholders.conditions;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.features.PlaceholderManagerImpl;
import me.neznamy.tab.shared.placeholders.PlaceholderReference;
@@ -9,7 +8,6 @@ import me.neznamy.tab.shared.placeholders.types.RelationalPlaceholderImpl;
import me.neznamy.tab.shared.placeholders.types.TabPlaceholder;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
@@ -24,7 +22,6 @@ import java.util.stream.Collectors;
* condition types that must be met in order to display specified
* text or make a condition requirement for a visual to be displayed.
*/
@RequiredArgsConstructor
@Getter
public class Condition {
@@ -53,68 +50,37 @@ public class Condition {
*/
private int refresh = -1;
/** List of all placeholders used inside this condition */
@NotNull
private final List<PlaceholderReference> placeholdersInConditions = new ArrayList<>();
@Nullable
private TabPlaceholder placeholder;
@Nullable
private RelationalPlaceholderImpl relationalPlaceholder;
/**
* Constructs new instance with given definition and registers
* this condition to list as well as the placeholder.
* Constructs new instance with given parameters and analyzes content
* to determine refresh interval.
*
* @param definition
* Condition definition from configuration
* @param name
* Name of this condition defined in configuration
* @param expressions
* All defined expressions inside this condition
* @param type
* Condition type, {@code true} for AND type and {@code false} for OR type
* @param yes
* Text to display if condition passed
* @param no
* Text to display if condition failed
* @param primitive
* Whether this is a primitive condition (TrueCondition or FalseCondition with no nested placeholders)
*/
public Condition(@NotNull ConditionsSection.ConditionDefinition definition) {
type = definition.isType();
name = definition.getName();
expressions = definition.getConditions().stream().map(expressionString -> {
ConditionalExpression expression = ConditionalExpression.compile(expressionString.trim());
if (expression == null) {
TAB.getInstance().getConfigHelper().startup().startupWarn("Line \"" + expressionString + "\" is not a valid conditional expression.");
}
return expression;
}).filter(Objects::nonNull).collect(Collectors.toList());
yes = definition.getYes();
no = definition.getNo();
analyzeContent();
}
public Condition(@NotNull String name, @NotNull List<ConditionalExpression> expressions, boolean type, @NotNull String yes, @NotNull String no, boolean primitive) {
this.name = name;
this.expressions = expressions;
this.type = type;
this.yes = yes;
this.no = no;
/**
* Constructs new instance from a condition string in short format.
* This constructor is used for anonymous conditions only.
*
* @param shortFormat
* Condition in short format
*/
public Condition(@NotNull String shortFormat) {
name = "AnonymousCondition[" + shortFormat + "]";
yes = "true";
no = "false";
List<String> conditions;
if (shortFormat.contains(";")) {
type = true;
conditions = Arrays.asList(shortFormat.split(";"));
} else {
type = false;
conditions = splitString(shortFormat);
}
expressions = conditions.stream().map(expressionString -> {
ConditionalExpression expression = ConditionalExpression.compile(expressionString.trim());
if (expression == null) {
TAB.getInstance().getConfigHelper().startup().startupWarn("Line \"" + expressionString + "\" is not a valid conditional expression.");
}
return expression;
}).filter(Objects::nonNull).collect(Collectors.toList());
analyzeContent();
}
if (primitive) return;
private void analyzeContent() {
List<PlaceholderReference> placeholdersInConditions = new ArrayList<>();
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
if (manager == null) return; // Header / footer config conversion, the placeholder manager is not initialized yet
placeholdersInConditions.addAll(manager.detectPlaceholders(yes).stream().map(manager::getPlaceholderReference).collect(Collectors.toList()));
placeholdersInConditions.addAll(manager.detectPlaceholders(no).stream().map(manager::getPlaceholderReference).collect(Collectors.toList()));
for (ConditionalExpression expression : expressions) {
if (expression instanceof Permission || expression instanceof NotPermission) {
int permissionRefresh = TAB.getInstance().getConfiguration().getConfig().getPermissionRefreshInterval();
@@ -125,50 +91,11 @@ public class Condition {
placeholdersInConditions.addAll(Arrays.stream(comparator.getRightSide().getPlaceholders()).map(ConditionPlaceholder::getRealPlaceholder).collect(Collectors.toList()));
}
}
}
/**
* Splits string using `|` symbol except cases where it is used as |- or -|.
* This method was 100% made by ChatGPT!
*
* @param input
* String to split
* @return Split string
*/
private List<String> splitString(@NotNull String input) {
List<String> result = new ArrayList<>();
// Define a regular expression pattern to match the desired delimiter
Pattern pattern = Pattern.compile("(?<!-)[|](?!-)");
// Use a Matcher to split the input string
Matcher matcher = pattern.matcher(input);
int start = 0;
while (matcher.find()) {
int end = matcher.start();
result.add(input.substring(start, end));
start = matcher.end();
}
// Add the remaining part of the string
result.add(input.substring(start));
return result;
}
/**
* Configures refresh interval and registers nested placeholders
*/
public void finishSetup() {
for (PlaceholderReference placeholder : placeholdersInConditions) {
if (placeholder.getRefresh() != -1 && (placeholder.getRefresh() < refresh || refresh == -1)) {
refresh = placeholder.getRefresh();
}
}
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
manager.addUsedPlaceholderReferences(placeholdersInConditions);
String identifier = getPlaceholderIdentifier();
String relIdentifier = getRelationalPlaceholderIdentifier();
TabPlaceholder placeholder;
RelationalPlaceholderImpl relationalPlaceholder;
if (hasRelationalContent()) {
relationalPlaceholder = manager.registerRelationalPlaceholder(
relIdentifier,
@@ -193,15 +120,11 @@ public class Condition {
);
}
for (PlaceholderReference reference : placeholdersInConditions) {
TabPlaceholder placeholder = reference.getHandle();
placeholder.addParent(this.placeholder);
reference.addParent(placeholder.getReference());
if (hasRelationalContent()) {
placeholder.addParent(relationalPlaceholder);
reference.addParent(relationalPlaceholder.getReference());
}
this.placeholder.addChild(placeholder);
relationalPlaceholder.addChild(placeholder);
}
TAB.getInstance().getPlaceholderManager().addUsedPlaceholderReferences(placeholdersInConditions);
}
/**
@@ -260,13 +183,14 @@ public class Condition {
*/
@NotNull
public Condition invert() {
return new Condition(new ConditionsSection.ConditionDefinition(
return new Condition(
"inverted:" + name,
expressions.stream().map(expr -> expr.invert().toShortFormat()).collect(Collectors.toList()),
expressions.stream().map(ConditionalExpression::invert).collect(Collectors.toList()),
!type,
yes,
no
));
no,
false
);
}
/**
@@ -312,4 +236,93 @@ public class Condition {
}
return false;
}
/**
* Creates a new condition instance based on definition.
*
* @param definition
* Condition definition from configuration
* @return A new Condition instance based on the provided definition
*/
@NotNull
public static Condition fromDefinition(@NotNull ConditionsSection.ConditionDefinition definition) {
return new Condition(
definition.getName(),
definition.getConditions().stream().map(expressionString -> {
ConditionalExpression expression = ConditionalExpression.compile(expressionString.trim());
if (expression == null) {
TAB.getInstance().getConfigHelper().startup().startupWarn("Line \"" + expressionString + "\" is not a valid conditional expression.");
}
return expression;
}).filter(Objects::nonNull).collect(Collectors.toList()),
definition.isType(),
definition.getYes(),
definition.getNo(),
false
);
}
/**
* Creates a new condition instance from a short format string.
*
* @param shortFormat
* Condition in short format
* @return A new Condition instance based on the provided short format string
*/
@NotNull
public static Condition fromShortFormat(@NotNull String shortFormat) {
boolean type;
List<String> conditions;
if (shortFormat.contains(";")) {
type = true;
conditions = Arrays.asList(shortFormat.split(";"));
} else {
type = false;
conditions = splitString(shortFormat);
}
return new Condition(
"AnonymousCondition[" + shortFormat + "]",
conditions.stream().map(expressionString -> {
ConditionalExpression expression = ConditionalExpression.compile(expressionString.trim());
if (expression == null) {
TAB.getInstance().getConfigHelper().startup().startupWarn("Line \"" + expressionString + "\" is not a valid conditional expression.");
}
return expression;
}).filter(Objects::nonNull).collect(Collectors.toList()),
type,
"true",
"false",
false
);
}
/**
* Splits string using `|` symbol except cases where it is used as |- or -|.
*
* @param input
* String to split
* @return Split string
*/
@NotNull
private static List<String> splitString(@NotNull String input) {
List<String> result = new ArrayList<>();
// Define a regular expression pattern to match the desired delimiter
Pattern pattern = Pattern.compile("(?<!-)[|](?!-)");
// Use a Matcher to split the input string
Matcher matcher = pattern.matcher(input);
int start = 0;
while (matcher.find()) {
int end = matcher.start();
result.add(input.substring(start, end));
start = matcher.end();
}
// Add the remaining part of the string
result.add(input.substring(start));
return result;
}
}
@@ -53,21 +53,9 @@ public class ConditionManager {
} else if (registeredConditions.containsKey(anonVersion)) {
return registeredConditions.get(anonVersion);
} else {
Condition c = new Condition(string);
c.finishSetup();
Condition c = Condition.fromShortFormat(string);
registerCondition(c);
return c;
}
}
/**
* Marks all placeholders used in the condition as used and registers them.
* Using a separate method to avoid premature registration of nested conditional placeholders
* before they are registered properly.
*/
public void finishSetups() {
for (Condition c : registeredConditions.values()) {
c.finishSetup();
}
}
}
@@ -25,17 +25,17 @@ public abstract class ConditionalExpression {
static {
registerConditionType(">=", Operator.GREATER_THAN_OR_EQUAL);
registerConditionType(">", Operator.GREATER_THAN);
registerConditionType("!=", Operator.NOT_EQUALS);
registerConditionType("<=", Operator.LESS_THAN_OR_EQUAL);
registerConditionType("=", Operator.EQUALS);
registerConditionType("!<-", Operator.NOT_CONTAINS);
registerConditionType("!|-", Operator.NOT_STARTS_WITH);
registerConditionType("!-|", Operator.NOT_ENDS_WITH);
registerConditionType("<-", Operator.CONTAINS);
registerConditionType("<", Operator.LESS_THAN);
registerConditionType("-|", Operator.ENDS_WITH);
registerConditionType("|-", Operator.STARTS_WITH);
registerConditionType("!=", Operator.NOT_EQUALS);
registerConditionType("=", Operator.EQUALS);
registerConditionType("<-", Operator.CONTAINS);
registerConditionType("<", Operator.LESS_THAN);
registerConditionType(">", Operator.GREATER_THAN);
conditionTypes.put("!permission:", line -> {
String node = splitAndTrim(line, ":")[1];
return new NotPermission(node);
@@ -14,7 +14,7 @@ public class FalseCondition extends Condition {
public static final FalseCondition INSTANCE = new FalseCondition();
private FalseCondition() {
super("false", Collections.emptyList(), true, "true", "false");
super("false", Collections.emptyList(), true, "true", "false", true);
}
@Override
@@ -14,7 +14,7 @@ public class TrueCondition extends Condition {
public static final TrueCondition INSTANCE = new TrueCondition();
private TrueCondition() {
super("true", Collections.emptyList(), true, "true", "false");
super("true", Collections.emptyList(), true, "true", "false", true);
}
@Override
@@ -23,7 +23,8 @@ import java.util.function.Function;
public class PlayerPlaceholderImpl extends TabPlaceholder implements PlayerPlaceholder {
/** Placeholder function returning fresh output on request */
@NonNull private final Function<me.neznamy.tab.api.TabPlayer, String> function;
@NonNull
private final Function<me.neznamy.tab.api.TabPlayer, String> function;
/**
* Constructs new instance with given parameters
@@ -48,10 +49,10 @@ public class PlayerPlaceholderImpl extends TabPlaceholder implements PlayerPlace
}
@Override
public void updateValue(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable String value) {
if (hasValueChanged((TabPlayer) player, value, true)) {
public void updateValue(@NonNull me.neznamy.tab.api.TabPlayer player, @Nullable String returnedValue) {
if (hasValueChanged((TabPlayer) player, returnedValue, true)) {
if (!player.isLoaded()) return; // Updated on join
for (RefreshableFeature r : getUsedByFeatures()) {
for (RefreshableFeature r : reference.getUsedByFeatures()) {
TimedCaughtTask task = new TimedCaughtTask(TAB.getInstance().getCpu(), () -> r.refresh((TabPlayer) player, false),
r.getFeatureName(), r.getRefreshDisplayName());
if (r instanceof CustomThreaded) {
@@ -77,7 +78,7 @@ public class PlayerPlaceholderImpl extends TabPlaceholder implements PlayerPlace
Set<RefreshableFeature> features = new HashSet<>();
for (Map.Entry<PlayerPlaceholderImpl, String> entry : values.entrySet()) {
if (entry.getKey().hasValueChanged(player, entry.getValue(), true)) {
features.addAll(entry.getKey().getUsedByFeatures());
features.addAll(entry.getKey().getReference().getUsedByFeatures());
}
}
if (!player.isLoaded()) return;
@@ -97,20 +98,21 @@ public class PlayerPlaceholderImpl extends TabPlaceholder implements PlayerPlace
*
* @param p
* Player to update value for
* @param value
* New value
* @param returnedValue
* New value returned by the placeholder function
* @param updateParents
* Whether parents should be updated or not
* @return {@code true} if value changed, {@code false} if not
*/
public boolean hasValueChanged(@NotNull TabPlayer p, @Nullable String value, boolean updateParents) {
if (value == null) return false; //bridge placeholders, they are updated using updateValue method
if (ERROR_VALUE.equals(value)) return false;
String newValue = replacements.findReplacement(setPlaceholders(value, p));
String lastValue = p.lastPlaceholderValues.put(this, newValue);
if (!newValue.equals(lastValue)) {
public boolean hasValueChanged(@NotNull TabPlayer p, @Nullable String returnedValue, boolean updateParents) {
if (returnedValue == null || returnedValue.equals(ERROR_VALUE)) return false;
p.lastPlaceholderReturnedValues.put(this, returnedValue);
String newEvaluatedValue = evaluate(returnedValue, p);
String lastEvaluatedValue = p.lastPlaceholderEvaluatedValues.put(this, newEvaluatedValue);
if (!newEvaluatedValue.equals(lastEvaluatedValue)) {
if (updateParents) updateParents(p);
p.expansionData.setPlaceholderValue(identifier, newValue);
p.expansionData.setPlaceholderValue(identifier, newEvaluatedValue);
return true;
}
return false;
@@ -118,25 +120,40 @@ public class PlayerPlaceholderImpl extends TabPlaceholder implements PlayerPlace
@Override
public void updateFromNested(@NonNull TabPlayer player) {
hasValueChanged(player, request(player), true);
updateValue(player, request(player));
}
@NotNull
public synchronized String getLastValue(@Nullable TabPlayer p) {
if (p == null) return identifier;
String value = p.lastPlaceholderValues.get(this);
String value = p.lastPlaceholderEvaluatedValues.get(this);
if (value != null) return value;
// Value not present, initialize
p.lastPlaceholderValues.put(this, replacements.findReplacement(identifier));
p.lastPlaceholderReturnedValues.put(this, identifier);
p.lastPlaceholderEvaluatedValues.put(this, replacements.findReplacement(identifier));
hasValueChanged(p, request(p), false);
return p.lastPlaceholderValues.get(this);
return p.lastPlaceholderEvaluatedValues.get(this);
}
@Override
@NotNull
public synchronized String getLastReturnedValue(@Nullable TabPlayer player) {
if (player == null) return identifier;
String value = player.lastPlaceholderReturnedValues.get(this);
if (value != null) return value;
// Value not present, initialize
player.lastPlaceholderReturnedValues.put(this, identifier);
player.lastPlaceholderEvaluatedValues.put(this, replacements.findReplacement(identifier));
hasValueChanged(player, request(player), false);
return player.lastPlaceholderReturnedValues.get(this);
}
@Override
@NotNull
public String getLastValueSafe(@NotNull TabPlayer player) {
return player.lastPlaceholderValues.getOrDefault(this, identifier);
return player.lastPlaceholderEvaluatedValues.getOrDefault(this, identifier);
}
/**
@@ -53,7 +53,7 @@ public class RelationalPlaceholderImpl extends TabPlaceholder implements Relatio
@Override
public void updateValue(@NonNull me.neznamy.tab.api.TabPlayer viewer, @NonNull me.neznamy.tab.api.TabPlayer target, @Nullable String value) {
if (hasValueChanged((TabPlayer) viewer, (TabPlayer) target, value)) {
for (RefreshableFeature r : getUsedByFeatures()) {
for (RefreshableFeature r : reference.getUsedByFeatures()) {
TimedCaughtTask task = new TimedCaughtTask(TAB.getInstance().getCpu(), () -> r.refresh((TabPlayer) target, true),
r.getFeatureName(), r.getRefreshDisplayName());
if (r instanceof CustomThreaded) {
@@ -96,7 +96,7 @@ public class RelationalPlaceholderImpl extends TabPlaceholder implements Relatio
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 : getUsedByFeatures()) {
for (RefreshableFeature f : reference.getUsedByFeatures()) {
TimedCaughtTask task = new TimedCaughtTask(TAB.getInstance().getCpu(), () -> f.refresh(target, true),
f.getFeatureName(), f.getRefreshDisplayName());
if (f instanceof CustomThreaded) {
@@ -108,7 +108,7 @@ public class RelationalPlaceholderImpl extends TabPlaceholder implements Relatio
updateParents(target);
}
if (!viewer.isLoaded()) return; // Updated on join
for (RefreshableFeature f : getUsedByFeatures()) {
for (RefreshableFeature f : reference.getUsedByFeatures()) {
TimedCaughtTask task = new TimedCaughtTask(TAB.getInstance().getCpu(), () -> f.refresh(viewer, true),
f.getFeatureName(), f.getRefreshDisplayName());
if (f instanceof CustomThreaded) {
@@ -144,7 +144,7 @@ public class RelationalPlaceholderImpl extends TabPlaceholder implements Relatio
private String retrieveValue(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
String output = request(viewer, target);
if (output == null) output = identifier;
return replacements.findReplacement(output);
return evaluate(output, target);
}
@Override
@@ -152,6 +152,11 @@ public class RelationalPlaceholderImpl extends TabPlaceholder implements Relatio
return identifier;
}
@Override
public @NotNull String getLastReturnedValue(@Nullable TabPlayer player) {
return identifier;
}
@Override
public @NotNull String getLastValueSafe(@NotNull TabPlayer player) {
return identifier;
@@ -20,12 +20,17 @@ import java.util.function.Supplier;
public class ServerPlaceholderImpl extends TabPlaceholder implements ServerPlaceholder {
/** Placeholder function returning fresh output on request */
@NotNull
private final Supplier<String> supplier;
/** Last known output of the placeholder */
@Getter
@NotNull
private String lastValue = identifier;
private String lastReturnedValue = identifier;
/** Last known value with all nested placeholders replaced and output replacements applied */
@NotNull
private String lastEvaluatedValue = identifier;
/**
* Constructs new instance with given parameters
@@ -53,7 +58,7 @@ public class ServerPlaceholderImpl extends TabPlaceholder implements ServerPlace
@Override
public void updateValue(@Nullable String value) {
if (hasValueChanged(value)) {
for (RefreshableFeature r : getUsedByFeatures()) {
for (RefreshableFeature r : reference.getUsedByFeatures()) {
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
if (!all.isLoaded()) return; // Updated on join
TimedCaughtTask task = new TimedCaughtTask(TAB.getInstance().getCpu(), () -> r.refresh(all, false), r.getFeatureName(), r.getRefreshDisplayName());
@@ -67,15 +72,16 @@ public class ServerPlaceholderImpl extends TabPlaceholder implements ServerPlace
}
}
public boolean hasValueChanged(@Nullable String value) {
if (value == null) return false;
String newValue = setPlaceholders(replacements.findReplacement(value), null);
public boolean hasValueChanged(@Nullable String returnedValue) {
if (returnedValue == null || returnedValue.equals(ERROR_VALUE)) return false;
lastReturnedValue = returnedValue;
String newEvaluatedValue = evaluate(returnedValue, null);
if (!ERROR_VALUE.equals(newValue) && !lastValue.equals(newValue)) {
lastValue = newValue;
if (!lastEvaluatedValue.equals(newEvaluatedValue)) {
lastEvaluatedValue = newEvaluatedValue;
for (TabPlayer player : TAB.getInstance().getOnlinePlayers()) {
updateParents(player);
player.expansionData.setPlaceholderValue(identifier, newValue);
player.expansionData.setPlaceholderValue(identifier, newEvaluatedValue);
}
return true;
}
@@ -84,19 +90,25 @@ public class ServerPlaceholderImpl extends TabPlaceholder implements ServerPlace
@Override
public void updateFromNested(@NonNull TabPlayer unused) {
hasValueChanged(request());
updateValue(request());
}
@Override
@NotNull
public String getLastValue(@Nullable TabPlayer p) {
return lastValue;
return lastEvaluatedValue;
}
@Override
@NotNull
public String getLastReturnedValue(@Nullable TabPlayer player) {
return lastReturnedValue;
}
@Override
@NotNull
public String getLastValueSafe(@NotNull TabPlayer player) {
return lastValue;
return lastEvaluatedValue;
}
/**
@@ -2,18 +2,20 @@ package me.neznamy.tab.shared.placeholders.types;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import me.neznamy.tab.api.placeholder.Placeholder;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.features.PlaceholderManagerImpl;
import me.neznamy.tab.shared.features.types.RefreshableFeature;
import me.neznamy.tab.shared.placeholders.PlaceholderIdentifier;
import me.neznamy.tab.shared.placeholders.PlaceholderReference;
import me.neznamy.tab.shared.placeholders.PlaceholderReplacementPattern;
import me.neznamy.tab.shared.platform.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.ArrayList;
import java.util.Map;
/**
* General collection of variables and functions shared between all placeholder types
@@ -36,17 +38,8 @@ public abstract class TabPlaceholder implements Placeholder {
/** Configured placeholder output replacements */
@NonNull protected final PlaceholderReplacementPattern replacements;
/**
* List of placeholders using this placeholder as a nested placeholder,
* mutual tracking allows faster parent placeholder changes when a nested
* placeholder changed value.
*/
protected final List<TabPlaceholder> parents = new ArrayList<>();
private final List<TabPlaceholder> children = new ArrayList<>();
/** Set of features using this placeholder, used to call refresh on them */
private final Set<RefreshableFeature> usedByFeatures = Collections.synchronizedSet(new HashSet<>());
@Setter
protected PlaceholderReference reference;
/**
* Constructs new instance with given parameters and loads placeholder output replacements
@@ -66,14 +59,11 @@ public abstract class TabPlaceholder implements Placeholder {
this.refresh = refresh;
Map<Object, Object> map = TAB.getInstance().getConfiguration().getConfig().getReplacements().getValues().get(identifier);
replacements = map == null ? PlaceholderReplacementPattern.EMPTY : PlaceholderReplacementPattern.create(identifier, map);
for (String nested : replacements.getNestedPlaceholders()) {
TAB.getInstance().getPlaceholderManager().getPlaceholder(nested).addParent(this);
}
}
/**
* Parses the placeholder for defined player, applying all nested placeholders
* found in output.
* found in returned placeholder output as well as output replacements.
*
* @param player
* player to parse placeholder for
@@ -81,9 +71,29 @@ public abstract class TabPlaceholder implements Placeholder {
*/
@NotNull
public String parse(@Nullable TabPlayer player) {
String value = getLastValue(player);
if (!value.contains("%") && !value.contains("<")) return value; // No nested placeholders, return immediately
return setPlaceholders(value, player);
return evaluate(getLastReturnedValue(player), player);
}
/**
* Evaluates given placeholder result string by applying all nested placeholders and output replacements for defined player.
*
* @param returnedValue
* placeholder result string to evaluate
* @param player
* player to parse placeholders for
* @return evaluated string with all nested placeholders and output replacements applied
*/
@NotNull
protected String evaluate(@NonNull String returnedValue, @Nullable TabPlayer player) {
String value = returnedValue;
if (value.contains("%") || value.contains("<")) {
value = setPlaceholders(value, player);
}
value = replacements.findReplacement(value);
if (value.contains("%") || value.contains("<")) {
value = setPlaceholders(value, player);
}
return value;
}
/**
@@ -100,40 +110,17 @@ public abstract class TabPlaceholder implements Placeholder {
String string = value;
if (identifier.equals(string)) return string; // Placeholder returned itself (probably invalid)
// Known children first for better performance thanks to skipped checks
for (TabPlaceholder child : children) {
if (string.contains(child.identifier)) {
string = string.replace(child.identifier, child.parse(player));
}
}
for (String s : PlaceholderManagerImpl.detectPlaceholders(string)) {
if (s.equals(identifier)) continue; // Prevent infinite loop when placeholder returns itself
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
TabPlaceholder nested = TAB.getInstance().getPlaceholderManager().getPlaceholder(s);
nested.addParent(this);
addChild(nested);
string = string.replace(s, nested.parse(player));
PlaceholderReference nested = TAB.getInstance().getPlaceholderManager().getPlaceholderReference(s);
nested.addParent(reference);
string = string.replace(s, nested.getHandle().parse(player));
}
return string;
}
/**
* Internal method used to mark placeholders as parents who use this placeholder
* inside their outputs for faster updates.
*
* @param parent
* parent placeholder using this placeholder in output
*/
public void addParent(@NonNull TabPlaceholder parent) {
if (parent == this) return; // ???
if (!parents.contains(parent)) {
usedByFeatures.addAll(parent.usedByFeatures);
parents.add(parent);
}
}
/**
* Updates all placeholders that use this placeholder
* as a nested placeholder
@@ -142,37 +129,11 @@ public abstract class TabPlaceholder implements Placeholder {
* Player to update placeholders for.
*/
public void updateParents(@NonNull TabPlayer player) {
if (parents.isEmpty()) return;
for (TabPlaceholder pl : new ArrayList<>(parents)) {
pl.updateFromNested(player);
pl.updateParents(player);
}
}
/**
* Registers a feature that is using this placeholder, to allow
* refreshing it when this placeholder changes.
*
* @param feature
* Feature using this placeholder
* @return true if feature was not already registered
*/
public boolean addUsedFeature(@NonNull RefreshableFeature feature) {
boolean added = usedByFeatures.add(feature);
if (added) {
for (TabPlaceholder child : children) {
child.addUsedFeature(feature);
}
}
return added;
}
public void addChild(@NonNull TabPlaceholder child) {
if (!children.contains(child)) {
children.add(child);
for (RefreshableFeature feature : usedByFeatures) {
child.addUsedFeature(feature);
}
if (reference == null) return;
if (reference.getParents().isEmpty()) return;
for (PlaceholderReference pl : new ArrayList<>(reference.getParents())) {
pl.getHandle().updateFromNested(player);
pl.getHandle().updateParents(player);
}
}
@@ -193,6 +154,17 @@ public abstract class TabPlaceholder implements Placeholder {
*/
public abstract @NotNull String getLastValue(@Nullable TabPlayer player);
/**
* Returns the last returned value of the placeholder for the given player, without applying any replacements or nested placeholders.
* This is the last known raw result of the given placeholder function.
*
* @param player
* player to get value of
* @return Last known raw value for specified player
*/
@NotNull
public abstract String getLastReturnedValue(@Nullable TabPlayer player);
/**
* Returns last known value of defined player without refreshing it if not present.
*
@@ -132,8 +132,11 @@ public abstract class TabPlayer implements me.neznamy.tab.api.TabPlayer {
/** LuckPerms user for fast access */
@Nullable public User luckPermsUser;
/** Last known values for each player placeholder as returned by function */
public final Map<PlayerPlaceholder, String> lastPlaceholderReturnedValues = new ConcurrentHashMap<>();
/** Last known values for each player placeholder after applying replacements and nested placeholders */
public final Map<PlayerPlaceholder, String> lastPlaceholderValues = new ConcurrentHashMap<>();
public final Map<PlayerPlaceholder, String> lastPlaceholderEvaluatedValues = new ConcurrentHashMap<>();
/** Last known values for each relational placeholder after applying replacements and nested placeholders */
public final Map<RelationalPlaceholder, Map<TabPlayer, String>> lastRelationalValues = new ConcurrentHashMap<>();
@@ -324,11 +324,11 @@ public abstract class SafeScoreboard<T extends TabPlayer> implements Scoreboard
* Team name in the packet
* @param players
* Players in the packet
* @return Modified collection of players
* @return Modified list of players
*/
@NotNull
public Collection<String> onTeamPacket(int action, @NonNull String teamName, @NonNull Collection<String> players) {
Collection<String> newList = new ArrayList<>();
public List<String> onTeamPacket(int action, @NonNull String teamName, @NonNull Collection<String> players) {
List<String> newList = new ArrayList<>();
if (action == TeamAction.CREATE || action == TeamAction.ADD_PLAYER) {
for (String entry : players) {
Team expectedTeam = getExpectedTeam(entry);
@@ -22,6 +22,7 @@ import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.chat.component.TabTextComponent;
import me.neznamy.tab.shared.features.injection.PipelineInjector;
import me.neznamy.tab.shared.features.proxy.ProxySupport;
import me.neznamy.tab.shared.features.scoreboard.ScoreboardManagerImpl;
import me.neznamy.tab.shared.placeholders.expansion.EmptyTabExpansion;
import me.neznamy.tab.shared.placeholders.expansion.TabExpansion;
import me.neznamy.tab.shared.platform.BossBar;
@@ -147,18 +148,22 @@ public class VelocityPlatform extends ProxyPlatform {
plugin.getServer().getEventManager().register(plugin, ObjectiveEvent.Display.class, e -> {
TAB tab = TAB.getInstance();
if (tab.isPluginDisabled()) return;
tab.getCPUManager().runTask(() -> {
TabPlayer player = tab.getPlayer(e.getPlayer().getUniqueId());
if (player != null) tab.getFeatureManager().onDisplayObjective(player, e.getNewSlot().ordinal(), e.getObjectiveName());
});
TabPlayer player = tab.getPlayer(e.getPlayer().getUniqueId());
if (player == null) return;
ScoreboardManagerImpl scoreboard = tab.getFeatureManager().getFeature(TabConstants.Feature.SCOREBOARD);
if (scoreboard != null) {
scoreboard.onVelocityScoreboardDisplay(player, e.getNewSlot().ordinal(), e.getObjectiveName());
}
});
plugin.getServer().getEventManager().register(plugin, ObjectiveEvent.Unregister.class, e -> {
TAB tab = TAB.getInstance();
if (tab.isPluginDisabled()) return;
tab.getCPUManager().runTask(() -> {
TabPlayer player = tab.getPlayer(e.getPlayer().getUniqueId());
if (player != null) tab.getFeatureManager().onObjective(player, Scoreboard.ObjectiveAction.UNREGISTER, e.getObjectiveName());
});
TabPlayer player = tab.getPlayer(e.getPlayer().getUniqueId());
if (player == null) return;
ScoreboardManagerImpl scoreboard = tab.getFeatureManager().getFeature(TabConstants.Feature.SCOREBOARD);
if (scoreboard != null) {
scoreboard.onVelocityScoreboardUnregister(player, e.getObjectiveName());
}
});
}
@@ -103,7 +103,7 @@ public class VelocityTabExpansion implements TabExpansion {
if (tabPlayer == null) {
return Tag.selfClosingInserting(Component.text("<Player is not loaded>"));
}
return createTag(pm.getPlaceholder(requestedPlaceholder).parse(tabPlayer));
return createTag(pm.getPlaceholderReference(requestedPlaceholder).getHandle().parse(tabPlayer));
});
builder.relationalPlaceholder("replace", (viewer, target, queue, ctx) -> {