Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6359d2f77 | ||
|
|
727c500b17 | ||
|
|
bebf2988fb | ||
|
|
e3cde1968b | ||
|
|
ef6b668c70 | ||
|
|
fef62e796b | ||
|
|
0d4b1dbce2 | ||
|
|
43d210dbf8 | ||
|
|
e0d2209e97 | ||
|
|
720cae3e94 | ||
|
|
f2ea34a717 | ||
|
|
2c33747e1b | ||
|
|
5f9d72eb90 | ||
|
|
3abcc1d96f | ||
|
|
7d458422c7 | ||
|
|
0feee14bfb | ||
|
|
0023cfcafb | ||
|
|
08f0d7f635 | ||
|
|
73a5b14084 | ||
|
|
da8befca86 | ||
|
|
33cc64a441 | ||
|
|
a13fb02c75 | ||
|
|
c9d9a8f04a | ||
|
|
5de6c850ed | ||
|
|
d98044c51c | ||
|
|
0575bf0239 | ||
|
|
992c96dd52 | ||
|
|
4d969ab320 | ||
|
|
283713ff8a | ||
|
|
231aa89686 | ||
|
|
d67372b72f | ||
|
|
93046154a9 | ||
|
|
2966f57d42 | ||
|
|
951cb260c5 | ||
|
|
2256e9ef77 | ||
|
|
188e0ea932 | ||
|
|
6e6848e007 | ||
|
|
fc83f0fec6 | ||
|
|
034a4b6b7c | ||
|
|
30593c1bfd | ||
|
|
89e69591e0 | ||
|
|
147279a8ca | ||
|
|
517ac4403f | ||
|
|
b4e88e570f | ||
|
|
9b5385ed61 | ||
|
|
284e3e91f3 | ||
|
|
0fb6519617 | ||
|
|
e2452f2780 | ||
|
|
f1c763861b | ||
|
|
ea99736142 | ||
|
|
21d661bd22 | ||
|
|
fc0666fe46 | ||
|
|
869c6191b6 | ||
|
|
a53f41336d | ||
|
|
ff4e82d2cc | ||
|
|
2de7e3bd7e | ||
|
|
c72e0c5108 | ||
|
|
11814e0fd2 | ||
|
|
d41d2e74bd | ||
|
|
122b54caee | ||
|
|
3a67785be6 | ||
|
|
01e2457611 | ||
|
|
dd13d244c0 | ||
|
|
fba8983f46 | ||
|
|
4a2af1f37a | ||
|
|
2b82c45176 | ||
|
|
3203c64287 | ||
|
|
1683e1faf6 | ||
|
|
eb9de8cc3f | ||
|
|
df6a8281a6 | ||
|
|
05568296d9 | ||
|
|
c024b28a45 | ||
|
|
0b0c457cdc | ||
|
|
001e26cbb0 | ||
|
|
3d75520863 | ||
|
|
63bc73fd98 | ||
|
|
b47e7d2d3b | ||
|
|
cf7b0eed28 | ||
|
|
b3b0652222 | ||
|
|
8e78e4e9ce | ||
|
|
f9d60a224a | ||
|
|
396678d996 | ||
|
|
fae2d7a532 | ||
|
|
73ab71d7c0 | ||
|
|
ec6eb51515 | ||
|
|
02a9f25148 | ||
|
|
ea4c16a162 | ||
|
|
c7e8c891e2 |
No files matched your search
@@ -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 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="Documentation: Wiki"><title>Documentation: Wiki</title><filter id="blur"><feGaussianBlur in="SourceGraphic" 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="126" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="93" height="20" fill="#555"/><rect x="93" width="33" height="20" fill="#d8b800"/><rect width="126" 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"><text aria-hidden="true" x="475" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="830">Documentation</text><text aria-hidden="true" x="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="830">Documentation</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="830">Documentation</text><text aria-hidden="true" x="1085" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="230">Wiki</text><text aria-hidden="true" x="1085" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="230">Wiki</text><text x="1085" y="140" transform="scale(.1)" fill="#fff" textLength="230">Wiki</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="Documentation: Wiki"><title>Documentation: Wiki</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="126" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="93" height="20" fill="#555"/><rect x="93" width="33" height="20" fill="#d8b800"/><rect width="126" 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="475" y="150" fill-opacity=".8" filter="url(#blur)" textLength="830">Documentation</text><text x="475" y="150" fill-opacity=".3" textLength="830">Documentation</text></g><text x="475" y="140" textLength="830">Documentation</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="1085" y="150" fill-opacity=".8" filter="url(#blur)" textLength="230">Wiki</text><text x="1085" y="150" fill-opacity=".3" textLength="230">Wiki</text></g><text x="1085" y="140" textLength="230">Wiki</text></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="20" role="img" aria-label="Java: 8+"><title>Java: 8+</title><filter id="blur"><feGaussianBlur in="SourceGraphic" 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="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="35" height="20" fill="#555"/><rect x="35" width="25" height="20" fill="#007ec6"/><rect width="60" 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"><text aria-hidden="true" x="185" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="250">Java</text><text aria-hidden="true" x="185" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="250">Java</text><text x="185" y="140" transform="scale(.1)" fill="#fff" textLength="250">Java</text><text aria-hidden="true" x="465" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="150">8+</text><text aria-hidden="true" x="465" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="150">8+</text><text x="465" y="140" transform="scale(.1)" fill="#fff" textLength="150">8+</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="20" role="img" aria-label="Java: 8+"><title>Java: 8+</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="60" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="35" height="20" fill="#555"/><rect x="35" width="25" height="20" fill="#007ec6"/><rect width="60" 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="185" y="150" fill-opacity=".8" filter="url(#blur)" textLength="250">Java</text><text x="185" y="150" fill-opacity=".3" textLength="250">Java</text></g><text x="185" y="140" textLength="250">Java</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="465" y="150" fill-opacity=".8" filter="url(#blur)" textLength="150">8+</text><text x="465" y="150" fill-opacity=".3" textLength="150">8+</text></g><text x="465" y="140" textLength="150">8+</text></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20" role="img" aria-label="Release: 6.0.3"><title>Release: 6.0.3</title><filter id="blur"><feGaussianBlur in="SourceGraphic" 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" fill="#fff"/></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"><text aria-hidden="true" x="275" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="430">Release</text><text aria-hidden="true" x="275" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">Release</text><text x="275" y="140" transform="scale(.1)" fill="#fff" textLength="430">Release</text><text aria-hidden="true" x="715" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="290">6.0.3</text><text aria-hidden="true" x="715" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="290">6.0.3</text><text x="715" y="140" transform="scale(.1)" fill="#fff" textLength="290">6.0.3</text></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.4 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="124" height="20" role="img" aria-label="GitHub: Source code"><title>GitHub: Source code</title><filter id="blur"><feGaussianBlur in="SourceGraphic" 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="124" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="47" height="20" fill="#555"/><rect x="47" width="77" height="20" fill="#d8b800"/><rect width="124" 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"><text aria-hidden="true" x="245" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="370">GitHub</text><text aria-hidden="true" x="245" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="370">GitHub</text><text x="245" y="140" transform="scale(.1)" fill="#fff" textLength="370">GitHub</text><text aria-hidden="true" x="845" y="150" fill="#010101" fill-opacity=".80" filter="url(#blur)" transform="scale(.1)" textLength="670">Source code</text><text aria-hidden="true" x="845" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="670">Source code</text><text x="845" y="140" transform="scale(.1)" fill="#fff" textLength="670">Source code</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="124" height="20" role="img" aria-label="GitHub: Source code"><title>GitHub: Source code</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="124" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="47" height="20" fill="#555"/><rect x="47" width="77" height="20" fill="#d8b800"/><rect width="124" 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="245" y="150" fill-opacity=".8" filter="url(#blur)" textLength="370">GitHub</text><text x="245" y="150" fill-opacity=".3" textLength="370">GitHub</text></g><text x="245" y="140" textLength="370">GitHub</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="845" y="150" fill-opacity=".8" filter="url(#blur)" textLength="670">Source code</text><text x="845" y="150" fill-opacity=".3" textLength="670">Source code</text></g><text x="845" y="140" textLength="670">Source code</text></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -274,7 +274,7 @@ if __name__ == "__main__":
|
||||
]
|
||||
)
|
||||
|
||||
release = "6.0.3"
|
||||
release = "6.1.3"
|
||||
download_image(f"https://img.shields.io/badge/Release-{release}-blue.svg", "Badge_Release.svg")
|
||||
|
||||
java = "8+"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -7,22 +7,16 @@
|
||||
* [Compatibility issues](#compatibility-issues)
|
||||
|
||||
# Requirements
|
||||
TAB does not depend on any other plugins. The experience can however be enhanced by installing other plugins, see below.
|
||||
|
||||
## Java
|
||||
TAB's shared module is compiled with Java 8. For the platforms themselves, TAB does have a higher requirement, however, it does not require a Java version higher than the server software itself does. Specifically:
|
||||
* Fabric and NeoForge require Java 25
|
||||
* Velocity requires Java 17
|
||||
* Bukkit and BungeeCord are ok with Java 8
|
||||
TAB does not depend on any other plugins. The experience can however be enhanced by installing other plugins, see below. There is also no additional Java version requirement. If your server runs on your Java version, TAB will too.
|
||||
|
||||
# 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.1.2. When new versions come out, TAB is always updated soon after to support them.
|
||||
Currently, the latest Minecraft version supported by TAB is **26.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.
|
||||
|
||||
@@ -44,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.
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
* [Compatibility with other plugins](#compatibility-with-other-plugins)
|
||||
* [Additional info](#additional-info)
|
||||
* [Additional note 1 - Copying nametag visibility rule](#additional-note-1---copying-nametag-visibility-rule)
|
||||
* [Additional note 2 - Hidden on sneak on 1.8](#additional-note-2---hidden-on-sneak-on-18)
|
||||
* [Additional note 3 - Visible on NPCs](#additional-note-3---visible-on-npcs)
|
||||
* [Additional note 2 - [1.8.x] Hidden on sneak](#additional-note-2---18x-hidden-on-sneak)
|
||||
* [Additional note 3 - [26.1.2-] Visible on NPCs](#additional-note-3---2612--visible-on-npcs)
|
||||
* [[26.1.x] Visible on all entities](#261x-visible-on-all-entities)
|
||||
* [Additional note 4 - Compatibility with modified clients](#additional-note-4---compatibility-with-modified-clients)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
@@ -18,13 +18,14 @@
|
||||
* [Example 2 - Hiding `title` for 1.20.3+ players](#example-2---hiding-title-for-1203-players)
|
||||
* [Example 3 - Displaying health as 0-10 or in %](#example-3---displaying-health-as-0-10-or-in-)
|
||||
* [Example 4 - Health bar using hearts](#example-4---health-bar-using-hearts)
|
||||
* [Example 5 - Conditionally hiding belowname for 26.2+ viewers](#example-5---conditionally-hiding-belowname-for-262-viewers)
|
||||
* [Tips & Tricks](#tips--tricks)
|
||||
* [Tip 1 - Heart symbol](#tip-1---heart-symbol)
|
||||
|
||||
# About
|
||||
This features gives you control over Minecraft's scoreboard objective feature with BELOW_NAME slot.
|
||||
This line is displayed below the nametags of all player entities in game. It is not possible to explicitly disable this feature for NPCs; however, this goal can be achieved (see below for more info).
|
||||
It is only visible when within an 8-block range of the player (the range is hardcoded in the client and cannot be changed with a plugin).
|
||||
This features gives you control over Minecraft's scoreboard objective feature with BELOW_NAME slot.
|
||||
On Minecraft 1.21.11 and below, it is visible on all player entities (including NPCs). On 26.1.x, it is visible on all player entities and all named entities (such as holograms), making this feature unusable. On 26.2 and above, it only appears on entities with a score assigned (the ideal implementation).
|
||||
It is only visible when the target player is within a 10-block range of the viewer (the value is configurable since Minecraft 26.1).
|
||||
|
||||
Example visual effect:
|
||||

|
||||
@@ -54,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`. |
|
||||
| fancy-value-default | NPC | [1.20.3+] 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 1.20.3+. |
|
||||
| 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).
|
||||
@@ -82,10 +84,10 @@ Note that if this was automatically generated by a plugin, it will probably be a
|
||||
The Minecraft feature is programmed to be affected by nametag visibility rule.
|
||||
This means that when the nametag is set to invisible, belowname will be invisible as well.
|
||||
|
||||
## Additional note 2 - Hidden on sneak on 1.8
|
||||
## Additional note 2 - [1.8.x] Hidden on sneak
|
||||
Belowname is not visible on 1.8.x clients when player is sneaking. This is client-sided behavior and cannot be changed by the server.
|
||||
|
||||
## Additional note 3 - Visible on NPCs
|
||||
## Additional note 3 - [26.1.2-] Visible on NPCs
|
||||
Belowname objective is automatically attached to all entities of player type with the default value of
|
||||
`0` (<1.20.3) or value configured as `fancy-value-default` (1.20.3+).
|
||||
This includes player NPCs.
|
||||
@@ -240,4 +242,24 @@ belowname-objective:
|
||||
title: "&4❤"
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
> Make sure your config is [saved in UTF-8 encoding](https://github.com/NEZNAMY/TAB/wiki/How-to-save-the-config-in-UTF8-encoding) to properly read the heart symbol.
|
||||
> Make sure your config is [saved in UTF-8 encoding](https://github.com/NEZNAMY/TAB/wiki/How-to-save-the-config-in-UTF8-encoding) to properly read the heart symbol.
|
||||
|
||||
## Example 5 - Conditionally hiding belowname for 26.2+ viewers
|
||||
Since Minecraft 26.2, belowname only appears on entities with a score. This can be used to disable the belowname on players with a condition. We are trying to achieve a condition like this: "If something, hide belowname for 26.2+ viewers".
|
||||
TAB is made to reset score (= hide belowname) of target players for 26.2+ viewers (<26.2 viewers are not affected, resetting scores for them would result in `fancy-value-default` to be shown, which is undesirable) if `fancy-value` evaluated to an empty string. Therefore, we will need to make a [relational condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders#relational-conditions) that checks the viewer's version and returns empty string if some condition is met.
|
||||
Here is an example of hiding it if target player has less than 10 health (5 hearts):
|
||||
```
|
||||
conditions:
|
||||
belowname-health:
|
||||
conditions:
|
||||
- "%viewer:player-version-id%>=776" # 26.2 uses 776, check for viewer's version, not target player's
|
||||
- "%health%<10" # This should check target player's health
|
||||
type: AND
|
||||
true: "" # Viewer is on 26.2+ and target has less than 10 health, return empty string to hide it
|
||||
false: "%health%" # Display health as normal
|
||||
```
|
||||
Then, use this condition as a relational condition (because it checks both viewer and target players):
|
||||
```
|
||||
belowname-objective:
|
||||
fancy-value: "%rel_condition:belowname-health%"
|
||||
```
|
||||
@@ -10,6 +10,8 @@
|
||||
* [Using in a field where condition is expected (short format)](#using-in-a-field-where-condition-is-expected-short-format)
|
||||
* [Relational conditions](#relational-conditions)
|
||||
* [Refresh interval](#refresh-interval)
|
||||
* [Additional info](#additional-info)
|
||||
* [Additional note 1 - Choosing the correct AND / OR type](#additional-note-1---choosing-the-correct-and--or-type)
|
||||
* [Examples](#examples)
|
||||
* [Example 1 - Chaining conditional placeholders](#example-1---chaining-conditional-placeholders)
|
||||
* [Example 2 - Combining AND and OR](#example-2---combining-and-and-or)
|
||||
@@ -52,6 +54,9 @@ condition must contain the altered output.
|
||||
To see the exact output of a placeholder including color codes, use `/tab parse <player> <placeholder>`.
|
||||
|
||||
## Permission
|
||||
> [!WARNING]
|
||||
> Giving someone OP status will automatically give them all permissions, which will make all the permission checks in conditions pass.
|
||||
|
||||
| Operation | Description | Example |
|
||||
|-----------------------|---------------------------------|----------------------------------------------------------------------------------------------|
|
||||
| `permission:<value>` | Permission requirement | `permission:my.permission` will pass if player has `my.permission` permission |
|
||||
@@ -146,8 +151,9 @@ There are 2 ways to use it:
|
||||
* display-condition of layout player groups (which is currently the only feature supporting it as it would not make sense elsewhere)
|
||||
* A placeholder using `%rel_condition:<condition>%` syntax
|
||||
|
||||
If trying to use relational condition in `%condition:name%` syntax, it will return a warning text instead of evaluating the condition.
|
||||
Same the other way around, if trying to use standard condition in `%rel_condition:name%` syntax, it will return a warning text.
|
||||
Using `%viewer:...%` will upgrade the condition to a relational condition and standard version will no longer be available.
|
||||
If trying to use relational condition in `%condition:name%` syntax, it will return a warning text instead of evaluating the condition.
|
||||
Same the other way around - if trying to use standard condition in `%rel_condition:name%` syntax, it will return a warning text.
|
||||
|
||||
# Refresh interval
|
||||
Conditions are just placeholders after all, and, as such, they must be refreshed periodically.
|
||||
@@ -157,6 +163,13 @@ Permission checks count as 1000ms.
|
||||
To configure refresh intervals of placeholders,
|
||||
check out the [Optimization guide](https://github.com/NEZNAMY/TAB/wiki/Optimizing-the-plugin#2---all-platforms-placeholder-refresh-intervals).
|
||||
|
||||
# Additional Info
|
||||
## Additional note 1 - Choosing the correct AND / OR type
|
||||
Sometimes you want to combine multiple expressions using `AND` or `OR`. When doing this, you must pick the correct one based on your needs.
|
||||
A common mistake is trying to turn spoken language directly into config, which can be wrong.
|
||||
|
||||
For example, you say you want to display scoreboard in servers `server1` **and** `server2`. As such, you may accidentally use `type: AND` or `;` in short format (`%server%=server1;%server%=server2`). This is, however, wrong, because that would mean your server must be `server1` **and** `server2` at the same time, which is obviously impossible. The correct type is `OR` here (`|`). So instead of thinking "I want to display scoreboard in `server1` **and** `server2`", you should think "I want to display scoreboard if player is in `server1` **or** `server2`.
|
||||
|
||||
# Examples
|
||||
## Example 1 - Chaining conditional placeholders
|
||||
Let's create colored ping by chaining conditions using multiple intervals for ping quality. This specific case would be easier done using [replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements), but this is just an example. Let's say we want <50 for good, <100 for medium and 100+ for bad. Let's create the first condition:
|
||||
|
||||
@@ -140,9 +140,6 @@ Here are TAB's internal placeholders you can use when this feature is enabled:
|
||||
For compatibility with vanish plugins, vanish status must be detected correctly. See [Additional information - Vanish detection](https://github.com/NEZNAMY/TAB/wiki/Additional-information#vanish-detection) for more info.
|
||||
|
||||
# Troubleshooting
|
||||
> [!NOTE]
|
||||
> This functionality is coming in the next TAB update (6.0.3 / 6.1.0). Adding it here to prepare the wiki in advance (and for users using dev builds).
|
||||
|
||||
This is a collection of tips to help you figure out why the feature isn't working as you expect.
|
||||
To get started, run `/tab dump <player looking at the tablist>` and open the generated link. Scroll down to `features` -> `GlobalPlayerList` and check the content:
|
||||
* If it says `GlobalPlayerList: Feature is disabled`, it means you did not enable the feature. Enable it by setting
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* [About](#about)
|
||||
* [Configuration](#configuration)
|
||||
* [Grouping playerlist from multiple worlds](#grouping-playerlist-from-multiple-worlds)
|
||||
* [Pattern Matching](#pattern-matching)
|
||||
* ~Commands~
|
||||
* ~Placeholders~
|
||||
* ~Limitations~
|
||||
@@ -40,4 +41,34 @@ Contains 2 groups, each of them consisting of 2 worlds.
|
||||
World group name can be anything (in our case, they're called "lobby" and "minigames").
|
||||
Under group name, list the actual world names.
|
||||
|
||||
With this setup, "lobby1" and "lobby2" will share playerlist, as well as "paintball" with "bedwars". All other worlds will have playerlist not shared with any other worlds. Creating a group for 1 world only is completely useless.
|
||||
With this setup, "lobby1" and "lobby2" will share playerlist, as well as "paintball" with "bedwars". All other worlds will have playerlist not shared with any other worlds. Creating a group for 1 world only is completely useless.
|
||||
|
||||
### Pattern Matching
|
||||
World names in groups support multiple pattern types for flexible configuration:
|
||||
|
||||
**Exact Match** - Matches server name exactly:
|
||||
```yaml
|
||||
shared-playerlist-world-groups:
|
||||
worlds:
|
||||
- world1
|
||||
- world2
|
||||
```
|
||||
|
||||
**Wildcard Patterns** - Use `*` for prefix or suffix matching:
|
||||
```yaml
|
||||
shared-playerlist-world-groups:
|
||||
worlds:
|
||||
- "World-*" # Matches: World-1, World-2, World-Hub, etc.
|
||||
- "*-world" # Matches: eu-world, us-world, etc.
|
||||
```
|
||||
|
||||
**Regex Patterns** - Use `regex:` prefix for advanced pattern matching:
|
||||
```yaml
|
||||
shared-playerlist-world-groups:
|
||||
worlds:
|
||||
- regex:World-[0-9]+ # Matches: World-1, World-23, but not World-Hub
|
||||
smp:
|
||||
- regex:SMP-(EU|US)-[0-9]+ # Matches: SMP-EU-1, SMP-US-2, etc.
|
||||
```
|
||||
|
||||
Note: Wildcard patterns are case-insensitive, while regex patterns are case-sensitive by default (use `(?i)` flag for case-insensitive regex).
|
||||
@@ -78,11 +78,8 @@ All of the options are explained in the following table.
|
||||
| toggle-command | /sb | Command that can be used to toggle scoreboard for the player running the command. Players need `tab.scoreboard.toggle` permission to use it. <br />**Note:** Commands are not supposed to be registered / unregistered dynamically at runtime. As such, you may run into small issues when changing the toggle command and reloading. After you modify the command to your liking, reconnect to properly see it in tabcomplete. If you don't use TAB on a proxy, consider restarting the server as well for the old command to actually be unregistered. |
|
||||
| remember-toggle-choice | false | When enabled, toggling decision is saved into a file to remember it even after reloads/restarts/reconnects |
|
||||
| hidden-by-default | false | If enabled, scoreboard will automatically be hidden on join until toggle command is used to show it. |
|
||||
| use-numbers | false | If enabled, numbers 1-15 will be used in the scoreboard. If disabled, `static-number` is shown everywhere. Will not be visible for 1.20.3+ players, instead, you can configure any text to show using `\|\|text` in the lines (scroll up for more info). |
|
||||
| static-number | 0 | If `use-numbers` is disabled, this is number to be in all lines. |
|
||||
| delay-on-join-milliseconds | 0 | Delay in milliseconds to send scoreboard after joining. |
|
||||
| scoreboards | *Map* | Scoreboards to display based on conditions (see below for more info).
|
||||
|
|
||||
| scoreboards | *Map* | Scoreboards to display based on conditions (see below for more info). |
|
||||
|
||||
## Scoreboard
|
||||
A scoreboard consists of the following options:
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
A collection of things people have requested that I might look into implementing when I get extremely bored sometime next decade.
|
||||
* Relational placeholder support in sorting
|
||||
* 1.21.2+ sorting support
|
||||
* disable-condition support in multi-server-support
|
||||
@@ -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,5 +1,5 @@
|
||||
# About the wiki
|
||||
The wiki explains the functionality of the plugin as of version **6.0.2**.
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -173,10 +164,12 @@ See table below for behavior based on your setup.
|
||||
# MiniMessage
|
||||
TAB has [MiniMessage](https://docs.advntr.dev/minimessage/format.html) hook, however, it does not include this library on its own. Including it would cause conflicts, so it has to be included in the server software.
|
||||
* It **is** included in **Paper 1.18.2+** and **Velocity**.
|
||||
* It **is not** included in **Spigot**, any server software for **1.18.1-**, **BungeeCord**, **Fabric**, **Forge** and **NeoForge**.
|
||||
* It **is not** included in **Spigot**, any server software for **1.18.1-**, **BungeeCord**, **Fabric**, **Forge** and **NeoForge**. However, you can install [adventure-platform-mod](https://modrinth.com/mod/adventure-platform-mod) on Fabric / NeoForge.
|
||||
|
||||
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.
|
||||
|
||||
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 |
|
||||
|--------------------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|
||||
@@ -27,7 +27,7 @@ Install [TAB-Bridge](https://github.com/NEZNAMY/TAB/wiki/TAB-Bridge) on your **b
|
||||
If you are using **Velocity**,
|
||||
install [VelocityScoreboardAPI](https://github.com/NEZNAMY/VelocityScoreboardAPI/) plugin **on the proxy** for scoreboard-related features to work.
|
||||
|
||||
> [!NOTE]
|
||||
> [!NOTE]
|
||||
> With proxy installation, use **/btab** command instead of **/tab**.
|
||||
|
||||
Advantages of proxy installation compared to installing on all backend servers instead:
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
# Content
|
||||
* [About](#about)
|
||||
* [Ways of optimizing](#ways-of-optimizing)
|
||||
* [#1 - [All platforms] Static text instead of placeholders](#1---all-platforms-static-text-instead-of-placeholders)
|
||||
* [#2 - [All platforms] Placeholder refresh intervals](#2---all-platforms-placeholder-refresh-intervals)
|
||||
* [#3 - [All platforms] Don't use RGB](#3---all-platforms-dont-use-rgb)
|
||||
* [#4 - [All platforms] Don't use animations](#4---all-platforms-dont-use-animations)
|
||||
* [#5 - [All platforms] Disable tablist name formatting](#5---all-platforms-disable-tablist-name-formatting)
|
||||
* [#6 - [All platforms] Increase permission refresh interval](#6---all-platforms-increase-permission-refresh-interval)
|
||||
* [#7 - [Proxies] Disable TAB expansion](#7---proxies-disable-tab-expansion)
|
||||
* [#8 - [Proxies] Disable update-latency option in global playerlist](#8---proxies-disable-update-latency-option-in-global-playerlist)
|
||||
* [Netty false positive in spark reports](#netty-false-positive-in-spark-reports)
|
||||
* [Why there is a false positive](#why-there-is-a-false-positive)
|
||||
* [How to read the real value](#how-to-read-the-real-value)
|
||||
* [Packet listening usage](#packet-listening-usage)
|
||||
* [Sending packets](#sending-packets)
|
||||
|
||||
# About
|
||||
This guide will explain all primary ways you can reduce plugin's CPU usage besides the obvious things like "don't spam animations" or "disable features you don't want".
|
||||
The usage can be checked at any time using [**/tab cpu**](https://github.com/NEZNAMY/TAB/wiki/Commands-&-Permissions#tab-cpu) command. If the numbers appear to be higher than they should be, check this guide for various ways to optimize the plugin. Most of these apply for all platforms, but some of them are only specific for some platforms.
|
||||
|
||||
# Content
|
||||
* [#1 - [All platforms] Static text instead of placeholders](#1---all-platforms-static-text-instead-of-placeholders)
|
||||
* [#2 - [All platforms] Placeholder refresh intervals](#2---all-platforms-placeholder-refresh-intervals)
|
||||
* [#3 - [All platforms] Don't use RGB](#3---all-platforms-dont-use-rgb)
|
||||
* [#4 - [All platforms] Don't use animations](#4---all-platforms-dont-use-animations)
|
||||
* [#5 - [All platforms] Disable tablist name formatting](#5---all-platforms-disable-tablist-name-formatting)
|
||||
* [#6 - [All platforms] Increase permission refresh interval](#6---all-platforms-increase-permission-refresh-interval)
|
||||
* [#7 - [Proxies] Disable TAB expansion](#7---proxies-disable-tab-expansion)
|
||||
* [#8 - [Proxies] Disable update-latency option in global playerlist](#8---proxies-disable-update-latency-option-in-global-playerlist)
|
||||
|
||||
# #1 - [All platforms] Static text instead of placeholders
|
||||
# Ways of optimizing
|
||||
## #1 - [All platforms] Static text instead of placeholders
|
||||
Static text doesn't need to be refreshed periodically, resulting in better performance.
|
||||
Some placeholders can be replaced with static text, such as placeholder for max player counts,
|
||||
since that value doesn't change at runtime.
|
||||
Another option is to configure prefixes per-group in config
|
||||
instead of using placeholders to take those values from your permission plugin.
|
||||
|
||||
# #2 - [All platforms] Placeholder refresh intervals
|
||||
## #2 - [All platforms] Placeholder refresh intervals
|
||||
Some placeholders take a long time to process and result in high cpu usage. However, this can be optimized by configuring refresh interval of placeholders.
|
||||
|
||||
Internal placeholders use a value hardcoded into the plugin by default. PlaceholderAPI placeholders use `default-refresh-interval`.
|
||||
@@ -45,16 +54,16 @@ making it look like the CPU is perfect, while on the next refresh it may be high
|
||||
For this reason, if you want to cleanly track usage, keep the interval at 10000 at most.
|
||||
Using a higher interval will help if needed, but may lead to misleading info in cpu output.
|
||||
|
||||
# #3 - [All platforms] Don't use RGB
|
||||
## #3 - [All platforms] Don't use RGB
|
||||
Though RGB colors look good, they require the text to be split into several components with `color` field used instead of the whole text pasted into the `text` field. This is a complicated process that takes quite some time. Sticking to legacy colors results in better performance.
|
||||
|
||||
# #4 - [All platforms] Don't use animations
|
||||
## #4 - [All platforms] Don't use animations
|
||||
While animations look cool, they also have a high resource usage.
|
||||
This is because they work by spamming updates every time an animation switches to a new frame.
|
||||
When animations are not used, no updates need to be sent (unless another placeholder changes value),
|
||||
not causing extra CPU usage.
|
||||
|
||||
# #5 - [All platforms] Disable tablist name formatting
|
||||
## #5 - [All platforms] Disable tablist name formatting
|
||||
As we already know from [Client-sided mechanics](https://github.com/NEZNAMY/TAB/wiki/Client%E2%80%90sided-mechanics#nametag-format-in-tablist) page,
|
||||
the nametag format appears in tablist if tablist name is not defined.
|
||||
You can take advantage of this to effectively disable one entire feature and its cpu usage,
|
||||
@@ -65,7 +74,7 @@ However, you can only do this if:
|
||||
|
||||
Whether you have another plugin attempting to handle them or not may not be easy to identify with a lot of plugins and not enough time spent configuring them. In that case, your best choice is to try it and see. If tablist names break, it means you have another plugin and cannot take advantage of this optimization option.
|
||||
|
||||
# #6 - [All platforms] Increase permission refresh interval
|
||||
## #6 - [All platforms] Increase permission refresh interval
|
||||
Plugin needs to constantly check permissions for:
|
||||
* Permission checks in conditions
|
||||
* Permission checks if sorting by permissions
|
||||
@@ -79,7 +88,7 @@ Refreshes them every second.
|
||||
If you don't need any permission/group changes to take effect within a second,
|
||||
you can increase this value for better performance.
|
||||
|
||||
# #7 - [Proxies] Disable TAB expansion
|
||||
## #7 - [Proxies] Disable TAB expansion
|
||||
To maximize the performance of TAB's response to PlaceholderAPI request,
|
||||
values are tracked in advance and then quickly returned.
|
||||
This process takes resources and is especially heavy on proxy installation,
|
||||
@@ -90,5 +99,57 @@ Disabling TAB's PlaceholderAPI expansion if you don't use it improves performanc
|
||||
placeholders:
|
||||
register-tab-expansion: false
|
||||
```
|
||||
# #8 - [Proxies] Disable update-latency option in global playerlist
|
||||
When [Global playerlist](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Global-playerlist) is enabled, you can enable / disable [update-latency](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Global-playerlist#:~:text=other%20unlisted%20servers.-,update%2Dlatency,-false) option (open the link for detailed description of the option). Disabling this option massively improves performance.
|
||||
## #8 - [Proxies] Disable update-latency option in global playerlist
|
||||
When [Global playerlist](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Global-playerlist) is enabled, you can enable / disable [update-latency](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Global-playerlist#:~:text=other%20unlisted%20servers.-,update%2Dlatency,-false) option (open the link for detailed description of the option). Disabling this option massively improves performance.
|
||||
|
||||
# Netty false positive in spark reports
|
||||
When taking a spark report with all threads monitored instead of just the server thread using `--thread *` argument (or on a proxy where this is enabled by default since there is no main thread to monitor by default), you may notice high usage (both CPU and RAM) in netty thread when grouping threads by plugin. **Vast majority of that usage is false positive.** Let's go over why this is the case and how to read the real value.
|
||||
|
||||
## Why there is a false positive
|
||||
When listening to an event, the server fires the events in a loop, where each event is the end point. As such, measuring every event handler individually is accurate. Here is how the hierarchy looks with example numbers:
|
||||
```
|
||||
Event processing in loop (6ms total)
|
||||
↳ Plugin 1 (1ms)
|
||||
↳ Plugin 2 (2ms)
|
||||
↳ Plugin 3 (3ms)
|
||||
End of loop
|
||||
```
|
||||
As such, the measured values are accurate and spark is able to separate each event handler based on plugin. Command executions and repeating tasks are even simpler.
|
||||
|
||||
The issue is with netty injection. The purpose is to listen to outgoing packets, react to them and possibly modify them. Unlike with events, these are not processed in parallel, but every handler forwards the packet to the next one (or doesn't if it should be cancelled). As such, the hierarchy looks like this (simplified version):
|
||||
```
|
||||
Beginning (12ms total)
|
||||
↳ Plugin 1 (1ms self, 12ms total)
|
||||
↳ Plugin 2 (1ms self, 11ms total)
|
||||
↳ Plugin 3 (1ms self, 10ms total)
|
||||
↳ Minecraft encoder (10ms self)
|
||||
```
|
||||
As such, spark will show 12ms total for Plugin 1, even though its own usage is only 1ms. This is due to a limitation of the design of Spark in combination with how netty handlers work. This is only the case for plugins with their own netty injection, it does not apply to plugins that use ProtocolLib, because that one offers event-like functionality explained above. In this case, the usage shown in spark report is 100% accurate.
|
||||
|
||||
## How to read the real value
|
||||
An actual usage of the netty threads can be split into 2 parts:
|
||||
* Listening to outgoing packets
|
||||
* Sending packets, which causes it to go though the pipeline and then encode
|
||||
|
||||
### Packet listening usage
|
||||
This one is simple and accurate. Take the shown number, expand the call stack and subtract `io.netty.channel.ChannelDuplexHandler.write()` usage, which is forwarding the packet to the next handler and would be there even if that plugin wasn't injected.
|
||||
Let's take a look at a practical example:
|
||||
<img width="1091" height="266" alt="image" src="https://github.com/user-attachments/assets/6d9e8c25-8375-413b-ab9b-31b52ff9262c" />
|
||||
|
||||
Here, spark reports `31.71%` usage by TAB. However, `30.87%` of it is forwarding it to the next handler (`io.netty.channel.ChannelDuplexHandler.write()`), which forwards it to the next one, until eventually the server encodes the packet. Therefore, TAB's real usage by listening to outgoing packets in this report is `31.71%` - `30.87%` = `0.84%`. Much less than what you originally see.
|
||||
|
||||
### Sending packets
|
||||
Sadly, this one is impossible to calculate directly. When a packet is scheduled for sending, it is only added to a queue, then consumed by a netty thread, not the caller thread. At that point we are losing information about where a packet comes from.
|
||||
There is some good news though.
|
||||
|
||||
First, vast majority of plugins barely send any packets. For example PlaceholderAPI is just an interface between plugins, chat plugins only modify chat message packets that would be sent anyway, ViaVersion same thing, and so on...
|
||||
|
||||
|
||||
Second, plugins that send a lot of packets usually send the kind that the server either doesn't send at all, or not much. Expanding the data from the previously shown report gives us this:
|
||||
<img width="1083" height="496" alt="image" src="https://github.com/user-attachments/assets/c6157e78-ef6a-4d9c-b4b7-7c7b32dd14f8" />
|
||||
|
||||
Here we can see that encoding team packets took 0.99%. Since TAB is the only team plugin you need for managing players, it is safe to assume most, if not all of it comes from TAB.
|
||||
Same here - 0.13% from header and footer.
|
||||
<img width="1085" height="93" alt="image" src="https://github.com/user-attachments/assets/559dce56-83d3-49b2-9f4b-29b6124f944c" />
|
||||
|
||||
Sadly there are other operations needed than just encoding packets, such as allocating buffers and other, so this part is not that simple anymore.
|
||||
@@ -5,6 +5,7 @@
|
||||
* [Backend only](#backend-only)
|
||||
* [Proxy only](#proxy-only)
|
||||
* [PlaceholderAPI](#placeholderapi)
|
||||
* [MiniPlaceholders](#miniplaceholders)
|
||||
* [Relational placeholders](#relational-placeholders)
|
||||
* [About](#about-1)
|
||||
* [Usage](#usage)
|
||||
@@ -107,7 +108,9 @@ Full list of placeholders (general + feature specific):
|
||||
| Identifier | Description |
|
||||
|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `%tab_replace_<placeholder>%` | Applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to a PlaceholderAPI placeholder (for example `%tab_replace_essentials_vanished%`) |
|
||||
| `%tab_placeholder_<placeholder>%` | returns value of tab's internal placeholder (such as `%tab_placeholder_animation:name%` for `%animation:name%`) (this also applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to them if configured) |
|
||||
| `%tab_placeholder_<placeholder>%` | Returns value of tab's internal placeholder (this also applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to them if configured). <br />Example: `%tab_placeholder_animation:name%` for `%animation:name%` |
|
||||
| `%rel_tab_replace_<rel_placeholder>%` | Applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to a **relationnal** PlaceholderAPI placeholder |
|
||||
| `%rel_tab_placeholder_<rel_placeholder>%` | Returns value of tab's internal **relational** placeholder (this also applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to them if configured). It may look strange that the `rel` is twice in there, but there's no other way. The first one tells PlaceholderAPI that this is a relational placeholder, the second one is part of the TAB's identifier, which also uses it to mark the placeholder as relational. <br />Example: `%rel_tab_placeholder_rel_condition:name%` for `%rel_condition:name%` |
|
||||
|
||||
**BossBar**:
|
||||
|
||||
@@ -143,6 +146,12 @@ Full list of placeholders (general + feature specific):
|
||||
| `%tab_customtabname_raw%` | Player's current raw customtabname with placeholder identifiers. |
|
||||
| `%tab_tabsuffix_raw%` | Player's current raw tabsuffix with placeholder identifiers. |
|
||||
|
||||
# MiniPlaceholders
|
||||
On **Velocity**, TAB integrates with [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) when the plugin is installed. TAB automatically detects and resolves MiniPlaceholders placeholders in config text, including global, per-player, and relational ones. Placeholders use angle brackets, with the expansion and name separated by an underscore (for example `<luckperms_prefix>`), and arguments added after a colon (for example `<luckperms_meta:key>`). See the [MiniPlaceholders placeholder list](https://miniplaceholders.github.io/docs/user-guide/Placeholders) for all available placeholders. TAB also registers its own expansion, offering placeholders such as `<tab_tabprefix>` and `<tab_placeholder:name>`. The `%placeholder%` syntax remains reserved for [PlaceholderAPI](https://github.com/PlaceholderAPI/PlaceholderAPI) (via TAB Bridge on Velocity).
|
||||
|
||||
> [!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) 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 [How to use Minecraft components - MiniMessage](https://github.com/NEZNAMY/TAB/wiki/How-to-use-Minecraft-components#minimessage) for more information.
|
||||
|
||||
# Relational placeholders
|
||||
## About
|
||||
Unlike classic placeholders which take either 0
|
||||
@@ -168,7 +177,7 @@ To properly distinguish viewer/target (if needed), your relational placeholder e
|
||||
|
||||
# Refreshing
|
||||
## Refresh intervals
|
||||
TAB is refreshing placeholders in intervals. For internal placeholders it's using values I found the most optimal and set directly in the plugin. Refresh intervals for PlaceholderAPI placeholders can be [configured](https://github.com/NEZNAMY/TAB/wiki/Optimizing-the-plugin#2---placeholder-refresh-intervals).
|
||||
TAB is refreshing placeholders in intervals. For internal placeholders it's using values I found the most optimal and set directly in the plugin. Refresh intervals for PlaceholderAPI placeholders can be [configured](https://github.com/NEZNAMY/TAB/wiki/Optimizing-the-plugin#2---all-platforms-placeholder-refresh-intervals).
|
||||
|
||||
## Sync refreshing
|
||||
By default, all placeholders are refreshed asynchronously,
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
distribution: 'temurin'
|
||||
java-version: 25
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4.2.0 # https://github.com/actions/cache/releases
|
||||
uses: actions/cache@v6 # https://github.com/actions/cache/releases
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
mv "$original" "$new_path"
|
||||
done
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v4.3.3 # https://github.com/actions/upload-artifact/releases
|
||||
uses: actions/upload-artifact@v7 # https://github.com/actions/upload-artifact/releases
|
||||
with:
|
||||
name: Click here to download
|
||||
path: "jar/build/libs/TAB v*+${{ github.run_number }}.jar"
|
||||
@@ -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);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.gradleup.shadow:shadow-gradle-plugin:9.4.1")
|
||||
implementation("io.freefair.gradle:lombok-plugin:9.2.0")
|
||||
implementation("dev.architectury.loom-no-remap:dev.architectury.loom-no-remap.gradle.plugin:1.14-SNAPSHOT")
|
||||
implementation("com.gradleup.shadow:shadow-gradle-plugin:9.4.2")
|
||||
implementation("io.freefair.gradle:lombok-plugin:9.5.0")
|
||||
implementation("dev.architectury.loom-no-remap:dev.architectury.loom-no-remap.gradle.plugin:1.17-SNAPSHOT")
|
||||
}
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
allprojects {
|
||||
group = "me.neznamy"
|
||||
version = "6.1.0"
|
||||
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",
|
||||
@@ -46,6 +44,7 @@ val platformPaths = setOf(
|
||||
":bungeecord",
|
||||
":velocity",
|
||||
":fabric",
|
||||
":fand",
|
||||
":neoforge"
|
||||
// ":forge"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import lombok.NonNull;
|
||||
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 net.minecraft.network.chat.Component;
|
||||
@@ -135,7 +136,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.NonNull;
|
||||
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.decorators.TrackedTabList;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
@@ -138,7 +139,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import lombok.NonNull;
|
||||
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 net.minecraft.network.chat.Component;
|
||||
@@ -136,7 +137,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import lombok.NonNull;
|
||||
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 net.minecraft.network.chat.Component;
|
||||
@@ -137,7 +138,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.NonNull;
|
||||
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 net.minecraft.network.chat.Component;
|
||||
@@ -139,7 +140,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
public class NMSImplementationProvider implements ImplementationProvider {
|
||||
|
||||
/** Flag tracking if this server is Canvas 26.1.2+ with new scoreboard checks */
|
||||
private final boolean isCanvas = ReflectionUtils.classExists("io.canvasmc.canvas.world.scores.TeamData");
|
||||
private final boolean isCanvas = ReflectionUtils.classExists("io.canvasmc.canvas.world.scores.TeamData") ||
|
||||
ReflectionUtils.classExists("io.canvasmc.canvas.threadedregions.scores.TeamData");
|
||||
|
||||
@NotNull
|
||||
private final ComponentConverter<?> componentConverter = new NMSComponentConverter();
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.NonNull;
|
||||
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.decorators.TrackedTabList;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
@@ -138,7 +139,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,10 +5,12 @@ import me.neznamy.tab.platforms.bukkit.platform.BukkitPlatform;
|
||||
import me.neznamy.tab.shared.backend.BackendTabPlayer;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.metadata.MetadataValue;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* TabPlayer implementation for Bukkit platform
|
||||
@@ -16,6 +18,18 @@ import org.jetbrains.annotations.NotNull;
|
||||
@SuppressWarnings("deprecation")
|
||||
public class BukkitTabPlayer extends BackendTabPlayer {
|
||||
|
||||
/** Below name distance attribute (26.1+) */
|
||||
@Nullable
|
||||
private static Attribute BELOW_NAME_DISTANCE;
|
||||
|
||||
static {
|
||||
try {
|
||||
BELOW_NAME_DISTANCE = (Attribute) Attribute.class.getDeclaredField("BELOW_NAME_DISTANCE").get(null);
|
||||
} catch (Throwable e) {
|
||||
// 1.21.11 and lower
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs new instance with given bukkit player
|
||||
*
|
||||
@@ -64,6 +78,13 @@ public class BukkitTabPlayer extends BackendTabPlayer {
|
||||
return (BukkitPlatform) platform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBelowNameDistance(double distance) {
|
||||
if (BELOW_NAME_DISTANCE != null) {
|
||||
getPlayer().getAttribute(BELOW_NAME_DISTANCE).setBaseValue(distance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVanished0() {
|
||||
for (MetadataValue v : getPlayer().getMetadata("vanished")) {
|
||||
|
||||
@@ -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
|
||||
@@ -389,6 +389,11 @@ public class BukkitPlatform implements BackendPlatform {
|
||||
Bukkit.getScheduler().runTask(plugin, task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLineOfSight(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
|
||||
return ((Player) viewer.getPlayer()).hasLineOfSight((Player) target.getPlayer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts component to string using bukkit RGB format if supported by the server.
|
||||
* If not, closest legacy color is used instead.
|
||||
|
||||
@@ -171,4 +171,9 @@ public class FoliaPlatform extends BukkitPlatform {
|
||||
if (!getPlugin().isEnabled()) return; // Server shutdown, no one cares anymore, everyone is about to be kicked
|
||||
globalScheduler_execute.invoke(globalScheduler, getPlugin(), task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLineOfSight(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
|
||||
return true; // Cross-entity line-of-sight checks are not safe from Folia's global scheduler.
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,222 +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.shared.chat.component.TabComponent;
|
||||
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
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 && nmsData.a().getMostSignificantBits() != 0) { // 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -152,7 +153,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -152,7 +153,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -150,7 +151,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -150,7 +151,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -151,7 +152,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -151,7 +152,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -153,7 +154,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -155,7 +156,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -155,7 +156,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -155,7 +156,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import com.mojang.authlib.properties.Property;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -157,7 +158,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import com.mojang.authlib.properties.Property;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
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;
|
||||
@@ -157,7 +158,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.a().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.a())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.decorators.TrackedTabList;
|
||||
import me.neznamy.tab.shared.util.ReflectionUtils;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -144,7 +145,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.decorators.TrackedTabList;
|
||||
import me.neznamy.tab.shared.util.ReflectionUtils;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -144,7 +145,7 @@ public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.platforms.bungeecord.BungeeTabPlayer;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
|
||||
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
|
||||
import me.neznamy.tab.shared.util.ReflectionUtils;
|
||||
import net.md_5.bungee.UserConnection;
|
||||
@@ -162,7 +163,7 @@ public abstract class BungeeTabList extends TrackedTabList<BungeeTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (update.getActions().contains(PlayerListItemUpdate.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && item.getUuid().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(item.getUuid())) { // Filter out layout entries
|
||||
item.setListed(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -273,6 +273,16 @@ public record FabricPlatform(MinecraftServer server) implements BackendPlatform
|
||||
return (float) server.getAverageTickTimeNanos() / 1000000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runSyncGlobal(@NotNull Runnable task) {
|
||||
server.execute(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLineOfSight(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
|
||||
return ((FabricTabPlayer) viewer).getPlayer().hasLineOfSight(((FabricTabPlayer) target).getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Object dump() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.mojang.authlib.properties.PropertyMap;
|
||||
import lombok.NonNull;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
|
||||
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
@@ -136,7 +137,7 @@ public class FabricTabList extends TrackedTabList<FabricTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
@@ -62,6 +63,11 @@ public class FabricTabPlayer extends BackendTabPlayer {
|
||||
return (FabricPlatform) platform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBelowNameDistance(double distance) {
|
||||
getPlayer().getAttribute(Attributes.BELOW_NAME_DISTANCE).setBaseValue(distance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVanished0() {
|
||||
return false;
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ public class PlaceholderAPIHook {
|
||||
private static String toTabString(@NotNull Component component) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (component.getStyle().getColor() != null) {
|
||||
sb.append("#").append(Integer.toHexString(component.getStyle().getColor().getValue()));
|
||||
sb.append(String.format("#%06X", component.getStyle().getColor().getValue()));
|
||||
}
|
||||
if (component.getStyle().isBold()) sb.append("§l");
|
||||
if (component.getStyle().isItalic()) sb.append("§o");
|
||||
@@ -56,7 +56,7 @@ public class PlaceholderAPIHook {
|
||||
if (component.getContents() instanceof PlainTextContents text) {
|
||||
sb.append(text.text());
|
||||
} else {
|
||||
sb.append(component.getContents()); // Fallback for non-plain text contents
|
||||
sb.append(component.getContents()); // Fallback for non-text contents
|
||||
}
|
||||
for (Component extra : component.getSiblings()) {
|
||||
sb.append(toTabString(extra));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
dependencies {
|
||||
implementation(projects.shared)
|
||||
compileOnly("io.fand:fand-api:0.8.3+build.1")
|
||||
}
|
||||
|
||||
tasks.compileJava {
|
||||
options.release.set(25)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.command.Arguments;
|
||||
import io.fand.api.command.CommandContext;
|
||||
import io.fand.api.command.CommandRegistration;
|
||||
import io.fand.api.command.CommandRegistry;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/** Shared Fand command adapter for TAB commands. */
|
||||
public abstract class FandCommand {
|
||||
|
||||
private final String commandName;
|
||||
|
||||
protected FandCommand(@NotNull String commandName) {
|
||||
this.commandName = commandName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CommandRegistration register(@NotNull CommandRegistry registry) {
|
||||
return registry.register(commandName, command -> {
|
||||
command.executes(context -> execute(context, new String[0]));
|
||||
command.argument("arguments", Arguments.greedyString(), arguments -> arguments
|
||||
.suggests(context -> complete(context, arguments(context)))
|
||||
.executes(context -> execute(context, arguments(context))));
|
||||
});
|
||||
}
|
||||
|
||||
private static String[] arguments(CommandContext context) {
|
||||
return context.args().toArray(String[]::new);
|
||||
}
|
||||
|
||||
protected abstract void execute(@NotNull CommandContext context, @NotNull String[] arguments);
|
||||
|
||||
@NotNull
|
||||
protected List<String> complete(@NotNull CommandContext context, @NotNull String[] arguments) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.entity.Player;
|
||||
import io.fand.api.event.EventPriority;
|
||||
import io.fand.api.event.player.PlayerChangedWorldEvent;
|
||||
import io.fand.api.event.player.PlayerDisguiseStateChangeEvent;
|
||||
import io.fand.api.event.player.PlayerGameModeChangeEvent;
|
||||
import io.fand.api.event.player.PlayerJoinEvent;
|
||||
import io.fand.api.event.player.PlayerQuitEvent;
|
||||
import io.fand.api.event.player.PlayerRespawnEvent;
|
||||
import io.fand.api.event.player.PlayerVanishStateChangeEvent;
|
||||
import io.fand.api.plugin.PluginContext;
|
||||
import java.util.UUID;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.platform.EventListener;
|
||||
import me.neznamy.tab.shared.platform.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/** Bridges Fand player lifecycle events into TAB. */
|
||||
public final class FandEventListener implements EventListener<Player> {
|
||||
|
||||
private final PluginContext context;
|
||||
|
||||
public FandEventListener(@NotNull PluginContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void register() {
|
||||
context.events().subscribe(PlayerJoinEvent.class, event -> join(event.player()));
|
||||
context.events().subscribe(PlayerQuitEvent.class, event -> quit(event.player().uniqueId()));
|
||||
context.events().subscribe(PlayerChangedWorldEvent.class,
|
||||
event -> worldChange(event.player().uniqueId(), event.toWorld().name()));
|
||||
context.events().subscribe(PlayerRespawnEvent.class, EventPriority.OBSERVER, event -> {
|
||||
replacePlayer(event.player().uniqueId(), event.player());
|
||||
worldChange(event.player().uniqueId(), event.respawnLocation().world().name());
|
||||
});
|
||||
context.events().subscribe(PlayerGameModeChangeEvent.class,
|
||||
event -> gameModeChange(event.player().uniqueId()));
|
||||
context.events().subscribe(PlayerVanishStateChangeEvent.class,
|
||||
this::vanishStateChange);
|
||||
context.events().subscribe(PlayerDisguiseStateChangeEvent.class,
|
||||
event -> disguiseStateChange(event.player().uniqueId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public TabPlayer createPlayer(@NotNull Player player) {
|
||||
return new FandTabPlayer((FandPlatform) TAB.getInstance().getPlatform(), player);
|
||||
}
|
||||
|
||||
private void gameModeChange(UUID playerId) {
|
||||
context.scheduler().runMain(() -> {
|
||||
FandTabPlayer player = player(playerId);
|
||||
if (player != null) {
|
||||
TAB.getInstance().getFeatureManager().onGameModeChange(player);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void vanishStateChange(PlayerVanishStateChangeEvent event) {
|
||||
FandTabPlayer player = player(event.player().uniqueId());
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
player.updateVanishState(event.vanished());
|
||||
TAB.getInstance().getFeatureManager().onVanishStatusChange(player);
|
||||
}
|
||||
|
||||
private void disguiseStateChange(UUID playerId) {
|
||||
FandTabPlayer player = player(playerId);
|
||||
if (player != null) {
|
||||
TAB.getInstance().getFeatureManager().onDisguiseStatusChange(player);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static FandTabPlayer player(UUID playerId) {
|
||||
TAB tab = TAB.getInstance();
|
||||
if (tab == null || tab.isPluginDisabled()) {
|
||||
return null;
|
||||
}
|
||||
TabPlayer player = tab.getPlayer(playerId);
|
||||
return player instanceof FandTabPlayer fandPlayer ? fandPlayer : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.packet.PacketController;
|
||||
import io.fand.api.packet.PacketPriority;
|
||||
import io.fand.api.packet.PacketType;
|
||||
import io.fand.api.packet.view.ClientboundPlayerInfoRemovePacketView;
|
||||
import io.fand.api.packet.view.ClientboundPlayerInfoUpdatePacketView;
|
||||
import io.fand.api.packet.view.ClientboundSetDisplayObjectivePacketView;
|
||||
import io.fand.api.packet.view.ClientboundSetObjectivePacketView;
|
||||
import io.fand.api.packet.view.ClientboundSetPlayerTeamPacketView;
|
||||
import io.fand.api.packet.view.ClientboundTabListPacketView;
|
||||
import io.fand.api.plugin.PluginContext;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/** Applies TAB's player-info anti-override rules through Fand's packet API. */
|
||||
final class FandPacketListener {
|
||||
|
||||
private final PluginContext context;
|
||||
|
||||
FandPacketListener(@NotNull PluginContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
void register() {
|
||||
context.packets().intercept(
|
||||
PacketType.PLAY_CLIENTBOUND_PLAYER_INFO_UPDATE,
|
||||
ClientboundPlayerInfoUpdatePacketView.class,
|
||||
PacketPriority.HIGHEST,
|
||||
this::rewritePlayerInfo);
|
||||
context.packets().intercept(
|
||||
PacketType.PLAY_CLIENTBOUND_PLAYER_INFO_REMOVE,
|
||||
ClientboundPlayerInfoRemovePacketView.class,
|
||||
PacketPriority.HIGHEST,
|
||||
this::observePlayerInfoRemove);
|
||||
context.packets().intercept(
|
||||
PacketType.PLAY_CLIENTBOUND_TAB_LIST,
|
||||
ClientboundTabListPacketView.class,
|
||||
PacketPriority.HIGHEST,
|
||||
this::rewriteHeaderFooter);
|
||||
context.packets().intercept(
|
||||
PacketType.PLAY_CLIENTBOUND_SET_DISPLAY_OBJECTIVE,
|
||||
ClientboundSetDisplayObjectivePacketView.class,
|
||||
PacketPriority.HIGHEST,
|
||||
this::observeDisplayObjective);
|
||||
context.packets().intercept(
|
||||
PacketType.PLAY_CLIENTBOUND_SET_OBJECTIVE,
|
||||
ClientboundSetObjectivePacketView.class,
|
||||
PacketPriority.HIGHEST,
|
||||
this::observeObjective);
|
||||
context.packets().intercept(
|
||||
PacketType.PLAY_CLIENTBOUND_SET_PLAYER_TEAM,
|
||||
ClientboundSetPlayerTeamPacketView.class,
|
||||
PacketPriority.HIGHEST,
|
||||
this::rewriteTeam);
|
||||
}
|
||||
|
||||
private void rewritePlayerInfo(PacketController<ClientboundPlayerInfoUpdatePacketView> packet) {
|
||||
FandTabList tabList = tabList(packet);
|
||||
if (tabList == null) {
|
||||
return;
|
||||
}
|
||||
var replacement = tabList.rewritePlayerInfo(packet.view());
|
||||
if (replacement != packet.view()) {
|
||||
packet.replace(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
private void observePlayerInfoRemove(PacketController<ClientboundPlayerInfoRemovePacketView> packet) {
|
||||
FandTabList tabList = tabList(packet);
|
||||
if (tabList != null) {
|
||||
tabList.observePlayerInfoRemove(packet.view());
|
||||
}
|
||||
}
|
||||
|
||||
private void rewriteHeaderFooter(PacketController<ClientboundTabListPacketView> packet) {
|
||||
FandTabList tabList = tabList(packet);
|
||||
if (tabList == null) {
|
||||
return;
|
||||
}
|
||||
var replacement = tabList.rewriteHeaderFooter(packet.view());
|
||||
if (replacement != packet.view()) {
|
||||
packet.replace(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
private void observeDisplayObjective(PacketController<ClientboundSetDisplayObjectivePacketView> packet) {
|
||||
FandScoreboard scoreboard = scoreboard(packet);
|
||||
if (scoreboard != null) {
|
||||
scoreboard.observeDisplayObjective(packet.view());
|
||||
}
|
||||
}
|
||||
|
||||
private void observeObjective(PacketController<ClientboundSetObjectivePacketView> packet) {
|
||||
FandScoreboard scoreboard = scoreboard(packet);
|
||||
if (scoreboard != null) {
|
||||
scoreboard.observeObjective(packet.view());
|
||||
}
|
||||
}
|
||||
|
||||
private void rewriteTeam(PacketController<ClientboundSetPlayerTeamPacketView> packet) {
|
||||
FandScoreboard scoreboard = scoreboard(packet);
|
||||
if (scoreboard == null) {
|
||||
return;
|
||||
}
|
||||
var replacement = scoreboard.rewriteTeam(packet.view());
|
||||
if (replacement != packet.view()) {
|
||||
packet.replace(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static FandTabList tabList(PacketController<?> packet) {
|
||||
FandTabPlayer player = player(packet);
|
||||
return player != null && player.getTabList() instanceof FandTabList tabList ? tabList : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static FandScoreboard scoreboard(PacketController<?> packet) {
|
||||
FandTabPlayer player = player(packet);
|
||||
return player != null && player.getScoreboard() instanceof FandScoreboard scoreboard ? scoreboard : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static FandTabPlayer player(PacketController<?> packet) {
|
||||
var viewer = packet.context().player().orElse(null);
|
||||
if (viewer == null || TAB.getInstance() == null || TAB.getInstance().isPluginDisabled()) {
|
||||
return null;
|
||||
}
|
||||
var tabPlayer = TAB.getInstance().getPlayer(viewer.uniqueId());
|
||||
return tabPlayer instanceof FandTabPlayer fandPlayer ? fandPlayer : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.Fand;
|
||||
import io.fand.api.entity.Player;
|
||||
import io.fand.api.plugin.PluginContext;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.TabConstants;
|
||||
import me.neznamy.tab.shared.data.World;
|
||||
import me.neznamy.tab.shared.features.PerWorldPlayerListConfiguration;
|
||||
import me.neznamy.tab.shared.features.types.JoinListener;
|
||||
import me.neznamy.tab.shared.features.types.Loadable;
|
||||
import me.neznamy.tab.shared.features.types.QuitListener;
|
||||
import me.neznamy.tab.shared.features.types.TabFeature;
|
||||
import me.neznamy.tab.shared.features.types.UnLoadable;
|
||||
import me.neznamy.tab.shared.features.types.VanishListener;
|
||||
import me.neznamy.tab.shared.features.types.WorldSwitchListener;
|
||||
import me.neznamy.tab.shared.platform.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/** Per-world player-list visibility backed by Fand's scoped tab-list service. */
|
||||
public final class FandPerWorldPlayerList extends TabFeature implements
|
||||
JoinListener, QuitListener, Loadable, UnLoadable, VanishListener, WorldSwitchListener {
|
||||
|
||||
private final PluginContext context;
|
||||
private final PerWorldPlayerListConfiguration configuration;
|
||||
private final Set<VisibilityPair> hiddenPairs = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public FandPerWorldPlayerList(
|
||||
@NotNull PluginContext context,
|
||||
@NotNull PerWorldPlayerListConfiguration configuration
|
||||
) {
|
||||
this.context = context;
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
for (Player player : Fand.server().players()) {
|
||||
checkPlayer(player);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
for (VisibilityPair pair : List.copyOf(hiddenPairs)) {
|
||||
show(pair);
|
||||
}
|
||||
hiddenPairs.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onJoin(@NotNull TabPlayer connectedPlayer) {
|
||||
checkPlayer(((FandTabPlayer) connectedPlayer).getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuit(@NotNull TabPlayer disconnectedPlayer) {
|
||||
UUID playerId = disconnectedPlayer.getUniqueId();
|
||||
Player disconnected = ((FandTabPlayer) disconnectedPlayer).getPlayer();
|
||||
for (VisibilityPair pair : List.copyOf(hiddenPairs)) {
|
||||
if (pair.viewerId().equals(playerId) || pair.targetId().equals(playerId)) {
|
||||
release(pair, disconnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWorldChange(@NotNull TabPlayer changed, @NotNull World from, @NotNull World to) {
|
||||
checkPlayer(((FandTabPlayer) changed).getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVanishStatusChange(@NotNull TabPlayer player) {
|
||||
checkPlayer(((FandTabPlayer) player).getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getFeatureName() {
|
||||
return "Per world PlayerList";
|
||||
}
|
||||
|
||||
private void checkPlayer(Player changed) {
|
||||
for (Player other : Fand.server().players()) {
|
||||
if (other.uniqueId().equals(changed.uniqueId())) {
|
||||
continue;
|
||||
}
|
||||
updateVisibility(changed, other);
|
||||
updateVisibility(other, changed);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateVisibility(Player viewer, Player target) {
|
||||
var pair = new VisibilityPair(viewer.uniqueId(), target.uniqueId());
|
||||
if (shouldSee(viewer, target)) {
|
||||
show(pair);
|
||||
} else if (hiddenPairs.add(pair)) {
|
||||
context.tabLists().setVisible(viewer, target, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void show(VisibilityPair pair) {
|
||||
if (!hiddenPairs.remove(pair)) {
|
||||
return;
|
||||
}
|
||||
var viewer = Fand.server().player(pair.viewerId()).orElse(null);
|
||||
var target = Fand.server().player(pair.targetId()).orElse(null);
|
||||
if (viewer != null && target != null) {
|
||||
context.tabLists().setVisible(viewer, target, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void release(VisibilityPair pair, Player disconnected) {
|
||||
if (!hiddenPairs.remove(pair)) {
|
||||
return;
|
||||
}
|
||||
var viewer = player(pair.viewerId(), disconnected);
|
||||
var target = player(pair.targetId(), disconnected);
|
||||
if (viewer != null && target != null) {
|
||||
context.tabLists().setVisible(viewer, target, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static Player player(UUID playerId, Player disconnected) {
|
||||
if (playerId.equals(disconnected.uniqueId())) {
|
||||
return disconnected;
|
||||
}
|
||||
TabPlayer tabPlayer = TAB.getInstance().getPlayer(playerId);
|
||||
if (tabPlayer instanceof FandTabPlayer fandPlayer) {
|
||||
return fandPlayer.getPlayer();
|
||||
}
|
||||
return Fand.server().player(playerId).orElse(null);
|
||||
}
|
||||
|
||||
private boolean shouldSee(Player viewer, Player target) {
|
||||
TabPlayer tabViewer = TAB.getInstance().getPlayer(viewer.uniqueId());
|
||||
TabPlayer tabTarget = TAB.getInstance().getPlayer(target.uniqueId());
|
||||
if (tabViewer != null && tabTarget != null && !tabViewer.canSee(tabTarget)) {
|
||||
return false;
|
||||
}
|
||||
if (configuration.isAllowBypassPermission()
|
||||
&& viewer.can(TabConstants.Permission.PER_WORLD_PLAYERLIST_BYPASS)) {
|
||||
return true;
|
||||
}
|
||||
String viewerWorld = viewer.world().name();
|
||||
if (configuration.getIgnoredWorlds().contains(viewerWorld)) {
|
||||
return true;
|
||||
}
|
||||
return computeWorldGroup(viewerWorld).equals(computeWorldGroup(target.world().name()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String computeWorldGroup(@NotNull String worldName) {
|
||||
for (Map.Entry<String, List<String>> group : configuration.getSharedWorlds().entrySet()) {
|
||||
List<String> worlds = group.getValue();
|
||||
if (worlds == null) {
|
||||
continue;
|
||||
}
|
||||
for (String world : worlds) {
|
||||
if (TAB.getInstance().getDataManager().matchesPattern(worldName, world)) {
|
||||
return group.getKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
return worldName + "-default";
|
||||
}
|
||||
|
||||
private record VisibilityPair(UUID viewerId, UUID targetId) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.Fand;
|
||||
import io.fand.api.command.CommandRegistration;
|
||||
import io.fand.api.entity.Player;
|
||||
import io.fand.api.placeholder.PlaceholderContext;
|
||||
import io.fand.api.plugin.PluginContext;
|
||||
import io.fand.api.visibility.DisguiseService;
|
||||
import io.fand.api.visibility.VanishService;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.regex.Pattern;
|
||||
import me.neznamy.tab.shared.GroupManager;
|
||||
import me.neznamy.tab.shared.ProjectVariables;
|
||||
import me.neznamy.tab.shared.ProtocolVersion;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.TabConstants;
|
||||
import me.neznamy.tab.shared.backend.BackendPlatform;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import me.neznamy.tab.shared.features.PerWorldPlayerListConfiguration;
|
||||
import me.neznamy.tab.shared.features.PlaceholderManagerImpl;
|
||||
import me.neznamy.tab.shared.features.injection.PipelineInjector;
|
||||
import me.neznamy.tab.shared.features.types.TabFeature;
|
||||
import me.neznamy.tab.shared.placeholders.expansion.TabExpansion;
|
||||
import me.neznamy.tab.shared.platform.BossBar;
|
||||
import me.neznamy.tab.shared.platform.Scoreboard;
|
||||
import me.neznamy.tab.shared.platform.TabList;
|
||||
import me.neznamy.tab.shared.platform.TabPlayer;
|
||||
import me.neznamy.tab.shared.platform.impl.AdventureBossBar;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/** Platform implementation backed exclusively by Fand's stable plugin API. */
|
||||
public final class FandPlatform implements BackendPlatform {
|
||||
|
||||
private static final Pattern LEGACY_FORMATTING = Pattern.compile("(?i)§(?:#[0-9a-f]{6}|[0-9a-fk-orx])");
|
||||
|
||||
private final PluginContext context;
|
||||
private final List<CommandRegistration> customCommands = new ArrayList<>();
|
||||
private final int protocolVersion;
|
||||
|
||||
public FandPlatform(@NotNull PluginContext context) {
|
||||
this.context = context;
|
||||
ProtocolVersion version = ProtocolVersion.fromFriendlyName(Fand.server().minecraftVersion());
|
||||
protocolVersion = version == ProtocolVersion.UNKNOWN
|
||||
? ProtocolVersion.V26_2.getNetworkId()
|
||||
: version.getNetworkId();
|
||||
}
|
||||
|
||||
PluginContext context() {
|
||||
return context;
|
||||
}
|
||||
|
||||
int protocolVersion() {
|
||||
return protocolVersion;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
VanishService vanishService() {
|
||||
return context.services().service(VanishService.class).orElse(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
DisguiseService disguiseService() {
|
||||
return context.services().service(DisguiseService.class).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerUnknownPlaceholder(@NotNull String identifier) {
|
||||
if (identifier.startsWith("%rel_")) {
|
||||
String fandIdentifier = identifier.substring("%rel_".length(), identifier.length() - 1);
|
||||
if (isFandPlaceholderIdentifier(fandIdentifier)) {
|
||||
registerRelationalPlaceholder(identifier);
|
||||
} else {
|
||||
registerDummyPlaceholder(identifier);
|
||||
}
|
||||
return;
|
||||
}
|
||||
String fandIdentifier = identifier;
|
||||
if (identifier.length() > 2 && identifier.startsWith("%") && identifier.endsWith("%")) {
|
||||
fandIdentifier = identifier.substring(1, identifier.length() - 1);
|
||||
}
|
||||
String resolvedIdentifier = fandIdentifier;
|
||||
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
|
||||
if (!isFandPlaceholderIdentifier(resolvedIdentifier)) {
|
||||
registerDummyPlaceholder(identifier);
|
||||
return;
|
||||
}
|
||||
if (identifier.startsWith("%server_")) {
|
||||
manager.registerServerPlaceholder(identifier, () -> context.placeholders()
|
||||
.resolve(null, resolvedIdentifier)
|
||||
.orElse(identifier));
|
||||
return;
|
||||
}
|
||||
manager.registerPlayerPlaceholder(identifier, player -> context.placeholders()
|
||||
.resolve(((FandTabPlayer) player).getPlayer(), resolvedIdentifier)
|
||||
.orElse(identifier));
|
||||
}
|
||||
|
||||
static boolean isFandPlaceholderIdentifier(@NotNull String identifier) {
|
||||
String normalized = identifier.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < normalized.length(); index++) {
|
||||
char value = normalized.charAt(index);
|
||||
if (!(value >= 'a' && value <= 'z')
|
||||
&& !(value >= 'A' && value <= 'Z')
|
||||
&& !(value >= '0' && value <= '9')
|
||||
&& value != '_'
|
||||
&& value != '-'
|
||||
&& value != '.') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerPlaceholders() {
|
||||
BackendPlatform.super.registerPlaceholders();
|
||||
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
|
||||
manager.registerPlayerPlaceholder("%fand_prefix%", player -> context.permissions()
|
||||
.prefix(((FandTabPlayer) player).getPlayer())
|
||||
.orElse(""));
|
||||
manager.registerPlayerPlaceholder("%fand_suffix%", player -> context.permissions()
|
||||
.suffix(((FandTabPlayer) player).getPlayer())
|
||||
.orElse(""));
|
||||
}
|
||||
|
||||
private void registerRelationalPlaceholder(@NotNull String identifier) {
|
||||
String fandIdentifier = identifier.substring("%rel_".length(), identifier.length() - 1);
|
||||
TAB.getInstance().getPlaceholderManager().registerRelationalPlaceholder(identifier, (viewer, target) ->
|
||||
context.placeholders().resolve(
|
||||
fandIdentifier,
|
||||
PlaceholderContext.builder()
|
||||
.viewer(((FandTabPlayer) viewer).getPlayer())
|
||||
.target(((FandTabPlayer) target).getPlayer())
|
||||
.build())
|
||||
.orElse(identifier));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public TabExpansion createTabExpansion() {
|
||||
return new FandTabExpansion(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadPlayers() {
|
||||
for (Player player : Fand.server().players()) {
|
||||
TAB.getInstance().addPlayer(new FandTabPlayer(this, player));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public PipelineInjector createPipelineInjector() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TabFeature getPerWorldPlayerList(@NotNull PerWorldPlayerListConfiguration configuration) {
|
||||
return new FandPerWorldPlayerList(context, configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logInfo(@NotNull TabComponent message) {
|
||||
context.logger().info("[TAB] {}", plainText(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logWarn(@NotNull TabComponent message) {
|
||||
context.logger().warn("[TAB] {}", plainText(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerListener() {
|
||||
new FandEventListener(context).register();
|
||||
new FandPacketListener(context).register();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCommand() {
|
||||
new FandTabCommand(getCommand()).register(context.commands());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startMetrics() {
|
||||
// Fand does not currently provide a metrics integration.
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public File getDataFolder() {
|
||||
return context.dataDirectory().toFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Object convertComponent(@NotNull TabComponent component) {
|
||||
return component.toAdventure();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Scoreboard createScoreboard(@NotNull TabPlayer player) {
|
||||
return new FandScoreboard((FandTabPlayer) player);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public BossBar createBossBar(@NotNull TabPlayer player) {
|
||||
return new AdventureBossBar(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public TabList createTabList(@NotNull TabPlayer player) {
|
||||
return new FandTabList((FandTabPlayer) player, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScoreboards() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCustomCommand(@NotNull String commandName,
|
||||
@NotNull BiConsumer<TabPlayer, String[]> function) {
|
||||
CommandRegistration registration = new FandCommand(commandName) {
|
||||
@Override
|
||||
protected void execute(@NotNull io.fand.api.command.CommandContext command,
|
||||
@NotNull String[] arguments) {
|
||||
if (!(command.sender() instanceof Player player)) {
|
||||
command.sender().sendMessage(TabComponent.fromColoredText(TAB.getInstance()
|
||||
.getConfiguration().getMessages().getCommandOnlyFromGame()).toAdventure());
|
||||
return;
|
||||
}
|
||||
TabPlayer tabPlayer = TAB.getInstance().getPlayer(player.uniqueId());
|
||||
if (tabPlayer != null) {
|
||||
function.accept(tabPlayer, arguments);
|
||||
}
|
||||
}
|
||||
}.register(context.commands());
|
||||
synchronized (customCommands) {
|
||||
customCommands.add(registration);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterAllCustomCommands() {
|
||||
synchronized (customCommands) {
|
||||
customCommands.forEach(CommandRegistration::unregister);
|
||||
customCommands.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public GroupManager detectPermissionPlugin() {
|
||||
return new GroupManager("Fand", player -> context.permissions()
|
||||
.primaryGroup(((FandTabPlayer) player).getPlayer())
|
||||
.orElse(TabConstants.NO_GROUP));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getTPS() {
|
||||
return Math.min(20.0, Fand.server().performance().oneSecond().ticksPerSecond().average());
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getMSPT() {
|
||||
return Fand.server().performance().currentMillisecondsPerTick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runSyncGlobal(@NotNull Runnable task) {
|
||||
context.scheduler().runMain(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLineOfSight(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
|
||||
return ((FandTabPlayer) viewer).getPlayer().lineOfSight(((FandTabPlayer) target).getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Object dump() {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("server-type", Fand.server().brand());
|
||||
data.put("server-version", Fand.server().version());
|
||||
data.put("minecraft-version", Fand.server().minecraftVersion());
|
||||
data.put("tab-version", ProjectVariables.PLUGIN_VERSION);
|
||||
List<String> plugins = Fand.server().plugins().loaded().stream()
|
||||
.map(plugin -> plugin.getClass().getName())
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.toList();
|
||||
data.put("plugins", plugins);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static String plainText(TabComponent message) {
|
||||
return LEGACY_FORMATTING.matcher(message.toLegacyText()).replaceAll("");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.packet.view.ClientboundSetDisplayObjectivePacketView;
|
||||
import io.fand.api.packet.view.ClientboundSetObjectivePacketView;
|
||||
import io.fand.api.packet.view.ClientboundSetPlayerTeamPacketView;
|
||||
import io.fand.api.scoreboard.PlayerScoreboard;
|
||||
import io.fand.api.scoreboard.ScoreDisplaySlot;
|
||||
import io.fand.api.scoreboard.ScoreNumberFormat;
|
||||
import io.fand.api.scoreboard.ScoreRenderType;
|
||||
import io.fand.api.scoreboard.ScoreboardObjective;
|
||||
import io.fand.api.scoreboard.ScoreboardTeam;
|
||||
import io.fand.api.scoreboard.TeamCollisionRule;
|
||||
import io.fand.api.scoreboard.TeamVisibility;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.chat.EnumChatFormat;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/** Per-viewer scoreboard implementation using Fand's public scoreboard API. */
|
||||
public final class FandScoreboard extends SafeScoreboard<FandTabPlayer> {
|
||||
|
||||
private static final NamedTextColor[] COLORS = {
|
||||
NamedTextColor.BLACK,
|
||||
NamedTextColor.DARK_BLUE,
|
||||
NamedTextColor.DARK_GREEN,
|
||||
NamedTextColor.DARK_AQUA,
|
||||
NamedTextColor.DARK_RED,
|
||||
NamedTextColor.DARK_PURPLE,
|
||||
NamedTextColor.GOLD,
|
||||
NamedTextColor.GRAY,
|
||||
NamedTextColor.DARK_GRAY,
|
||||
NamedTextColor.BLUE,
|
||||
NamedTextColor.GREEN,
|
||||
NamedTextColor.AQUA,
|
||||
NamedTextColor.RED,
|
||||
NamedTextColor.LIGHT_PURPLE,
|
||||
NamedTextColor.YELLOW,
|
||||
NamedTextColor.WHITE
|
||||
};
|
||||
|
||||
private final PlayerScoreboard scoreboard;
|
||||
|
||||
public FandScoreboard(@NotNull FandTabPlayer player) {
|
||||
super(player);
|
||||
scoreboard = player.getPlayer().scoreboard();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerObjective(@NotNull Objective objective) {
|
||||
scoreboard.registerObjective(
|
||||
objective.getName(),
|
||||
objective.getTitle().toAdventure(),
|
||||
ScoreRenderType.valueOf(objective.getHealthDisplay().name()));
|
||||
ScoreboardObjective registered = objective(objective.getName());
|
||||
registered.setNumberFormat(numberFormat(objective.getNumberFormat()));
|
||||
objective.setPlatformObjective(registered);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDisplaySlot(@NotNull Objective objective) {
|
||||
scoreboard.setDisplayedObjective(displaySlot(objective.getDisplaySlot()), objective(objective));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterObjective(@NotNull Objective objective) {
|
||||
scoreboard.removeObjective(objective.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateObjective(@NotNull Objective objective) {
|
||||
ScoreboardObjective target = objective(objective);
|
||||
target.setDisplayName(objective.getTitle().toAdventure());
|
||||
target.setRenderType(ScoreRenderType.valueOf(objective.getHealthDisplay().name()));
|
||||
target.setNumberFormat(numberFormat(objective.getNumberFormat()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScore(@NotNull Score score) {
|
||||
var target = objective(score.getObjective()).score(score.getHolder());
|
||||
target.setValue(score.getValue());
|
||||
target.setDisplayName(score.getDisplayName() == null ? null : score.getDisplayName().toAdventure());
|
||||
target.setNumberFormat(numberFormat(score.getNumberFormat()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeScore(@NotNull Score score) {
|
||||
objective(score.getObjective()).resetScore(score.getHolder());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Object createTeam(@NotNull String name) {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTeam(@NotNull Team team) {
|
||||
scoreboard.registerTeam(team.getName());
|
||||
ScoreboardTeam registered = team(team.getName());
|
||||
team.setPlatformTeam(registered);
|
||||
applyTeamProperties(team, registered);
|
||||
team.getPlayers().forEach(registered::addMember);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTeam(@NotNull Team team) {
|
||||
scoreboard.removeTeam(team.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTeam(@NotNull Team team) {
|
||||
applyTeamProperties(team, team(team));
|
||||
}
|
||||
|
||||
private void applyTeamProperties(Team source, ScoreboardTeam target) {
|
||||
target.setPrefix(source.getPrefix().toAdventure());
|
||||
target.setSuffix(source.getSuffix().toAdventure());
|
||||
target.setAllowFriendlyFire((source.getOptions() & 0x01) != 0);
|
||||
target.setSeeFriendlyInvisibles((source.getOptions() & 0x02) != 0);
|
||||
target.setNameTagVisibility(TeamVisibility.valueOf(source.getVisibility().name()));
|
||||
target.setCollisionRule(TeamCollisionRule.valueOf(source.getCollision().name()));
|
||||
target.setColor(color(source.getColor()));
|
||||
}
|
||||
|
||||
private ScoreboardObjective objective(Objective objective) {
|
||||
Object platformObjective = objective.getPlatformObjective();
|
||||
if (platformObjective instanceof ScoreboardObjective value) {
|
||||
return value;
|
||||
}
|
||||
ScoreboardObjective value = objective(objective.getName());
|
||||
objective.setPlatformObjective(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private ScoreboardObjective objective(String name) {
|
||||
return scoreboard.objective(name)
|
||||
.orElseThrow(() -> new IllegalStateException("Missing Fand scoreboard objective: " + name));
|
||||
}
|
||||
|
||||
private ScoreboardTeam team(Team team) {
|
||||
Object platformTeam = team.getPlatformTeam();
|
||||
if (platformTeam instanceof ScoreboardTeam value) {
|
||||
return value;
|
||||
}
|
||||
ScoreboardTeam value = team(team.getName());
|
||||
team.setPlatformTeam(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private ScoreboardTeam team(String name) {
|
||||
return scoreboard.team(name)
|
||||
.orElseThrow(() -> new IllegalStateException("Missing Fand scoreboard team: " + name));
|
||||
}
|
||||
|
||||
private static ScoreDisplaySlot displaySlot(DisplaySlot slot) {
|
||||
return slot == DisplaySlot.PLAYER_LIST ? ScoreDisplaySlot.LIST : ScoreDisplaySlot.valueOf(slot.name());
|
||||
}
|
||||
|
||||
private static ScoreNumberFormat numberFormat(TabComponent component) {
|
||||
return component == null ? ScoreNumberFormat.DEFAULT : ScoreNumberFormat.fixed(component.toAdventure());
|
||||
}
|
||||
|
||||
private static NamedTextColor color(EnumChatFormat color) {
|
||||
return color.isColor() ? COLORS[color.ordinal()] : null;
|
||||
}
|
||||
|
||||
void observeDisplayObjective(@NotNull ClientboundSetDisplayObjectivePacketView packet) {
|
||||
String objectiveName = packet.objectiveName();
|
||||
ScoreDisplaySlot slot = packet.value("slot", ScoreDisplaySlot.class);
|
||||
notifyDisplayObjective(slot, objectiveName);
|
||||
}
|
||||
|
||||
private void notifyDisplayObjective(ScoreDisplaySlot slot, String objectiveName) {
|
||||
TAB tab = TAB.getInstance();
|
||||
if (tab == null || tab.isPluginDisabled() || tab.getPlayer(player.getUniqueId()) != player) {
|
||||
return;
|
||||
}
|
||||
tab.getFeatureManager().onDisplayObjective(player, slot.ordinal(), objectiveName);
|
||||
}
|
||||
|
||||
void observeObjective(@NotNull ClientboundSetObjectivePacketView packet) {
|
||||
TAB.getInstance().getFeatureManager().onObjective(player, packet.method(), packet.objectiveName());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
ClientboundSetPlayerTeamPacketView rewriteTeam(@NotNull ClientboundSetPlayerTeamPacketView packet) {
|
||||
if (packet.method() == TeamAction.UPDATE) {
|
||||
return packet;
|
||||
}
|
||||
Collection<?> rawPlayers = packet.value("players", Collection.class);
|
||||
List<String> players = new ArrayList<>(rawPlayers.size());
|
||||
for (Object entry : rawPlayers) {
|
||||
if (!(entry instanceof String playerName)) {
|
||||
return packet;
|
||||
}
|
||||
players.add(playerName);
|
||||
}
|
||||
Collection<String> replacement = onTeamPacket(packet.method(), packet.name(), players);
|
||||
if (players.equals(replacement)) {
|
||||
return packet;
|
||||
}
|
||||
return packet.with(
|
||||
"players",
|
||||
List.copyOf(replacement),
|
||||
ClientboundSetPlayerTeamPacketView.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.plugin.Plugin;
|
||||
import io.fand.api.plugin.PluginContext;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
|
||||
/** Fand plugin entry point. */
|
||||
public final class FandTAB implements Plugin {
|
||||
|
||||
@Override
|
||||
public void onEnable(PluginContext context) {
|
||||
TAB.create(new FandPlatform(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(PluginContext context) {
|
||||
if (TAB.getInstance() != null) {
|
||||
TAB.getInstance().unload();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.command.CommandContext;
|
||||
import io.fand.api.entity.Player;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.TabConstants;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import me.neznamy.tab.shared.platform.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/** Handler for TAB's main command on Fand. */
|
||||
public final class FandTabCommand extends FandCommand {
|
||||
|
||||
public FandTabCommand(@NotNull String commandName) {
|
||||
super(commandName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandContext context, @NotNull String[] arguments) {
|
||||
if (TAB.getInstance().isPluginDisabled()) {
|
||||
boolean reload = context.sender().can(TabConstants.Permission.COMMAND_RELOAD);
|
||||
boolean admin = context.sender().can(TabConstants.Permission.COMMAND_ALL);
|
||||
for (TabComponent message : TAB.getInstance().getDisabledCommand().execute(arguments, reload, admin)) {
|
||||
context.sender().sendMessage(message.toAdventure());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.sender() instanceof Player player) {
|
||||
TabPlayer tabPlayer = TAB.getInstance().getPlayer(player.uniqueId());
|
||||
if (tabPlayer != null) {
|
||||
TAB.getInstance().getCommand().execute(tabPlayer, arguments);
|
||||
}
|
||||
} else {
|
||||
TAB.getInstance().getCommand().execute(null, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
protected List<String> complete(@NotNull CommandContext context, @NotNull String[] arguments) {
|
||||
TabPlayer tabPlayer = null;
|
||||
if (context.sender() instanceof Player player) {
|
||||
tabPlayer = TAB.getInstance().getPlayer(player.uniqueId());
|
||||
if (tabPlayer == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
return TAB.getInstance().getCommand().complete(tabPlayer, arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.entity.Player;
|
||||
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 me.neznamy.tab.api.placeholder.Placeholder;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.features.PlaceholderManagerImpl;
|
||||
import me.neznamy.tab.shared.placeholders.expansion.TabExpansion;
|
||||
import me.neznamy.tab.shared.placeholders.types.RelationalPlaceholderImpl;
|
||||
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 {
|
||||
|
||||
private static final String NAMESPACE_PREFIX = "tab_";
|
||||
private static final int MAX_REPLACEMENT_PASSES = 32;
|
||||
|
||||
private final PluginContext context;
|
||||
private final PlaceholderRegistration registration;
|
||||
|
||||
public FandTabExpansion(@NotNull PluginContext context) {
|
||||
this.context = context;
|
||||
registration = context.placeholders().register(
|
||||
"tab",
|
||||
PlaceholderProvider.contextual(this::resolve));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterExpansion() {
|
||||
registration.unregister();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String resolve(@NotNull PlaceholderContext placeholderContext, @NotNull String identifier) {
|
||||
if (!identifier.startsWith(NAMESPACE_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
String expansionIdentifier = identifier.substring(NAMESPACE_PREFIX.length());
|
||||
if (expansionIdentifier.startsWith("replace_")) {
|
||||
return resolveReplacement(placeholderContext, expansionIdentifier.substring("replace_".length()));
|
||||
}
|
||||
|
||||
Player viewer = placeholderContext.viewer();
|
||||
Player target = placeholderContext.target();
|
||||
if (target != null) {
|
||||
return resolveRelational(viewer, target, expansionIdentifier);
|
||||
}
|
||||
if (viewer == null) {
|
||||
return "<Player cannot be null>";
|
||||
}
|
||||
TabPlayer player = TAB.getInstance().getPlayer(viewer.uniqueId());
|
||||
if (player == null) {
|
||||
return "<Player is not loaded>";
|
||||
}
|
||||
if (expansionIdentifier.startsWith("placeholder_")) {
|
||||
String requestedPlaceholder = "%" + expansionIdentifier.substring("placeholder_".length()) + "%";
|
||||
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
|
||||
manager.addUsedPlaceholder(requestedPlaceholder, manager);
|
||||
return manager.getPlaceholderReference(requestedPlaceholder).getHandle().parse(player);
|
||||
}
|
||||
return player.expansionData.getValue(expansionIdentifier);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String resolveRelational(
|
||||
@Nullable Player viewer,
|
||||
@NotNull Player target,
|
||||
@NotNull String expansionIdentifier
|
||||
) {
|
||||
if (viewer == null) {
|
||||
return "<Player cannot be null>";
|
||||
}
|
||||
if (!expansionIdentifier.startsWith("placeholder_")) {
|
||||
return "<Unknown identifier: \"" + expansionIdentifier + "\">";
|
||||
}
|
||||
|
||||
String requestedPlaceholder = "%" + expansionIdentifier.substring("placeholder_".length()) + "%";
|
||||
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
|
||||
manager.addUsedPlaceholder(requestedPlaceholder, manager);
|
||||
Placeholder placeholder = manager.getPlaceholder(requestedPlaceholder);
|
||||
if (!(placeholder instanceof RelationalPlaceholderImpl relational)) {
|
||||
return "<Not a relational placeholder: " + requestedPlaceholder + ">";
|
||||
}
|
||||
|
||||
TabPlayer tabViewer = TAB.getInstance().getPlayer(viewer.uniqueId());
|
||||
TabPlayer tabTarget = TAB.getInstance().getPlayer(target.uniqueId());
|
||||
if (tabViewer == null || tabTarget == null) {
|
||||
return "<Player is not loaded>";
|
||||
}
|
||||
return relational.getLastValue(tabViewer, tabTarget);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String resolveReplacement(
|
||||
@NotNull PlaceholderContext placeholderContext,
|
||||
@NotNull String requestedIdentifier
|
||||
) {
|
||||
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
|
||||
return expandReplacement("%" + requestedIdentifier + "%", text -> {
|
||||
String expanded = text;
|
||||
for (String placeholder : PlaceholderManagerImpl.detectPlaceholders(text)) {
|
||||
String identifier = placeholder.substring(1, placeholder.length() - 1);
|
||||
String value = context.placeholders().resolve(identifier, placeholderContext).orElse(placeholder);
|
||||
expanded = expanded.replace(placeholder, manager.findReplacement(placeholder, value));
|
||||
}
|
||||
return expanded;
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static String expandReplacement(@NotNull String initial, @NotNull UnaryOperator<String> expansionPass) {
|
||||
Set<String> seen = new HashSet<>();
|
||||
String text = initial;
|
||||
for (int pass = 0; pass < MAX_REPLACEMENT_PASSES && seen.add(text); pass++) {
|
||||
String expanded = expansionPass.apply(text);
|
||||
if (expanded.equals(text)) {
|
||||
return text;
|
||||
}
|
||||
text = expanded;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.Fand;
|
||||
import io.fand.api.entity.GameMode;
|
||||
import io.fand.api.entity.Player;
|
||||
import io.fand.api.packet.PlayerInfoEntry;
|
||||
import io.fand.api.packet.view.ClientboundPlayerInfoRemovePacketView;
|
||||
import io.fand.api.packet.view.ClientboundPlayerInfoUpdatePacketView;
|
||||
import io.fand.api.packet.view.ClientboundTabListPacketView;
|
||||
import io.fand.api.player.PlayerProfile;
|
||||
import io.fand.api.player.PlayerSkin;
|
||||
import io.fand.api.plugin.PluginContext;
|
||||
import io.fand.api.tablist.TabListEntry;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
|
||||
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/** Per-viewer tab list implementation using Fand's structured packet API. */
|
||||
public final class FandTabList extends TrackedTabList<FandTabPlayer> {
|
||||
|
||||
private final PluginContext context;
|
||||
private final ConcurrentMap<UUID, TabListEntry> state = new ConcurrentHashMap<>();
|
||||
private final Set<UUID> managedEntries = ConcurrentHashMap.newKeySet();
|
||||
private final Set<UUID> observedEntries = ConcurrentHashMap.newKeySet();
|
||||
private final Set<UUID> removedEntries = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public FandTabList(@NotNull FandTabPlayer player, @NotNull PluginContext context) {
|
||||
super(player);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeEntry(@NotNull UUID entry) {
|
||||
state.remove(entry);
|
||||
managedEntries.remove(entry);
|
||||
observedEntries.remove(entry);
|
||||
markRemoved(entry);
|
||||
context.packets().sender().send(
|
||||
player.getPlayer(),
|
||||
context.packets().playerInfo().remove(List.of(entry)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDisplayName0(@NotNull UUID entry, @Nullable TabComponent displayName) {
|
||||
update(
|
||||
entry,
|
||||
value -> value.withDisplayName(displayName == null ? null : displayName.toAdventure()),
|
||||
"UPDATE_DISPLAY_NAME");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLatency(@NotNull UUID entry, int latency) {
|
||||
update(entry, value -> value.withLatency(latency), "UPDATE_LATENCY");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateGameMode(@NotNull UUID entry, int gameMode) {
|
||||
update(entry, value -> value.withGameMode(gameMode(gameMode)), "UPDATE_GAME_MODE");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateListed(@NotNull UUID entry, boolean listed) {
|
||||
update(entry, value -> value.withListed(listed), "UPDATE_LISTED");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateListOrder(@NotNull UUID entry, int listOrder) {
|
||||
update(entry, value -> value.withOrder(listOrder), "UPDATE_LIST_ORDER");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateHat(@NotNull UUID entry, boolean showHat) {
|
||||
update(entry, value -> value.withShowHat(showHat), "UPDATE_HAT");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addEntry0(@NotNull Entry entry) {
|
||||
PlayerSkin skin = entry.getSkin() == null
|
||||
? null
|
||||
: new PlayerSkin(entry.getSkin().getValue(), entry.getSkin().getSignature());
|
||||
PlayerProfile profile = new PlayerProfile(entry.getUniqueId(), entry.getName(), skin);
|
||||
TabListEntry fandEntry = TabListEntry.builder(profile)
|
||||
.listed(entry.isListed())
|
||||
.latency(entry.getLatency())
|
||||
.gameMode(gameMode(entry.getGameMode()))
|
||||
.displayName(entry.getDisplayName() == null ? null : entry.getDisplayName().toAdventure())
|
||||
.order(entry.getListOrder())
|
||||
.showHat(entry.isShowHat())
|
||||
.build();
|
||||
state.put(entry.getUniqueId(), fandEntry);
|
||||
managedEntries.add(entry.getUniqueId());
|
||||
observedEntries.add(entry.getUniqueId());
|
||||
removedEntries.remove(entry.getUniqueId());
|
||||
context.packets().sender().send(
|
||||
player.getPlayer(),
|
||||
context.packets().playerInfo().add(List.of(fandEntry)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayerListHeaderFooter0(@NotNull TabComponent header, @NotNull TabComponent footer) {
|
||||
player.getPlayer().sendTabList(header.toAdventure(), footer.toAdventure());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Skin getSkin() {
|
||||
return player.getPlayer().skin()
|
||||
.map(skin -> new Skin(skin.value(), skin.signatureOrNull()))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsEntry(@NotNull UUID entry) {
|
||||
return getEntries().contains(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Collection<UUID> getEntries() {
|
||||
Set<UUID> entries = new LinkedHashSet<>();
|
||||
Player viewer = player.getPlayer();
|
||||
for (Player target : Fand.server().players()) {
|
||||
UUID targetId = target.uniqueId();
|
||||
if (!removedEntries.contains(targetId) && context.tabLists().visible(viewer, target)) {
|
||||
entries.add(targetId);
|
||||
}
|
||||
}
|
||||
entries.addAll(observedEntries);
|
||||
entries.addAll(managedEntries);
|
||||
entries.removeAll(removedEntries);
|
||||
return List.copyOf(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Object onPacketSend(@NotNull Object packet) {
|
||||
if (packet instanceof ClientboundPlayerInfoUpdatePacketView playerInfo) {
|
||||
return rewritePlayerInfo(playerInfo);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
ClientboundPlayerInfoUpdatePacketView rewritePlayerInfo(
|
||||
@NotNull ClientboundPlayerInfoUpdatePacketView packet
|
||||
) {
|
||||
List<String> actions = packet.actions();
|
||||
List<PlayerInfoEntry> entries = new ArrayList<>(packet.entries().size());
|
||||
Integer forcedLatency = getForcedLatency();
|
||||
boolean rewrite = false;
|
||||
for (PlayerInfoEntry entry : packet.entries()) {
|
||||
PlayerInfoEntry updated = entry;
|
||||
UUID profileId = entry.profileId();
|
||||
if (actions.contains("UPDATE_DISPLAY_NAME")) {
|
||||
TabComponent forcedName = getForcedDisplayNames().get(profileId);
|
||||
if (forcedName != null && !Objects.equals(forcedName.toAdventure(), updated.displayName())) {
|
||||
updated = updated.withDisplayName(forcedName.toAdventure());
|
||||
}
|
||||
}
|
||||
if (actions.contains("UPDATE_GAME_MODE")
|
||||
&& getBlockedSpectators().contains(profileId)
|
||||
&& updated.gameMode() == GameMode.SPECTATOR) {
|
||||
updated = updated.withGameMode(GameMode.SURVIVAL);
|
||||
}
|
||||
if (actions.contains("UPDATE_LATENCY") && forcedLatency != null
|
||||
&& updated.latency() != forcedLatency) {
|
||||
updated = updated.withLatency(forcedLatency);
|
||||
}
|
||||
if (actions.contains("UPDATE_LISTED") && allPlayersHidden &&
|
||||
!LayoutManagerImpl.UUIDS_SET.contains(profileId) && updated.listed()) {
|
||||
updated = updated.withListed(false);
|
||||
}
|
||||
if (actions.contains("ADD_PLAYER")) {
|
||||
observedEntries.add(profileId);
|
||||
removedEntries.remove(profileId);
|
||||
if (entry.profile() != null) {
|
||||
TAB.getInstance().getFeatureManager().onEntryAdd(
|
||||
player, profileId, entry.profile().name());
|
||||
}
|
||||
}
|
||||
rewrite |= updated != entry;
|
||||
entries.add(updated);
|
||||
}
|
||||
return rewrite ? packet.withEntries(List.copyOf(entries)) : packet;
|
||||
}
|
||||
|
||||
void observePlayerInfoRemove(@NotNull ClientboundPlayerInfoRemovePacketView packet) {
|
||||
for (Object value : packet.profileIds()) {
|
||||
if (value instanceof UUID profileId) {
|
||||
state.remove(profileId);
|
||||
managedEntries.remove(profileId);
|
||||
observedEntries.remove(profileId);
|
||||
markRemoved(profileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markRemoved(UUID profileId) {
|
||||
updateRemovalState(removedEntries, profileId, Fand.server().player(profileId).isPresent());
|
||||
}
|
||||
|
||||
static void updateRemovalState(Set<UUID> removedEntries, UUID profileId, boolean onlineRealPlayer) {
|
||||
if (onlineRealPlayer) {
|
||||
removedEntries.add(profileId);
|
||||
} else {
|
||||
removedEntries.remove(profileId);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
ClientboundTabListPacketView rewriteHeaderFooter(@NotNull ClientboundTabListPacketView packet) {
|
||||
if (header == null || footer == null) {
|
||||
return packet;
|
||||
}
|
||||
Component expectedHeader = header.toAdventure();
|
||||
Component expectedFooter = footer.toAdventure();
|
||||
Component currentHeader = packet.value("header", Component.class);
|
||||
Component currentFooter = packet.value("footer", Component.class);
|
||||
if (Objects.equals(currentHeader, expectedHeader) && Objects.equals(currentFooter, expectedFooter)) {
|
||||
return packet;
|
||||
}
|
||||
return packet.with("header", expectedHeader)
|
||||
.with("footer", expectedFooter)
|
||||
.as(ClientboundTabListPacketView.class);
|
||||
}
|
||||
|
||||
private void update(UUID entryId, UnaryOperator<TabListEntry> operation, String action) {
|
||||
TabListEntry updated = state.compute(entryId, (ignored, current) ->
|
||||
operation.apply(current == null ? currentEntry(entryId) : current));
|
||||
context.packets().sender().send(
|
||||
player.getPlayer(),
|
||||
context.packets().playerInfo().update(List.of(updated))
|
||||
.with("actions", List.of(action)));
|
||||
}
|
||||
|
||||
private static TabListEntry currentEntry(UUID entryId) {
|
||||
return Fand.server().player(entryId)
|
||||
.map(FandTabList::realPlayerEntry)
|
||||
.orElseGet(() -> TabListEntry.builder(entryId, entryId.toString().substring(0, 16)).build());
|
||||
}
|
||||
|
||||
private static TabListEntry realPlayerEntry(Player player) {
|
||||
return TabListEntry.builder(player.profile())
|
||||
.listed(true)
|
||||
.latency(player.ping())
|
||||
.gameMode(player.gameMode())
|
||||
.displayName(player.tabListDisplayName().orElse(null))
|
||||
.order(player.tabListOrder())
|
||||
.showHat(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static GameMode gameMode(int gameMode) {
|
||||
GameMode[] values = GameMode.values();
|
||||
return gameMode >= 0 && gameMode < values.length ? values[gameMode] : GameMode.SURVIVAL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package me.neznamy.tab.platforms.fand;
|
||||
|
||||
import io.fand.api.entity.AttributeKey;
|
||||
import io.fand.api.entity.GameMode;
|
||||
import io.fand.api.entity.Player;
|
||||
import io.fand.api.visibility.DisguiseService;
|
||||
import io.fand.api.visibility.VanishService;
|
||||
import me.neznamy.tab.shared.backend.BackendTabPlayer;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import me.neznamy.tab.shared.platform.TabPlayer;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/** TAB player backed by Fand's public player API. */
|
||||
public final class FandTabPlayer extends BackendTabPlayer {
|
||||
|
||||
private static final Key INVISIBILITY = Key.key("minecraft", "invisibility");
|
||||
private static final Key DEATHS = Key.key("minecraft", "deaths");
|
||||
|
||||
public FandTabPlayer(@NotNull FandPlatform platform, @NotNull Player player) {
|
||||
super(platform, player, player.uniqueId(), player.name(), player.world().name(), platform.protocolVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@NotNull String permission) {
|
||||
return getPlayer().can(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPing() {
|
||||
return getPlayer().ping();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@NotNull TabComponent message) {
|
||||
getPlayer().sendMessage(message.toAdventure());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasInvisibilityPotion() {
|
||||
return getPlayer().effect(INVISIBILITY).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisguised() {
|
||||
DisguiseService service = getPlatform().disguiseService();
|
||||
return service != null && service.disguised(getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Player getPlayer() {
|
||||
return (Player) player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FandPlatform getPlatform() {
|
||||
return (FandPlatform) platform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBelowNameDistance(double distance) {
|
||||
getPlayer().attribute(AttributeKey.BELOW_NAME_DISTANCE).ifPresent(attr -> attr.setBaseValue(distance));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVanished0() {
|
||||
VanishService service = getPlatform().vanishService();
|
||||
return service != null && service.vanished(getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSee(@NotNull TabPlayer target) {
|
||||
if (target == this) {
|
||||
return true;
|
||||
}
|
||||
VanishService service = getPlatform().vanishService();
|
||||
if (service != null && target instanceof FandTabPlayer fandTarget) {
|
||||
return service.canSee(getPlayer(), fandTarget.getPlayer());
|
||||
}
|
||||
return super.canSee(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeaths() {
|
||||
return getPlayer().statistic(DEATHS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGamemode() {
|
||||
GameMode gameMode = getPlayer().gameMode();
|
||||
return gameMode.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getHealth() {
|
||||
return getPlayer().health();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return getPlayer().name();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"id": "@id@",
|
||||
"version": "@version@",
|
||||
"mainClass": "me.neznamy.tab.platforms.fand.FandTAB",
|
||||
"description": "@description@",
|
||||
"website": "@website@",
|
||||
"license": "Apache-2.0",
|
||||
"apiVersion": "0.1.1",
|
||||
"authors": ["@author@"],
|
||||
"depends": [],
|
||||
"loadAfter": ["luckperms", "viaversion", "floodgate"],
|
||||
"loadBefore": [],
|
||||
"permissions": [
|
||||
{
|
||||
"node": "tab.admin",
|
||||
"defaultAccess": "OPERATOR",
|
||||
"children": {
|
||||
"tab.tabcomplete": true,
|
||||
"tab.reload": true,
|
||||
"tab.cpu": true,
|
||||
"tab.debug": true,
|
||||
"tab.dump": true,
|
||||
"tab.groupinfo": true,
|
||||
"tab.grouplist": true,
|
||||
"tab.parse": true,
|
||||
"tab.setcollision": true,
|
||||
"tab.remove": true,
|
||||
"tab.change.*": true,
|
||||
"tab.announce.bar": true,
|
||||
"tab.bossbar.toggle": true,
|
||||
"tab.bossbar.toggle.other": true,
|
||||
"tab.send.bar": true,
|
||||
"tab.mysql.download": true,
|
||||
"tab.mysql.upload": true,
|
||||
"tab.nametag.visibility": true,
|
||||
"tab.nametag.visibility.other": true,
|
||||
"tab.nametag.view": true,
|
||||
"tab.nametag.view.other": true,
|
||||
"tab.announce.scoreboard": true,
|
||||
"tab.scoreboard.toggle": true,
|
||||
"tab.scoreboard.toggle.other": true,
|
||||
"tab.scoreboard.show": true,
|
||||
"tab.scoreboard.show.other": true,
|
||||
"tab.staff": true,
|
||||
"tab.group.*": true,
|
||||
"tab.bypass": true,
|
||||
"tab.seevanished": true,
|
||||
"tab.spectatorbypass": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,11 +15,12 @@ repositories {
|
||||
maven("https://repo.papermc.io/repository/maven-public/") // Adventure
|
||||
}
|
||||
|
||||
val minecraftVersion = "26.1.2"
|
||||
val minecraftVersion = "26.2"
|
||||
|
||||
// Forge API versions for each Minecraft version for easier backporting
|
||||
// Official website (for updating in the future): https://files.minecraftforge.net/net/minecraftforge/forge/
|
||||
val forgeApiVersions = mapOf(
|
||||
"26.2" to "26.2-65.0.0",
|
||||
"26.1.2" to "26.1.2-64.0.4"
|
||||
)
|
||||
|
||||
|
||||
@@ -262,6 +262,16 @@ public record ForgePlatform(MinecraftServer server) implements BackendPlatform {
|
||||
return (float) server.getAverageTickTimeNanos() / 1000000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runSyncGlobal(@NotNull Runnable task) {
|
||||
server.execute(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLineOfSight(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
|
||||
return ((ForgeTabPlayer) viewer).getPlayer().hasLineOfSight(((ForgeTabPlayer) target).getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Object dump() {
|
||||
@@ -271,7 +281,7 @@ public record ForgePlatform(MinecraftServer server) implements BackendPlatform {
|
||||
map.put("server-version", SharedConstants.getCurrentVersion().name());
|
||||
map.put("tab-version", ProjectVariables.PLUGIN_VERSION);
|
||||
Map<String, Object> mods = new LinkedHashMap<>();
|
||||
IModInfo[] modArray = ModList.get().getMods().toArray(new IModInfo[0]);
|
||||
IModInfo[] modArray = ModList.getMods().toArray(new IModInfo[0]);
|
||||
Arrays.sort(modArray, Comparator.comparing(IModInfo::getModId, String.CASE_INSENSITIVE_ORDER));
|
||||
for (IModInfo mod : modArray) {
|
||||
mods.put(mod.getModId(), mod.getVersion().toString());
|
||||
|
||||
@@ -5,17 +5,18 @@ import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
|
||||
import me.neznamy.tab.shared.util.ReflectionUtils;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.numbers.FixedFormat;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.*;
|
||||
import net.minecraft.world.scores.PlayerTeam;
|
||||
import net.minecraft.world.scores.Scoreboard;
|
||||
import net.minecraft.world.scores.TeamColor;
|
||||
import net.minecraft.world.scores.criteria.ObjectiveCriteria;
|
||||
import net.minecraft.world.scores.criteria.ObjectiveCriteria.RenderType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -24,7 +25,7 @@ import java.util.Optional;
|
||||
*/
|
||||
public class ForgeScoreboard extends SafeScoreboard<ForgeTabPlayer> {
|
||||
|
||||
private static final ChatFormatting[] formats = ChatFormatting.values();
|
||||
private static final Optional<TeamColor>[] colors = Arrays.stream(TeamColor.values()).map(Optional::of).toArray(Optional[]::new);
|
||||
private static final net.minecraft.world.scores.Team.CollisionRule[] collisions = net.minecraft.world.scores.Team.CollisionRule.values();
|
||||
private static final net.minecraft.world.scores.Team.Visibility[] visibilities = net.minecraft.world.scores.Team.Visibility.values();
|
||||
private static final Scoreboard dummyScoreboard = new Scoreboard();
|
||||
@@ -118,7 +119,7 @@ public class ForgeScoreboard extends SafeScoreboard<ForgeTabPlayer> {
|
||||
PlayerTeam t = (PlayerTeam) team.getPlatformTeam();
|
||||
t.setAllowFriendlyFire((team.getOptions() & 0x01) != 0);
|
||||
t.setSeeFriendlyInvisibles((team.getOptions() & 0x02) != 0);
|
||||
t.setColor(formats[team.getColor().ordinal()]);
|
||||
t.setColor(colors[Math.min(team.getColor().ordinal(), colors.length - 1)]);
|
||||
t.setCollisionRule(collisions[team.getCollision().ordinal()]);
|
||||
t.setNameTagVisibility(visibilities[team.getVisibility().ordinal()]);
|
||||
t.setPlayerPrefix(team.getPrefix().convert());
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import me.neznamy.tab.shared.TAB;
|
||||
import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
|
||||
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
|
||||
import me.neznamy.tab.shared.util.ReflectionUtils;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -142,7 +143,7 @@ public class ForgeTabList extends TrackedTabList<ForgeTabPlayer> {
|
||||
}
|
||||
}
|
||||
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
|
||||
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
|
||||
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
|
||||
listed = false;
|
||||
rewriteEntry = rewritePacket = true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import me.neznamy.tab.shared.chat.component.TabComponent;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
@@ -63,6 +64,11 @@ public class ForgeTabPlayer extends BackendTabPlayer {
|
||||
return (ForgePlatform) platform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBelowNameDistance(double distance) {
|
||||
getPlayer().getAttribute(Attributes.BELOW_NAME_DISTANCE).setBaseValue(distance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVanished0() {
|
||||
if (VanishHook.isInstalled()) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
public class LuckPermsAPIHook {
|
||||
|
||||
/** Flag tracking presence of LuckPerms API */
|
||||
private static final boolean luckPerms = ModList.get().isLoaded("luckperms");
|
||||
private static final boolean luckPerms = ModList.isLoaded("luckperms");
|
||||
|
||||
/**
|
||||
* Checks for permission and returns the result.
|
||||
|
||||
@@ -19,7 +19,7 @@ public class PlaceholderAPIHook {
|
||||
|
||||
static {
|
||||
try {
|
||||
if (ModList.get().isLoaded("forgeplaceholderapi")) {
|
||||
if (ModList.isLoaded("forgeplaceholderapi")) {
|
||||
replaceIdentifiers = Class.forName("com.envyful.papi.api.util.UtilPlaceholder").getMethod("replaceIdentifiers", Object.class, String.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -20,7 +20,7 @@ public class VanishHook {
|
||||
|
||||
static {
|
||||
try {
|
||||
if (ModList.get().isLoaded("vmod")) {
|
||||
if (ModList.isLoaded("vmod")) {
|
||||
isVanished = Class.forName("redstonedubstep.mods.vanishmod.VanishUtil").getMethod("isVanished", Player.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -15,6 +15,6 @@ displayURL = "@website@"
|
||||
[[dependencies.tab]]
|
||||
modId = "minecraft"
|
||||
mandatory = true
|
||||
versionRange = "[26.1, 26.1.2]"
|
||||
versionRange = "[26.2]"
|
||||
ordering = "NONE"
|
||||
side = "SERVER"
|
||||