Compare commits

...
115 Commits
Author SHA1 Message Date
NEZNAMY 827197f916 5.1.1 2025-03-31 16:52:47 +02:00
NEZNAMY 29a35cf1cc [ViaVersion hook] Decrease check interval from 100 to 50 2025-03-30 18:51:39 +02:00
NEZNAMY f26516ecae [Code style] Separate objective register and set display slot functions 2025-03-30 18:47:39 +02:00
NEZNAMY f26a7ad2b1 [Bukkit] Add direct NMS hook for v1_8_R3 and v1_12_R1 2025-03-30 17:46:23 +02:00
NEZNAMY 96e0a29573 Merge pull request #1464 from MineLAT/fix/viaversion-delay
Fix ViaVersion packets delay
2025-03-29 17:12:29 +01:00
NEZNAMY f30d667ca8 [Code style] Add return value annotations and remove unused methods in ConfigurationFile 2025-03-29 17:05:53 +01:00
Rubén 8db7a1d6d9 Fix ViaVersion packets delay 2025-03-29 10:04:46 -06:00
NEZNAMY d285a0512e [Multi-server support] Provide some feedback about connection status (success/fail) (doesn't work for redis) 2025-03-29 16:52:13 +01:00
NEZNAMY 256d0d5c84 Revert "[Placeholders] Parse nested PAPI placeholders using {}"
This reverts commit 6501480980.
2025-03-29 09:57:02 +01:00
NEZNAMY b808e1f9d1 [Bukkit] Refactor scoreboard / tablist implementation finding for players 2025-03-29 09:55:52 +01:00
NEZNAMY 07cd781ec1 [Sponge] Drop Sponge 7 module 2025-03-28 15:43:37 +01:00
NEZNAMY 9115b687f4 [Sponge] Drop support for sponge versions 1.16.5 - 1.20.5 2025-03-27 18:51:28 +01:00
NEZNAMY 7c51377203 [Bukkit] Update paper submodule for 1.21.5 and allow it there 2025-03-27 18:24:02 +01:00
NEZNAMY 193cd02072 [Configuration] Fix startup error when some config options were missing 2025-03-27 16:49:01 +01:00
NEZNAMY dbb6d91f0c [Configuration] Add missing playerlist-objective.render-type option to default config 2025-03-26 19:48:20 +01:00
NEZNAMY a82714e811 [BungeeCord] Fix error (#1462) 2025-03-26 19:00:53 +01:00
NEZNAMY bcd1bbac62 5.1.1-SNAPSHOT 2025-03-26 18:53:58 +01:00
NEZNAMY 2fde0e68ca 5.1.0 2025-03-26 17:53:56 +01:00
NEZNAMY e6e9b1c5ae Add 1.21.5 protocol constant 2025-03-25 18:46:53 +01:00
NEZNAMY 48b789c51d [Fabric] Update to 1.21.5 2025-03-25 18:36:41 +01:00
NEZNAMY 433b39209d [Fabric] Remove server version detection as it is no longer used 2025-03-24 18:26:56 +01:00
NEZNAMY b6df7a9f33 [Code style] Improve parity between Fabric and Paper Scoreboard implementations 2025-03-24 17:16:54 +01:00
NEZNAMY ad1d55537f [Code style] Improve parity between Fabric and Paper TabList implementations 2025-03-24 17:02:35 +01:00
NEZNAMY 9982d2c6ef [Fabric] Update to 1.21.5-rc2 2025-03-24 16:32:17 +01:00
NEZNAMY 1e3f622a98 Remove server version checks for new content as everything is now supported thanks to new ViaVersion hook 2025-03-24 16:26:31 +01:00
NEZNAMY ce05d5f6a6 Merge pull request #1443 from MineLAT/feat/viaversion-packets
Add ViaVersion packets support
2025-03-24 16:15:50 +01:00
NEZNAMY 19378e2095 [RGB] Fix gradient pattern for <#RRGGBB></#RRGGBB> to not capture a newer color used inside of it (caused by using one of MiniMessage's RGB syntaxes) 2025-03-23 18:47:57 +01:00
Rubén 0d992f1e50 Execute delayed tasks at once 2025-03-23 10:39:10 -06:00
Rubén de7cabfd54 Add missing method call 2025-03-23 09:31:10 -06:00
Rubén cfe73a00ee Fix early packet send on viaversion decoder 2025-03-23 09:06:11 -06:00
Rubén 35a7acbbd9 Merge remote-tracking branch 'origin/master' into feat/viaversion-packets
# Conflicts:
#	bukkit/src/main/java/me/neznamy/tab/platforms/bukkit/BukkitTabPlayer.java
#	bukkit/src/main/java/me/neznamy/tab/platforms/bukkit/platform/BukkitPlatform.java
2025-03-23 08:53:00 -06:00
NEZNAMY 20eebca395 [Belowname & Playerlist objective] Add startup warn if configured "value" will never evaluate to a number 2025-03-23 15:47:32 +01:00
NEZNAMY 7c2a4deb1a [Code style] Move getSkin() method to TabList 2025-03-23 13:40:27 +01:00
NEZNAMY e227e6a0f4 [Bukkit] Add back Paper submodule, but only for the latest MC version instead of an actual supported range 2025-03-23 13:09:30 +01:00
NEZNAMY e015c29a3d [Code style] Move legacy color rgb codes to TextColor class 2025-03-23 11:26:13 +01:00
NEZNAMY a2859937e2 [Bukkit] [Performance] Avoid using reflection for getting online players on 1.8+ 2025-03-22 19:53:59 +01:00
NEZNAMY cd26252444 [Scoreboard] Fix error when more than 21 lines are defined (will now error at 100) 2025-03-22 10:42:03 +01:00
NEZNAMY 47894a6e74 [Performance] Tiny performance boost of %luckperms-prefix% and suffix placeholders 2025-03-20 18:37:37 +01:00
NEZNAMY bcf94180d5 [Fabric] Update Fabric to 1.21.5-rc1 2025-03-20 18:33:00 +01:00
NEZNAMY 6d2eeda28c Merge pull request #1457 from Radu-Voinea/master2
Added %luckperms-weight% as a built-in placeholder
2025-03-20 12:09:57 +01:00
Radu Voinea 049c768013 Moved player lookup from LP to TabPlayer 2025-03-20 13:05:53 +02:00
Radu Voinea ffae488d1e Removed auto-formatted text 2025-03-20 12:54:36 +02:00
NEZNAMY 3ea0ae6e5b [Performance] Create a constant for empty component to avoid recreating it over and over 2025-03-19 17:54:56 +01:00
NEZNAMY 530c25655c [Error logging] Add missing " into error message 2025-03-19 17:44:03 +01:00
NEZNAMY ca830fd64c [Playerlist Objective] Make render type explicitly configurable 2025-03-19 17:42:16 +01:00
NEZNAMY 4272476230 [Playerlist Objective] Make title configurable (#1454) 2025-03-19 17:05:26 +01:00
NEZNAMY b7e44246ce [Fabric] Update to 1.21.5-pre3 2025-03-18 16:54:18 +01:00
NEZNAMY 33b9578fd1 [BossBar] Add %bossbar_announce_time_left_<bossbar>% and %bossbar_announce_time_total_<bossbar>% placeholders 2025-03-16 15:03:59 +01:00
NEZNAMY df0a96843d [Error Logging] Fix typo 2025-03-16 15:00:32 +01:00
NEZNAMY 5b5f7dfc12 [Code style] Remove unused fields 2025-03-15 16:49:21 +01:00
NEZNAMY 8b094adcca [Nametags] Add a lot more subcommands for manipulating nametag visibility 2025-03-15 16:48:34 +01:00
NEZNAMY de9d9ab844 [Nametags] Refactor classes and create enum for nametag invisibility reason 2025-03-13 18:08:17 +01:00
NEZNAMY 2fd6d2f50b [Fabric] Update to 1.21.5-pre2 2025-03-13 16:35:37 +01:00
Rubén 349c9eca4b Rename classes for more consistency 2025-03-12 18:18:09 -06:00
Rubén 1f119233d7 Move via module into bukkit module 2025-03-11 18:47:46 -06:00
Rubén 17c2d63892 Use instance method 2025-03-11 14:06:13 -06:00
NEZNAMY 79c76e913d [Fabric] Update to 1.21.5-pre1 2025-03-11 17:06:48 +01:00
NEZNAMY 6501480980 [Placeholders] Parse nested PAPI placeholders using {} 2025-03-08 13:40:58 +01:00
NEZNAMY 5ad9bbba37 [Global playerlist] Simplify expression (servers are never equal) 2025-03-08 09:44:31 +01:00
NEZNAMY f012448758 [Description] Improve image generator script 2025-03-07 20:25:11 +01:00
Rubén 99b6d635d7 Remove unchecked object handling 2025-03-07 08:37:14 -06:00
Rubén 5f742627e9 Avoid component italic format being reset 2025-03-07 07:58:19 -06:00
Rubén a9e725002a Use simple text component 2025-03-07 07:51:16 -06:00
Rubén 49150f9b1e Remove ViaVersion configuration support 2025-03-06 22:43:49 -06:00
Rubén 89aa1ba94c Fix class cast 2025-03-06 22:32:14 -06:00
Rubén 6aca313bd8 Use dummy object 2025-03-06 22:31:49 -06:00
Rubén f76d5a958f Fix component writing on newer versions 2025-03-06 22:15:51 -06:00
NEZNAMY b833a10de4 [Fabric] Update to 25w10a 2025-03-05 17:14:38 +01:00
Rubén f3ce5632fb Add safe check for ViaVersion internals usage 2025-03-05 07:54:09 -06:00
Rubén f5bf4c3831 Merge component conversion methods 2025-03-04 07:27:01 -06:00
Rubén 08a3acc5e0 Use direct return for viaversion providers 2025-03-04 07:26:13 -06:00
Rubén 5b63034607 Apply component style on compatible components 2025-03-03 07:36:33 -06:00
Rubén d4543032d9 Use the correct annotation 2025-03-02 09:25:52 -06:00
Rubén 254a91c3bd Fix legacy text check 2025-03-02 09:18:46 -06:00
NEZNAMY 4c01ddaaef [Description] Remove now no longer used images 2025-03-02 14:10:06 +01:00
NEZNAMY 2993d1450a [Description] Change text into code-generated images 2025-03-02 13:49:37 +01:00
Rubén 582c3a4c1a Initial ViaVersion packets support 2025-03-01 16:31:24 -06:00
NEZNAMY dd6b964405 [Fabric] Remove support for all versions below 1.21.4 2025-03-01 18:31:50 +01:00
NEZNAMY 16827bfbac Update references to version 5.1.0 2025-03-01 18:05:53 +01:00
NEZNAMY 3c4d72bb71 Bump version to 5.1.0-SNAPSHOT 2025-03-01 17:26:30 +01:00
NEZNAMY 004036e019 [BungeeCord / Velocity] Read server list from configuration for %online_<server>% placeholders instead of dynamically registering them when used 2025-03-01 16:15:03 +01:00
NEZNAMY ba06f80baa [BungeeCord / Velocity] Block bridge communication messages from player to avoid exploits 2025-03-01 14:07:50 +01:00
NEZNAMY ee47428a80 [Floodgate hook] Remove warning when floodgate is not configured correctly 2025-03-01 09:10:23 +01:00
NEZNAMY 1e40cae27b [Wiki] Monthly wiki backup for March 2025 2025-03-01 08:46:19 +01:00
NEZNAMY f5b7bb5549 [Belowname] Rename messages to use belowname "value" instead of "number" 2025-02-28 16:58:57 +01:00
NEZNAMY 115883f8d0 [Fabric] Update to 25w09b 2025-02-28 16:57:37 +01:00
NEZNAMY a73446a1c5 [Placeholders] Make LP placeholders also use configured value instead of permission refresh interval 2025-02-27 18:45:37 +01:00
NEZNAMY 7501c4f76b [Commands] Make placeholder refresh interval color in cpu output different if it was overridden in config 2025-02-27 18:40:46 +01:00
NEZNAMY 9f038fae63 [Placeholders] Allow placeholder refresh intervals to define overrides for internal placeholders 2025-02-27 18:21:49 +01:00
NEZNAMY a61a520570 [Fabric] Update to 25w09a 2025-02-26 18:27:51 +01:00
NEZNAMY 885783cd9a [Bukkit] Refactor feature provider loaders 2025-02-25 17:16:54 +01:00
NEZNAMY 181f850c58 Fix compilation 2025-02-23 17:39:25 +01:00
NEZNAMY fe8742b580 Merge pull request #1435 from MineLAT/feat/proxy-support
Expand multi-server support
2025-02-23 17:11:33 +01:00
Rubén daeba7caa9 Remove unused belowname code 2025-02-23 10:01:29 -06:00
NEZNAMY 1340e6dc1b [Code style] [Bukkit] Refactor team packet data 2025-02-23 09:24:27 +01:00
NEZNAMY cba7655f0d [Code style] Move method into a different class 2025-02-22 18:32:31 +01:00
NEZNAMY ac7e2a9aef [RGB] Fully support magic codes inside gradients, they no longer must all be at the beginning 2025-02-22 18:27:33 +01:00
Rubén fcc0bfbf32 Optimize dependencies 2025-02-22 10:40:58 -06:00
NEZNAMY a7e86bdb64 [Fabric] Update to 25w08a 2025-02-19 17:57:19 +01:00
Rubén c3eecd8d14 Remove duplicated check 2025-02-18 10:39:48 -06:00
Rubén 25e6da3ac5 Add safe check for already connected players 2025-02-18 10:37:49 -06:00
Rubén a7e5786fee Use global player list instead 2025-02-18 09:56:48 -06:00
Rubén a6bccc3dd3 Add missing config values 2025-02-17 18:12:19 -06:00
Rubén d8cd39dc3e Remove duplicated yellow number send 2025-02-17 14:13:14 -06:00
Rubén 6eacf833f5 Remove duplicated name tag send 2025-02-17 14:11:54 -06:00
Rubén dbdd417e0d Avoid bad configuration update 2025-02-17 14:11:30 -06:00
Rubén 4c8dd52f28 Ignore same player 2025-02-17 13:56:46 -06:00
Rubén 31a2d8a02b Add supported types 2025-02-17 12:49:38 -06:00
Rubén f246d57a7e Add backend messaging 2025-02-17 12:37:15 -06:00
Rubén 4ae856630f Add proxy support via messenger 2025-02-17 12:02:27 -06:00
Rubén 8652baf2ae Rename redis feature 2025-02-17 11:50:18 -06:00
NEZNAMY 4a18853e8c [BungeeCord] Remove warning about a waterfall option as it is now long abandoned 2025-02-16 19:58:03 +01:00
NEZNAMY a60be9d8c1 [Spectator fix] Fix not updating on proxies (#1434) 2025-02-16 19:21:07 +01:00
NEZNAMY e16b47409c [Code style] Keep expected tablist display names as TabComponent instead of platform components 2025-02-16 17:45:57 +01:00
NEZNAMY 6f14cbe746 5.0.8-SNAPSHOT 2025-02-16 16:32:10 +01:00
242 changed files with 5869 additions and 5138 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
TAB is an all-in-one plugin for displaying information in various places, which aims to outperform all similar plugins in terms of features, compatibility and performance.
The compact configuration allows you to get the plugin to work the way you want regardless of how simple or complex you want it to be or how experienced you are. The default configuration already contains useful values and examples to give you a better understanding and get you started quickly with instant results. The extensive [URL='https://github.com/NEZNAMY/TAB/wiki']wiki[/URL] answers any of your questions.
[IMG]https://img.shields.io/badge/Release-5.0.5-blue.svg[/IMG]
[IMG]https://img.shields.io/badge/Release-5.0.7-blue.svg[/IMG]
[IMG]https://img.shields.io/badge/Minecraft-1.5%20--%201.21.4-blue.svg[/IMG]
[IMG]https://img.shields.io/badge/Java-8+-blue.svg[/IMG]
[URL='https://github.com/NEZNAMY/TAB'][IMG]https://img.shields.io/badge/GitHub-Source%20code-yellow.svg[/IMG][/URL]
+1 -1
View File
@@ -4,7 +4,7 @@ The compact configuration allows you to get the plugin to work the way you want
The default configuration already contains useful values and examples to give you a better understanding and get you started quickly with instant results.
The extensive [wiki](https://github.com/NEZNAMY/TAB/wiki) answers any of your questions.
![https://img.shields.io/badge/Release-5.0.5-blue.svg](https://img.shields.io/badge/Release-5.0.5-blue.svg)
![https://img.shields.io/badge/Release-5.0.7-blue.svg](https://img.shields.io/badge/Release-5.0.7-blue.svg)
![https://img.shields.io/badge/Minecraft-1.5%20--%201.21.4-blue.svg](https://img.shields.io/badge/Minecraft-1.5%20--%201.21.4-blue.svg)
![https://img.shields.io/badge/Java-8+-blue.svg](https://img.shields.io/badge/Java-8+-blue.svg)
[![https://img.shields.io/badge/GitHub-Source%20code-yellow.svg](https://img.shields.io/badge/GitHub-Source%20code-yellow.svg)](https://github.com/NEZNAMY/TAB)
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

@@ -0,0 +1,262 @@
from PIL import Image, ImageDraw, ImageFont
line_spacing = 10
padding = 10
class Text:
def __init__(self, text, color="black", font=ImageFont.truetype("arial.ttf", 20)):
self.text = text
self.color = color
self.font = font
class ImageGenerator:
def __init__(self, overlay_text, second_images, output_path, texts):
self.overlay_text = overlay_text
self.second_images = second_images
self.output_path = output_path
self.texts = texts
self.header_template = Image.open("Template_Small.png")
self.example_images = [Image.open(path) for path in second_images]
def calculate_total_height(self):
total_text_height = 0
for text in self.texts:
text_width, text_height = ImageDraw.Draw(self.header_template).textbbox((0, 0), text.text, font=text.font)[2:]
total_text_height += text_height + line_spacing
total_second_images_height = sum([img.height+padding for img in self.example_images])
return self.header_template.height + total_second_images_height + total_text_height + (3 * padding)
def draw_rounded_border(self, final_img, draw, radius, color, width):
draw.rounded_rectangle([(0, 0), (final_img.width - 1, final_img.height - 1)], radius=radius, outline=color, width=width)
def paste_top_image(self, final_img, font):
header_with_text = self.header_template.copy()
draw = ImageDraw.Draw(header_with_text)
text_width, text_height = draw.textbbox((0, 0), self.overlay_text, font=font)[2:]
text_x = (header_with_text.width - text_width) // 2 # Center text horizontally
text_y = 20 # Padding from the top
draw.text((text_x, text_y), self.overlay_text, fill="white", font=font)
final_img.paste(header_with_text, (padding, padding))
def paste_examples(self, final_img):
y_offset = self.header_template.height + 2 * padding
for img2 in self.example_images:
img2_x = (final_img.width - img2.width) // 2 # Center horizontally
final_img.paste(img2, (img2_x, y_offset))
y_offset += img2.height + padding
return y_offset
def paste_text(self, final_draw, y_offset):
y_offset += padding # Extra padding before text
for text in self.texts:
fancy_line = f"• {text.text}" # Add bullet point
final_draw.text((padding, y_offset), fancy_line, fill=text.color, font=text.font)
y_offset += ImageDraw.Draw(self.header_template).textbbox((0, 0), fancy_line, font=text.font)[3] + line_spacing # Add space between lines
def save(self, final_img):
final_img.save(self.output_path)
def create_image(overlay_text, second_images, output_path, texts):
generator = ImageGenerator(overlay_text, second_images, output_path, texts)
final_img = Image.new("RGB", (generator.header_template.width + 2 * padding, generator.calculate_total_height()), "white")
final_draw = ImageDraw.Draw(final_img)
# Rounded border
generator.draw_rounded_border(final_img, final_draw, 15, (39, 0, 114), 5)
# Template with text
generator.paste_top_image(final_img, ImageFont.truetype("arial.ttf", 60))
# Examples
y_offset = generator.paste_examples(final_img)
# Text
generator.paste_text(final_draw, y_offset)
# Save
generator.save(final_img)
if __name__ == "__main__":
create_image("Header & Footer", ["Example_HeaderFooter.png"], "Generated_HeaderFooter.png",
[
Text("Per-world / per-server support"),
Text("Per-group / per-player support"),
Text("Disable if a specified condition is met")
]
)
create_image("NameTags", ["Example_NameTags.png"], "Generated_NameTags.png",
[
Text("Configurable prefix and suffix"),
Text("Per-world / per-server support"),
Text("Per-group / per-player support"),
Text("Disable if a specified condition is met"),
Text("Make nametags completely invisible"),
Text("Control collision rule"),
Text("Automatic compensation for 1.8.x client sided bug making nametags of invisible players still visible"),
Text("Anti-override to prevent other plugins from overriding this one")
]
)
create_image("Sorting", ["Example_Sorting.png"], "Generated_Sorting.png",
[
Text("Sorting by primary permission group"),
Text("Sorting by permission nodes"),
Text("Sorting by a numeric placeholder from lowest to highest or highest to lowest"),
Text("Alphabetically by a placeholder (A-Z or Z-A)"),
Text("Sorting by pre-defined placeholder values"),
Text("Combination of multiple sorting types"),
Text("Enable / disable case-sensitive sorting")
]
)
create_image("Tablist Formatting", ["Example_TablistFormatting.png"], "Generated_TablistFormatting.png",
[
Text("Configurable prefix, name and suffix"),
Text("Per-world / per-server support"),
Text("Per-group / per-player support"),
Text("Disable if a specified condition is met"),
Text("Anti-override to prevent other plugins from overriding this one")
]
)
create_image("Playerlist Objective",
[
"Example_PlayerlistObjectiveNumber.png",
"Example_PlayerlistObjectiveHealth.png",
"Example_PlayerlistObjectiveFancy.png"
],
"Generated_PlayerlistObjective.png",
[
Text("Display value of any numeric placeholder or hearts"),
Text("Display any text on 1.20.3+"),
Text("Disable if a specified condition is met")
]
)
create_image("Belowname", ["Example_BelownameComparison.png"], "Generated_Belowname.png",
[
Text("Display value of any numeric placeholder (<1.20.3) or any text (1.20.3+)"),
Text("Configurable text"),
Text("Disable if a specified condition is met")
]
)
create_image("Bossbar", ["Example_BossBar.png"], "Generated_Bossbar.png",
[
Text("Configurable text, progress, color and style"),
Text("Define display condition to see a bossbar"),
Text("Announce defined bossbar for specified amount of time"),
Text("Configurable toggle command and message"),
Text("Option to remember toggle choice between logins")
]
)
create_image("Global Playerlist", [], "Generated_GlobalPlayerlist.png",
[
Text("Display players from all servers on a proxy network in tablist"),
Text("Configurable server groups to share playerlist instead of with all servers"),
Text("Ability to create isolated servers"),
Text("Servers where people will see everyone"),
Text("Compatible with vanish plugins"),
Text("Option to display players from other servers as spectators")
]
)
create_image("Layout", ["Example_Layout.png"], "Generated_Layout.png",
[
Text("Customize all 80 tablist slots"),
Text("Create static slots with any text and skin"),
Text("Create player groups based on conditions"),
Text("Multiple layouts based on conditions")
]
)
create_image("Scoreboard", ["Example_ScoreboardComparison.png"], "Generated_Scoreboard.png",
[
Text("Display up to 15 lines"),
Text("Option to show 0 (or any other static number) in every line instead of 1-15"),
Text("Fully configurable text on right side of lines on 1.20.3+"),
Text("Displaying different scoreboards based on conditions (output of placeholder, permissions)"),
Text("Up to 64 characters per line for <1.13, unlimited for 1.13+ clients"),
Text("Customizable toggle command and message"),
Text("Option to remember toggle choice between logins"),
Text("Automatic detection of scoreboards from other plugins to hide own and show it again once other plugin hides its scoreboard")
]
)
create_image("Per world Playerlist", [], "Generated_PerWorldPlayerlist.png",
[
Text("Only display players from current world in tablist"),
Text("Group worlds which should share players"),
Text("Worlds where players see everyone"),
Text("Option to see all players in all worlds for players with permission")
]
)
create_image("API", [], "Generated_API.png",
[
Text("Tablist formatting - Change player prefix/name/suffix"),
Text("Header / Footer - Change values"),
Text("Teams - Change nametag prefix / suffix, change visibility, collision rule"),
Text("Sorting - Change sorting priority"),
Text("Scoreboard - Create custom scoreboards, show different scoreboard, toggle scoreboard, announce scoreboard"),
Text("Bossbar - Create custom boss bars, toggle bossbar visibility, announce/send bossbar"),
Text("Layout - Create custom layouts, send existing layouts"),
Text("Placeholders - Create custom placeholders")
]
)
create_image("Placeholders", [], "Generated_Placeholders.png",
[
Text("Internal placeholders with customizable output"),
Text("Create and use custom animations"),
Text("All placeholders and animations are supported in all features"),
Text("Full PlaceholderAPI support including relational placeholders"),
Text("PlaceholderAPI support on BungeeCord / Velocity"),
Text("Conditional placeholders")
]
)
create_image("Conditional placeholders", [], "Generated_ConditionalPlaceholders.png",
[
Text("Conditions based on placeholder output (comparing numeric value, exact output)"),
Text("Conditions based on permission node"),
Text("Combine conditions with AND or OR"),
Text("Configurable output of both cases when condition is and isn't met"),
Text("Placeholder support in condition output")
]
)
create_image("Placeholder replacements", [], "Generated_PlaceholderOutputReplacements.png",
[
Text("Modify output of any placeholder, including PlaceholderAPI placeholders"),
Text("Replace exact text with another text"),
Text("Change output if it's in a number interval"),
Text("Set value if none of the above is met"),
Text("Use original placeholder's output in new one"),
Text("Support for nested placeholders"),
Text("Can be used in other plugins as well")
]
)
create_image("RGB Support", [], "Generated_RGB.png",
[
Text("5 supported RGB patterns"),
Text("3 supported gradient patterns"),
Text("Automatically display closest color to legacy clients"),
Text("Supported in all features where possible")
]
)
create_image("High Performance", [], "Generated_HighPerformance.png",
[
Text("Full control of CPU usage by optimizing configuration and disabling unwanted features"),
Text("Fully asynchronous - no impact on TPS"),
Text("Check plugin's CPU usage using /tab cpu"),
Text("Constant improvements to reduce usage"),
Text("Handle thousands of players")
]
)
create_image("And more!", [], "Generated_More.png",
[
Text("RedisBungee support for multiple proxies"),
Text("MySQL support to store groups/users"),
Text("Potential errors logged into a file instead of spamming the console"),
Text("Yaml error assistant for more user-friendly messages and fix suggestions"),
Text("Many misconfiguration checks to help solve issues much faster")
]
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

+4 -6
View File
@@ -28,7 +28,7 @@ Permission `tab.seevanished` allows players to:
For a player to be detected, player has to have `vanished` metadata flag set to `true`.
Most, (if not all) vanish plugins do this, including, but not limited to CMI,
Essentials, SuperVanish, PremiumVanish and probably more.
If TAB is installed on BungeeCord,
If TAB is installed on proxy,
[Bridge](https://github.com/NEZNAMY/TAB/wiki/TAB-Bridge) must be installed on backend servers
to forward vanish status to the proxy.
@@ -71,10 +71,8 @@ You can check what version the plugin thinks player has by using any of the foll
* Set `debug: true` in config and check console messages on player join, it will send a message containing game version ViaVersion API has returned
Player game version detection may fail. Here are a few possible scenarios why:
* ViaVersion is installed on BungeeCord
* And TAB on BungeeCord as well - ViaVersion makes BungeeCord API return version of the backend server player is connected to instead of player's actual version
* And TAB is on backend servers - There is no ViaVersion to hook into, therefore, there is no suspicion of player not using server's version and not having a way to verify it
* ViaVersion API returns `-1` - There is currently no known cause when or why this happens. However, when this happens, TAB prints a console warn notifying this has happened. Plugin will assume player is using the same version as the server in such cases.
* ViaVersion is installed on proxy and TAB is on backend servers - There is no ViaVersion to hook into, therefore, there is no suspicion of player not using server's version and not having a way to verify it.
* ViaVersion API returns `-1` - This was an issue in the past and hasn't been reported in years now.
When a player's game version is not detected correctly, player's experience will be very limited, see below.
@@ -86,7 +84,7 @@ You will not experience all of these, only those that affect the version differe
| Change of logic | Negative side effect if version is not detected correctly |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Disabling [Layout](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Layout) for 1.7 players | [Layout](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Layout) entries being sent into the tablist of the player, breaking it as the layout is not designed support 1.7 |
| use alternate way of sorting [Layout](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Layout) entries for 1.19.3 - 1.21.1 as the optimal sorting way cannot be used | [Layout](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Layout) entries being out of order for 1.19.3 - 1.21.1 <br />Layout entries appearing in chat complete for <1.19.3 even if they don't have to |
| Use alternate way of sorting [Layout](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Layout) entries for 1.19.3 - 1.21.1 as the optimal sorting way cannot be used | [Layout](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Layout) entries being out of order for 1.19.3 - 1.21.1 <br />Layout entries appearing in chat complete for <1.19.3 even if they don't have to |
| Show 1.9+ bossbar on 1.8 servers for 1.9+ players using ViaVersion API | No 1.9+ bossbar for 1.9+ players on 1.8 server |
| Disabling [Tablist name formatting](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Tablist-name-formatting) for 1.7 and lower due to a 16-character limit total to take advantage of the mechanic where nametags are displayed in tablist, which extends the limit to up to 48 characters (16 prefix, 16 name, 16 suffix) | Trying to fit the entire formatting into 16 characters, resulting in massive cutting and nametag feature not working |
| Play around <1.13 limitations of [Scoreboard](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Scoreboard) | Scoreboard lines limited to ~26 characters, even for 1.13+ clients <br />Scoreboard lines limited to 14 characters for < 1.13 clients |
+2 -2
View File
@@ -130,8 +130,8 @@ Sending scoreboard packets (scoreboard-teams, belowname-objective, playerlist-ob
* **[Tablisknu](https://forums.skunity.com/resources/tablisknu.727/)** (skript addon) prevents TAB from assigning teams (sorting & nametags).
* **SkBee** (skript addon) sends empty scoreboard, causing TAB's to not show sometimes.
* **Waterfall**'s `disable_tab_list_rewrite: true` **may** cause tablist to use offline UUIDs while TAB expects online uuids, causing various problems (most notably tablist formatting not working). Checking for this option is not an option either, because tablist rewrite might still be enabled despite being disabled (don't ask how, I have no idea). Set the option to `false` if you are experiencing issues.
* **ViaVersion on BungeeCord and TAB on backend** acts like a client-sided protocol hack, making it impossible for TAB to know player's real version and causing issues related to it, see [Per-version experience](https://github.com/NEZNAMY/TAB/wiki/Additional-information#per-version-experience) for more info. Avoid this combination. Either install ViaVersion on all backend servers instead or install TAB on BungeeCord instead.
* **ViaVersion**'s `hide-scoreboard-numbers` config option makes [Belowname](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Belowname)'s `value` and [Playerlist objective](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Playerlist-Objective)'s `value` not visible for 1.20.3+ clients on <1.20.3 server when enabled. When using this setup, you'll need to keep the option disabled (sadly scoreboard numbers will not be hidden, solving this would require an implementation on ViaVersion's side).
* **ViaVersion on proxy and TAB on backend** acts like a client-sided protocol hack, making it impossible for TAB to know player's real version and causing issues related to it, see [Per-version experience](https://github.com/NEZNAMY/TAB/wiki/Additional-information#per-version-experience) for more info. Avoid this combination. Either install ViaVersion on all backend servers instead or install TAB on the proxy as well.
* **ViaVersion**'s `hide-scoreboard-numbers` and **Nexo**'s `hide_scoreboard_numbers` config options (may apply to ItemsAdder and Oraxen as well) make [Belowname](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Belowname)'s `value` and [Playerlist objective](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Playerlist-Objective)'s `value` not visible for 1.20.3+ clients on <1.20.3 server when enabled. When using this setup, you'll need to keep the option disabled (sadly scoreboard numbers will not be hidden, solving this would require an implementation on ViaVersion's side).
* **Custom clients / resource packs** - Unofficially modified minecraft clients often tend to break things. Just Lunar client has tons of bugs that can be reproduced with TAB. Resource packs may also contain modifications you are not aware of, making things not look the way you want them to. If you are experiencing any visual issue and are using a custom client or resource pack, try it with a clean vanilla client. If it works there, it's an issue with the client / resource pack and TAB cannot do anything about it.
For example, here are a few bugs in LunarClient / FeatherClient that you may run into when using TAB:
* They add their icon to players in tablist, but don't widen the entries. This results in player names overlapping with latency bar. You can avoid this by configuring some spaces in tabsuffix.
+3 -3
View File
@@ -26,7 +26,7 @@ For Maven users, you can add the API dependency like this:
<dependency>
<groupId>com.github.NEZNAMY</groupId>
<artifactId>TAB-API</artifactId>
<version>5.0.5</version>
<version>5.0.7</version>
<scope>provided</scope>
</dependency>
```
@@ -40,7 +40,7 @@ repositories {
}
dependencies {
compileOnly 'com.github.NEZNAMY:TAB-API:5.0.5'
compileOnly 'com.github.NEZNAMY:TAB-API:5.0.7'
}
```
@@ -51,7 +51,7 @@ repositories {
}
dependencies {
compileOnly("com.github.NEZNAMY", "TAB-API", "5.0.5")
compileOnly("com.github.NEZNAMY", "TAB-API", "5.0.7")
}
```
@@ -25,44 +25,35 @@ They have 2 main uses in the plugin:
# Condition types
## Number comparations
`>=`: greater than or equal to
Example: `%ping%>=100` will pass if the player's ping is greater than or equal to `100`.
`>`: greater than
Example: `%ping%>100` will pass if the player's ping is greater than `100`.
`<=`: less than or equal to
Example: `%ping%<=100` will pass if the player's ping is less than or equal to `100`.
`<`: less than
Example: `%ping%<100` will pass if the player's ping is less than `100`.
| Operation | Description | Example |
|-------------------|---------------------|---------|
| `>=` | Greater than or equal to | `%ping%>=100` will pass if the player's ping is greater than or equal to `100` |
| `>` | Greater than | `%ping%>100` will pass if the player's ping is greater than `100` |
| `<=` | Less than or equal to | `%ping%<=100` will pass if the player's ping is less than or equal to `100` |
| `<` | Less than | `%ping%<100` will pass if the player's ping is less than `100` |
## Text operations
`=`: equal to (must match exactly)
Example: `%world%=world` will pass if player is in world `world`.
| Operation | Description | Example |
|-------------------|---------------------|---------|
| `=` | Equal to | `%world%=world` will pass if player is in world `world` |
| `!=` | Not equal to | `%world%!=world` will pass if player is in any world except `world` |
| `<-` | Contains (left side for full text, right side text to contain) | `%world%<-lobby-` will pass if player is in any world that contains `lobby-` (such as `lobby-1` etc.) |
| `\|-` | Starts with (left side for full text, right side text to start with) | `%world%\|-lobby-` will pass if player is in any world that starts with `lobby-` (such as `lobby-1` etc.) |
| `-\|` | Ends with (left side for full text, right side text to end with) | `%world%-\|nether` will pass if player is in any world that ends with `nether` (such as `world_nether` etc.) |
`!=`: not equal to (opposite of the above)
Example: `%world%!=world` will pass if player is in any world except `world`.
> [!NOTE]
> For `=` and `!=` you can check for empty output of a placeholder using `%my_placeholder%=` and `%my_placeholder%!=`.
**Note 1**: For `=` and `!=` you can check for empty output of a placeholder using `%my_placeholder%=` and `%my_placeholder%!=`.
`<-`: contains (left side for full text, right side text to contain)
Example: `%world%<-lobby-` will pass if player is in any world that contains `lobby-` (such as `lobby-1` etc.).
`|-`: starts with (left side for full text, right side text to start with)
Example: `%world%|-lobby-` will pass if player is in any world that starts with `lobby-` (such as `lobby-1` etc.).
`-|`: ends with (left side for full text, right side text to end with)
Example: `%world%-|nether` will pass if player is in any world that ends with `nether` (such as `world_nether` etc.).
**Note 2**: For string operators, the text must match placeholder's output exactly, including color codes.
> [!NOTE]
> For string operations, the text must match placeholder's output exactly, including color codes.
If you are using [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements),
condition must contain the altered output.
To see the exact output of a placeholder including color codes, use `/tab parse <player> <placeholder>`.
## Permission
`permission:<value>`: permission requirement
Example: `permission:my.permission` will pass if player has `my.permission` permission.
| Operation | Description | Example |
|-------------------|---------------------|---------|
| `permission:<value>` | Permission requirement | `permission:my.permission` will pass if player has `my.permission` permission |
# Multiple condition requirements
Each condition has a `conditions` parameter, which a list of conditions. If you define more than 1 condition, you must specify the condition type.
@@ -166,7 +157,7 @@ Refresh intervals of conditions are not directly configurable.
They are based on placeholders used inside (subconditions, yes/no values).
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---placeholder-refresh-intervals).
check out the [Optimization guide](https://github.com/NEZNAMY/TAB/wiki/Optimizing-the-plugin#2---all-platforms-placeholder-refresh-intervals).
# Examples
## Example 1 - Chaining conditional placeholders
+1 -1
View File
@@ -67,7 +67,7 @@ header-footer:
> [!NOTE]
> To make per-world work on proxy installation,
> install the [TAB-Bridge](https://www.mc-market.org/resources/21641) plugin on your backend servers.
> install the [TAB-Bridge](https://github.com/NEZNAMY/TAB/wiki/TAB-Bridge) plugin on your backend servers.
## Per-group / per-player
**groups.yml**
+12 -3
View File
@@ -14,6 +14,7 @@
* [Additional note 3 - Changing name itself](#additional-note-3---changing-name-itself)
* [Additional note 4 - F1 view](#additional-note-4---f1-view)
* [Additional note 5 - Transparent players](#additional-note-5---transparent-players)
* [Additional note 6 - Vanish hook](#additional-note-6---vanish-hook)
* [API](#api)
* [Changing prefix and suffix](#changing-prefix-and-suffix)
* [Collision](#collision)
@@ -138,7 +139,7 @@ _DEFAULT_:
```
# Limitations
* [1.5 - 1.12.2] Prefix/suffix length is limited to 16 characters (including color codes).
* [1.5 - 1.12.2] Prefix/suffix length is limited to 16 characters (including color codes). Any characters beyond that will be cut to prevent players getting disconnected.
* [1.13+] The name can only have one code. That is either color or magic code (such as &4 or &l), but not both.
* The name cannot be effectively changed and the plugin doesn't offer it.
* Name does not support RGB codes. Any used RGB colors will be rounded to the nearest legacy code.
@@ -183,6 +184,13 @@ scoreboard-teams:
```
In config.yml. Keep in mind, you'll not be able to use any team features if you do so.
# Additional note 6 - Vanish hook
When a player is [vanished](https://github.com/NEZNAMY/TAB/wiki/Additional-information#vanish-detection), their team is unregistered for all other players who do **not** have `tab.seevanished` permission.
The purpose of this is to avoid players figuring out a staff member is still online but just vanished by "checking" registered teams and their members. The easiest way of "exploiting" this is to have a 3rd party client spawn a player entity with name of a chosen player (staff member) and seeing if their name is formatted (= team is registered = player is online) or not (offline).
For this reason, make sure you give all of your staff members who can see vanished players the `tab.seevanished` permission, otherwise the vanished player will appear on top of the tablist (players without team are above players with team) and their nametag will not be formatted.
# API
*To get started with the API, see [Developer API](https://github.com/NEZNAMY/TAB/wiki/Developer-API) page.*
@@ -192,7 +200,8 @@ To access this feature, you'll need to obtain `NameTagManager` instance. Get it
To set the values for the respective formatting, use the following:
* `NameTagManager#setPrefix(TabPlayer, String)`
* `NameTagManager#setSuffix(TabPlayer, String)`
To reset them, set values to `null`.
To reset them, set values to `null`.
To get custom values previously set using the API (they will return `null` if no custom value is set):
* `NameTagManager#getCustomPrefix(TabPlayer)`
@@ -209,8 +218,8 @@ To get the original value set by the plugin based on configuration:
> use [commands](https://github.com/NEZNAMY/TAB/wiki/Commands-&-Permissions#tab-playergroupplayeruuid-name-property-value-options).
## Collision
* `NameTagManager#getCollisionRule(TabPlayer)` - Returns forced collision rule using the API, `null` if no value was forced and player follows the configuration.
* `NameTagManager#setCollisionRule(TabPlayer, Boolean)` - Forces collision rule to the player. Use `null` to reset value and make it follow configuration again.
* `NameTagManager#getCollisionRule(TabPlayer)` - Returns forced collision rule using the API, `null` if no value was forced and player follows the configuration.
## Manipulating visibility
* `NameTagManager#hideNametag(TabPlayer)` - Hides player's nametag from all players
+9
View File
@@ -0,0 +1,9 @@
# About
A collection of things people have requested that I might look into implementing when I get extremely bored sometime next decade.
* Less than 4 columns in Layout
* `invisible-nametags` supporting conditions like `enable-collision` does
* Using ViaVersion API to send 1.20.3+ scoreboard content on <1.20.3 server.
* Relational placeholder support in sorting
* Conditional header/footer
* Relational conditions for layout player groups
* Improve bossbar announce placeholder(s)
+1 -1
View File
@@ -75,7 +75,7 @@ Most common reasons for a placeholder to not work include:
* Using % symbol by itself, which breaks placeholder starts and ends and therefore breaks all placeholders after it (use %% to display the symbol)
* Trying to use a PlaceholderAPI placeholder without downloading its expansion or not having PlaceholderAPI installed at all
* Trying to use [TAB's internal bukkit-only placeholders](https://github.com/NEZNAMY/TAB/wiki/Placeholders#bukkit-only) on BungeeCord
* Trying to use [PlaceholderAPI placeholders on BungeeCord](https://github.com/NEZNAMY/TAB/wiki/How-to-set-up-PlaceholderAPI-support-on-bungeecord) without installing [bridge plugin](https://www.mc-market.org/resources/20631/)
* Trying to use [PlaceholderAPI placeholders on BungeeCord](https://github.com/NEZNAMY/TAB/wiki/How-to-set-up-PlaceholderAPI-support-on-bungeecord) without installing [bridge plugin](https://github.com/NEZNAMY/TAB/wiki/TAB-Bridge)
The full list of reasons can be found on the [Placeholders](https://github.com/NEZNAMY/TAB/wiki/Placeholders#placeholder-is-not-working) page.
+1 -1
View File
@@ -1,5 +1,5 @@
# About the wiki
The wiki explains the functionality of the plugin as of version **5.0.5**.
The wiki explains the functionality of the plugin as of version **5.0.7**.
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.
@@ -201,15 +201,37 @@ public interface NameTagManager {
/**
* Toggles name tag visibility view on all players for specified player.
* On first call, name tags of all players will become invisible for specified
* player. On second call, they will become visible again.
* player. On the second call, they will become visible again.
*
* @param player
* player to toggle name tag visibility view for
* @param sendToggleMessage
* {@code true} if configured toggle message should be sent, {@code false} if not
* {@code true} if the configured toggle message should be sent, {@code false} if not
*/
void toggleNameTagVisibilityView(@NonNull TabPlayer player, boolean sendToggleMessage);
/**
* Shows back name tag visibility view on all players for specified player.
* If the view is not hidden, nothing happens.
*
* @param player
* player to show name tag visibility view for
* @param sendToggleMessage
* {@code true} if the configured toggle message should be sent, {@code false} if not
*/
void showNameTagVisibilityView(@NonNull TabPlayer player, boolean sendToggleMessage);
/**
* Hides name tag visibility view on all players for specified player.
* If the view is already hidden, nothing happens.
*
* @param player
* player to show name tag visibility view for
* @param sendToggleMessage
* {@code true} if the configured toggle message should be sent, {@code false} if not
*/
void hideNameTagVisibilityView(@NonNull TabPlayer player, boolean sendToggleMessage);
/**
* Returns {@code true} if player has hidden name tags by either calling
* {@link #toggleNameTagVisibilityView(TabPlayer, boolean)} or using a command,
@@ -3,6 +3,10 @@ plugins {
id("com.github.johnrengelman.shadow")
}
tasks.withType<Zip>().configureEach {
isZip64 = true
}
tasks {
shadowJar {
archiveFileName.set("TAB-${project.name}-${project.version}.jar")
@@ -11,5 +15,10 @@ tasks {
relocate("net.kyori.event", "me.neznamy.tab.libs.net.kyori.event")
relocate("me.neznamy.yamlassist", "me.neznamy.tab.libs.me.neznamy.yamlassist")
relocate("org.yaml.snakeyaml", "me.neznamy.tab.libs.org.yaml.snakeyaml")
relocate("redis.clients.jedis", "me.neznamy.tab.libs.redis.clients.jedis")
relocate("org.apache.commons.pool2", "me.neznamy.tab.libs.org.apache.commons.pool2")
relocate("org.json", "me.neznamy.tab.libs.org.json")
relocate("com.rabbitmq", "me.neznamy.tab.libs.com.rabbitmq")
relocate("com.saicone.delivery4j", "me.neznamy.tab.libs.com.saicone.delivery4j")
}
}
+6 -8
View File
@@ -4,7 +4,7 @@ plugins {
allprojects {
group = "me.neznamy"
version = "5.0.7"
version = "5.1.1"
description = "An all-in-one solution that works"
ext.set("id", "tab")
@@ -14,16 +14,14 @@ allprojects {
val platforms = setOf(
projects.bukkit,
projects.bukkit.paper,
projects.bukkit.v18R3,
projects.bukkit.v112R1,
projects.bungeecord,
projects.component,
projects.velocity,
projects.sponge7,
projects.sponge8,
projects.fabric,
projects.fabric.v1144,
projects.fabric.v1182,
projects.fabric.v1203,
projects.fabric.v1213
projects.sponge,
projects.fabric
).map { it.dependencyProject }
val special = setOf(
+2
View File
@@ -15,4 +15,6 @@ dependencies {
exclude("net.md-5", "bungeecord-chat")
}
compileOnly("com.github.LeonMangler:PremiumVanishAPI:2.8.8")
compileOnlyApi("com.viaversion:viaversion-api:5.2.1")
compileOnly("com.viaversion:viaversion-common:5.2.1")
}
+22
View File
@@ -0,0 +1,22 @@
plugins {
id("io.papermc.paperweight.userdev") version "2.0.0-beta.8"
}
repositories {
maven("https://jitpack.io") // YamlAssist
maven("https://repo.opencollab.dev/maven-snapshots/")
maven("https://repo.viaversion.com/")
maven("https://oss.sonatype.org/content/repositories/snapshots")
}
val version = "1.21.5-R0.1-SNAPSHOT"
dependencies {
implementation(projects.bukkit)
paperweight.paperDevBundle("1.21.5-no-moonrise-SNAPSHOT") // Just temporary
compileOnly("io.papermc.paper:paper-api:${version}")
}
tasks.compileJava {
options.release.set(21)
}
@@ -0,0 +1,50 @@
package me.neznamy.tab.platforms.paper;
import me.neznamy.chat.ChatModifier;
import me.neznamy.tab.platforms.bukkit.nms.converter.ComponentConverter;
import net.minecraft.network.chat.*;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct mojang-mapped code.
*/
public class PaperComponentConverter extends ComponentConverter {
@Override
@NotNull
public Object newTextComponent(@NotNull String text) {
return Component.literal(text);
}
@Override
@NotNull
public Object newTranslatableComponent(@NotNull String key) {
return Component.translatable(key);
}
@Override
@NotNull
public Object newKeybindComponent(@NotNull String keybind) {
return Component.keybind(keybind);
}
@Override
public void applyStyle(@NotNull Object nmsComponent, @NotNull ChatModifier modifier) {
Style style = Style.EMPTY
.withColor(modifier.getColor() == null ? null : TextColor.fromRgb(modifier.getColor().getRgb()))
.withBold(modifier.getBold())
.withItalic(modifier.getItalic())
.withUnderlined(modifier.getUnderlined())
.withStrikethrough(modifier.getStrikethrough())
.withObfuscated(modifier.getObfuscated())
.withFont(modifier.getFont() == null ? null : ResourceLocation.tryParse(modifier.getFont()));
if (modifier.getShadowColor() != null) style = style.withShadowColor(modifier.getShadowColor()); // withShadowColor takes int instead of Integer, bug?
((MutableComponent)nmsComponent).setStyle(style);
}
@Override
public void addSibling(@NotNull Object parent, @NotNull Object child) {
((MutableComponent)parent).append((Component) child);
}
}
@@ -0,0 +1,40 @@
package me.neznamy.tab.platforms.paper;
import io.netty.channel.Channel;
import lombok.Getter;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.converter.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.util.function.FunctionWithException;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
/**
* Implementation provider using direct Mojang-mapped NMS code (1.20.5+).
*/
@Getter
public class PaperImplementationProvider implements ImplementationProvider {
@NotNull
private final ComponentConverter componentConverter = new PaperComponentConverter();
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
return new PaperPacketScoreboard(player);
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return new PaperPacketTabList(player);
}
@Override
@NotNull
public FunctionWithException<BukkitTabPlayer, Channel> getChannelFunction() {
return player -> ((CraftPlayer)player.getPlayer()).getHandle().connection.connection.channel;
}
}
@@ -0,0 +1,162 @@
package me.neznamy.tab.platforms.paper;
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 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.criteria.ObjectiveCriteria;
import net.minecraft.world.scores.criteria.ObjectiveCriteria.RenderType;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
/**
* Scoreboard implementation using direct mojang-mapped code.
*/
public class PaperPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
private static final ChatFormatting[] formats = ChatFormatting.values();
private static final net.minecraft.world.scores.Team.CollisionRule[] collisions = net.minecraft.world.scores.Team.CollisionRule.values();
private static final net.minecraft.world.scores.Team.Visibility[] visibilities = net.minecraft.world.scores.Team.Visibility.values();
private static final Scoreboard dummyScoreboard = new Scoreboard();
private static final Field method = ReflectionUtils.getInstanceFields(ClientboundSetPlayerTeamPacket.class, int.class).getFirst();
private static final Field players = ReflectionUtils.getOnlyField(ClientboundSetPlayerTeamPacket.class, Collection.class);
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public PaperPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
@Override
public void registerObjective(@NonNull Objective objective) {
net.minecraft.world.scores.Objective obj = new net.minecraft.world.scores.Objective(
dummyScoreboard,
objective.getName(),
ObjectiveCriteria.DUMMY,
objective.getTitle().convert(),
RenderType.values()[objective.getHealthDisplay().ordinal()],
false,
objective.getNumberFormat() == null ? null : objective.getNumberFormat().toFixedFormat(FixedFormat::new)
);
objective.setPlatformObjective(obj);
sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.REGISTER));
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
sendPacket(new ClientboundSetDisplayObjectivePacket(
net.minecraft.world.scores.DisplaySlot.values()[objective.getDisplaySlot().ordinal()],
(net.minecraft.world.scores.Objective) objective.getPlatformObjective()
));
}
@Override
public void unregisterObjective(@NonNull Objective objective) {
sendPacket(new ClientboundSetObjectivePacket((net.minecraft.world.scores.Objective) objective.getPlatformObjective(), ObjectiveAction.UNREGISTER));
}
@Override
public void updateObjective(@NonNull Objective objective) {
net.minecraft.world.scores.Objective obj = (net.minecraft.world.scores.Objective) objective.getPlatformObjective();
obj.setDisplayName(objective.getTitle().convert());
obj.setRenderType(RenderType.values()[objective.getHealthDisplay().ordinal()]);
sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.UPDATE));
}
@Override
public void setScore(@NonNull Score score) {
sendPacket(new ClientboundSetScorePacket(
score.getHolder(),
score.getObjective().getName(),
score.getValue(),
Optional.ofNullable(score.getDisplayName() == null ? null : score.getDisplayName().convert()),
Optional.ofNullable(score.getNumberFormat() == null ? null : score.getNumberFormat().toFixedFormat(FixedFormat::new))
));
}
@Override
public void removeScore(@NonNull Score score) {
sendPacket(new ClientboundResetScorePacket(score.getHolder(), score.getObjective().getName()));
}
@Override
@NotNull
public Object createTeam(@NonNull String name) {
return new PlayerTeam(dummyScoreboard, name);
}
@Override
public void registerTeam(@NonNull Team team) {
updateTeamProperties(team);
PlayerTeam t = (PlayerTeam) team.getPlatformTeam();
t.getPlayers().addAll(team.getPlayers());
sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(t, true));
}
@Override
public void unregisterTeam(@NonNull Team team) {
sendPacket(ClientboundSetPlayerTeamPacket.createRemovePacket((PlayerTeam) team.getPlatformTeam()));
}
@Override
public void updateTeam(@NonNull Team team) {
updateTeamProperties(team);
sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket((PlayerTeam) team.getPlatformTeam(), false));
}
private void updateTeamProperties(@NonNull Team team) {
PlayerTeam t = (PlayerTeam) team.getPlatformTeam();
t.setAllowFriendlyFire((team.getOptions() & 0x01) != 0);
t.setSeeFriendlyInvisibles((team.getOptions() & 0x02) != 0);
t.setColor(formats[team.getColor().getLegacyColor().ordinal()]);
t.setCollisionRule(collisions[team.getCollision().ordinal()]);
t.setNameTagVisibility(visibilities[team.getVisibility().ordinal()]);
t.setPlayerPrefix(team.getPrefix().convert());
t.setPlayerSuffix(team.getSuffix().convert());
}
@Override
@SneakyThrows
public void onPacketSend(@NonNull Object packet) {
if (isAntiOverrideScoreboard()) {
if (packet instanceof ClientboundSetDisplayObjectivePacket display) {
TAB.getInstance().getFeatureManager().onDisplayObjective(player, display.getSlot().ordinal(), display.getObjectiveName());
}
if (packet instanceof ClientboundSetObjectivePacket objective) {
TAB.getInstance().getFeatureManager().onObjective(player, objective.getMethod(), objective.getObjectiveName());
}
}
if (isAntiOverrideTeams() && packet instanceof ClientboundSetPlayerTeamPacket team) {
int action = method.getInt(team);
if (action == TeamAction.UPDATE) return;
players.set(team, onTeamPacket(action, team.getName(), team.getPlayers() == null ? Collections.emptyList() : team.getPlayers()));
}
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
private void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().connection.sendPacket(packet);
}
}
@@ -0,0 +1,202 @@
package me.neznamy.tab.platforms.paper;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.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.Component;
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.ClientboundTabListPacket;
import net.minecraft.world.level.GameType;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.*;
/**
* TabList implementation using direct mojang-mapped code.
*/
public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> addPlayer = EnumSet.allOf(ClientboundPlayerInfoUpdatePacket.Action.class);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> updateDisplayName = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> updateLatency = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> updateGameMode = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> updateListed = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> updateListOrder = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER);
private static final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> updateHat = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT);
private static final Field entries;
static {
try {
entries = ReflectionUtils.setAccessible(ClientboundPlayerInfoUpdatePacket.class.getDeclaredField("entries"));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs new instance.
*
* @param player
* Player this tablist will belong to
*/
public PaperPacketTabList(@NotNull BukkitTabPlayer player) {
super(player);
}
@Override
public void removeEntry(@NonNull UUID entry) {
sendPacket(new ClientboundPlayerInfoRemovePacket(Collections.singletonList(entry)));
}
@Override
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
sendPacket(updateDisplayName, entry, "", null, false, 0, 0, displayName, 0, false);
}
@Override
public void updateLatency(@NonNull UUID entry, int latency) {
sendPacket(updateLatency, entry, "", null, false, latency, 0, null, 0, false);
}
@Override
public void updateGameMode(@NonNull UUID entry, int gameMode) {
sendPacket(updateGameMode, entry, "", null, false, 0, gameMode, null, 0, false);
}
@Override
public void updateListed(@NonNull UUID entry, boolean listed) {
sendPacket(updateListed, entry, "", null, listed, 0, 0, null, 0, false);
}
@Override
public void updateListOrder(@NonNull UUID entry, int listOrder) {
sendPacket(updateListOrder, entry, "", null, false, 0, 0, null, listOrder, false);
}
@Override
public void updateHat(@NonNull UUID entry, boolean showHat) {
sendPacket(updateHat, entry, "", null, false, 0, 0, null, 0, showHat);
}
@Override
public void addEntry0(@NonNull Entry entry) {
sendPacket(addPlayer, entry.getUniqueId(), entry.getName(), entry.getSkin(), entry.isListed(), entry.getLatency(),
entry.getGameMode(), entry.getDisplayName(), entry.getListOrder(), entry.isShowHat());
}
@Override
public void setPlayerListHeaderFooter(@NonNull TabComponent header, @NonNull TabComponent footer) {
sendPacket(new ClientboundTabListPacket(header.convert(), footer.convert()));
}
@Override
public boolean containsEntry(@NonNull UUID entry) {
return true; // TODO?
}
@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.value(), property.signature());
}
@Override
@SneakyThrows
public void onPacketSend(@NonNull Object packet) {
if (packet instanceof ClientboundPlayerInfoUpdatePacket info) {
EnumSet<ClientboundPlayerInfoUpdatePacket.Action> actions = info.actions();
List<ClientboundPlayerInfoUpdatePacket.Entry> updatedList = new ArrayList<>();
boolean rewritePacket = false;
for (ClientboundPlayerInfoUpdatePacket.Entry nmsData : info.entries()) {
boolean rewriteEntry = false;
Component displayName = nmsData.displayName();
int latency = nmsData.latency();
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME)) {
TabComponent expectedDisplayName = getExpectedDisplayNames().get(nmsData.profileId());
if (expectedDisplayName != null && expectedDisplayName.convert() != displayName) {
displayName = expectedDisplayName.convert();
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY)) {
int newLatency = TAB.getInstance().getFeatureManager().onLatencyChange(player, nmsData.profileId(), latency);
if (newLatency != latency) {
latency = newLatency;
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER)) {
TAB.getInstance().getFeatureManager().onEntryAdd(player, nmsData.profileId(), nmsData.profile().getName());
}
updatedList.add(rewriteEntry ? new ClientboundPlayerInfoUpdatePacket.Entry(
nmsData.profileId(), nmsData.profile(), nmsData.listed(), latency, nmsData.gameMode(), displayName,
nmsData.showHat(), nmsData.listOrder(), nmsData.chatSession()
) : nmsData);
}
if (rewritePacket) entries.set(info, updatedList);
}
}
private void sendPacket(@NonNull EnumSet<ClientboundPlayerInfoUpdatePacket.Action> action, @NonNull UUID id, @NonNull String name, @Nullable Skin skin,
boolean listed, int latency, int gameMode, @Nullable TabComponent displayName, int listOrder, boolean showHat) {
ClientboundPlayerInfoUpdatePacket packet = new ClientboundPlayerInfoUpdatePacket(action, new ClientboundPlayerInfoUpdatePacket.Entry(
id,
action.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER) ? createProfile(id, name, skin) : null,
listed,
latency,
GameType.byId(gameMode),
displayName == null ? null : displayName.convert(),
showHat,
listOrder,
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().connection.send(packet);
}
}
@@ -3,15 +3,11 @@ package me.neznamy.tab.platforms.bukkit;
import io.netty.channel.Channel;
import lombok.Setter;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.shared.features.injection.NettyPipelineInjector;
import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.util.ReflectionUtils;
import me.neznamy.tab.shared.util.function.FunctionWithException;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
/**
* Pipeline injection for Bukkit 1.8+.
*/
@@ -28,33 +24,6 @@ public class BukkitPipelineInjector extends NettyPipelineInjector {
super("packet_handler");
}
/**
* Attempts to load required classes, fields and methods and marks class as available.
* If something fails, an error message is printed and class is not marked as available.
*/
public static void tryLoad() {
try {
Class<?> NetworkManager = BukkitReflection.getClass("network.Connection", "network.NetworkManager", "NetworkManager");
Class<?> PlayerConnection = BukkitReflection.getClass("server.network.ServerGamePacketListenerImpl",
"server.network.PlayerConnection", "PlayerConnection");
Class<?> EntityPlayer = BukkitReflection.getClass("server.level.ServerPlayer", "server.level.EntityPlayer", "EntityPlayer");
Field PLAYER_CONNECTION = ReflectionUtils.getOnlyField(EntityPlayer, PlayerConnection);
Field NETWORK_MANAGER;
if (BukkitReflection.is1_20_2Plus()) {
NETWORK_MANAGER = ReflectionUtils.getOnlyField(PlayerConnection.getSuperclass(), NetworkManager);
} else {
NETWORK_MANAGER = ReflectionUtils.getOnlyField(PlayerConnection, NetworkManager);
}
Field CHANNEL = ReflectionUtils.getOnlyField(NetworkManager, Channel.class);
getChannel = player -> (Channel) CHANNEL.get(NETWORK_MANAGER.get(PLAYER_CONNECTION.get(player.getHandle())));
} catch (Exception e) {
BukkitUtils.compatibilityError(e, "network channel injection", null,
"Anti-override for tablist & nametags not working",
"Compatibility with nickname plugins changing player names will not work",
"Scoreboard will not be checking for other plugins");
}
}
/**
* Returns {@code true} if pipeline injection is available, {@code false} if not.
*
@@ -2,14 +2,12 @@ package me.neznamy.tab.platforms.bukkit;
import lombok.Getter;
import lombok.SneakyThrows;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.hook.LibsDisguisesHook;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.platforms.bukkit.nms.PingRetriever;
import me.neznamy.tab.platforms.bukkit.platform.BukkitPlatform;
import me.neznamy.tab.platforms.bukkit.tablist.TabListBase;
import me.neznamy.tab.shared.backend.BackendTabPlayer;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.shared.platform.TabList;
import org.bukkit.entity.Player;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.potion.PotionEffectType;
@@ -70,12 +68,6 @@ public class BukkitTabPlayer extends BackendTabPlayer {
return LibsDisguisesHook.isDisguised(this);
}
@Override
@Nullable
public TabList.Skin getSkin() {
return ((TabListBase<?>)getTabList()).getSkin();
}
@Override
@NotNull
public Player getPlayer() {
@@ -2,15 +2,11 @@ package me.neznamy.tab.platforms.bukkit;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import me.neznamy.tab.shared.ProtocolVersion;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
/**
* Class containing static util methods used on Bukkit.
*/
@@ -22,26 +18,6 @@ public class BukkitUtils {
private static boolean compatibilityIssue;
/**
* Returns online players from Bukkit API. This requires reflection, as return type changed in 1.8,
* and we want to avoid errors.
*
* @return Online players from Bukkit API.
*/
@SuppressWarnings("unchecked")
@SneakyThrows
@NotNull
public static Player[] getOnlinePlayers() {
Object players = Bukkit.class.getMethod("getOnlinePlayers").invoke(null);
if (players instanceof Player[]) {
//1.7-
return (Player[]) players;
} else {
//1.8+
return ((Collection<Player>)players).toArray(new Player[0]);
}
}
/**
* Prints a console warn that some compatibility issue was found.
*
@@ -1,6 +1,6 @@
package me.neznamy.tab.platforms.bukkit.features;
import me.neznamy.tab.platforms.bukkit.BukkitUtils;
import me.neznamy.tab.platforms.bukkit.platform.BukkitPlatform;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.features.PerWorldPlayerListConfiguration;
@@ -26,6 +26,10 @@ import java.util.Map.Entry;
@SuppressWarnings("deprecation")
public class PerWorldPlayerList extends TabFeature implements Listener, Loadable, UnLoadable {
/** Reference to platform */
@NotNull
private final BukkitPlatform platform;
/** Config options */
@NotNull
private final PerWorldPlayerListConfiguration configuration;
@@ -35,25 +39,28 @@ public class PerWorldPlayerList extends TabFeature implements Listener, Loadable
*
* @param plugin
* Plugin instance to register events
* @param platform
* Platform reference
* @param configuration
* Feature configuration
*/
public PerWorldPlayerList(@NotNull JavaPlugin plugin, @NotNull PerWorldPlayerListConfiguration configuration) {
public PerWorldPlayerList(@NotNull JavaPlugin plugin, @NotNull BukkitPlatform platform, @NotNull PerWorldPlayerListConfiguration configuration) {
this.configuration = configuration;
this.platform = platform;
Bukkit.getPluginManager().registerEvents(this, plugin);
}
@Override
public void load() {
for (Player p : BukkitUtils.getOnlinePlayers()) {
for (Player p : platform.getOnlinePlayers()) {
checkPlayer(p);
}
}
@Override
public void unload() {
for (Player p : BukkitUtils.getOnlinePlayers()) {
for (Player pl : BukkitUtils.getOnlinePlayers()) {
for (Player p : platform.getOnlinePlayers()) {
for (Player pl : platform.getOnlinePlayers()) {
p.showPlayer(pl);
}
}
@@ -95,7 +102,7 @@ public class PerWorldPlayerList extends TabFeature implements Listener, Loadable
* Player to update
*/
private void checkPlayer(@NotNull Player p) {
for (Player all : BukkitUtils.getOnlinePlayers()) {
for (Player all : platform.getOnlinePlayers()) {
if (all == p) continue;
if (!shouldSee(p, all) && p.canSee(all)) p.hidePlayer(all);
if (shouldSee(p, all) && !p.canSee(all)) p.showPlayer(all);
@@ -0,0 +1,16 @@
package me.neznamy.tab.platforms.bukkit.header;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import org.jetbrains.annotations.NotNull;
/**
* An empty implementation that does not do anything.
*/
public class DummyHeaderFooter extends HeaderFooter {
@Override
public void set(@NotNull BukkitTabPlayer player, @NotNull TabComponent header, @NotNull TabComponent footer) {
// Do nothing
}
}
@@ -1,51 +1,14 @@
package me.neznamy.tab.platforms.bukkit.header;
import lombok.Getter;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.BukkitUtils;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A class responsible for sending header/footer to players on request.
*/
public abstract class HeaderFooter {
/** Instance of the class, null if no implementation is available */
@Getter
@Nullable
private static HeaderFooter instance;
/**
* Finds the best available instance for current server software.
*/
public static void findInstance() {
instance = findInstance0();
}
@Nullable
private static HeaderFooter findInstance0() {
try {
return new PacketHeaderFooter();
} catch (Exception e) {
if (PaperHeaderFooter.isAvailable()) {
BukkitUtils.compatibilityError(e, "sending Header/Footer", "Paper API");
return new PaperHeaderFooter();
}
if (BukkitHeaderFooter.isAvailable()) {
BukkitUtils.compatibilityError(e, "sending Header/Footer", "Bukkit API",
"Header/Footer having drastically increased CPU usage",
"Header/Footer not supporting fonts (1.16+)");
return new BukkitHeaderFooter();
} else {
BukkitUtils.compatibilityError(e, "sending Header/Footer", null,
"Header/Footer feature not working");
return null;
}
}
}
/**
* Sends header/footer to player.
*
@@ -1,13 +1,12 @@
package me.neznamy.tab.platforms.bukkit.header;
import lombok.SneakyThrows;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.platforms.bukkit.nms.converter.ComponentConverter;
import me.neznamy.tab.platforms.bukkit.nms.PacketSender;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.shared.util.function.BiFunctionWithException;
import me.neznamy.tab.shared.util.ReflectionUtils;
import me.neznamy.tab.shared.util.function.BiFunctionWithException;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Constructor;
@@ -32,7 +31,6 @@ public class PacketHeaderFooter extends HeaderFooter {
Class<?> Component = BukkitReflection.getClass("network.chat.Component", "network.chat.IChatBaseComponent", "IChatBaseComponent");
Class<?> HeaderFooterClass = BukkitReflection.getClass("network.protocol.game.ClientboundTabListPacket",
"network.protocol.game.PacketPlayOutPlayerListHeaderFooter", "PacketPlayOutPlayerListHeaderFooter");
if (ComponentConverter.INSTANCE == null) throw new IllegalStateException("Component converter is not available");
if (BukkitReflection.getMinorVersion() >= 17) {
Constructor<?> newHeaderFooter = HeaderFooterClass.getConstructor(Component, Component);
createPacket = newHeaderFooter::newInstance;
@@ -7,6 +7,7 @@ import me.neznamy.tab.shared.util.function.FunctionWithException;
import me.neznamy.tab.shared.util.ReflectionUtils;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Method;
import java.util.Arrays;
@@ -23,6 +24,7 @@ public class BukkitReflection {
public static final Method CraftPlayer_getHandle = getHandle();
/** Server version data */
@Getter
private static final ServerVersion serverVersion = detectServerVersion();
/** Flag determining whether the server version is at least 1.20.2 or not */
@@ -38,6 +40,9 @@ public class BukkitReflection {
private static final boolean is1_21_4Plus = serverVersion.minorVersion >= 21 &&
ReflectionUtils.getFields(getClass("network.chat.Style", "network.chat.ChatModifier"), Integer.class).size() == 1;
@Getter
private static final boolean mojangMapped = ReflectionUtils.classExists("org.bukkit.craftbukkit.CraftServer");
@NotNull
@SneakyThrows
private static Method getHandle() {
@@ -56,11 +61,12 @@ public class BukkitReflection {
ClassLoader loader = BukkitReflection.class.getClassLoader();
classFunction = name -> loader.loadClass("net.minecraft.server." + serverPackage + "." + name);
}
return new ServerVersion(classFunction, minorVersion, serverPackage);
} else {
// Paper without CB relocation
minorVersion = Integer.parseInt(Bukkit.getBukkitVersion().split("-")[0].split("\\.")[1]);
return new ServerVersion(classFunction, minorVersion, null);
}
return new ServerVersion(classFunction, minorVersion);
}
/**
@@ -99,9 +105,12 @@ public class BukkitReflection {
*/
@RequiredArgsConstructor
@Getter
private static class ServerVersion {
public static class ServerVersion {
private final FunctionWithException<String, Class<?>> getClass;
private final int minorVersion;
@Nullable
private final String serverPackage;
}
}
@@ -5,20 +5,13 @@ import me.neznamy.chat.component.KeybindComponent;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.chat.component.TextComponent;
import me.neznamy.chat.component.TranslatableComponent;
import me.neznamy.tab.platforms.bukkit.BukkitUtils;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Interface for converting TAB components into NMS components (1.7+).
*/
public abstract class ComponentConverter {
/** Instance of this class */
@Nullable
public static ComponentConverter INSTANCE;
/**
* Converts TAB component to NMS component.
*
@@ -100,26 +93,4 @@ public abstract class ComponentConverter {
* Child component to append
*/
public abstract void addSibling(@NotNull Object parent, @NotNull Object child);
/**
* Attempts to load component converter.
*/
public static void tryLoad() {
try {
if (BukkitReflection.getMinorVersion() >= 19) {
// 1.19+
INSTANCE = new ModernComponentConverter();
} else if (BukkitReflection.getMinorVersion() >= 16) {
// 1.16 - 1.18.2
INSTANCE = new ModerateComponentConverter();
} else {
// 1.7 - 1.15.2
INSTANCE = new LegacyComponentConverter();
}
} catch (Exception e) {
if (BukkitUtils.PRINT_EXCEPTIONS) {
e.printStackTrace();
}
}
}
}
@@ -9,13 +9,13 @@ import me.neznamy.tab.platforms.bukkit.bossbar.BukkitBossBar;
import me.neznamy.tab.platforms.bukkit.bossbar.ViaBossBar;
import me.neznamy.tab.platforms.bukkit.features.BukkitTabExpansion;
import me.neznamy.tab.platforms.bukkit.features.PerWorldPlayerList;
import me.neznamy.tab.platforms.bukkit.header.HeaderFooter;
import me.neznamy.tab.platforms.bukkit.header.*;
import me.neznamy.tab.platforms.bukkit.hook.BukkitPremiumVanishHook;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.platforms.bukkit.nms.PingRetriever;
import me.neznamy.tab.platforms.bukkit.nms.converter.ComponentConverter;
import me.neznamy.tab.platforms.bukkit.scoreboard.ScoreboardLoader;
import me.neznamy.tab.platforms.bukkit.tablist.TabListBase;
import me.neznamy.tab.platforms.bukkit.provider.BukkitImplementationProvider;
import me.neznamy.tab.platforms.bukkit.provider.ImplementationProvider;
import me.neznamy.tab.platforms.bukkit.provider.ViaVersionImplementationProvider;
import me.neznamy.tab.shared.GroupManager;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
@@ -52,6 +52,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
/**
* Implementation of Platform interface for Bukkit platform
@@ -79,12 +82,30 @@ public class BukkitPlatform implements BackendPlatform {
/** Detection for presence of Paper's MSPT getter */
private final boolean paperMspt = ReflectionUtils.methodExists(Bukkit.class, "getAverageTickTime");
/** Header/footer implementation */
@Getter
@NotNull
private final HeaderFooter headerFooter;
/** Flag tracking if direct mojang-mapped code can be used or not */
private final boolean canUseDirectNMS = BukkitReflection.isMojangMapped() &&
(serverVersion == ProtocolVersion.V1_21_4 || serverVersion == ProtocolVersion.V1_21_5);
/** Implementation for creating new instances using content available on the server */
@NotNull
private final ImplementationProvider serverImplementationProvider;
/** Implementation for sending new content to new players on old servers */
@NotNull
private final ViaVersionImplementationProvider viaVersionImplementationProvider = new ViaVersionImplementationProvider(serverVersion);
/**
* Constructs new instance with given plugin.
*
* @param plugin
* Plugin
*/
@SneakyThrows
public BukkitPlatform(@NotNull JavaPlugin plugin) {
this.plugin = plugin;
long time = System.currentTimeMillis();
@@ -97,21 +118,47 @@ public class BukkitPlatform implements BackendPlatform {
if (Bukkit.getPluginManager().isPluginEnabled("PremiumVanish")) {
new BukkitPremiumVanishHook().register();
}
PingRetriever.tryLoad();
ComponentConverter.tryLoad();
ScoreboardLoader.findInstance();
TabListBase.findInstance();
if (BukkitReflection.getMinorVersion() >= 8) {
HeaderFooter.findInstance();
BukkitPipelineInjector.tryLoad();
if (canUseDirectNMS) {
serverImplementationProvider = (ImplementationProvider) Class.forName("me.neznamy.tab.platforms.paper.PaperImplementationProvider").getConstructor().newInstance();
} else if ("v1_12_R1".equals(BukkitReflection.getServerVersion().getServerPackage())) {
serverImplementationProvider = (ImplementationProvider) Class.forName("me.neznamy.tab.platforms.bukkit.v1_12_R1.NMSImplementationProvider").getConstructor().newInstance();
} else if ("v1_8_R3".equals(BukkitReflection.getServerVersion().getServerPackage())) {
serverImplementationProvider = (ImplementationProvider) Class.forName("me.neznamy.tab.platforms.bukkit.v1_8_R3.NMSImplementationProvider").getConstructor().newInstance();
} else {
serverImplementationProvider = new BukkitImplementationProvider();
}
headerFooter = findHeaderFooter();
PingRetriever.tryLoad();
BukkitPipelineInjector.setGetChannel(serverImplementationProvider.getChannelFunction());
BukkitUtils.sendCompatibilityMessage();
Bukkit.getConsoleSender().sendMessage("[TAB] §7Loaded NMS hook in " + (System.currentTimeMillis()-time) + "ms");
}
@NotNull
private HeaderFooter findHeaderFooter() {
if (BukkitReflection.getMinorVersion() >= 8) {
try {
Objects.requireNonNull(serverImplementationProvider.getComponentConverter());
return new PacketHeaderFooter();
} catch (Exception e) {
if (PaperHeaderFooter.isAvailable()) return new PaperHeaderFooter();
if (BukkitHeaderFooter.isAvailable()) {
BukkitUtils.compatibilityError(e, "sending Header/Footer", "Bukkit API",
"Header/Footer having drastically increased CPU usage",
"Header/Footer not supporting fonts (1.16+)");
return new BukkitHeaderFooter();
} else {
BukkitUtils.compatibilityError(e, "sending Header/Footer", null,
"Header/Footer feature not working");
}
}
}
return new DummyHeaderFooter();
}
@Override
public void loadPlayers() {
for (Player p : BukkitUtils.getOnlinePlayers()) {
for (Player p : getOnlinePlayers()) {
TAB.getInstance().addPlayer(new BukkitTabPlayer(this, p));
}
}
@@ -119,20 +166,18 @@ public class BukkitPlatform implements BackendPlatform {
@Override
public void registerPlaceholders() {
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
manager.registerServerPlaceholder("%vault-prefix%", -1, () -> "");
manager.registerServerPlaceholder("%vault-suffix%", -1, () -> "");
manager.registerInternalServerPlaceholder("%vault-prefix%", -1, () -> "");
manager.registerInternalServerPlaceholder("%vault-suffix%", -1, () -> "");
if (Bukkit.getPluginManager().isPluginEnabled("Vault")) {
RegisteredServiceProvider<Chat> rspChat = Bukkit.getServicesManager().getRegistration(Chat.class);
if (rspChat != null) {
Chat chat = rspChat.getProvider();
int refresh = TAB.getInstance().getConfiguration().getConfig().getPermissionRefreshInterval();
manager.registerPlayerPlaceholder("%vault-prefix%", refresh, p -> chat.getPlayerPrefix((Player) p.getPlayer()));
manager.registerPlayerPlaceholder("%vault-suffix%", refresh, p -> chat.getPlayerSuffix((Player) p.getPlayer()));
manager.registerInternalPlayerPlaceholder("%vault-prefix%", 1000, p -> chat.getPlayerPrefix((Player) p.getPlayer()));
manager.registerInternalPlayerPlaceholder("%vault-suffix%", 1000, p -> chat.getPlayerSuffix((Player) p.getPlayer()));
}
}
// Override for the PAPI placeholder to prevent console errors on unsupported server versions when ping field changes
manager.registerPlayerPlaceholder("%player_ping%", manager.getRefreshInterval("%player_ping%"),
p -> PerformanceUtil.toString(((TabPlayer) p).getPing()));
manager.registerPlayerPlaceholder("%player_ping%", p -> PerformanceUtil.toString(((TabPlayer) p).getPing()));
BackendPlatform.super.registerPlaceholders();
}
@@ -156,7 +201,7 @@ public class BukkitPlatform implements BackendPlatform {
@Override
@Nullable
public TabFeature getPerWorldPlayerList(@NotNull PerWorldPlayerListConfiguration configuration) {
return new PerWorldPlayerList(plugin, configuration);
return new PerWorldPlayerList(plugin, this, configuration);
}
@Override
@@ -166,34 +211,31 @@ public class BukkitPlatform implements BackendPlatform {
return;
}
PlaceholderManagerImpl pl = TAB.getInstance().getPlaceholderManager();
int refresh = pl.getRefreshInterval(identifier);
if (identifier.startsWith("%rel_")) {
//relational placeholder
TAB.getInstance().getPlaceholderManager().registerRelationalPlaceholder(identifier, pl.getRefreshInterval(identifier), (viewer, target) ->
TAB.getInstance().getPlaceholderManager().registerRelationalPlaceholder(identifier, (viewer, target) ->
PlaceholderAPI.setRelationalPlaceholders((Player) viewer.getPlayer(), (Player) target.getPlayer(), identifier));
} else if (identifier.startsWith("%sync:")) {
registerSyncPlaceholder(identifier, refresh);
registerSyncPlaceholder(identifier);
} else if (identifier.startsWith("%server_")) {
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier, refresh,
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier,
() -> PlaceholderAPI.setPlaceholders(null, identifier));
} else {
TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier, refresh,
TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier,
p -> PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), identifier));
}
}
/**
* Registers a sync placeholder with given identifier and refresh.
* Registers a sync placeholder with given identifier and automatically decided refresh.
*
* @param identifier
* Placeholder identifier
* @param refresh
* Placeholder refresh
*/
public void registerSyncPlaceholder(@NotNull String identifier, int refresh) {
public void registerSyncPlaceholder(@NotNull String identifier) {
String syncedPlaceholder = "%" + identifier.substring(6);
PlayerPlaceholderImpl[] ppl = new PlayerPlaceholderImpl[1];
ppl[0] = TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier, refresh, p -> {
ppl[0] = TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier, p -> {
Bukkit.getScheduler().runTask(plugin, () -> {
long time = System.nanoTime();
ppl[0].updateValue(p, placeholderAPI ? PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), syncedPlaceholder) : identifier);
@@ -232,7 +274,7 @@ public class BukkitPlatform implements BackendPlatform {
command.setExecutor(cmd);
command.setTabCompleter(cmd);
} else {
logWarn(new SimpleTextComponent("Failed to register command, is it defined in plugin.yml?"));
logWarn(SimpleTextComponent.text("Failed to register command, is it defined in plugin.yml?"));
}
}
@@ -254,8 +296,8 @@ public class BukkitPlatform implements BackendPlatform {
@Override
@NotNull
public Object convertComponent(@NotNull TabComponent component) {
if (ComponentConverter.INSTANCE != null) {
return ComponentConverter.INSTANCE.convert(component);
if (serverImplementationProvider.getComponentConverter() != null) {
return serverImplementationProvider.getComponentConverter().convert(component);
} else {
return component;
}
@@ -265,7 +307,9 @@ public class BukkitPlatform implements BackendPlatform {
@NotNull
@SneakyThrows
public Scoreboard createScoreboard(@NotNull TabPlayer player) {
return ScoreboardLoader.getInstance().apply((BukkitTabPlayer) player);
Scoreboard scoreboard = viaVersionImplementationProvider.newScoreboard((BukkitTabPlayer) player);
if (scoreboard != null) return scoreboard;
return serverImplementationProvider.newScoreboard((BukkitTabPlayer) player);
}
@Override
@@ -288,17 +332,9 @@ public class BukkitPlatform implements BackendPlatform {
@NotNull
@SneakyThrows
public TabList createTabList(@NotNull TabPlayer player) {
return TabListBase.getInstance().apply((BukkitTabPlayer) player);
}
@Override
public boolean supportsNumberFormat() {
return serverVersion.getNetworkId() >= ProtocolVersion.V1_20_3.getNetworkId();
}
@Override
public boolean supportsListOrder() {
return serverVersion.getNetworkId() >= ProtocolVersion.V1_21_2.getNetworkId();
TabList tabList = viaVersionImplementationProvider.newTabList((BukkitTabPlayer) player);
if (tabList != null) return tabList;
return serverImplementationProvider.newTabList((BukkitTabPlayer) player);
}
@Override
@@ -391,4 +427,20 @@ public class BukkitPlatform implements BackendPlatform {
}
return sb.toString();
}
/**
* Returns online players from Bukkit API.
* This method may use reflections, because the return type changed in 1.8,
* and we want to avoid errors.
*
* @return Online players from Bukkit API.
*/
@SneakyThrows
@NotNull
public Collection<? extends Player> getOnlinePlayers() {
if (serverVersion.getMinorVersion() >= 8) {
return Bukkit.getOnlinePlayers();
}
return Arrays.asList((Player[]) Bukkit.class.getMethod("getOnlinePlayers").invoke(null));
}
}
@@ -38,7 +38,7 @@ public class FoliaPlatform extends BukkitPlatform {
super.loadPlayers();
// Values are never updated in the API, warn users
logWarn(new SimpleTextComponent("Folia never updates MSPT and TPS values in the API, making " +
logWarn(SimpleTextComponent.text("Folia never updates MSPT and TPS values in the API, making " +
"%mspt% and %tps% return the default values (0 and 20)."));
// Folia never calls PlayerChangedWorldEvent, this is a workaround
@@ -57,10 +57,10 @@ public class FoliaPlatform extends BukkitPlatform {
}
@Override
public void registerSyncPlaceholder(@NotNull String identifier, int refresh) {
public void registerSyncPlaceholder(@NotNull String identifier) {
String syncedPlaceholder = "%" + identifier.substring(6);
PlayerPlaceholderImpl[] ppl = new PlayerPlaceholderImpl[1];
ppl[0] = TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier, refresh, p -> {
ppl[0] = TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier, p -> {
runSync((Entity) p.getPlayer(), () -> {
long time = System.nanoTime();
String output = isPlaceholderAPI() ? PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), syncedPlaceholder) : identifier;
@@ -0,0 +1,175 @@
package me.neznamy.tab.platforms.bukkit.provider;
import com.google.common.collect.Lists;
import io.netty.channel.Channel;
import lombok.Getter;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.BukkitUtils;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.platforms.bukkit.nms.converter.ComponentConverter;
import me.neznamy.tab.platforms.bukkit.nms.converter.LegacyComponentConverter;
import me.neznamy.tab.platforms.bukkit.nms.converter.ModerateComponentConverter;
import me.neznamy.tab.platforms.bukkit.nms.converter.ModernComponentConverter;
import me.neznamy.tab.platforms.bukkit.scoreboard.BukkitScoreboard;
import me.neznamy.tab.platforms.bukkit.scoreboard.PaperScoreboard;
import me.neznamy.tab.platforms.bukkit.scoreboard.packet.PacketScoreboard;
import me.neznamy.tab.platforms.bukkit.tablist.BukkitTabList;
import me.neznamy.tab.platforms.bukkit.tablist.PacketTabList1193;
import me.neznamy.tab.platforms.bukkit.tablist.PacketTabList17;
import me.neznamy.tab.platforms.bukkit.tablist.PacketTabList18;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.impl.DummyScoreboard;
import me.neznamy.tab.shared.util.ReflectionUtils;
import me.neznamy.tab.shared.util.function.FunctionWithException;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
/**
* Implementation provider using reflection for NMS / Bukkit implementations as fallback options.
*/
@Getter
public class BukkitImplementationProvider implements ImplementationProvider {
@Nullable
private final FunctionWithException<BukkitTabPlayer, Channel> channelFunction = findChannelFunction();
@Nullable
private final ComponentConverter componentConverter = findComponentConverter();
@NotNull
private final Function<BukkitTabPlayer, Scoreboard> scoreboardProvider = findScoreboardProvider();
@NotNull
private final Function<BukkitTabPlayer, TabList> tablistProvider = findTablistProvider();
@Nullable
private FunctionWithException<BukkitTabPlayer, Channel> findChannelFunction() {
if (BukkitReflection.getMinorVersion() < 8) return null;
try {
Class<?> NetworkManager = BukkitReflection.getClass("network.Connection", "network.NetworkManager", "NetworkManager");
Class<?> PlayerConnection = BukkitReflection.getClass("server.network.ServerGamePacketListenerImpl",
"server.network.PlayerConnection", "PlayerConnection");
Class<?> EntityPlayer = BukkitReflection.getClass("server.level.ServerPlayer", "server.level.EntityPlayer", "EntityPlayer");
Field PLAYER_CONNECTION = ReflectionUtils.getOnlyField(EntityPlayer, PlayerConnection);
Field NETWORK_MANAGER;
if (BukkitReflection.is1_20_2Plus()) {
NETWORK_MANAGER = ReflectionUtils.getOnlyField(PlayerConnection.getSuperclass(), NetworkManager);
} else {
NETWORK_MANAGER = ReflectionUtils.getOnlyField(PlayerConnection, NetworkManager);
}
Field CHANNEL = ReflectionUtils.getOnlyField(NetworkManager, Channel.class);
return player -> (Channel) CHANNEL.get(NETWORK_MANAGER.get(PLAYER_CONNECTION.get(player.getHandle())));
} catch (Exception e) {
BukkitUtils.compatibilityError(e, "network channel injection", null,
"Anti-override for tablist & nametags not working",
"Compatibility with nickname plugins changing player names will not work",
"Scoreboard will not be checking for other plugins");
return null;
}
}
@Nullable
@SneakyThrows
private ComponentConverter findComponentConverter() {
try {
if (BukkitReflection.getMinorVersion() >= 19) {
// 1.19+
return new ModernComponentConverter();
} else if (BukkitReflection.getMinorVersion() >= 16) {
// 1.16 - 1.18.2
return new ModerateComponentConverter();
} else {
// 1.7 - 1.15.2
return new LegacyComponentConverter();
}
} catch (Exception e) {
Bukkit.getConsoleSender().sendMessage("§c[TAB] Failed to initialize converter from TAB components to Minecraft components. " +
"This will negatively impact most features, see below.");
if (BukkitUtils.PRINT_EXCEPTIONS) {
e.printStackTrace();
}
return null;
}
}
@NotNull
private Function<BukkitTabPlayer, Scoreboard> findScoreboardProvider() {
try {
if (BukkitReflection.getMinorVersion() >= 7) Objects.requireNonNull(componentConverter);
PacketScoreboard.load();
return PacketScoreboard::new;
} catch (Exception e) {
if (PaperScoreboard.isAvailable()) {
BukkitUtils.compatibilityError(e, "Scoreboards", "Paper API", "Compatibility with other plugins being reduced");
return PaperScoreboard::new;
} else if (BukkitScoreboard.isAvailable()) {
List<String> missingFeatures = Lists.newArrayList(
"Compatibility with other plugins being reduced",
"Features receiving new artificial character limits"
);
if (BukkitReflection.is1_20_3Plus()) {
missingFeatures.add("1.20.3+ visuals not working due to lack of API"); // soontm?
}
BukkitUtils.compatibilityError(e, "Scoreboards", "Bukkit API", missingFeatures.toArray(new String[0]));
return BukkitScoreboard::new;
} else if (BukkitReflection.getMinorVersion() >= 5) {
BukkitUtils.compatibilityError(e, "Scoreboards", null,
"Scoreboard feature will not work",
"Belowname feature will not work",
"Player objective feature will not work",
"Scoreboard teams feature will not work (nametags & sorting)");
}
}
return DummyScoreboard::new;
}
@NotNull
@SneakyThrows
private Function<BukkitTabPlayer, TabList> findTablistProvider() {
try {
if (ReflectionUtils.classExists("net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket")) {
// 1.19.3+
Objects.requireNonNull(componentConverter);
PacketTabList1193.loadNew();
return PacketTabList1193::new;
} else if (BukkitReflection.getMinorVersion() >= 8) {
// 1.8 - 1.19.2
Objects.requireNonNull(componentConverter);
PacketTabList18.load();
return PacketTabList18::new;
} else {
// 1.7.10 and lower
PacketTabList17.load();
return PacketTabList17::new;
}
} catch (Exception e) {
BukkitUtils.compatibilityError(e, "tablist entry management", "Bukkit API",
"Layout feature will not work",
"Prevent-spectator-effect feature will not work",
"Ping spoof feature will not work",
"Tablist formatting missing anti-override",
"Tablist formatting not supporting relational placeholders");
return BukkitTabList::new;
}
}
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
return scoreboardProvider.apply(player);
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return tablistProvider.apply(player);
}
}
@@ -0,0 +1,25 @@
package me.neznamy.tab.platforms.bukkit.provider;
import io.netty.channel.Channel;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.converter.ComponentConverter;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.util.function.FunctionWithException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface ImplementationProvider {
@NotNull
Scoreboard newScoreboard(@NotNull BukkitTabPlayer player);
@NotNull
TabList newTabList(@NotNull BukkitTabPlayer player);
@Nullable
ComponentConverter getComponentConverter();
@Nullable
FunctionWithException<BukkitTabPlayer, Channel> getChannelFunction();
}
@@ -0,0 +1,78 @@
package me.neznamy.tab.platforms.bukkit.provider;
import lombok.RequiredArgsConstructor;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.scoreboard.viaversion.ViaScoreboard113;
import me.neznamy.tab.platforms.bukkit.scoreboard.viaversion.ViaScoreboard116;
import me.neznamy.tab.platforms.bukkit.scoreboard.viaversion.ViaScoreboard1203;
import me.neznamy.tab.platforms.bukkit.tablist.viaversion.ViaTabList116;
import me.neznamy.tab.platforms.bukkit.tablist.viaversion.ViaTabList1193;
import me.neznamy.tab.platforms.bukkit.tablist.viaversion.ViaTabList1212;
import me.neznamy.tab.platforms.bukkit.tablist.viaversion.ViaTabList1214;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.util.ReflectionUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
/**
* Implementation for sending new features on old servers using ViaVersion API.
* If no implementation is required for a player, {@code null} is returned.
*/
@RequiredArgsConstructor
public class ViaVersionImplementationProvider {
/** Map of tablist implementations by each new version */
private static final Map<ProtocolVersion, Function<BukkitTabPlayer, TabList>> TABLIST_IMPLEMENTATIONS = new LinkedHashMap<>();
/** Map of scoreboard implementations by each new version */
private static final Map<ProtocolVersion, Function<BukkitTabPlayer, Scoreboard>> SCOREBOARD_IMPLEMENTATIONS = new LinkedHashMap<>();
static {
TABLIST_IMPLEMENTATIONS.put(ProtocolVersion.V1_21_4, ViaTabList1214::new);
TABLIST_IMPLEMENTATIONS.put(ProtocolVersion.V1_21_2, ViaTabList1212::new);
TABLIST_IMPLEMENTATIONS.put(ProtocolVersion.V1_19_3, ViaTabList1193::new);
TABLIST_IMPLEMENTATIONS.put(ProtocolVersion.V1_16, ViaTabList116::new);
SCOREBOARD_IMPLEMENTATIONS.put(ProtocolVersion.V1_20_3, ViaScoreboard1203::new);
SCOREBOARD_IMPLEMENTATIONS.put(ProtocolVersion.V1_16, ViaScoreboard116::new);
SCOREBOARD_IMPLEMENTATIONS.put(ProtocolVersion.V1_13, ViaScoreboard113::new);
}
private final ProtocolVersion serverVersion;
@Nullable
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
if (!ReflectionUtils.classExists("com.viaversion.viaversion.protocols.v1_20_2to1_20_3.Protocol1_20_2To1_20_3")) {
return null;
}
int serverVer = serverVersion.getNetworkId();
int playerVer = player.getVersion().getNetworkId();
for (Map.Entry<ProtocolVersion, Function<BukkitTabPlayer, Scoreboard>> entry : SCOREBOARD_IMPLEMENTATIONS.entrySet()) {
if (serverVer < entry.getKey().getNetworkId() && playerVer >= entry.getKey().getNetworkId()) {
return entry.getValue().apply(player);
}
}
return null;
}
@Nullable
public TabList newTabList(@NotNull BukkitTabPlayer player) {
if (!ReflectionUtils.classExists("com.viaversion.viaversion.protocols.v1_21_2to1_21_4.Protocol1_21_2To1_21_4")) {
return null;
}
int serverVer = serverVersion.getNetworkId();
int playerVer = player.getVersion().getNetworkId();
for (Map.Entry<ProtocolVersion, Function<BukkitTabPlayer, TabList>> entry : TABLIST_IMPLEMENTATIONS.entrySet()) {
if (serverVer < entry.getKey().getNetworkId() && playerVer >= entry.getKey().getNetworkId()) {
return entry.getValue().apply(player);
}
}
return null;
}
}
@@ -41,7 +41,7 @@ import java.util.concurrent.CountDownLatch;
* making per-player view of teams, especially sidebar not working.
*/
// Throw the NPE if something is not as expected, parent class should ensure it anyway
@SuppressWarnings({"deprecation", "DataFlowIssue"})
@SuppressWarnings("deprecation")
public class BukkitScoreboard extends SafeScoreboard<BukkitTabPlayer> {
/** Version in which team colors were added */
@@ -100,10 +100,16 @@ public class BukkitScoreboard extends SafeScoreboard<BukkitTabPlayer> {
checkPlayerScoreboard();
org.bukkit.scoreboard.Objective obj = newObjective(objective.getName(), "dummy", objective.getTitle(), objective.getHealthDisplay());
setObjectiveNumberFormat(obj, objective.getNumberFormat());
obj.setDisplaySlot(slots[objective.getDisplaySlot().ordinal()]);
objective.setPlatformObjective(obj);
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
checkPlayerScoreboard();
org.bukkit.scoreboard.Objective obj = (org.bukkit.scoreboard.Objective) objective.getPlatformObjective();
obj.setDisplaySlot(slots[objective.getDisplaySlot().ordinal()]);
}
@Override
public void unregisterObjective(@NonNull Objective objective) {
checkPlayerScoreboard();
@@ -1,55 +0,0 @@
package me.neznamy.tab.platforms.bukkit.scoreboard;
import com.google.common.collect.Lists;
import lombok.Getter;
import lombok.Setter;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.BukkitUtils;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.platforms.bukkit.scoreboard.packet.PacketScoreboard;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.impl.DummyScoreboard;
import me.neznamy.tab.shared.util.function.FunctionWithException;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Loader finding the best available Scoreboard implementation.
*/
public class ScoreboardLoader {
/** Instance function */
@Getter
@Setter
@NotNull
private static FunctionWithException<BukkitTabPlayer, Scoreboard> instance = DummyScoreboard::new;
/**
* Finds the best available instance for current server software.
*/
public static void findInstance() {
if (PacketScoreboard.isAvailable()) {
instance = PacketScoreboard::new;
} else if (PaperScoreboard.isAvailable()) {
instance = PaperScoreboard::new;
BukkitUtils.compatibilityError(PacketScoreboard.getException(), "Scoreboards", "Paper API", "Compatibility with other plugins being reduced");
} else if (BukkitScoreboard.isAvailable()) {
instance = BukkitScoreboard::new;
List<String> missingFeatures = Lists.newArrayList(
"Compatibility with other plugins being reduced",
"Features receiving new artificial character limits"
);
if (BukkitReflection.is1_20_3Plus()) {
missingFeatures.add("1.20.3+ visuals not working due to lack of API"); // soontm?
}
BukkitUtils.compatibilityError(PacketScoreboard.getException(), "Scoreboards", "Bukkit API", missingFeatures.toArray(new String[0]));
} else {
BukkitUtils.compatibilityError(PacketScoreboard.getException(), "Scoreboards", null,
"Scoreboard feature will not work",
"Belowname feature will not work",
"Player objective feature will not work",
"Scoreboard teams feature will not work (nametags & sorting)");
}
}
}
@@ -3,13 +3,12 @@ package me.neznamy.tab.platforms.bukkit.scoreboard.packet;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.platforms.bukkit.nms.converter.ComponentConverter;
import me.neznamy.tab.platforms.bukkit.nms.PacketSender;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.TAB;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import me.neznamy.tab.shared.util.ReflectionUtils;
import org.jetbrains.annotations.NotNull;
@@ -18,6 +17,7 @@ import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.List;
/**
@@ -27,12 +27,6 @@ import java.util.List;
*/
public class PacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
@Getter
private static boolean available;
@Getter
private static Exception exception;
static Class<?> Component;
static Class<?> Scoreboard;
static Class<?> ScoreboardObjective;
@@ -56,60 +50,54 @@ public class PacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
@Getter private static DisplayPacketData displayPacketData;
private static PacketSender packetSender;
static {
try {
int minorVersion = BukkitReflection.getMinorVersion();
Scoreboard = BukkitReflection.getClass("world.scores.Scoreboard", "Scoreboard");
ScoreboardObjective = BukkitReflection.getClass("world.scores.Objective", "world.scores.ScoreboardObjective", "ScoreboardObjective");
Class<?> IScoreboardCriteria = BukkitReflection.getClass(
"world.scores.criteria.ObjectiveCriteria", // Mojang mapped
"world.scores.criteria.IScoreboardCriteria", // Bukkit 1.17.+
"IScoreboardCriteria", // 1.5.1 - 1.16.5
"IObjective" // 1.5.0
);
ObjectivePacketClass = BukkitReflection.getClass(
"network.protocol.game.ClientboundSetObjectivePacket", // Mojang mapped
"network.protocol.game.PacketPlayOutScoreboardObjective", // Bukkit 1.17+
"PacketPlayOutScoreboardObjective", // 1.7 - 1.16.5
"Packet206SetScoreboardObjective" // 1.5 - 1.6.4
);
emptyScoreboard = Scoreboard.getConstructor().newInstance();
Objective_OBJECTIVE_NAME = ReflectionUtils.getFields(ObjectivePacketClass, String.class).get(0);
List<Field> list = ReflectionUtils.getFields(ObjectivePacketClass, int.class);
Objective_METHOD = list.get(list.size()-1);
newObjectivePacket = ObjectivePacketClass.getConstructor(ScoreboardObjective, int.class);
IScoreboardCriteria_dummy = ReflectionUtils.getFields(IScoreboardCriteria, IScoreboardCriteria).get(0).get(null);
newScoreboardObjective = ReflectionUtils.getOnlyConstructor(ScoreboardObjective);
if (minorVersion >= 7) {
Component = BukkitReflection.getClass("network.chat.Component", "network.chat.IChatBaseComponent", "IChatBaseComponent");
if (ComponentConverter.INSTANCE == null) throw new IllegalStateException("Component converter is not available");
}
if (minorVersion >= 8) {
Class<?> EnumScoreboardHealthDisplay = BukkitReflection.getClass(
"world.scores.criteria.ObjectiveCriteria$RenderType",
"world.scores.criteria.IScoreboardCriteria$EnumScoreboardHealthDisplay",
"IScoreboardCriteria$EnumScoreboardHealthDisplay",
"EnumScoreboardHealthDisplay");
healthDisplays = (Enum<?>[]) EnumScoreboardHealthDisplay.getMethod("values").invoke(null);
if (minorVersion < 13) {
Objective_RENDER_TYPE = ReflectionUtils.getOnlyField(ObjectivePacketClass, EnumScoreboardHealthDisplay);
}
}
if (minorVersion < 13) {
ScoreboardObjective_setDisplayName = ReflectionUtils.getOnlyMethod(ScoreboardObjective, void.class, String.class);
}
if (BukkitReflection.is1_20_3Plus()) {
NumberFormat = BukkitReflection.getClass("network.chat.numbers.NumberFormat");
newFixedFormat = BukkitReflection.getClass("network.chat.numbers.FixedFormat").getConstructor(Component);
}
scorePacketData = new ScorePacketData();
teamPacketData = new TeamPacketData();
displayPacketData = new DisplayPacketData();
packetSender = new PacketSender();
available = true;
} catch (Exception e) {
exception = e;
public static void load() throws ReflectiveOperationException {
int minorVersion = BukkitReflection.getMinorVersion();
Scoreboard = BukkitReflection.getClass("world.scores.Scoreboard", "Scoreboard");
ScoreboardObjective = BukkitReflection.getClass("world.scores.Objective", "world.scores.ScoreboardObjective", "ScoreboardObjective");
Class<?> IScoreboardCriteria = BukkitReflection.getClass(
"world.scores.criteria.ObjectiveCriteria", // Mojang mapped
"world.scores.criteria.IScoreboardCriteria", // Bukkit 1.17.+
"IScoreboardCriteria", // 1.5.1 - 1.16.5
"IObjective" // 1.5.0
);
ObjectivePacketClass = BukkitReflection.getClass(
"network.protocol.game.ClientboundSetObjectivePacket", // Mojang mapped
"network.protocol.game.PacketPlayOutScoreboardObjective", // Bukkit 1.17+
"PacketPlayOutScoreboardObjective", // 1.7 - 1.16.5
"Packet206SetScoreboardObjective" // 1.5 - 1.6.4
);
emptyScoreboard = Scoreboard.getConstructor().newInstance();
Objective_OBJECTIVE_NAME = ReflectionUtils.getFields(ObjectivePacketClass, String.class).get(0);
List<Field> list = ReflectionUtils.getFields(ObjectivePacketClass, int.class);
Objective_METHOD = list.get(list.size()-1);
newObjectivePacket = ObjectivePacketClass.getConstructor(ScoreboardObjective, int.class);
IScoreboardCriteria_dummy = ReflectionUtils.getFields(IScoreboardCriteria, IScoreboardCriteria).get(0).get(null);
newScoreboardObjective = ReflectionUtils.getOnlyConstructor(ScoreboardObjective);
if (minorVersion >= 7) {
Component = BukkitReflection.getClass("network.chat.Component", "network.chat.IChatBaseComponent", "IChatBaseComponent");
}
if (minorVersion >= 8) {
Class<?> EnumScoreboardHealthDisplay = BukkitReflection.getClass(
"world.scores.criteria.ObjectiveCriteria$RenderType",
"world.scores.criteria.IScoreboardCriteria$EnumScoreboardHealthDisplay",
"IScoreboardCriteria$EnumScoreboardHealthDisplay",
"EnumScoreboardHealthDisplay");
healthDisplays = (Enum<?>[]) EnumScoreboardHealthDisplay.getMethod("values").invoke(null);
if (minorVersion < 13) {
Objective_RENDER_TYPE = ReflectionUtils.getOnlyField(ObjectivePacketClass, EnumScoreboardHealthDisplay);
}
}
if (minorVersion < 13) {
ScoreboardObjective_setDisplayName = ReflectionUtils.getOnlyMethod(ScoreboardObjective, void.class, String.class);
}
if (BukkitReflection.is1_20_3Plus()) {
NumberFormat = BukkitReflection.getClass("network.chat.numbers.NumberFormat");
newFixedFormat = BukkitReflection.getClass("network.chat.numbers.FixedFormat").getConstructor(Component);
}
scorePacketData = new ScorePacketData();
teamPacketData = new TeamPacketData();
displayPacketData = new DisplayPacketData();
packetSender = new PacketSender();
}
/**
@@ -125,6 +113,10 @@ public class PacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
@Override
public void registerObjective(@NonNull Objective objective) {
packetSender.sendPacket(player, newObjectivePacket(ObjectiveAction.REGISTER, objective));
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
packetSender.sendPacket(player, displayPacketData.setDisplaySlot(objective.getDisplaySlot().ordinal(), newObjective(objective)));
}
@@ -152,23 +144,30 @@ public class PacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
@Override
@NotNull
@SneakyThrows
public Object createTeam(@NonNull String name) {
return teamPacketData.createTeam(name);
return teamPacketData.createTeam.apply(name);
}
@Override
@SneakyThrows
public void registerTeam(@NonNull Team team) {
packetSender.sendPacket(player, teamPacketData.registerTeam(team, player.getVersion()));
teamPacketData.ScoreboardTeam_players.set(team.getPlatformTeam(), new HashSet<>(team.getPlayers()));
teamPacketData.updateTeamData(team, player.getVersion());
packetSender.sendPacket(player, teamPacketData.newRegisterTeamPacket.apply(team, player.getVersion()));
}
@Override
@SneakyThrows
public void unregisterTeam(@NonNull Team team) {
packetSender.sendPacket(player, teamPacketData.unregisterTeam(team));
packetSender.sendPacket(player, teamPacketData.newUnregisterTeamPacket.apply(team));
}
@Override
@SneakyThrows
public void updateTeam(@NonNull Team team) {
packetSender.sendPacket(player, teamPacketData.updateTeam(team, player.getVersion()));
teamPacketData.updateTeamData(team, player.getVersion());
packetSender.sendPacket(player, teamPacketData.newUpdateTeamPacket.apply(team, player.getVersion()));
}
@Override
@@ -1,19 +1,20 @@
package me.neznamy.tab.platforms.bukkit.scoreboard.packet;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard.Team;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.Scoreboard.TeamAction;
import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.util.function.BiConsumerWithException;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard.Team;
import me.neznamy.tab.shared.util.ReflectionUtils;
import me.neznamy.tab.shared.util.function.BiConsumerWithException;
import me.neznamy.tab.shared.util.function.BiFunctionWithException;
import me.neznamy.tab.shared.util.function.ConsumerWithException;
import me.neznamy.tab.shared.util.function.FunctionWithException;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Constructor;
@@ -22,6 +23,7 @@ import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* Class storing all team related fields and methods.
@@ -29,33 +31,45 @@ import java.util.List;
@SuppressWarnings("unchecked")
public class TeamPacketData {
/** Flag tracking if server uses modern team data using components */
private final boolean MODERN_TEAM_DATA_VERSION = BukkitReflection.getMinorVersion() >= 13;
// Classes
private final Class<?> ScoreboardClass = BukkitReflection.getClass("world.scores.Scoreboard", "Scoreboard");
private final Class<?> ScoreboardTeamClass = BukkitReflection.getClass("world.scores.PlayerTeam", "world.scores.ScoreboardTeam", "ScoreboardTeam");
private final Class<?> TeamPacketClass = BukkitReflection.getClass(
"network.protocol.game.ClientboundSetPlayerTeamPacket", // Mojang mapped
"network.protocol.game.PacketPlayOutScoreboardTeam", // Bukkit 1.17+
"PacketPlayOutScoreboardTeam", // Bukkit 1.7 - 1.16.5
"Packet209SetScoreboardTeam" // 1.5 - 1.6.4
);
/** Flag tracking if server uses static constructor-like methods */
private final boolean STATIC_CONSTRUCTOR_VERSION = BukkitReflection.getMinorVersion() >= 17;
// Constructors
private final Constructor<?> newScoreboardTeam = ScoreboardTeamClass.getConstructor(ScoreboardClass, String.class);
private final Object emptyScoreboard;
@Getter private final Class<?> TeamPacketClass;
private Constructor<?> newTeamPacket;
private final Constructor<?> newScoreboardTeam;
private Method TeamPacketConstructor_of;
private Method TeamPacketConstructor_ofBoolean;
private final Field TeamPacket_NAME;
// Fields
public final Field ScoreboardTeam_players = ReflectionUtils.getFields(ScoreboardTeamClass, Set.class).get(0);
private final Field ScoreboardTeam_allowFriendlyFire = ReflectionUtils.getFields(ScoreboardTeamClass, boolean.class).get(0);
private final Field ScoreboardTeam_seeFriendlyInvisibles = ReflectionUtils.getFields(ScoreboardTeamClass, boolean.class).get(1);
private final Field TeamPacket_NAME = ReflectionUtils.getFields(TeamPacketClass, String.class).get(0);
private final Field TeamPacket_ACTION;
private final Field TeamPacket_PLAYERS;
private final Method ScoreboardTeam_setPrefix;
private final Method ScoreboardTeam_setSuffix;
private Method ScoreboardTeam_setColor;
private final Method ScoreboardTeam_setAllowFriendlyFire;
private final Method ScoreboardTeam_setCanSeeFriendlyInvisibles;
private final Enum<?>[] chatFormats;
private final Field TeamPacket_PLAYERS = ReflectionUtils.getOnlyField(TeamPacketClass, Collection.class);
private BiConsumerWithException<Object, Scoreboard.NameVisibility> setVisibility = (team, visibility) -> {};
private BiConsumerWithException<Object, Scoreboard.CollisionRule> setCollision = (team, collision) -> {};
// Objects
private final Object emptyScoreboard = ScoreboardClass.getConstructor().newInstance();
// Team / packet creation
public final FunctionWithException<String, Object> createTeam = name -> newScoreboardTeam.newInstance(emptyScoreboard, name);
public final BiFunctionWithException<Team, ProtocolVersion, Object> newRegisterTeamPacket;
public final FunctionWithException<Team, Object> newUnregisterTeamPacket;
public final BiFunctionWithException<Team, ProtocolVersion, Object> newUpdateTeamPacket;
// Team property setters
private final BiConsumerWithException<Team, ProtocolVersion> updatePlatformTeamPrefix = loadPrefix();
private final BiConsumerWithException<Team, ProtocolVersion> updatePlatformTeamSuffix = loadSuffix();
private final ConsumerWithException<Team> updatePlatformTeamColor = loadColor();
private final ConsumerWithException<Team> updatePlatformTeamVisibility = loadVisibility();
private final ConsumerWithException<Team> updatePlatformTeamCollision = loadCollision();
/**
* Constructs new instance and loads all required NMS classes, fields and methods.
* Constructs new instance and loads all required NMS classes, fields, and methods.
* If anything fails, exception is thrown.
*
* @throws ReflectiveOperationException
@@ -63,151 +77,108 @@ public class TeamPacketData {
*/
public TeamPacketData() throws ReflectiveOperationException {
int minorVersion = BukkitReflection.getMinorVersion();
Class<?> Scoreboard = BukkitReflection.getClass("world.scores.Scoreboard", "Scoreboard");
Class<?> scoreboardTeam = BukkitReflection.getClass("world.scores.PlayerTeam", "world.scores.ScoreboardTeam", "ScoreboardTeam");
Class<?> enumChatFormatClass = BukkitReflection.getClass("ChatFormatting", "EnumChatFormat");
TeamPacketClass = BukkitReflection.getClass(
"network.protocol.game.ClientboundSetPlayerTeamPacket", // Mojang mapped
"network.protocol.game.PacketPlayOutScoreboardTeam", // Bukkit 1.17+
"PacketPlayOutScoreboardTeam", // Bukkit 1.7 - 1.16.5
"Packet209SetScoreboardTeam" // 1.5 - 1.6.4
);
emptyScoreboard = Scoreboard.getConstructor().newInstance();
newScoreboardTeam = scoreboardTeam.getConstructor(Scoreboard, String.class);
TeamPacket_NAME = ReflectionUtils.getFields(TeamPacketClass, String.class).get(0);
List<Field> intFields = ReflectionUtils.getInstanceFields(TeamPacketClass, int.class);
if (minorVersion >= 8 && minorVersion <= 12) {
TeamPacket_ACTION = intFields.get(1);
} else {
TeamPacket_ACTION = intFields.get(0);
}
TeamPacket_PLAYERS = ReflectionUtils.getOnlyField(TeamPacketClass, Collection.class);
chatFormats = (Enum<?>[]) enumChatFormatClass.getMethod("values").invoke(null);
ScoreboardTeam_setAllowFriendlyFire = ReflectionUtils.getMethod(
scoreboardTeam,
new String[]{"func_96660_a", "setAllowFriendlyFire", "a", "m_83355_"}, // {Thermos, 1.5.1+, 1.5 & 1.18+, Mohist 1.18.2}
boolean.class
);
ScoreboardTeam_setCanSeeFriendlyInvisibles = ReflectionUtils.getMethod(
scoreboardTeam,
new String[]{"func_98300_b", "setCanSeeFriendlyInvisibles", "b", "m_83362_", "setSeeFriendlyInvisibles"}, // {Thermos, 1.5.1+, 1.5 & 1.18+, Mohist 1.18.2, 1.20.2+}
boolean.class
);
if (minorVersion >= 8) loadVisibility(scoreboardTeam);
if (minorVersion >= 9) loadCollision(scoreboardTeam);
if (MODERN_TEAM_DATA_VERSION) {
// Packet constructors
if (BukkitReflection.getMinorVersion() >= 17) {
Method TeamPacketConstructor_of = ReflectionUtils.getOnlyMethod(TeamPacketClass, TeamPacketClass, ScoreboardTeamClass);
Method TeamPacketConstructor_ofBoolean = ReflectionUtils.getOnlyMethod(TeamPacketClass, TeamPacketClass, ScoreboardTeamClass, boolean.class);
newRegisterTeamPacket = (team, version) -> TeamPacketConstructor_ofBoolean.invoke(null, team.getPlatformTeam(), true);
newUnregisterTeamPacket = team -> TeamPacketConstructor_of.invoke(null, team.getPlatformTeam());
newUpdateTeamPacket = (team, version) -> TeamPacketConstructor_ofBoolean.invoke(null, team.getPlatformTeam(), false);
} else {
Constructor<?> newTeamPacket = TeamPacketClass.getConstructor(ScoreboardTeamClass, int.class);
newRegisterTeamPacket = (team, version) -> newTeamPacket.newInstance(team.getPlatformTeam(), TeamAction.CREATE);
newUnregisterTeamPacket = team -> newTeamPacket.newInstance(team.getPlatformTeam(), TeamAction.REMOVE);
newUpdateTeamPacket = (team, version) -> newTeamPacket.newInstance(team.getPlatformTeam(), TeamAction.UPDATE);
}
}
@SneakyThrows
@NotNull
private BiConsumerWithException<Team, ProtocolVersion> loadPrefix() {
if (BukkitReflection.getMinorVersion() >= 13) {
Class<?> Component = BukkitReflection.getClass("network.chat.Component", "network.chat.IChatBaseComponent", "IChatBaseComponent");
ScoreboardTeam_setColor = ReflectionUtils.getOnlyMethod(scoreboardTeam, void.class, enumChatFormatClass);
ScoreboardTeam_setPrefix = ReflectionUtils.getMethod(
scoreboardTeam,
new String[]{"setPrefix", "b", "m_83360_", "setPlayerPrefix"}, // {1.17.1-, 1.18 - 1.20.1, Mohist 1.18.2, 1.20.2+}
Component
);
ScoreboardTeam_setSuffix = ReflectionUtils.getMethod(
scoreboardTeam,
new String[]{"setSuffix", "c", "m_83365_", "setPlayerSuffix"}, // {1.17.1-, 1.18 - 1.20.1, Mohist 1.18.2, 1.20.2+}
Component
);
Field prefix = ReflectionUtils.getFields(ScoreboardTeamClass, Component).get(1);
return (team, version) -> prefix.set(team.getPlatformTeam(), team.getPrefix().convert());
} else {
ScoreboardTeam_setPrefix = ReflectionUtils.getMethod(
scoreboardTeam,
new String[]{"func_96666_b", "setPrefix", "b"}, // {Thermos, 1.5.1+, 1.5}
String.class
);
ScoreboardTeam_setSuffix = ReflectionUtils.getMethod(
scoreboardTeam,
new String[]{"func_96662_c", "setSuffix", "c"}, // {Thermos, 1.5.1+, 1.5}
String.class
);
}
if (STATIC_CONSTRUCTOR_VERSION) {
TeamPacketConstructor_of = ReflectionUtils.getOnlyMethod(TeamPacketClass, TeamPacketClass, scoreboardTeam);
TeamPacketConstructor_ofBoolean = ReflectionUtils.getOnlyMethod(TeamPacketClass, TeamPacketClass, scoreboardTeam, boolean.class);
} else {
newTeamPacket = TeamPacketClass.getConstructor(scoreboardTeam, int.class);
Field prefix = ReflectionUtils.getFields(ScoreboardTeamClass, String.class).get(2);
return (team, version) -> {
String legacy = team.getPrefix().toLegacyText();
if (version.getMinorVersion() < 13 || TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) {
legacy = SafeScoreboard.cutTo(legacy, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13);
}
prefix.set(team.getPlatformTeam(), legacy);
};
}
}
@SneakyThrows
private void loadVisibility(@NonNull Class<?> scoreboardTeam) {
Class<?> enumNameTagVisibility = BukkitReflection.getClass(
"world.scores.Team$Visibility", // Mojang mapped
"world.scores.ScoreboardTeamBase$EnumNameTagVisibility", // Bukkit 1.17+
"ScoreboardTeamBase$EnumNameTagVisibility", // Bukkit 1.8.1 - 1.16.5
"EnumNameTagVisibility" // Bukkit 1.8.0
);
Enum<?>[] nameVisibilities = (Enum<?>[]) enumNameTagVisibility.getMethod("values").invoke(null);
Method setNameTagVisibility = ReflectionUtils.getMethod(
scoreboardTeam,
new String[]{"setNameTagVisibility", "a", "m_83346_"}, // {1.8.1+, 1.8 & 1.18+, Mohist 1.18.2}
enumNameTagVisibility
);
setVisibility = (team, visibility) -> setNameTagVisibility.invoke(team, nameVisibilities[visibility.ordinal()]);
}
@SneakyThrows
private void loadCollision(@NonNull Class<?> scoreboardTeam) {
Class<?> enumTeamPush = BukkitReflection.getClass("world.scores.Team$CollisionRule",
"world.scores.ScoreboardTeamBase$EnumTeamPush", "ScoreboardTeamBase$EnumTeamPush");
Enum<?>[] collisionRules = (Enum<?>[]) enumTeamPush.getMethod("values").invoke(null);
Method setCollisionRule = ReflectionUtils.getOnlyMethod(scoreboardTeam, void.class, enumTeamPush);
setCollision = (team, collision) -> setCollisionRule.invoke(team, collisionRules[collision.ordinal()]);
}
/**
* Creates team register packet with specified parameters.
*
* @param team
* Team to register
* @param clientVersion
* Version to create the values for
* @return Register team packet with specified parameters
*/
@SneakyThrows
public Object registerTeam(@NonNull Team team, @NotNull ProtocolVersion clientVersion) {
updateTeamData(team, clientVersion);
Object packet;
if (STATIC_CONSTRUCTOR_VERSION) {
packet = TeamPacketConstructor_ofBoolean.invoke(null, team.getPlatformTeam(), true);
@NotNull
private BiConsumerWithException<Team, ProtocolVersion> loadSuffix() {
if (BukkitReflection.getMinorVersion() >= 13) {
Class<?> Component = BukkitReflection.getClass("network.chat.Component", "network.chat.IChatBaseComponent", "IChatBaseComponent");
Field suffix = ReflectionUtils.getFields(ScoreboardTeamClass, Component).get(2);
return (team, version) -> suffix.set(team.getPlatformTeam(), team.getSuffix().convert());
} else {
packet = newTeamPacket.newInstance(team.getPlatformTeam(), TeamAction.CREATE);
}
TeamPacket_PLAYERS.set(packet, team.getPlayers());
return packet;
}
/**
* Creates unregister team packet with given team.
*
* @param team
* Team to unregister
* @return Packet for unregistering team
*/
@SneakyThrows
public Object unregisterTeam(@NonNull Team team) {
if (STATIC_CONSTRUCTOR_VERSION) {
return TeamPacketConstructor_of.invoke(null, team.getPlatformTeam());
} else {
return newTeamPacket.newInstance(team.getPlatformTeam(), TeamAction.REMOVE);
Field suffix = ReflectionUtils.getFields(ScoreboardTeamClass, String.class).get(3);
return (team, version) -> {
String legacy = team.getSuffix().toLegacyText();
if (version.getMinorVersion() < 13 || TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) {
legacy = SafeScoreboard.cutTo(legacy, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13);
}
suffix.set(team.getPlatformTeam(), legacy);
};
}
}
/**
* Creates team update packet with specified parameters.
*
* @param team
* Team to update
* @param clientVersion
* Version to create the values for
* @return Update team packet with specified parameters
*/
@SneakyThrows
public Object updateTeam(@NonNull Team team, @NotNull ProtocolVersion clientVersion) {
updateTeamData(team, clientVersion);
if (STATIC_CONSTRUCTOR_VERSION) {
return TeamPacketConstructor_ofBoolean.invoke(null, team.getPlatformTeam(), false);
@NotNull
private ConsumerWithException<Team> loadColor() {
if (BukkitReflection.getMinorVersion() >= 13) {
Class<?> EnumChatFormatClass = BukkitReflection.getClass("ChatFormatting", "EnumChatFormat");
Enum<?>[] chatFormats = (Enum<?>[]) EnumChatFormatClass.getMethod("values").invoke(null);
Method setColor = ReflectionUtils.getOnlyMethod(ScoreboardTeamClass, void.class, EnumChatFormatClass);
return team -> setColor.invoke(team.getPlatformTeam(), chatFormats[team.getColor().getLegacyColor().ordinal()]);
} else {
return newTeamPacket.newInstance(team.getPlatformTeam(), TeamAction.UPDATE);
return team -> {};
}
}
@SneakyThrows
@NotNull
private ConsumerWithException<Team> loadVisibility() {
if (BukkitReflection.getMinorVersion() >= 8) {
Class<?> enumNameTagVisibility = BukkitReflection.getClass(
"world.scores.Team$Visibility", // Mojang mapped
"world.scores.ScoreboardTeamBase$EnumNameTagVisibility", // Bukkit 1.17+
"ScoreboardTeamBase$EnumNameTagVisibility", // Bukkit 1.8.1 - 1.16.5
"EnumNameTagVisibility" // Bukkit 1.8.0
);
Enum<?>[] nameVisibilities = (Enum<?>[]) enumNameTagVisibility.getMethod("values").invoke(null);
Field ScoreboardTeam_nameTagVisibility = ReflectionUtils.getFields(ScoreboardTeamClass, enumNameTagVisibility).get(0);
return team -> ScoreboardTeam_nameTagVisibility.set(team.getPlatformTeam(), nameVisibilities[team.getVisibility().ordinal()]);
} else {
return team -> {};
}
}
@SneakyThrows
@NotNull
private ConsumerWithException<Team> loadCollision() {
if (BukkitReflection.getMinorVersion() >= 9) {
Class<?> enumTeamPush = BukkitReflection.getClass("world.scores.Team$CollisionRule",
"world.scores.ScoreboardTeamBase$EnumTeamPush", "ScoreboardTeamBase$EnumTeamPush");
Enum<?>[] collisionRules = (Enum<?>[]) enumTeamPush.getMethod("values").invoke(null);
Method setCollisionRule = ReflectionUtils.getOnlyMethod(ScoreboardTeamClass, void.class, enumTeamPush);
return team -> setCollisionRule.invoke(team.getPlatformTeam(), collisionRules[team.getCollision().ordinal()]);
} else {
return team -> {};
}
}
@@ -220,49 +191,25 @@ public class TeamPacketData {
* Version to create the values for
*/
@SneakyThrows
private void updateTeamData(@NonNull Team team, @NotNull ProtocolVersion clientVersion) {
public void updateTeamData(@NonNull Team team, @NotNull ProtocolVersion clientVersion) {
Object nmsTeam = team.getPlatformTeam();
ScoreboardTeam_setAllowFriendlyFire.invoke(nmsTeam, (team.getOptions() & 0x1) > 0);
ScoreboardTeam_setCanSeeFriendlyInvisibles.invoke(nmsTeam, (team.getOptions() & 0x2) > 0);
if (MODERN_TEAM_DATA_VERSION) {
ScoreboardTeam_setPrefix.invoke(nmsTeam, (Object) team.getPrefix().convert());
ScoreboardTeam_setSuffix.invoke(nmsTeam, (Object) team.getSuffix().convert());
ScoreboardTeam_setColor.invoke(nmsTeam, chatFormats[team.getColor().getLegacyColor().ordinal()]);
} else {
String prefix = team.getPrefix().toLegacyText();
String suffix = team.getSuffix().toLegacyText();
if (clientVersion.getMinorVersion() < 13 || TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) {
prefix = SafeScoreboard.cutTo(prefix, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13);
suffix = SafeScoreboard.cutTo(suffix, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13);
}
ScoreboardTeam_setPrefix.invoke(nmsTeam, prefix);
ScoreboardTeam_setSuffix.invoke(nmsTeam, suffix);
}
setVisibility.accept(nmsTeam, team.getVisibility());
setCollision.accept(nmsTeam, team.getCollision());
ScoreboardTeam_allowFriendlyFire.set(nmsTeam, (team.getOptions() & 0x1) > 0);
ScoreboardTeam_seeFriendlyInvisibles.set(nmsTeam, (team.getOptions() & 0x2) > 0);
updatePlatformTeamPrefix.accept(team, clientVersion);
updatePlatformTeamSuffix.accept(team, clientVersion);
updatePlatformTeamColor.accept(team);
updatePlatformTeamVisibility.accept(team);
updatePlatformTeamCollision.accept(team);
}
/**
* Creates a team with given name.
*
* @param name
* Team name
* @return Team with specified name
*/
@SneakyThrows
@NotNull
public Object createTeam(@NonNull String name) {
return newScoreboardTeam.newInstance(emptyScoreboard, name);
}
/**
* Checks if packet is team packet and removes all real players from team
* Checks if the packet is a team packet and removes all real players from the team
* if sent by other plugins and anti-override is fully active on a player.
*
* @param player
* Player who received the packet
* @param packet
* Received packet
* The received packet
*/
@SneakyThrows
public void onPacketSend(@NonNull TabPlayer player, @NonNull Object packet) {
@@ -0,0 +1,189 @@
package me.neznamy.tab.platforms.bukkit.scoreboard.viaversion;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.api.connection.UserConnection;
import com.viaversion.viaversion.api.protocol.Protocol;
import com.viaversion.viaversion.api.protocol.packet.PacketType;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.protocol.packet.State;
import com.viaversion.viaversion.api.type.Types;
import lombok.NonNull;
import me.neznamy.chat.component.SimpleTextComponent;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Scoreboard abstract implementation using ViaVersion packets,
* focused to unlock new features on older versions, and also
* optimize packet conversion between versions.
*/
public abstract class ViaScoreboard extends SafeScoreboard<BukkitTabPlayer> {
private static final long CHECK_DELAY = 50;
private static final Object DUMMY = new Object();
protected final Class<? extends Protocol> protocol;
protected final PacketType setDisplayObjective;
private final PacketType setObjective;
protected final PacketType setScore;
private final PacketType setPlayerTeam;
/** User connection this scoreboard belongs to */
protected final UserConnection connection;
private transient ScheduledFuture<?> task;
private transient final Queue<PacketWrapper> queuedPackets = new ConcurrentLinkedQueue<>();
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
* @param protocol
* Protocol to be sent packets through
* @param setDisplayObjective
* Objective creation packet
* @param setObjective
* Objective update packet
* @param setScore
* Score update packet
* @param setPlayerTeam
* Player team packet
*/
public ViaScoreboard(@NonNull BukkitTabPlayer player, @NonNull Class<? extends Protocol> protocol, @NonNull PacketType setDisplayObjective, @NonNull PacketType setObjective, @NonNull PacketType setScore, @NonNull PacketType setPlayerTeam) {
super(player);
this.protocol = protocol;
this.setDisplayObjective = setDisplayObjective;
this.setObjective = setObjective;
this.setScore = setScore;
this.setPlayerTeam = setPlayerTeam;
this.connection = Via.getManager().getConnectionManager().getConnectedClient(player.getUniqueId());
// Queue packets until connection is available
// Is important to use scheduleWithFixedDelay() to avoid task overlap
task = connection.getChannel().eventLoop().scheduleWithFixedDelay(() -> {
if (connection.getProtocolInfo().getClientState() == State.PLAY) {
PacketWrapper packet;
while ((packet = queuedPackets.poll()) != null) {
packet.send(protocol);
}
task.cancel(true);
}
}, 0L, CHECK_DELAY, TimeUnit.MILLISECONDS);
}
@Override
public void registerObjective(@NonNull Objective objective) {
sendObjectiveUpdate(ObjectiveAction.REGISTER, objective);
}
@Override
public void unregisterObjective(@NonNull Objective objective) {
sendObjectiveUpdate(ObjectiveAction.UNREGISTER, objective);
}
@Override
public void updateObjective(@NonNull Objective objective) {
sendObjectiveUpdate(ObjectiveAction.UPDATE, objective);
}
@Override
public @NonNull Object createTeam(@NonNull String name) {
// This implementation does not use team objects
return DUMMY;
}
@Override
public void registerTeam(@NonNull Team team) {
sendTeamUpdate(TeamAction.CREATE, team);
}
@Override
public void unregisterTeam(@NonNull Team team) {
sendTeamUpdate(TeamAction.REMOVE, team);
}
@Override
public void updateTeam(@NonNull Team team) {
sendTeamUpdate(TeamAction.UPDATE, team);
}
protected void sendObjectiveUpdate(int mode, @NonNull Objective objective) {
final PacketWrapper packet = PacketWrapper.create(setObjective, null, connection);
// Objective name
packet.write(Types.STRING, objective.getName());
// Mode
packet.write(Types.BYTE, (byte) mode);
// Optional
if (mode == ObjectiveAction.REGISTER || mode == ObjectiveAction.UPDATE) {
writeObjectiveDisplay(packet, objective);
}
send(packet);
}
@SuppressWarnings("fallthrough")
protected void sendTeamUpdate(int mode, @NonNull Team team) {
final PacketWrapper packet = PacketWrapper.create(setPlayerTeam, null, connection);
// Team name (max 16 characters on pre 1.20.3)
packet.write(Types.STRING, team.getName());
// Method
packet.write(Types.BYTE, (byte) mode);
switch (mode) {
case TeamAction.REMOVE:
break;
case TeamAction.CREATE:
case TeamAction.UPDATE:
// Team display name
writeComponent(packet, SimpleTextComponent.text(team.getName()));
// Friendly flags
packet.write(Types.BYTE, (byte) team.getOptions());
// Name tag visibility
packet.write(Types.STRING, team.getVisibility().toString());
// Collision rule
packet.write(Types.STRING, team.getCollision().toString());
// Team color
packet.write(Types.VAR_INT, team.getColor().getLegacyColor().ordinal());
// Team prefix
writeComponent(packet, team.getPrefix());
// Team suffix
writeComponent(packet, team.getSuffix());
if (mode == TeamAction.UPDATE) {
break;
}
// Intentional fallthrough for CREATE mode
case TeamAction.ADD_PLAYER:
case TeamAction.REMOVE_PLAYER:
// Entities
packet.write(Types.VAR_INT, team.getPlayers().size());
for (String entity : team.getPlayers()) {
// Entity (player username in this case) (max 40 characters on pre 1.20.3)
packet.write(Types.STRING, entity);
}
break;
default:
throw new IllegalArgumentException("Invalid team update mode: " + mode);
}
send(packet);
}
protected abstract void writeComponent(@NonNull PacketWrapper packet, @NonNull TabComponent component);
protected abstract void writeObjectiveDisplay(@NonNull PacketWrapper packet, @NonNull Objective objective);
protected void send(@NonNull PacketWrapper packet) {
if (!task.isCancelled()) {
queuedPackets.add(packet);
} else {
packet.scheduleSend(protocol);
}
}
}
@@ -0,0 +1,82 @@
package me.neznamy.tab.platforms.bukkit.scoreboard.viaversion;
import com.viaversion.viaversion.api.protocol.Protocol;
import com.viaversion.viaversion.api.protocol.packet.PacketType;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.protocols.v1_12_2to1_13.Protocol1_12_2To1_13;
import com.viaversion.viaversion.protocols.v1_12_2to1_13.packet.ClientboundPackets1_13;
import com.viaversion.viaversion.util.ComponentUtil;
import lombok.NonNull;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
/**
* Scoreboard implementation using ViaVersion packets
* to unlock +1.13 player features on pre 1.13 servers.
*/
public class ViaScoreboard113 extends ViaScoreboard {
/**
* Constructs new instance with given player.
*
* @param player Player this scoreboard will belong to
*/
public ViaScoreboard113(@NonNull BukkitTabPlayer player) {
this(player, Protocol1_12_2To1_13.class, ClientboundPackets1_13.SET_DISPLAY_OBJECTIVE, ClientboundPackets1_13.SET_OBJECTIVE, ClientboundPackets1_13.SET_SCORE, ClientboundPackets1_13.SET_PLAYER_TEAM);
}
protected ViaScoreboard113(@NonNull BukkitTabPlayer player, @NonNull Class<? extends Protocol> protocol, @NonNull PacketType setDisplayObjective, @NonNull PacketType setObjective, @NonNull PacketType setScore, @NonNull PacketType setPlayerTeam) {
super(player, protocol, setDisplayObjective, setObjective, setScore, setPlayerTeam);
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
PacketWrapper packet = PacketWrapper.create(setDisplayObjective, null, connection);
packet.write(Types.BYTE, (byte) objective.getDisplaySlot().ordinal());
packet.write(Types.STRING, objective.getName());
send(packet);
}
@Override
public void setScore(@NonNull Score score) {
sendScoreUpdate(ScoreAction.CHANGE, score);
}
@Override
public void removeScore(@NonNull Score score) {
sendScoreUpdate(ScoreAction.REMOVE, score);
}
protected void sendScoreUpdate(int action, @NonNull Score score) {
final PacketWrapper packet = PacketWrapper.create(setScore, null, connection);
// Entity name (player username in this case)
packet.write(Types.STRING, score.getHolder());
// Action
packet.write(Types.VAR_INT, action);
// Objective name (max 16 characters)
packet.write(Types.STRING, score.getObjective().getName());
if (action == 0) {
// Value
packet.write(Types.VAR_INT, score.getValue());
}
send(packet);
}
@Override
protected void writeComponent(@NonNull PacketWrapper packet, @NonNull TabComponent component) {
packet.write(Types.COMPONENT, ComponentUtil.legacyToJson(component.toLegacyText()));
}
@Override
protected void writeObjectiveDisplay(@NonNull PacketWrapper packet, @NonNull Objective objective) {
// Objective value
writeComponent(packet, objective.getTitle());
// Type
packet.write(Types.VAR_INT, objective.getHealthDisplay() == HealthDisplay.INTEGER ? 0 : 1);
}
}
@@ -0,0 +1,36 @@
package me.neznamy.tab.platforms.bukkit.scoreboard.viaversion;
import com.viaversion.viaversion.api.protocol.Protocol;
import com.viaversion.viaversion.api.protocol.packet.PacketType;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.protocols.v1_15_2to1_16.Protocol1_15_2To1_16;
import com.viaversion.viaversion.protocols.v1_15_2to1_16.packet.ClientboundPackets1_16;
import lombok.NonNull;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
/**
* Scoreboard implementation using ViaVersion packets
* to unlock +1.16 player features on pre 1.16 servers.
*/
public class ViaScoreboard116 extends ViaScoreboard113 {
/**
* Constructs new instance with given player.
*
* @param player Player this scoreboard will belong to
*/
public ViaScoreboard116(@NonNull BukkitTabPlayer player) {
this(player, Protocol1_15_2To1_16.class, ClientboundPackets1_16.SET_DISPLAY_OBJECTIVE, ClientboundPackets1_16.SET_OBJECTIVE, ClientboundPackets1_16.SET_SCORE, ClientboundPackets1_16.SET_PLAYER_TEAM);
}
protected ViaScoreboard116(@NonNull BukkitTabPlayer player, @NonNull Class<? extends Protocol> protocol, @NonNull PacketType setDisplayObjective, @NonNull PacketType setObjective, @NonNull PacketType setScore, @NonNull PacketType setPlayerTeam) {
super(player, protocol, setDisplayObjective, setObjective, setScore, setPlayerTeam);
}
@Override
protected void writeComponent(@NonNull PacketWrapper packet, @NonNull TabComponent component) {
packet.write(Types.COMPONENT, component.toViaVersion());
}
}
@@ -0,0 +1,101 @@
package me.neznamy.tab.platforms.bukkit.scoreboard.viaversion;
import com.viaversion.viaversion.api.protocol.Protocol;
import com.viaversion.viaversion.api.protocol.packet.PacketType;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.protocols.v1_20_2to1_20_3.Protocol1_20_2To1_20_3;
import com.viaversion.viaversion.protocols.v1_20_2to1_20_3.packet.ClientboundPackets1_20_3;
import lombok.NonNull;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import org.jetbrains.annotations.Nullable;
/**
* Scoreboard implementation using ViaVersion packets
* to unlock +1.20.3 player features on pre 1.20.3 servers.
*/
public class ViaScoreboard1203 extends ViaScoreboard116 {
private final PacketType resetScore;
/**
* Constructs new instance with given player.
*
* @param player Player this scoreboard will belong to
*/
public ViaScoreboard1203(@NonNull BukkitTabPlayer player) {
this(player, Protocol1_20_2To1_20_3.class, ClientboundPackets1_20_3.SET_DISPLAY_OBJECTIVE, ClientboundPackets1_20_3.SET_OBJECTIVE, ClientboundPackets1_20_3.SET_SCORE, ClientboundPackets1_20_3.RESET_SCORE, ClientboundPackets1_20_3.SET_PLAYER_TEAM);
}
protected ViaScoreboard1203(@NonNull BukkitTabPlayer player, @NonNull Class<? extends Protocol> protocol, @NonNull PacketType setDisplayObjective, @NonNull PacketType setObjective, @NonNull PacketType setScore, @NonNull PacketType resetScore, @NonNull PacketType setPlayerTeam) {
super(player, protocol, setDisplayObjective, setObjective, setScore, setPlayerTeam);
this.resetScore = resetScore;
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
PacketWrapper packet = PacketWrapper.create(setDisplayObjective, null, connection);
packet.write(Types.VAR_INT, objective.getDisplaySlot().ordinal());
packet.write(Types.STRING, objective.getName());
send(packet);
}
@Override
public void setScore(@NonNull Score score) {
final PacketWrapper packet = PacketWrapper.create(setScore, null, connection);
// Entity name (player username in this case)
packet.write(Types.STRING, score.getHolder());
// Objective name
packet.write(Types.STRING, score.getObjective().getName());
// Value
packet.write(Types.VAR_INT, score.getValue());
// Display name
packet.write(Types.OPTIONAL_TAG, score.getDisplayName() == null ? null : score.getDisplayName().toViaVersionTag());
// Number format
writeNumberFormat(packet, score.getNumberFormat());
send(packet);
}
@Override
public void removeScore(@NonNull Score score) {
final PacketWrapper packet = PacketWrapper.create(resetScore, null, connection);
// Entity name (player username in this case)
packet.write(Types.STRING, score.getHolder());
// Objective name
packet.write(Types.OPTIONAL_STRING, score.getObjective().getName().isEmpty() ? null : score.getObjective().getName());
send(packet);
}
@Override
protected void writeComponent(@NonNull PacketWrapper packet, @NonNull TabComponent component) {
packet.write(Types.TAG, component.toViaVersionTag());
}
@Override
protected void writeObjectiveDisplay(@NonNull PacketWrapper packet, @NonNull Objective objective) {
super.writeObjectiveDisplay(packet, objective);
writeNumberFormat(packet, objective.getNumberFormat());
}
protected void writeNumberFormat(@NonNull PacketWrapper packet, @Nullable TabComponent content) {
// For now, only fixed format is supported by TAB
if (content == null) {
// Has number format
packet.write(Types.BOOLEAN, false);
} else {
// Has number format
packet.write(Types.BOOLEAN, true);
// Fixed format
packet.write(Types.VAR_INT, 2);
// Content
packet.write(Types.TAG, content.toViaVersionTag());
}
}
}
@@ -3,6 +3,7 @@ package me.neznamy.tab.platforms.bukkit.tablist;
import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
@@ -12,7 +13,7 @@ import java.util.UUID;
/**
* TabList handler using the almighty Bukkit API.
*/
public class BukkitTabList extends TabListBase<String> {
public class BukkitTabList extends TrackedTabList<BukkitTabPlayer> {
/**
* Constructs new instance with given player.
@@ -31,10 +32,10 @@ public class BukkitTabList extends TabListBase<String> {
@Override
@SuppressWarnings("deprecation")
public void updateDisplayName(@NonNull UUID entry, @Nullable String displayName) {
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
Player p = Bukkit.getPlayer(entry);
if (p == null) return;
p.setPlayerListName(displayName);
p.setPlayerListName(displayName == null ? null : displayName.toLegacyText());
}
@Override
@@ -63,13 +64,23 @@ public class BukkitTabList extends TabListBase<String> {
}
@Override
public void addEntry(@NonNull UUID id, @NonNull String name, @Nullable Skin skin, boolean listed, int latency,
int gameMode, @Nullable String displayName, int listOrder, boolean showHat) {
public void addEntry0(@NonNull Entry entry) {
// Shrug
}
@Override
public String toComponent(@NonNull TabComponent component) {
return player.getPlatform().toBukkitFormat(component);
public boolean containsEntry(@NonNull UUID entry) {
return true;
}
@Override
public void setPlayerListHeaderFooter(@NonNull TabComponent header, @NonNull TabComponent footer) {
player.getPlatform().getHeaderFooter().set(player, header, footer);
}
@Override
@Nullable
public Skin getSkin() {
return null; // Shrug
}
}
@@ -3,6 +3,7 @@ package me.neznamy.tab.platforms.bukkit.tablist;
import com.mojang.authlib.GameProfile;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.shared.ProtocolVersion;
@@ -144,7 +145,7 @@ public class PacketTabList1193 extends PacketTabList18 {
@NotNull
@Override
public Object createPacket(@NonNull Action action, @NonNull UUID id, @NonNull String name, @Nullable Skin skin,
boolean listed, int latency, int gameMode, @Nullable Object displayName, int listOrder, boolean showHat) {
boolean listed, int latency, int gameMode, @Nullable TabComponent displayName, int listOrder, boolean showHat) {
Object packet = newPlayerInfo.newInstance(actionToEnumSet.get(action), Collections.emptyList());
PLAYERS.set(packet, Collections.singletonList(newPlayerInfoData(
id,
@@ -152,7 +153,7 @@ public class PacketTabList1193 extends PacketTabList18 {
listed,
latency,
gameModes[gameMode],
displayName,
displayName == null ? null : displayName.convert(),
showHat,
listOrder,
null
@@ -176,9 +177,9 @@ public class PacketTabList1193 extends PacketTabList18 {
int listOrder = v1_21_2Plus ? PlayerInfoData_ListOrder.getInt(nmsData) : 0;
boolean showHat = BukkitReflection.is1_21_4Plus() && PlayerInfoData_ShowHat.getBoolean(nmsData);
if (actions.contains(actionUpdateDisplayName)) {
Object expectedName = getExpectedDisplayNames().get(id);
if (expectedName != null && expectedName != displayName) {
displayName = expectedName;
TabComponent expectedName = getExpectedDisplayNames().get(id);
if (expectedName != null && expectedName.convert() != displayName) {
displayName = expectedName.convert();
rewriteEntry = rewritePacket = true;
}
}
@@ -7,6 +7,7 @@ import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.platforms.bukkit.nms.PacketSender;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import me.neznamy.tab.shared.util.ReflectionUtils;
import me.neznamy.tab.shared.util.function.TriFunctionWithException;
import org.jetbrains.annotations.NotNull;
@@ -21,7 +22,7 @@ import java.util.UUID;
/**
* TabList handler for 1.7- servers using packets.
*/
public class PacketTabList17 extends TabListBase<String> {
public class PacketTabList17 extends TrackedTabList<BukkitTabPlayer> {
private static TriFunctionWithException<String, Boolean, Integer, Object> newPacket;
private static PacketSender packetSender;
@@ -83,10 +84,10 @@ public class PacketTabList17 extends TabListBase<String> {
@Override
@SneakyThrows
public void updateDisplayName(@NonNull UUID entry, @Nullable String displayName) {
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
if (!displayNames.containsKey(entry)) return; // Entry not tracked by TAB
packetSender.sendPacket(player, newPacket.apply(displayNames.get(entry), false, 0));
addEntry(entry, userNames.get(entry), null, false, 0, 0, displayName, 0, false);
addEntry0(new Entry(entry, userNames.get(entry), null, false, 0, 0, displayName, 0, false));
}
@Override
@@ -117,19 +118,33 @@ public class PacketTabList17 extends TabListBase<String> {
}
@Override
@SneakyThrows
public void addEntry(@NonNull UUID id, @NonNull String name, @Nullable Skin skin, boolean listed, int latency,
int gameMode, @Nullable String displayName, int listOrder, boolean showHat) {
String display = displayName == null ? name : displayName;
packetSender.sendPacket(player, newPacket.apply(display, true, latency));
userNames.put(id, name);
displayNames.put(id, display);
public boolean containsEntry(@NonNull UUID entry) {
return true;
}
@Override
public String toComponent(@NonNull TabComponent component) {
String name = component.toLegacyText();
if (name.length() > Limitations.MAX_DISPLAY_NAME_LENGTH_1_7) name = name.substring(0, Limitations.MAX_DISPLAY_NAME_LENGTH_1_7);
return name;
public void setPlayerListHeaderFooter(@NonNull TabComponent header, @NonNull TabComponent footer) {
// Added in 1.8
}
@Override
@SneakyThrows
public void addEntry0(@NonNull Entry entry) {
String displayName;
if (entry.getDisplayName() != null) {
displayName = entry.getDisplayName().toLegacyText();
if (displayName.length() > Limitations.MAX_DISPLAY_NAME_LENGTH_1_7) displayName = displayName.substring(0, Limitations.MAX_DISPLAY_NAME_LENGTH_1_7);
} else {
displayName = entry.getName();
}
packetSender.sendPacket(player, newPacket.apply(displayName, true, entry.getLatency()));
userNames.put(entry.getUniqueId(), entry.getName());
displayNames.put(entry.getUniqueId(), displayName);
}
@Override
@Nullable
public Skin getSkin() {
return null; // Added in 1.8
}
}
@@ -5,13 +5,14 @@ import com.mojang.authlib.properties.Property;
import lombok.NonNull;
import lombok.Setter;
import lombok.SneakyThrows;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.BukkitUtils;
import me.neznamy.tab.platforms.bukkit.nms.converter.ComponentConverter;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.platforms.bukkit.nms.PacketSender;
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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,7 +26,10 @@ import java.util.*;
*/
@Setter
@SuppressWarnings({"unchecked", "rawtypes"})
public class PacketTabList18 extends TabListBase<Object> {
public class PacketTabList18 extends TrackedTabList<BukkitTabPlayer> {
@Nullable
public static SkinData skinData;
protected static Class<?> PlayerInfoClass;
protected static Constructor<?> newPlayerInfo;
@@ -100,7 +104,6 @@ public class PacketTabList18 extends TabListBase<Object> {
Enum.valueOf(gameMode, "SPECTATOR")
};
packetSender = new PacketSender();
if (ComponentConverter.INSTANCE == null) throw new IllegalStateException("Component converter is not available");
try {
skinData = new SkinData();
} catch (Exception e) {
@@ -116,7 +119,7 @@ public class PacketTabList18 extends TabListBase<Object> {
}
@Override
public void updateDisplayName(@NonNull UUID entry, @Nullable Object displayName) {
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
packetSender.sendPacket(player,
createPacket(Action.UPDATE_DISPLAY_NAME, entry, "", null, false, 0, 0, displayName, 0, false));
}
@@ -149,10 +152,15 @@ public class PacketTabList18 extends TabListBase<Object> {
}
@Override
public void addEntry(@NonNull UUID id, @NonNull String name, @Nullable Skin skin, boolean listed, int latency,
int gameMode, @Nullable Object displayName, int listOrder, boolean showHat) {
public void addEntry0(@NonNull Entry entry) {
packetSender.sendPacket(player,
createPacket(Action.ADD_PLAYER, id, name, skin, listed, latency, gameMode, displayName, listOrder, showHat));
createPacket(Action.ADD_PLAYER, entry.getUniqueId(), entry.getName(), entry.getSkin(), entry.isListed(),
entry.getLatency(), entry.getGameMode(), entry.getDisplayName(), entry.getListOrder(), entry.isShowHat()));
}
@Override
public void setPlayerListHeaderFooter(@NonNull TabComponent header, @NonNull TabComponent footer) {
player.getPlatform().getHeaderFooter().set(player, header, footer);
}
/**
@@ -183,7 +191,7 @@ public class PacketTabList18 extends TabListBase<Object> {
@SneakyThrows
@NotNull
public Object createPacket(@NonNull Action action, @NonNull UUID id, @NonNull String name, @Nullable Skin skin,
boolean listed, int latency, int gameMode, @Nullable Object displayName, int listOrder, boolean showHat) {
boolean listed, int latency, int gameMode, @Nullable TabComponent displayName, int listOrder, boolean showHat) {
Object packet = newPlayerInfo.newInstance(Enum.valueOf(ActionClass, action.name()), Collections.emptyList());
List<Object> parameters = new ArrayList<>();
if (newPlayerInfoData.getParameterTypes()[0] == PlayerInfoClass) {
@@ -192,7 +200,7 @@ public class PacketTabList18 extends TabListBase<Object> {
parameters.add(createProfile(id, name, skin));
parameters.add(latency);
parameters.add(gameModes[gameMode]);
parameters.add(displayName);
parameters.add(displayName == null ? null : displayName.convert());
if (BukkitReflection.getMinorVersion() >= 19) parameters.add(null);
PLAYERS.set(packet, Collections.singletonList(newPlayerInfoData.newInstance(parameters.toArray())));
return packet;
@@ -228,8 +236,8 @@ public class PacketTabList18 extends TabListBase<Object> {
GameProfile profile = (GameProfile) PlayerInfoData_Profile.get(nmsData);
UUID id = profile.getId();
if (action.equals(Action.UPDATE_DISPLAY_NAME.name()) || action.equals(Action.ADD_PLAYER.name())) {
Object expectedName = getExpectedDisplayNames().get(id);
if (expectedName != null) PlayerInfoData_DisplayName.set(nmsData, expectedName);
TabComponent expectedName = getExpectedDisplayNames().get(id);
if (expectedName != null) PlayerInfoData_DisplayName.set(nmsData, expectedName.convert());
}
if (action.equals(Action.UPDATE_LATENCY.name()) || action.equals(Action.ADD_PLAYER.name())) {
int oldLatency = PlayerInfoData_Latency.getInt(nmsData);
@@ -243,4 +251,16 @@ public class PacketTabList18 extends TabListBase<Object> {
}
}
}
@Override
public boolean containsEntry(@NonNull UUID entry) {
return true; // TODO?
}
@Nullable
@Override
public Skin getSkin() {
if (skinData == null) return null;
return skinData.getSkin(player);
}
}
@@ -1,92 +0,0 @@
package me.neznamy.tab.platforms.bukkit.tablist;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.BukkitUtils;
import me.neznamy.tab.platforms.bukkit.header.HeaderFooter;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import me.neznamy.tab.shared.util.ReflectionUtils;
import me.neznamy.tab.shared.util.function.FunctionWithException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
/**
* Base TabList class for all implementations.
*
* @param <C>
* Component class
*/
public abstract class TabListBase<C> extends TrackedTabList<BukkitTabPlayer, C> {
/** Instance function */
@Getter
@Setter
private static FunctionWithException<BukkitTabPlayer, TabListBase<?>> instance;
@Nullable
protected static SkinData skinData;
/**
* Constructs new instance.
*
* @param player
* Player this tablist will belong to
*/
protected TabListBase(@NotNull BukkitTabPlayer player) {
super(player);
}
/**
* Finds the best available instance for current server software.
*/
public static void findInstance() {
try {
if (ReflectionUtils.classExists("net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket")) {
PacketTabList1193.loadNew();
instance = PacketTabList1193::new;
} else if (BukkitReflection.getMinorVersion() >= 8) {
PacketTabList18.load();
instance = PacketTabList18::new;
} else {
PacketTabList17.load();
instance = PacketTabList17::new;
}
} catch (Exception e) {
BukkitUtils.compatibilityError(e, "tablist entry management", "Bukkit API",
"Layout feature will not work",
"Prevent-spectator-effect feature will not work",
"Ping spoof feature will not work",
"Tablist formatting missing anti-override",
"Tablist formatting not supporting relational placeholders");
instance = BukkitTabList::new;
}
}
@Override
public void setPlayerListHeaderFooter(@NonNull TabComponent header, @NonNull TabComponent footer) {
if (HeaderFooter.getInstance() != null) HeaderFooter.getInstance().set(player, header, footer);
}
@Override
public boolean containsEntry(@NonNull UUID entry) {
return true; // TODO?
}
/**
* Returns player's skin. If NMS fields did not load or server is in
* offline mode, returns {@code null}.
*
* @return Player's skin or {@code null}.
*/
@Nullable
public Skin getSkin() {
if (skinData == null) return null;
return skinData.getSkin(player);
}
}
@@ -0,0 +1,178 @@
package me.neznamy.tab.platforms.bukkit.tablist.viaversion;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.api.connection.UserConnection;
import com.viaversion.viaversion.api.protocol.Protocol;
import com.viaversion.viaversion.api.protocol.packet.PacketType;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.protocol.packet.State;
import com.viaversion.viaversion.api.type.Types;
import lombok.NonNull;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.tablist.PacketTabList18;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.BitSet;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* TabList abstract handler using ViaVersion packets,
* focused to unlock new features on older versions,
* and also optimize packet conversion between versions.
*/
public abstract class ViaTabList extends TrackedTabList<BukkitTabPlayer> {
private static final long CHECK_DELAY = 50;
protected final Class<? extends Protocol> protocol;
private final PacketType playerInfoUpdate;
private final PacketType tabList;
/** User connection this tablist belongs to */
protected final UserConnection connection;
private transient ScheduledFuture<?> task;
private transient final Queue<PacketWrapper> queuedPackets = new ConcurrentLinkedQueue<>();
/**
*
* @param player
* Player this tablist will belong to
* @param protocol
* Protocol to be sent packets through
* @param playerInfoUpdate
* Player information update packet
* @param tabList
* Tab list packet
*/
public ViaTabList(@NonNull BukkitTabPlayer player, @NonNull Class<? extends Protocol> protocol, @NonNull PacketType playerInfoUpdate, @NonNull PacketType tabList) {
super(player);
this.protocol = protocol;
this.playerInfoUpdate = playerInfoUpdate;
this.tabList = tabList;
this.connection = Via.getManager().getConnectionManager().getConnectedClient(player.getUniqueId());
// Queue packets until connection is available
// Is important to use scheduleWithFixedDelay() to avoid task overlap
task = connection.getChannel().eventLoop().scheduleWithFixedDelay(() -> {
if (connection.getProtocolInfo().getClientState() == State.PLAY) {
PacketWrapper packet;
while ((packet = queuedPackets.poll()) != null) {
packet.send(protocol);
}
task.cancel(true);
}
}, 0L, CHECK_DELAY, TimeUnit.MILLISECONDS);
}
@Override
public boolean containsEntry(@NonNull UUID entry) {
return true; // TODO?
}
@Override
public void addEntry0(@NonNull Entry entry) {
sendInfoUpdate(Action.ADD_PLAYER, entry.getUniqueId(), entry);
}
@Override
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
sendInfoUpdate(Action.UPDATE_DISPLAY_NAME, entry, displayName);
}
@Override
public void updateLatency(@NonNull UUID entry, int latency) {
sendInfoUpdate(Action.UPDATE_LATENCY, entry, latency);
}
@Override
public void updateGameMode(@NonNull UUID entry, int gameMode) {
sendInfoUpdate(Action.UPDATE_GAME_MODE, entry, gameMode);
}
@Override
public void setPlayerListHeaderFooter(@NonNull TabComponent header, @NonNull TabComponent footer) {
final PacketWrapper packet = PacketWrapper.create(tabList, null, connection);
writeComponent(packet, header);
writeComponent(packet, footer);
send(packet);
}
@Override
public @Nullable Skin getSkin() {
if (PacketTabList18.skinData == null) return null;
return PacketTabList18.skinData.getSkin(player);
}
protected void sendInfoUpdate(@NonNull Action action, @NonNull UUID uniqueId, Object value) {
final PacketWrapper packet = PacketWrapper.create(playerInfoUpdate, null, connection);
// Action
writeAction(packet, action);
// Players size
packet.write(Types.VAR_INT, 1);
// Players
packet.write(Types.UUID, uniqueId);
switch (action) {
case ADD_PLAYER:
writeEntry(packet, (Entry) value);
break;
case REMOVE_PLAYER:
break;
case UPDATE_GAME_MODE:
case UPDATE_LATENCY:
case UPDATE_LIST_ORDER:
packet.write(Types.VAR_INT, (int) value);
break;
case UPDATE_LISTED:
case UPDATE_HAT:
packet.write(Types.BOOLEAN, (boolean) value);
break;
case UPDATE_DISPLAY_NAME:
writeOptionalComponent(packet, (TabComponent) value);
break;
default:
throw new IllegalArgumentException("Cannot send info update with action " + action.name());
}
send(packet);
}
protected abstract void writeComponent(@NonNull PacketWrapper packet, @NonNull TabComponent component);
protected abstract void writeOptionalComponent(@NonNull PacketWrapper packet, @Nullable TabComponent component);
protected abstract void writeAction(@NonNull PacketWrapper packet, @NonNull Action action);
protected abstract void writeEntry(@NonNull PacketWrapper packet, @NonNull Entry entry);
@NotNull
protected static BitSet bitSet(int nbits, int action) {
final BitSet bitSet = new BitSet(nbits);
bitSet.set(action);
return bitSet;
}
@NotNull
protected static BitSet bitSet(int nbits, int fromAction, int toAction) {
final BitSet bit = new BitSet(nbits);
bit.set(fromAction, toAction);
return bit;
}
protected void send(@NonNull PacketWrapper packet) {
if (!task.isCancelled()) {
queuedPackets.add(packet);
} else {
packet.scheduleSend(protocol);
}
}
}
@@ -0,0 +1,114 @@
package me.neznamy.tab.platforms.bukkit.tablist.viaversion;
import com.viaversion.viaversion.api.protocol.Protocol;
import com.viaversion.viaversion.api.protocol.packet.PacketType;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.protocols.v1_15_2to1_16.Protocol1_15_2To1_16;
import com.viaversion.viaversion.protocols.v1_15_2to1_16.packet.ClientboundPackets1_16;
import lombok.NonNull;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
/**
* TabList handler using ViaVersion packets,
* to unlock +1.16 player features on pre 1.16 servers.
*/
public class ViaTabList116 extends ViaTabList {
private static final int ADD_PLAYER = 0;
private static final int UPDATE_GAME_MODE = 1;
private static final int UPDATE_LATENCY = 2;
private static final int UPDATE_DISPLAY_NAME = 3;
private static final int REMOVE_PLAYER = 4;
/**
* Constructs new instance with given parameters.
*
* @param player
* Player this tablist will belong to
*/
public ViaTabList116(@NotNull BukkitTabPlayer player) {
this(player, Protocol1_15_2To1_16.class, ClientboundPackets1_16.PLAYER_INFO, ClientboundPackets1_16.TAB_LIST);
}
protected ViaTabList116(@NonNull BukkitTabPlayer player, @NonNull Class<? extends Protocol> protocol, @NonNull PacketType playerInfoUpdate, @NonNull PacketType tabList) {
super(player, protocol, playerInfoUpdate, tabList);
}
@Override
public void removeEntry(@NonNull UUID entry) {
sendInfoUpdate(Action.REMOVE_PLAYER, entry, null);
}
@Override
public void updateListed(@NonNull UUID entry, boolean listed) {
// Added on 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
protected void writeComponent(@NonNull PacketWrapper packet, @NonNull TabComponent component) {
packet.write(Types.COMPONENT, component.toViaVersion());
}
@Override
protected void writeOptionalComponent(@NonNull PacketWrapper packet, @Nullable TabComponent component) {
packet.write(Types.OPTIONAL_COMPONENT, component == null ? null : component.toViaVersion());
}
@Override
protected void writeAction(@NonNull PacketWrapper packet, @NonNull Action action) {
switch (action) {
case ADD_PLAYER:
packet.write(Types.VAR_INT, ADD_PLAYER);
break;
case REMOVE_PLAYER:
packet.write(Types.VAR_INT, REMOVE_PLAYER);
break;
case UPDATE_DISPLAY_NAME:
packet.write(Types.VAR_INT, UPDATE_DISPLAY_NAME);
break;
case UPDATE_LATENCY:
packet.write(Types.VAR_INT, UPDATE_LATENCY);
break;
case UPDATE_GAME_MODE:
packet.write(Types.VAR_INT, UPDATE_GAME_MODE);
break;
default:
throw new IllegalStateException("Cannot write " + action.name() + " for 1.16 packet");
}
}
@Override
protected void writeEntry(@NonNull PacketWrapper packet, @NonNull Entry entry) {
packet.write(Types.STRING, entry.getName());
if (entry.getSkin() == null) {
// Properties size
packet.write(Types.VAR_INT, 0);
} else {
// Properties size
packet.write(Types.VAR_INT, 1);
// Properties
packet.write(Types.STRING, entry.getName());
packet.write(Types.STRING, entry.getSkin().getValue());
packet.write(Types.OPTIONAL_STRING, entry.getSkin().getSignature());
}
packet.write(Types.VAR_INT, entry.getGameMode());
packet.write(Types.VAR_INT, entry.getLatency());
writeOptionalComponent(packet, entry.getDisplayName());
}
}
@@ -0,0 +1,105 @@
package me.neznamy.tab.platforms.bukkit.tablist.viaversion;
import com.viaversion.viaversion.api.protocol.Protocol;
import com.viaversion.viaversion.api.protocol.packet.PacketType;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.protocols.v1_19_1to1_19_3.packet.ClientboundPackets1_19_3;
import com.viaversion.viaversion.protocols.v1_19_1to1_19_3.Protocol1_19_1To1_19_3;
import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.BitSet;
import java.util.UUID;
/**
* TabList handler using ViaVersion packets,
* to unlock +1.19.3 player features on pre 1.19.3 servers.
*/
public class ViaTabList1193 extends ViaTabList116 {
private static final BitSet ADD_PLAYER = bitSet(6, 0, 6);
//private static final BitSet INITIALIZE_CHAT = bitSet(6, 1);
private static final BitSet UPDATE_GAME_MODE = bitSet(6, 2);
private static final BitSet UPDATE_LISTED = bitSet(6, 3);
private static final BitSet UPDATE_LATENCY = bitSet(6, 4);
private static final BitSet UPDATE_DISPLAY_NAME = bitSet(6, 5);
private final PacketType playerInfoRemove;
/**
* Constructs new instance with given parameters.
*
* @param player
* Player this tablist will belong to
*/
public ViaTabList1193(@NotNull BukkitTabPlayer player) {
this(player, Protocol1_19_1To1_19_3.class, ClientboundPackets1_19_3.PLAYER_INFO_REMOVE, ClientboundPackets1_19_3.PLAYER_INFO_UPDATE, ClientboundPackets1_19_3.TAB_LIST);
}
protected ViaTabList1193(@NonNull BukkitTabPlayer player, @NonNull Class<? extends Protocol> protocol, @Nullable PacketType playerInfoRemove, @NonNull PacketType playerInfoUpdate, @NonNull PacketType tabList) {
super(player, protocol, playerInfoUpdate, tabList);
this.playerInfoRemove = playerInfoRemove;
}
@Override
public void removeEntry(@NonNull UUID entry) {
final PacketWrapper packet = PacketWrapper.create(playerInfoRemove, null, connection);
// Players
packet.write(Types.UUID_ARRAY, new UUID[] { entry });
send(packet);
}
@Override
public void updateListed(@NonNull UUID entry, boolean listed) {
sendInfoUpdate(Action.UPDATE_LISTED, entry, listed);
}
@Override
protected void writeAction(@NonNull PacketWrapper packet, @NonNull Action action) {
switch (action) {
case ADD_PLAYER:
packet.write(Types.PROFILE_ACTIONS_ENUM1_19_3, ADD_PLAYER);
break;
case UPDATE_DISPLAY_NAME:
packet.write(Types.PROFILE_ACTIONS_ENUM1_19_3, UPDATE_DISPLAY_NAME);
break;
case UPDATE_LATENCY:
packet.write(Types.PROFILE_ACTIONS_ENUM1_19_3, UPDATE_LATENCY);
break;
case UPDATE_GAME_MODE:
packet.write(Types.PROFILE_ACTIONS_ENUM1_19_3, UPDATE_GAME_MODE);
break;
case UPDATE_LISTED:
packet.write(Types.PROFILE_ACTIONS_ENUM1_19_3, UPDATE_LISTED);
break;
default:
throw new IllegalStateException("Cannot write " + action.name() + " for 1.19.3 packet");
}
}
@Override
protected void writeEntry(@NonNull PacketWrapper packet, @NonNull Entry entry) {
packet.write(Types.STRING, entry.getName());
if (entry.getSkin() == null) {
// Properties size
packet.write(Types.VAR_INT, 0);
} else {
// Properties size
packet.write(Types.VAR_INT, 1);
// Properties
packet.write(Types.STRING, entry.getName());
packet.write(Types.STRING, entry.getSkin().getValue());
packet.write(Types.OPTIONAL_STRING, entry.getSkin().getSignature());
}
packet.write(Types.BOOLEAN, false); // No chat session data
packet.write(Types.VAR_INT, entry.getGameMode());
packet.write(Types.BOOLEAN, entry.isListed());
packet.write(Types.VAR_INT, entry.getLatency());
writeOptionalComponent(packet, entry.getDisplayName());
}
}
@@ -0,0 +1,92 @@
package me.neznamy.tab.platforms.bukkit.tablist.viaversion;
import com.viaversion.viaversion.api.protocol.Protocol;
import com.viaversion.viaversion.api.protocol.packet.PacketType;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.protocols.v1_21to1_21_2.packet.ClientboundPackets1_21_2;
import com.viaversion.viaversion.protocols.v1_21to1_21_2.Protocol1_21To1_21_2;
import lombok.NonNull;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.BitSet;
import java.util.UUID;
/**
* TabList handler using ViaVersion packets,
* to unlock +1.21.2 player features on pre 1.21.2 servers.
*/
public class ViaTabList1212 extends ViaTabList1193 {
private static final BitSet ADD_PLAYER = bitSet(7, 0, 7);
//private static final BitSet INITIALIZE_CHAT = bitSet(7, 1);
private static final BitSet UPDATE_GAME_MODE = bitSet(7, 2);
private static final BitSet UPDATE_LISTED = bitSet(7, 3);
private static final BitSet UPDATE_LATENCY = bitSet(7, 4);
private static final BitSet UPDATE_DISPLAY_NAME = bitSet(7, 5);
private static final BitSet UPDATE_LIST_ORDER = bitSet(7, 6);
/**
* Constructs new instance with given parameters.
*
* @param player
* Player this tablist will belong to
*/
public ViaTabList1212(@NotNull BukkitTabPlayer player) {
super(player, Protocol1_21To1_21_2.class, ClientboundPackets1_21_2.PLAYER_INFO_REMOVE, ClientboundPackets1_21_2.PLAYER_INFO_UPDATE, ClientboundPackets1_21_2.TAB_LIST);
}
protected ViaTabList1212(@NonNull BukkitTabPlayer player, @NonNull Class<? extends Protocol> protocol, @Nullable PacketType playerInfoRemove, @NonNull PacketType playerInfoUpdate, @NonNull PacketType tabList) {
super(player, protocol, playerInfoRemove, playerInfoUpdate, tabList);
}
@Override
public void updateListOrder(@NonNull UUID entry, int listOrder) {
sendInfoUpdate(Action.UPDATE_LIST_ORDER, entry, listOrder);
}
@Override
protected void writeComponent(@NonNull PacketWrapper packet, @NonNull TabComponent component) {
packet.write(Types.TAG, component.toViaVersionTag());
}
@Override
protected void writeOptionalComponent(@NonNull PacketWrapper packet, @Nullable TabComponent component) {
packet.write(Types.OPTIONAL_TAG, component == null ? null : component.toViaVersionTag());
}
@Override
protected void writeAction(@NonNull PacketWrapper packet, @NonNull Action action) {
switch (action) {
case ADD_PLAYER:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_2, ADD_PLAYER);
break;
case UPDATE_DISPLAY_NAME:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_2, UPDATE_DISPLAY_NAME);
break;
case UPDATE_LATENCY:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_2, UPDATE_LATENCY);
break;
case UPDATE_GAME_MODE:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_2, UPDATE_GAME_MODE);
break;
case UPDATE_LISTED:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_2, UPDATE_LISTED);
break;
case UPDATE_LIST_ORDER:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_2, UPDATE_LIST_ORDER);
break;
default:
throw new IllegalStateException("Cannot write " + action.name() + " for 1.21.2 packet");
}
}
@Override
protected void writeEntry(@NonNull PacketWrapper packet, @NonNull Entry entry) {
super.writeEntry(packet, entry);
packet.write(Types.VAR_INT, entry.getListOrder());
}
}
@@ -0,0 +1,78 @@
package me.neznamy.tab.platforms.bukkit.tablist.viaversion;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.protocols.v1_21_2to1_21_4.Protocol1_21_2To1_21_4;
import com.viaversion.viaversion.protocols.v1_21to1_21_2.packet.ClientboundPackets1_21_2;
import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.BitSet;
import java.util.UUID;
/**
* TabList handler using ViaVersion packets,
* to unlock +1.21.4 player features on pre 1.21.4 servers.
*/
public class ViaTabList1214 extends ViaTabList1212 {
private static final BitSet ADD_PLAYER = bitSet(8, 0, 8);
//private static final BitSet INITIALIZE_CHAT = bitSet(8, 1);
private static final BitSet UPDATE_GAME_MODE = bitSet(8, 2);
private static final BitSet UPDATE_LISTED = bitSet(8, 3);
private static final BitSet UPDATE_LATENCY = bitSet(8, 4);
private static final BitSet UPDATE_DISPLAY_NAME = bitSet(8, 5);
private static final BitSet UPDATE_LIST_ORDER = bitSet(8, 6);
private static final BitSet UPDATE_HAT = bitSet(8, 7);
/**
* Constructs new instance with given parameters.
*
* @param player
* Player this tablist will belong to
*/
public ViaTabList1214(@NotNull BukkitTabPlayer player) {
super(player, Protocol1_21_2To1_21_4.class, ClientboundPackets1_21_2.PLAYER_INFO_REMOVE, ClientboundPackets1_21_2.PLAYER_INFO_UPDATE, ClientboundPackets1_21_2.TAB_LIST);
}
@Override
public void updateHat(@NonNull UUID entry, boolean showHat) {
sendInfoUpdate(Action.UPDATE_HAT, entry, showHat);
}
@Override
protected void writeAction(@NonNull PacketWrapper packet, @NonNull Action action) {
switch (action) {
case ADD_PLAYER:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_4, ADD_PLAYER);
break;
case UPDATE_DISPLAY_NAME:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_4, UPDATE_DISPLAY_NAME);
break;
case UPDATE_LATENCY:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_4, UPDATE_LATENCY);
break;
case UPDATE_GAME_MODE:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_4, UPDATE_GAME_MODE);
break;
case UPDATE_LISTED:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_4, UPDATE_LISTED);
break;
case UPDATE_LIST_ORDER:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_4, UPDATE_LIST_ORDER);
break;
case UPDATE_HAT:
packet.write(Types.PROFILE_ACTIONS_ENUM1_21_4, UPDATE_HAT);
break;
default:
throw new IllegalStateException("Cannot write " + action.name() + " for 1.21.4 packet");
}
}
@Override
protected void writeEntry(@NonNull PacketWrapper packet, @NonNull Entry entry) {
super.writeEntry(packet, entry);
packet.write(Types.BOOLEAN, entry.isShowHat());
}
}
+4
View File
@@ -0,0 +1,4 @@
dependencies {
implementation(projects.bukkit)
compileOnly("org.spigotmc:spigot:1.12-R0.1-SNAPSHOT")
}
@@ -0,0 +1,48 @@
package me.neznamy.tab.platforms.bukkit.v1_12_R1;
import me.neznamy.chat.ChatModifier;
import me.neznamy.tab.platforms.bukkit.nms.converter.ComponentConverter;
import net.minecraft.server.v1_12_R1.*;
import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct NMS code.
*/
public class NMSComponentConverter extends ComponentConverter {
@Override
@NotNull
public Object newTextComponent(@NotNull String text) {
return new ChatComponentText(text);
}
@Override
@NotNull
public Object newTranslatableComponent(@NotNull String key) {
return new ChatMessage(key);
}
@Override
@NotNull
public Object newKeybindComponent(@NotNull String keybind) {
return new ChatComponentKeybind(keybind);
}
@Override
public void applyStyle(@NotNull Object nmsComponent, @NotNull ChatModifier modifier) {
((IChatBaseComponent)nmsComponent).setChatModifier(
new net.minecraft.server.v1_12_R1.ChatModifier()
.setColor(modifier.getColor() == null ? null : EnumChatFormat.valueOf(modifier.getColor().getLegacyColor().name()))
.setBold(modifier.getBold())
.setItalic(modifier.getItalic())
.setUnderline(modifier.getUnderlined())
.setStrikethrough(modifier.getStrikethrough())
.setRandom(modifier.getObfuscated())
);
}
@Override
public void addSibling(@NotNull Object parent, @NotNull Object child) {
((IChatBaseComponent)parent).addSibling((IChatBaseComponent) child);
}
}
@@ -0,0 +1,45 @@
package me.neznamy.tab.platforms.bukkit.v1_12_R1;
import io.netty.channel.Channel;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.converter.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.util.function.FunctionWithException;
import org.bukkit.craftbukkit.v1_12_R1.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Implementation provider using direct NMS code for 1.8.8.
*/
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
public @Nullable ComponentConverter getComponentConverter() {
return componentConverter;
}
@Override
@NotNull
public FunctionWithException<BukkitTabPlayer, Channel> getChannelFunction() {
return player -> ((CraftPlayer)player.getPlayer()).getHandle().playerConnection.networkManager.channel;
}
}
@@ -0,0 +1,159 @@
package me.neznamy.tab.platforms.bukkit.v1_12_R1;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.minecraft.server.v1_12_R1.*;
import org.bukkit.craftbukkit.v1_12_R1.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
/**
* 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 Field TeamPacket_NAME = ReflectionUtils.getFields(PacketPlayOutScoreboardTeam.class, String.class).get(0);
private static final Field TeamPacket_ACTION = ReflectionUtils.getInstanceFields(PacketPlayOutScoreboardTeam.class, int.class).get(1);
private static final Field TeamPacket_PLAYERS = ReflectionUtils.getOnlyField(PacketPlayOutScoreboardTeam.class, Collection.class);
private static final Field Objective_OBJECTIVE_NAME = ReflectionUtils.getFields(PacketPlayOutScoreboardObjective.class, String.class).get(0);
private static final Field Objective_METHOD = ReflectionUtils.getOnlyField(PacketPlayOutScoreboardObjective.class, int.class);
private static final Field DisplayObjective_OBJECTIVE_NAME = ReflectionUtils.getOnlyField(PacketPlayOutScoreboardDisplayObjective.class, String.class);
private static final Field DisplayObjective_POSITION = ReflectionUtils.getOnlyField(PacketPlayOutScoreboardDisplayObjective.class, int.class);
/**
* 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.b);
obj.setDisplayName(cutTo(objective.getTitle().toLegacyText(), Limitations.SCOREBOARD_TITLE_PRE_1_13));
obj.a(IScoreboardCriteria.EnumScoreboardHealthDisplay.valueOf(objective.getHealthDisplay().name()));
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();
String title = objective.getTitle().toLegacyText();
if (player.getVersion().getMinorVersion() < 13 || TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) {
title = cutTo(title, Limitations.SCOREBOARD_TITLE_PRE_1_13);
}
obj.setDisplayName(title);
obj.a(IScoreboardCriteria.EnumScoreboardHealthDisplay.valueOf(objective.getHealthDisplay().name()));
sendPacket(new PacketPlayOutScoreboardObjective(obj, ObjectiveAction.UPDATE));
}
@Override
public void setScore(@NonNull Score score) {
ScoreboardScore s = new ScoreboardScore(dummyScoreboard, (ScoreboardObjective) score.getObjective().getPlatformObjective(), score.getHolder());
s.setScore(score.getValue());
sendPacket(new PacketPlayOutScoreboardScore(s));
}
@Override
public void removeScore(@NonNull Score score) {
sendPacket(new PacketPlayOutScoreboardScore(score.getHolder()));
}
@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.getPlayerNameSet().addAll(team.getPlayers());
sendPacket(new PacketPlayOutScoreboardTeam(t, TeamAction.CREATE));
}
@Override
public void unregisterTeam(@NonNull Team team) {
sendPacket(new PacketPlayOutScoreboardTeam((ScoreboardTeam) team.getPlatformTeam(), TeamAction.REMOVE));
}
@Override
public void updateTeam(@NonNull Team team) {
updateTeamProperties(team);
sendPacket(new PacketPlayOutScoreboardTeam((ScoreboardTeam) team.getPlatformTeam(), TeamAction.UPDATE));
}
private void updateTeamProperties(@NonNull Team team) {
ScoreboardTeam t = (ScoreboardTeam) team.getPlatformTeam();
t.setAllowFriendlyFire((team.getOptions() & 0x01) != 0);
t.setCanSeeFriendlyInvisibles((team.getOptions() & 0x02) != 0);
t.setNameTagVisibility(visibilities[team.getVisibility().ordinal()]);
t.setCollisionRule(collisions[team.getCollision().ordinal()]);
t.setPrefix(cutTo(team.getPrefix().toLegacyText(), Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13));
t.setSuffix(cutTo(team.getSuffix().toLegacyText(), Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13));
}
@Override
@SneakyThrows
@SuppressWarnings("unchecked")
public void onPacketSend(@NonNull Object packet) {
if (isAntiOverrideScoreboard()) {
if (packet instanceof PacketPlayOutScoreboardDisplayObjective) {
TAB.getInstance().getFeatureManager().onDisplayObjective(player, DisplayObjective_POSITION.getInt(packet),
(String) DisplayObjective_OBJECTIVE_NAME.get(packet));
}
if (packet instanceof PacketPlayOutScoreboardObjective) {
TAB.getInstance().getFeatureManager().onObjective(player,
Objective_METHOD.getInt(packet), (String) Objective_OBJECTIVE_NAME.get(packet));
}
}
if (isAntiOverrideTeams() && packet instanceof PacketPlayOutScoreboardTeam) {
int action = TeamPacket_ACTION.getInt(packet);
if (action == TeamAction.UPDATE) return;
Collection<String> players = (Collection<String>) TeamPacket_PLAYERS.get(packet);
if (players == null) players = Collections.emptyList();
TeamPacket_PLAYERS.set(packet, ((SafeScoreboard<?>)player.getScoreboard()).onTeamPacket(
action, (String) TeamPacket_NAME.get(packet), players));
}
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
private void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().playerConnection.sendPacket(packet);
}
}
@@ -0,0 +1,189 @@
package me.neznamy.tab.platforms.bukkit.v1_12_R1;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.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.server.v1_12_R1.*;
import org.bukkit.craftbukkit.v1_12_R1.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
/**
* TabList implementation using direct NMS code.
*/
public class NMSPacketTabList extends TrackedTabList<BukkitTabPlayer> {
// PlayerInfoData subclass is broken, using reflection to get it
private static final Class<?> PlayerInfoData = PacketPlayOutPlayerInfo.class.getDeclaredClasses()[0];
private static final Constructor<?> newPlayerInfoData = ReflectionUtils.getOnlyConstructor(PlayerInfoData);
private static final Field ACTION = ReflectionUtils.getOnlyField(PacketPlayOutPlayerInfo.class, PacketPlayOutPlayerInfo.EnumPlayerInfoAction.class);
private static final Field PLAYERS = ReflectionUtils.getOnlyField(PacketPlayOutPlayerInfo.class, List.class);
private static final Field PlayerInfoData_Profile = ReflectionUtils.getOnlyField(PlayerInfoData, GameProfile.class);
private static final Field PlayerInfoData_Latency = ReflectionUtils.getFields(PlayerInfoData, int.class).get(0);
private static final Field PlayerInfoData_DisplayName = ReflectionUtils.getOnlyField(PlayerInfoData, IChatBaseComponent.class);
private static final Field HEADER = ReflectionUtils.getFields(PacketPlayOutPlayerListHeaderFooter.class, IChatBaseComponent.class).get(0);
private static final Field FOOTER = ReflectionUtils.getFields(PacketPlayOutPlayerListHeaderFooter.class, IChatBaseComponent.class).get(1);
/**
* 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(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entry, "", null, 0, 0, null);
}
@Override
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
sendPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_DISPLAY_NAME, entry, "", null, 0, 0, displayName);
}
@Override
public void updateLatency(@NonNull UUID entry, int latency) {
sendPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_LATENCY, entry, "", null, latency, 0, null);
}
@Override
public void updateGameMode(@NonNull UUID entry, int gameMode) {
sendPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_GAME_MODE, 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.20.2
}
@Override
public void updateHat(@NonNull UUID entry, boolean showHat) {
// Added in 1.20.4
}
@Override
public void addEntry0(@NonNull Entry entry) {
sendPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, entry.getUniqueId(), entry.getName(), entry.getSkin(), entry.getLatency(),
entry.getGameMode(), entry.getDisplayName());
}
@Override
@SneakyThrows
public void setPlayerListHeaderFooter(@NonNull TabComponent header, @NonNull TabComponent footer) {
PacketPlayOutPlayerListHeaderFooter packet = new PacketPlayOutPlayerListHeaderFooter();
HEADER.set(packet, header.convert());
FOOTER.set(packet, footer.convert());
sendPacket(packet);
}
@Override
public boolean containsEntry(@NonNull UUID entry) {
return true; // TODO?
}
@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
@SuppressWarnings("unchecked")
public void onPacketSend(@NonNull Object packet) {
if (!(packet instanceof PacketPlayOutPlayerInfo)) return;
String action = ACTION.get(packet).toString();
for (Object nmsData : (List<Object>) PLAYERS.get(packet)) {
GameProfile profile = (GameProfile) PlayerInfoData_Profile.get(nmsData);
UUID id = profile.getId();
if (action.equals(Action.UPDATE_DISPLAY_NAME.name()) || action.equals(Action.ADD_PLAYER.name())) {
TabComponent expectedName = getExpectedDisplayNames().get(id);
if (expectedName != null) PlayerInfoData_DisplayName.set(nmsData, expectedName.convert());
}
if (action.equals(Action.UPDATE_LATENCY.name()) || action.equals(Action.ADD_PLAYER.name())) {
int oldLatency = PlayerInfoData_Latency.getInt(nmsData);
int newLatency = TAB.getInstance().getFeatureManager().onLatencyChange(player, id, oldLatency);
if (oldLatency != newLatency) {
PlayerInfoData_Latency.set(nmsData, newLatency);
}
}
if (action.equals(Action.ADD_PLAYER.name())) {
TAB.getInstance().getFeatureManager().onEntryAdd(player, id, profile.getName());
}
}
}
@SneakyThrows
private void sendPacket(@NonNull PacketPlayOutPlayerInfo.EnumPlayerInfoAction action, @NonNull UUID id, @NonNull String name,
@Nullable Skin skin, int latency, int gameMode, @Nullable TabComponent displayName) {
PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo(action);
PLAYERS.set(packet, Collections.singletonList(newPlayerInfoData.newInstance(
packet,
createProfile(id, name, skin),
latency,
EnumGamemode.values()[gameMode],
displayName == null ? null : displayName.convert())
));
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().playerConnection.sendPacket(packet);
}
}
+4
View File
@@ -0,0 +1,4 @@
dependencies {
implementation(projects.bukkit)
compileOnly("org.bukkit:craftbukkit:1.8.8-R0.1-SNAPSHOT")
}
@@ -0,0 +1,51 @@
package me.neznamy.tab.platforms.bukkit.v1_8_R3;
import me.neznamy.chat.ChatModifier;
import me.neznamy.tab.platforms.bukkit.nms.converter.ComponentConverter;
import net.minecraft.server.v1_8_R3.ChatComponentText;
import net.minecraft.server.v1_8_R3.ChatMessage;
import net.minecraft.server.v1_8_R3.EnumChatFormat;
import net.minecraft.server.v1_8_R3.IChatBaseComponent;
import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct NMS code.
*/
public class NMSComponentConverter extends ComponentConverter {
@Override
@NotNull
public Object newTextComponent(@NotNull String text) {
return new ChatComponentText(text);
}
@Override
@NotNull
public Object newTranslatableComponent(@NotNull String key) {
return new ChatMessage(key);
}
@Override
@NotNull
public Object newKeybindComponent(@NotNull String keybind) {
throw new UnsupportedOperationException("Keybind components were added in 1.12");
}
@Override
public void applyStyle(@NotNull Object nmsComponent, @NotNull ChatModifier modifier) {
((IChatBaseComponent)nmsComponent).setChatModifier(
new net.minecraft.server.v1_8_R3.ChatModifier()
.setColor(modifier.getColor() == null ? null : EnumChatFormat.valueOf(modifier.getColor().getLegacyColor().name()))
.setBold(modifier.getBold())
.setItalic(modifier.getItalic())
.setUnderline(modifier.getUnderlined())
.setStrikethrough(modifier.getStrikethrough())
.setRandom(modifier.getObfuscated())
);
}
@Override
public void addSibling(@NotNull Object parent, @NotNull Object child) {
((IChatBaseComponent)parent).addSibling((IChatBaseComponent) child);
}
}
@@ -0,0 +1,40 @@
package me.neznamy.tab.platforms.bukkit.v1_8_R3;
import io.netty.channel.Channel;
import lombok.Getter;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.converter.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.util.function.FunctionWithException;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
/**
* Implementation provider using direct NMS code for 1.8.8.
*/
@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 FunctionWithException<BukkitTabPlayer, Channel> getChannelFunction() {
return player -> ((CraftPlayer)player.getPlayer()).getHandle().playerConnection.networkManager.channel;
}
}
@@ -0,0 +1,157 @@
package me.neznamy.tab.platforms.bukkit.v1_8_R3;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.minecraft.server.v1_8_R3.*;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
/**
* Scoreboard implementation using direct NMS code.
*/
public class NMSPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
private static final ScoreboardTeamBase.EnumNameTagVisibility[] visibilities = ScoreboardTeamBase.EnumNameTagVisibility.values();
private static final Scoreboard dummyScoreboard = new Scoreboard();
private static final Field TeamPacket_NAME = ReflectionUtils.getFields(PacketPlayOutScoreboardTeam.class, String.class).get(0);
private static final Field TeamPacket_ACTION = ReflectionUtils.getInstanceFields(PacketPlayOutScoreboardTeam.class, int.class).get(1);
private static final Field TeamPacket_PLAYERS = ReflectionUtils.getOnlyField(PacketPlayOutScoreboardTeam.class, Collection.class);
private static final Field Objective_OBJECTIVE_NAME = ReflectionUtils.getFields(PacketPlayOutScoreboardObjective.class, String.class).get(0);
private static final Field Objective_METHOD = ReflectionUtils.getOnlyField(PacketPlayOutScoreboardObjective.class, int.class);
private static final Field DisplayObjective_OBJECTIVE_NAME = ReflectionUtils.getOnlyField(PacketPlayOutScoreboardDisplayObjective.class, String.class);
private static final Field DisplayObjective_POSITION = ReflectionUtils.getOnlyField(PacketPlayOutScoreboardDisplayObjective.class, int.class);
/**
* 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.b);
obj.setDisplayName(cutTo(objective.getTitle().toLegacyText(), Limitations.SCOREBOARD_TITLE_PRE_1_13));
obj.a(IScoreboardCriteria.EnumScoreboardHealthDisplay.valueOf(objective.getHealthDisplay().name()));
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();
String title = objective.getTitle().toLegacyText();
if (player.getVersion().getMinorVersion() < 13 || TAB.getInstance().getConfiguration().getConfig().isPacketEventsCompensation()) {
title = cutTo(title, Limitations.SCOREBOARD_TITLE_PRE_1_13);
}
obj.setDisplayName(title);
obj.a(IScoreboardCriteria.EnumScoreboardHealthDisplay.valueOf(objective.getHealthDisplay().name()));
sendPacket(new PacketPlayOutScoreboardObjective(obj, ObjectiveAction.UPDATE));
}
@Override
public void setScore(@NonNull Score score) {
ScoreboardScore s = new ScoreboardScore(dummyScoreboard, (ScoreboardObjective) score.getObjective().getPlatformObjective(), score.getHolder());
s.setScore(score.getValue());
sendPacket(new PacketPlayOutScoreboardScore(s));
}
@Override
public void removeScore(@NonNull Score score) {
sendPacket(new PacketPlayOutScoreboardScore(score.getHolder()));
}
@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.getPlayerNameSet().addAll(team.getPlayers());
sendPacket(new PacketPlayOutScoreboardTeam(t, TeamAction.CREATE));
}
@Override
public void unregisterTeam(@NonNull Team team) {
sendPacket(new PacketPlayOutScoreboardTeam((ScoreboardTeam) team.getPlatformTeam(), TeamAction.REMOVE));
}
@Override
public void updateTeam(@NonNull Team team) {
updateTeamProperties(team);
sendPacket(new PacketPlayOutScoreboardTeam((ScoreboardTeam) team.getPlatformTeam(), TeamAction.UPDATE));
}
private void updateTeamProperties(@NonNull Team team) {
ScoreboardTeam t = (ScoreboardTeam) team.getPlatformTeam();
t.setAllowFriendlyFire((team.getOptions() & 0x01) != 0);
t.setCanSeeFriendlyInvisibles((team.getOptions() & 0x02) != 0);
t.setNameTagVisibility(visibilities[team.getVisibility().ordinal()]);
t.setPrefix(cutTo(team.getPrefix().toLegacyText(), Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13));
t.setSuffix(cutTo(team.getSuffix().toLegacyText(), Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13));
}
@Override
@SneakyThrows
@SuppressWarnings("unchecked")
public void onPacketSend(@NonNull Object packet) {
if (isAntiOverrideScoreboard()) {
if (packet instanceof PacketPlayOutScoreboardDisplayObjective) {
TAB.getInstance().getFeatureManager().onDisplayObjective(player, DisplayObjective_POSITION.getInt(packet),
(String) DisplayObjective_OBJECTIVE_NAME.get(packet));
}
if (packet instanceof PacketPlayOutScoreboardObjective) {
TAB.getInstance().getFeatureManager().onObjective(player,
Objective_METHOD.getInt(packet), (String) Objective_OBJECTIVE_NAME.get(packet));
}
}
if (isAntiOverrideTeams() && packet instanceof PacketPlayOutScoreboardTeam) {
int action = TeamPacket_ACTION.getInt(packet);
if (action == TeamAction.UPDATE) return;
Collection<String> players = (Collection<String>) TeamPacket_PLAYERS.get(packet);
if (players == null) players = Collections.emptyList();
TeamPacket_PLAYERS.set(packet, ((SafeScoreboard<?>)player.getScoreboard()).onTeamPacket(
action, (String) TeamPacket_NAME.get(packet), players));
}
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
private void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().playerConnection.sendPacket(packet);
}
}
@@ -0,0 +1,178 @@
package me.neznamy.tab.platforms.bukkit.v1_8_R3;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.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.server.v1_8_R3.*;
import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo.PlayerInfoData;
import org.bukkit.craftbukkit.v1_8_R3.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 ACTION = ReflectionUtils.getOnlyField(PacketPlayOutPlayerInfo.class, PacketPlayOutPlayerInfo.EnumPlayerInfoAction.class);
private static final Field PLAYERS = ReflectionUtils.getOnlyField(PacketPlayOutPlayerInfo.class, List.class);
private static final Field PlayerInfoData_Latency = ReflectionUtils.getFields(PlayerInfoData.class, int.class).get(0);
private static final Field PlayerInfoData_DisplayName = ReflectionUtils.getOnlyField(PlayerInfoData.class, IChatBaseComponent.class);
private static final Field FOOTER = ReflectionUtils.getFields(PacketPlayOutPlayerListHeaderFooter.class, IChatBaseComponent.class).get(1);
/**
* 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(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entry, "", null, 0, 0, null);
}
@Override
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
sendPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_DISPLAY_NAME, entry, "", null, 0, 0, displayName);
}
@Override
public void updateLatency(@NonNull UUID entry, int latency) {
sendPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_LATENCY, entry, "", null, latency, 0, null);
}
@Override
public void updateGameMode(@NonNull UUID entry, int gameMode) {
sendPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_GAME_MODE, 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.20.2
}
@Override
public void updateHat(@NonNull UUID entry, boolean showHat) {
// Added in 1.20.4
}
@Override
public void addEntry0(@NonNull Entry entry) {
sendPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, entry.getUniqueId(), entry.getName(), entry.getSkin(), entry.getLatency(),
entry.getGameMode(), entry.getDisplayName());
}
@Override
@SneakyThrows
public void setPlayerListHeaderFooter(@NonNull TabComponent header, @NonNull TabComponent footer) {
PacketPlayOutPlayerListHeaderFooter packet = new PacketPlayOutPlayerListHeaderFooter(header.convert());
FOOTER.set(packet, footer.convert());
sendPacket(packet);
}
@Override
public boolean containsEntry(@NonNull UUID entry) {
return true; // TODO?
}
@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
@SuppressWarnings("unchecked")
public void onPacketSend(@NonNull Object packet) {
if (!(packet instanceof PacketPlayOutPlayerInfo)) return;
String action = ACTION.get(packet).toString();
for (PlayerInfoData nmsData : (List<PlayerInfoData>) PLAYERS.get(packet)) {
GameProfile profile = nmsData.a();
UUID id = profile.getId();
if (action.equals(Action.UPDATE_DISPLAY_NAME.name()) || action.equals(Action.ADD_PLAYER.name())) {
TabComponent expectedName = getExpectedDisplayNames().get(id);
if (expectedName != null) PlayerInfoData_DisplayName.set(nmsData, expectedName.convert());
}
if (action.equals(Action.UPDATE_LATENCY.name()) || action.equals(Action.ADD_PLAYER.name())) {
int oldLatency = PlayerInfoData_Latency.getInt(nmsData);
int newLatency = TAB.getInstance().getFeatureManager().onLatencyChange(player, id, oldLatency);
if (oldLatency != newLatency) {
PlayerInfoData_Latency.set(nmsData, newLatency);
}
}
if (action.equals(Action.ADD_PLAYER.name())) {
TAB.getInstance().getFeatureManager().onEntryAdd(player, id, profile.getName());
}
}
}
@SneakyThrows
private void sendPacket(@NonNull PacketPlayOutPlayerInfo.EnumPlayerInfoAction action, @NonNull UUID id, @NonNull String name,
@Nullable Skin skin, int latency, int gameMode, @Nullable TabComponent displayName) {
PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo(action);
PLAYERS.set(packet, Collections.singletonList(packet.new PlayerInfoData(
createProfile(id, name, skin),
latency,
WorldSettings.EnumGamemode.values()[gameMode],
displayName == null ? null : displayName.convert())
));
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().playerConnection.sendPacket(packet);
}
}
@@ -97,8 +97,9 @@ public class BungeeEventListener implements EventListener<ProxiedPlayer>, Listen
@EventHandler
public void onPluginMessage(PluginMessageEvent e) {
if (!e.getTag().equals(TabConstants.PLUGIN_MESSAGE_CHANNEL_NAME)) return;
e.setCancelled(true); // Also cancel messages from players to prevent exploits
if (TAB.getInstance().isPluginDisabled()) return;
if (e.getReceiver() instanceof ProxiedPlayer) {
e.setCancelled(true);
pluginMessage(((ProxiedPlayer) e.getReceiver()).getUniqueId(), e.getData());
}
}
@@ -1,6 +1,11 @@
package me.neznamy.tab.platforms.bungeecord;
import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI;
import me.neznamy.chat.ChatModifier;
import me.neznamy.chat.component.KeybindComponent;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.chat.component.TextComponent;
import me.neznamy.chat.component.TranslatableComponent;
import me.neznamy.tab.platforms.bungeecord.features.BungeeRedisSupport;
import me.neznamy.tab.platforms.bungeecord.hook.BungeePremiumVanishHook;
import me.neznamy.tab.platforms.bungeecord.injection.BungeePipelineInjector;
@@ -10,20 +15,18 @@ import me.neznamy.tab.platforms.bungeecord.tablist.BungeeTabList18;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.chat.ChatModifier;
import me.neznamy.chat.component.*;
import me.neznamy.tab.shared.features.injection.PipelineInjector;
import me.neznamy.tab.shared.features.redis.RedisSupport;
import me.neznamy.tab.shared.features.proxy.ProxySupport;
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.DummyBossBar;
import me.neznamy.tab.shared.proxy.ProxyPlatform;
import me.neznamy.tab.shared.util.PerformanceUtil;
import me.neznamy.tab.shared.util.ReflectionUtils;
import me.neznamy.tab.shared.util.cache.Cache;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyConfig;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
@@ -32,7 +35,7 @@ import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.Color;
import java.awt.*;
import java.io.File;
/**
@@ -59,29 +62,35 @@ public class BungeePlatform extends ProxyPlatform {
}
}
@SuppressWarnings("deprecation")
@Override
public void loadPlayers() {
try {
ProxyConfig config = ProxyServer.getInstance().getConfig();
if ((boolean) config.getClass().getMethod("isDisableTabListRewrite").invoke(config)) {
logWarn(new SimpleTextComponent("Waterfall's \"disable_tab_list_rewrite: true\" option may cause " +
"the plugin to not work correctly. Disable it to avoid issues."));
}
} catch (Exception e) {
// Not waterfall
}
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
TAB.getInstance().addPlayer(new BungeeTabPlayer(this, p));
}
}
@Override
public void registerPlaceholders() {
super.registerPlaceholders();
for (String server : ProxyServer.getInstance().getConfig().getServers().keySet()) {
TAB.getInstance().getPlaceholderManager().registerInternalServerPlaceholder("%online_" + server + "%", 1000, () -> {
int count = 0;
for (TabPlayer player : TAB.getInstance().getOnlinePlayers()) {
if (player.server.equals(server) && !player.isVanished()) count++;
}
return PerformanceUtil.toString(count);
});
}
}
@Override
@Nullable
public RedisSupport getRedisSupport() {
if (ReflectionUtils.classExists("com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI") &&
RedisBungeeAPI.getRedisBungeeApi() != null) {
return new BungeeRedisSupport(plugin);
public ProxySupport getProxySupport(@NotNull String plugin) {
if (plugin.equalsIgnoreCase("RedisBungee")) {
if (ReflectionUtils.classExists("com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI") &&
RedisBungeeAPI.getRedisBungeeApi() != null) {
return new BungeeRedisSupport(this.plugin);
}
}
return null;
}
@@ -241,16 +250,6 @@ public class BungeePlatform extends ProxyPlatform {
}
}
@Override
public boolean supportsNumberFormat() {
return true;
}
@Override
public boolean supportsListOrder() {
return true;
}
@Override
public boolean supportsScoreboards() {
return true;
@@ -44,6 +44,10 @@ public class BungeeScoreboard extends SafeScoreboard<BungeeTabPlayer> {
@Override
public void registerObjective(@NonNull Objective objective) {
sendObjectivePacket(objective, (byte) ObjectiveAction.REGISTER);
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
player.sendPacket(new ScoreboardDisplay(objective.getDisplaySlot().ordinal(), objective.getName()));
}
@@ -113,7 +117,7 @@ public class BungeeScoreboard extends SafeScoreboard<BungeeTabPlayer> {
player.sendPacket(new net.md_5.bungee.protocol.packet.Team(
team.getName(),
action,
either(new SimpleTextComponent(team.getName()), Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13),
either(SimpleTextComponent.text(team.getName()), Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13),
either(team.getPrefix(), Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13),
either(team.getSuffix(), Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13),
net.md_5.bungee.protocol.packet.Team.NameTagVisibility.valueOf(team.getVisibility().name()),
@@ -1,17 +1,13 @@
package me.neznamy.tab.platforms.bungeecord;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.proxy.ProxyTabPlayer;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.connection.LoginResult;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.Property;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* TabPlayer implementation for BungeeCord
@@ -45,21 +41,6 @@ public class BungeeTabPlayer extends ProxyTabPlayer {
getPlayer().sendMessage(getPlatform().transformComponent(message, getVersion()));
}
@Override
@Nullable
public TabList.Skin getSkin() {
LoginResult loginResult = ((InitialHandler)getPlayer().getPendingConnection()).getLoginProfile();
if (loginResult == null) return null;
Property[] properties = loginResult.getProperties();
if (properties == null) return null; //Offline mode
for (Property property : properties) {
if (property.getName().equals(TabList.TEXTURES_PROPERTY)) {
return new TabList.Skin(property.getValue(), property.getSignature());
}
}
return null;
}
@Override
@NotNull
public ProxiedPlayer getPlayer() {
@@ -73,7 +54,8 @@ public class BungeeTabPlayer extends ProxyTabPlayer {
@Override
public void sendPluginMessage(byte[] message) {
getPlayer().getServer().sendData(TabConstants.PLUGIN_MESSAGE_CHANNEL_NAME, message);
Server server = getPlayer().getServer();
if (server != null) server.sendData(TabConstants.PLUGIN_MESSAGE_CHANNEL_NAME, message);
}
/**
@@ -6,7 +6,7 @@ import lombok.AllArgsConstructor;
import me.neznamy.tab.platforms.bungeecord.BungeeTAB;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.features.redis.RedisSupport;
import me.neznamy.tab.shared.features.proxy.ProxySupport;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
@@ -16,7 +16,7 @@ import org.jetbrains.annotations.NotNull;
* RedisBungee implementation for BungeeCord
*/
@AllArgsConstructor
public class BungeeRedisSupport extends RedisSupport implements Listener {
public class BungeeRedisSupport extends ProxySupport implements Listener {
/** Plugin reference for registering listener */
@NotNull
@@ -30,26 +30,26 @@ public class BungeeRedisSupport extends RedisSupport implements Listener {
*/
@EventHandler
public void onMessage(@NotNull PubSubMessageEvent e) {
if (!e.getChannel().equals(TabConstants.REDIS_CHANNEL_NAME)) return;
if (!e.getChannel().equals(TabConstants.PROXY_CHANNEL_NAME)) return;
processMessage(e.getMessage());
}
@Override
public void register() {
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
RedisBungeeAPI.getRedisBungeeApi().registerPubSubChannels(TabConstants.REDIS_CHANNEL_NAME);
RedisBungeeAPI.getRedisBungeeApi().registerPubSubChannels(TabConstants.PROXY_CHANNEL_NAME);
}
@Override
public void unregister() {
ProxyServer.getInstance().getPluginManager().unregisterListener(this);
RedisBungeeAPI.getRedisBungeeApi().unregisterPubSubChannels(TabConstants.REDIS_CHANNEL_NAME);
RedisBungeeAPI.getRedisBungeeApi().unregisterPubSubChannels(TabConstants.PROXY_CHANNEL_NAME);
}
@Override
public void sendMessage(@NotNull String message) {
try {
RedisBungeeAPI.getRedisBungeeApi().sendChannelMessage(TabConstants.REDIS_CHANNEL_NAME, message);
RedisBungeeAPI.getRedisBungeeApi().sendChannelMessage(TabConstants.PROXY_CHANNEL_NAME, message);
} catch (Exception e) {
TAB.getInstance().getErrorManager().redisBungeeMessageSendFail(e);
}
@@ -29,7 +29,7 @@ public class BungeePremiumVanishHook extends PremiumVanishHook {
canSeeEnabled = true;
} else {
canSeeEnabled = false;
platform.logWarn(new SimpleTextComponent("Detected an outdated version of " +
platform.logWarn(SimpleTextComponent.text("Detected an outdated version of " +
"PremiumVanish with limited API. Vanish compatibility " +
"may not work as expected. Update PremiumVanish to version 2.7.11+ for optimal experience."));
}
Loaded 100 of 242 files, more files were not shown because too many files have changed in this diff. Show more