Compare commits

...
98 Commits
Author SHA1 Message Date
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
211 changed files with 4774 additions and 4139 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")
}
}
+3 -6
View File
@@ -4,7 +4,7 @@ plugins {
allprojects {
group = "me.neznamy"
version = "5.0.7"
version = "5.1.0"
description = "An all-in-one solution that works"
ext.set("id", "tab")
@@ -14,16 +14,13 @@ allprojects {
val platforms = setOf(
projects.bukkit,
projects.bukkit.paper,
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.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.4-R0.1-SNAPSHOT"
dependencies {
implementation(projects.bukkit)
paperweight.paperDevBundle(version)
compileOnly("io.papermc.paper:paper-api:${version}")
}
tasks.compileJava {
options.release.set(21)
}
@@ -0,0 +1,51 @@
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.
*/
@SuppressWarnings("unused") // Used via reflection
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,17 @@
package me.neznamy.tab.platforms.paper;
import io.netty.channel.Channel;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.util.function.FunctionWithException;
import org.bukkit.craftbukkit.entity.CraftPlayer;
/**
* Loader for Paper to speed up functions by using direct NMS code instead of reflection.
*/
@SuppressWarnings("unused") // Via reflection
public class PaperLoader {
/** Channel getter using direct NMS code */
public static final FunctionWithException<BukkitTabPlayer, Channel> getChannel =
player -> ((CraftPlayer)player.getPlayer()).getHandle().connection.connection.channel;
}
@@ -0,0 +1,164 @@
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.Collections;
import java.util.Optional;
/**
* Scoreboard implementation using direct mojang-mapped code.
*/
@SuppressWarnings("unused") // Used via reflection
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;
private static final Field players;
static {
try {
method = ReflectionUtils.setAccessible(ClientboundSetPlayerTeamPacket.class.getDeclaredField("method"));
players = ReflectionUtils.setAccessible(ClientboundSetPlayerTeamPacket.class.getDeclaredField("players"));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
/**
* 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));
sendPacket(new ClientboundSetDisplayObjectivePacket(net.minecraft.world.scores.DisplaySlot.values()[objective.getDisplaySlot().ordinal()], obj));
}
@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,203 @@
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.
*/
@SuppressWarnings("unused") // Used via reflection
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);
}
}
@@ -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;
@@ -38,6 +38,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() {
@@ -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();
}
}
}
}
@@ -1,5 +1,7 @@
package me.neznamy.tab.platforms.bukkit.platform;
import com.google.common.collect.Lists;
import io.netty.channel.Channel;
import lombok.Getter;
import lombok.SneakyThrows;
import me.clip.placeholderapi.PlaceholderAPI;
@@ -9,13 +11,18 @@ 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.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.*;
import me.neznamy.tab.shared.GroupManager;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
@@ -26,6 +33,7 @@ import me.neznamy.tab.shared.features.PlaceholderManagerImpl;
import me.neznamy.tab.shared.features.injection.PipelineInjector;
import me.neznamy.tab.shared.features.types.TabFeature;
import me.neznamy.tab.shared.hook.LuckPermsHook;
import me.neznamy.tab.shared.hook.ViaVersionHook;
import me.neznamy.tab.shared.placeholders.expansion.EmptyTabExpansion;
import me.neznamy.tab.shared.placeholders.expansion.TabExpansion;
import me.neznamy.tab.shared.placeholders.types.PlayerPlaceholderImpl;
@@ -35,8 +43,10 @@ import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.platform.impl.AdventureBossBar;
import me.neznamy.tab.shared.platform.impl.DummyBossBar;
import me.neznamy.tab.shared.platform.impl.DummyScoreboard;
import me.neznamy.tab.shared.util.PerformanceUtil;
import me.neznamy.tab.shared.util.ReflectionUtils;
import me.neznamy.tab.shared.util.function.FunctionWithException;
import net.kyori.adventure.audience.Audience;
import net.milkbowl.vault.chat.Chat;
import net.milkbowl.vault.permission.Permission;
@@ -52,6 +62,11 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* Implementation of Platform interface for Bukkit platform
@@ -79,12 +94,34 @@ public class BukkitPlatform implements BackendPlatform {
/** Detection for presence of Paper's MSPT getter */
private final boolean paperMspt = ReflectionUtils.methodExists(Bukkit.class, "getAverageTickTime");
/** Component converter from TAB to minecraft components */
@Nullable
private final ComponentConverter componentConverter = findComponentConverter();
/** Provider for scoreboard implementation */
@NotNull
private final FunctionWithException<BukkitTabPlayer, Scoreboard> scoreboardProvider = findScoreboardProvider();
/** Provider for tablist implementation */
@NotNull
private final FunctionWithException<BukkitTabPlayer, TabList> tablistProvider = findTablistProvider();
/** Header/footer implementation */
@Getter
@NotNull
private final HeaderFooter headerFooter = findHeaderFooter();
/** Flag tracking if direct mojang-mapped code can be used or not */
private final boolean canUseDirectNMS = BukkitReflection.isMojangMapped() && serverVersion == ProtocolVersion.V1_21_4;
/**
* Constructs new instance with given plugin.
*
* @param plugin
* Plugin
*/
@SneakyThrows
@SuppressWarnings("unchecked")
public BukkitPlatform(@NotNull JavaPlugin plugin) {
this.plugin = plugin;
long time = System.currentTimeMillis();
@@ -98,20 +135,156 @@ public class BukkitPlatform implements BackendPlatform {
new BukkitPremiumVanishHook().register();
}
PingRetriever.tryLoad();
ComponentConverter.tryLoad();
ScoreboardLoader.findInstance();
TabListBase.findInstance();
if (BukkitReflection.getMinorVersion() >= 8) {
HeaderFooter.findInstance();
BukkitPipelineInjector.tryLoad();
if (canUseDirectNMS) {
BukkitPipelineInjector.setGetChannel((FunctionWithException<BukkitTabPlayer, Channel>) Class.forName("me.neznamy.tab.platforms.paper.PaperLoader").getDeclaredField("getChannel").get(null));
} else {
BukkitPipelineInjector.tryLoad();
}
}
BukkitUtils.sendCompatibilityMessage();
Bukkit.getConsoleSender().sendMessage("[TAB] §7Loaded NMS hook in " + (System.currentTimeMillis()-time) + "ms");
}
@NotNull
@SneakyThrows
private FunctionWithException<BukkitTabPlayer, Scoreboard> findScoreboardProvider() {
final FunctionWithException<BukkitTabPlayer, Scoreboard> base;
if (ViaVersionHook.getInstance().isInstalled()) {
base = ViaVersionPlatform.findScoreboardProvider(serverVersion);
} else {
base = FunctionWithException.empty();
}
if (canUseDirectNMS) {
Constructor<?> constructor = Class.forName("me.neznamy.tab.platforms.paper.PaperPacketScoreboard").getConstructor(BukkitTabPlayer.class);
return base.fallback(player -> (Scoreboard) constructor.newInstance(player));
}
try {
if (BukkitReflection.getMinorVersion() >= 7) Objects.requireNonNull(componentConverter);
PacketScoreboard.load();
return base.fallback(PacketScoreboard::new);
} catch (Exception e) {
if (PaperScoreboard.isAvailable()) {
BukkitUtils.compatibilityError(e, "Scoreboards", "Paper API", "Compatibility with other plugins being reduced");
return base.fallback(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 base.fallback(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 base.fallback(DummyScoreboard::new);
}
@NotNull
@SneakyThrows
private FunctionWithException<BukkitTabPlayer, TabList> findTablistProvider() {
final FunctionWithException<BukkitTabPlayer, TabList> base;
if (ViaVersionHook.getInstance().isInstalled()) {
base = ViaVersionPlatform.findTablistProvider(serverVersion);
} else {
base = FunctionWithException.empty();
}
if (canUseDirectNMS) {
Constructor<?> constructor = Class.forName("me.neznamy.tab.platforms.paper.PaperPacketTabList").getConstructor(BukkitTabPlayer.class);
return base.fallback(player -> (TabList) constructor.newInstance(player));
}
try {
if (ReflectionUtils.classExists("net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket")) {
// 1.19.3+
Objects.requireNonNull(componentConverter);
PacketTabList1193.loadNew();
return base.fallback(PacketTabList1193::new);
} else if (BukkitReflection.getMinorVersion() >= 8) {
// 1.8 - 1.19.2
Objects.requireNonNull(componentConverter);
PacketTabList18.load();
return base.fallback(PacketTabList18::new);
} else {
// 1.7.10 and lower
PacketTabList17.load();
return base.fallback(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 base.fallback(BukkitTabList::new);
}
}
@NotNull
private HeaderFooter findHeaderFooter() {
if (BukkitReflection.getMinorVersion() >= 8) {
try {
Objects.requireNonNull(componentConverter);
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();
}
/**
* Attempts to load component converter.
*
* @return Instance or {@code null} if not available
*/
@Nullable
@SneakyThrows
public ComponentConverter findComponentConverter() {
if (canUseDirectNMS) {
return (ComponentConverter) Class.forName("me.neznamy.tab.platforms.paper.PaperComponentConverter").getConstructor().newInstance();
}
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;
}
}
@Override
public void loadPlayers() {
for (Player p : BukkitUtils.getOnlinePlayers()) {
for (Player p : getOnlinePlayers()) {
TAB.getInstance().addPlayer(new BukkitTabPlayer(this, p));
}
}
@@ -119,20 +292,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 +327,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,37 +337,36 @@ 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) ->
// relational placeholder
pl.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.contains("{") && identifier.contains("}")) {
// has nested bracket placeholders
pl.registerPlayerPlaceholder(identifier, p -> PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), PlaceholderAPI.setBracketPlaceholders((Player) p.getPlayer(), identifier)));
} else if (identifier.startsWith("%server_")) {
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier, refresh,
() -> PlaceholderAPI.setPlaceholders(null, identifier));
// placeholder with the same output for all players, register as server for better performance
pl.registerServerPlaceholder(identifier, () -> PlaceholderAPI.setPlaceholders(null, identifier));
} else {
TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier, refresh,
p -> PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), identifier));
pl.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);
ppl[0].updateValue(p, placeholderAPI ? PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), PlaceholderAPI.setBracketPlaceholders((Player) p.getPlayer(), syncedPlaceholder)) : identifier);
TAB.getInstance().getCPUManager().addPlaceholderTime(identifier, System.nanoTime() - time);
});
return null;
@@ -232,7 +402,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 +424,8 @@ public class BukkitPlatform implements BackendPlatform {
@Override
@NotNull
public Object convertComponent(@NotNull TabComponent component) {
if (ComponentConverter.INSTANCE != null) {
return ComponentConverter.INSTANCE.convert(component);
if (componentConverter != null) {
return componentConverter.convert(component);
} else {
return component;
}
@@ -265,7 +435,7 @@ public class BukkitPlatform implements BackendPlatform {
@NotNull
@SneakyThrows
public Scoreboard createScoreboard(@NotNull TabPlayer player) {
return ScoreboardLoader.getInstance().apply((BukkitTabPlayer) player);
return scoreboardProvider.apply((BukkitTabPlayer) player);
}
@Override
@@ -288,17 +458,7 @@ 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();
return tablistProvider.apply((BukkitTabPlayer) player);
}
@Override
@@ -391,4 +551,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,118 @@
package me.neznamy.tab.platforms.bukkit.platform;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.scoreboard.viaversion.ViaScoreboard1203;
import me.neznamy.tab.platforms.bukkit.scoreboard.viaversion.ViaScoreboard113;
import me.neznamy.tab.platforms.bukkit.scoreboard.viaversion.ViaScoreboard116;
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.platforms.bukkit.tablist.viaversion.ViaTabList116;
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 me.neznamy.tab.shared.util.function.FunctionWithException;
import org.jetbrains.annotations.NotNull;
public class ViaVersionPlatform {
@NotNull
public static FunctionWithException<BukkitTabPlayer, Scoreboard> findScoreboardProvider(@NotNull ProtocolVersion serverVersion) {
if (!ReflectionUtils.classExists("com.viaversion.viaversion.protocols.v1_20_2to1_20_3.Protocol1_20_2To1_20_3")) {
return FunctionWithException.empty();
}
if (serverVersion.getNetworkId() < ProtocolVersion.V1_13.getNetworkId()) {
return player -> {
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_20_3.getNetworkId()) {
return new ViaScoreboard1203(player);
}
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_16.getNetworkId()) {
return new ViaScoreboard116(player);
}
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_13.getNetworkId()) {
return new ViaScoreboard113(player);
}
return null;
};
}
if (serverVersion.getNetworkId() < ProtocolVersion.V1_16.getNetworkId()) {
return player -> {
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_20_3.getNetworkId()) {
return new ViaScoreboard1203(player);
}
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_16.getNetworkId()) {
return new ViaScoreboard116(player);
}
return null;
};
}
if (serverVersion.getNetworkId() < ProtocolVersion.V1_20_3.getNetworkId()) {
return player -> {
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_20_3.getNetworkId()) {
return new ViaScoreboard1203(player);
}
return null;
};
}
return FunctionWithException.empty();
}
@NotNull
public static FunctionWithException<BukkitTabPlayer, TabList> findTablistProvider(@NotNull ProtocolVersion serverVersion) {
if (!ReflectionUtils.classExists("com.viaversion.viaversion.protocols.v1_21_2to1_21_4.Protocol1_21_2To1_21_4")) {
return FunctionWithException.empty();
}
if (serverVersion.getNetworkId() < ProtocolVersion.V1_16.getNetworkId()) {
return player -> {
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_21_4.getNetworkId()) {
return new ViaTabList1214(player);
}
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_21_2.getNetworkId()) {
return new ViaTabList1212(player);
}
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_19_3.getNetworkId()) {
return new ViaTabList1193(player);
}
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_16.getNetworkId()) {
return new ViaTabList116(player);
}
return null;
};
}
if (serverVersion.getNetworkId() < ProtocolVersion.V1_19_3.getNetworkId()) {
return player -> {
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_21_4.getNetworkId()) {
return new ViaTabList1214(player);
}
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_21_2.getNetworkId()) {
return new ViaTabList1212(player);
}
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_19_3.getNetworkId()) {
return new ViaTabList1193(player);
}
return null;
};
}
if (serverVersion.getNetworkId() < ProtocolVersion.V1_21_2.getNetworkId()) {
return player -> {
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_21_4.getNetworkId()) {
return new ViaTabList1214(player);
}
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_21_2.getNetworkId()) {
return new ViaTabList1212(player);
}
return null;
};
}
if (serverVersion.getNetworkId() < ProtocolVersion.V1_21_4.getNetworkId()) {
return player -> {
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_21_4.getNetworkId()) {
return new ViaTabList1214(player);
}
return null;
};
}
return FunctionWithException.empty();
}
}
@@ -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();
}
/**
@@ -152,23 +140,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,192 @@
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.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.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 DELAY = 500;
private static final Object DUMMY = new Object();
protected final Class<? extends Protocol> protocol;
private final PacketType setDisplayObjective;
private final PacketType setObjective;
protected final PacketType setScore;
private final PacketType setPlayerTeam;
/** User connection this tablist belongs to */
protected final UserConnection connection;
private transient final Queue<Runnable> queue = new ConcurrentLinkedQueue<>();
private transient boolean delayed = true;
/**
* 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());
// First-time delay
connection.getChannel().eventLoop().schedule(() -> {
Runnable task;
while ((task = queue.poll()) != null) {
task.run();
}
delayed = false;
}, DELAY, TimeUnit.MILLISECONDS);
}
@Override
public void registerObjective(@NonNull Objective objective) {
sendObjectiveUpdate(ObjectiveAction.REGISTER, objective);
final PacketWrapper packet = PacketWrapper.create(setDisplayObjective, null, connection);
writeObjective(packet, objective);
send(packet);
}
@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 writeObjective(@NonNull PacketWrapper packet, @NonNull Objective objective);
protected abstract void writeObjectiveDisplay(@NonNull PacketWrapper packet, @NonNull Objective objective);
protected void send(@NonNull PacketWrapper packet) {
if (delayed) {
queue.add(() -> packet.send(protocol));
} else {
packet.scheduleSend(protocol);
}
}
}
@@ -0,0 +1,78 @@
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 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 writeObjective(@NonNull PacketWrapper packet, @NonNull Objective objective) {
packet.write(Types.BYTE, (byte) objective.getDisplaySlot().ordinal());
packet.write(Types.STRING, objective.getName());
}
@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,97 @@
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 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 writeObjective(@NonNull PacketWrapper packet, @NonNull Objective objective) {
packet.write(Types.VAR_INT, objective.getDisplaySlot().ordinal());
packet.write(Types.STRING, objective.getName());
}
@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,173 @@
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.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.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 DELAY = 500;
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 final Queue<Runnable> queue = new ConcurrentLinkedQueue<>();
private transient boolean delayed = true;
/**
*
* @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());
// First-time delay
connection.getChannel().eventLoop().schedule(() -> {
Runnable task;
while ((task = queue.poll()) != null) {
task.run();
}
delayed = false;
}, 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 (delayed) {
queue.add(() -> packet.send(protocol));
} 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());
}
}
@@ -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;
@@ -113,7 +113,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,12 @@
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.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 +40,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() {
@@ -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."));
}
@@ -2,13 +2,15 @@ package me.neznamy.tab.platforms.bungeecord.tablist;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bungeecord.BungeeTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.connection.LoginResult;
import net.md_5.bungee.protocol.Property;
import net.md_5.bungee.protocol.packet.PlayerListHeaderFooter;
import net.md_5.bungee.protocol.packet.PlayerListItem;
@@ -25,7 +27,7 @@ import java.util.UUID;
* Abstract TabList class for BungeeCord containing
* common code for all implementations.
*/
public abstract class BungeeTabList extends TrackedTabList<BungeeTabPlayer, BaseComponent> {
public abstract class BungeeTabList extends TrackedTabList<BungeeTabPlayer> {
/** Pointer to UUIDs in player's TabList */
private final Collection<UUID> uuids;
@@ -63,44 +65,27 @@ public abstract class BungeeTabList extends TrackedTabList<BungeeTabPlayer, Base
}
/**
* Converts entry data to item.
* Converts entry to item.
*
* @param id
* Entry UUID
* @param name
* Entry name
* @param skin
* Entry skin
* @param listed
* Whether entry should be listed or not
* @param latency
* Entry latency
* @param gameMode
* Entry game mode
* @param displayName
* Entry display name
* @param listOrder
* Entry list order
* @param showHat
* Show hat flag
* @return Converted item from parameters
* @param entry
* Entry to convert
* @return Converted item from entry
*/
@NotNull
public Item entryToItem(@NonNull UUID id, @NonNull String name, @Nullable Skin skin, boolean listed, int latency,
int gameMode, @Nullable BaseComponent displayName, int listOrder, boolean showHat) {
Item item = item(id);
item.setUsername(name);
item.setDisplayName(displayName);
item.setGamemode(gameMode);
item.setListed(listed);
item.setPing(latency);
if (skin != null) {
item.setProperties(new Property[]{new Property(TEXTURES_PROPERTY, skin.getValue(), skin.getSignature())});
public Item entryToItem(@NonNull Entry entry) {
Item item = item(entry.getUniqueId());
item.setUsername(entry.getName());
item.setDisplayName(entry.getDisplayName() == null ? null : toComponent(entry.getDisplayName()));
item.setGamemode(entry.getGameMode());
item.setListed(entry.isListed());
item.setPing(entry.getLatency());
if (entry.getSkin() != null) {
item.setProperties(new Property[]{new Property(TEXTURES_PROPERTY, entry.getSkin().getValue(), entry.getSkin().getSignature())});
} else {
item.setProperties(new Property[0]);
}
item.setListOrder(listOrder);
item.setShowHat(showHat);
item.setListOrder(entry.getListOrder());
item.setShowHat(entry.isShowHat());
return item;
}
@@ -130,8 +115,8 @@ public abstract class BungeeTabList extends TrackedTabList<BungeeTabPlayer, Base
PlayerListItem listItem = (PlayerListItem) packet;
for (PlayerListItem.Item item : listItem.getItems()) {
if (listItem.getAction() == PlayerListItem.Action.UPDATE_DISPLAY_NAME || listItem.getAction() == PlayerListItem.Action.ADD_PLAYER) {
BaseComponent expectedDisplayName = getExpectedDisplayNames().get(item.getUuid());
if (expectedDisplayName != null) item.setDisplayName(expectedDisplayName);
TabComponent expectedDisplayName = getExpectedDisplayNames().get(item.getUuid());
if (expectedDisplayName != null) item.setDisplayName(toComponent(expectedDisplayName));
}
if (listItem.getAction() == PlayerListItem.Action.UPDATE_LATENCY || listItem.getAction() == PlayerListItem.Action.ADD_PLAYER) {
item.setPing(TAB.getInstance().getFeatureManager().onLatencyChange(player, item.getUuid(), item.getPing()));
@@ -144,8 +129,8 @@ public abstract class BungeeTabList extends TrackedTabList<BungeeTabPlayer, Base
PlayerListItemUpdate update = (PlayerListItemUpdate) packet;
for (PlayerListItem.Item item : update.getItems()) {
if (update.getActions().contains(PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME)) {
BaseComponent expectedDisplayName = getExpectedDisplayNames().get(item.getUuid());
if (expectedDisplayName != null) item.setDisplayName(expectedDisplayName);
TabComponent expectedDisplayName = getExpectedDisplayNames().get(item.getUuid());
if (expectedDisplayName != null) item.setDisplayName(toComponent(expectedDisplayName));
}
if (update.getActions().contains(PlayerListItemUpdate.Action.UPDATE_LATENCY)) {
item.setPing(TAB.getInstance().getFeatureManager().onLatencyChange(player, item.getUuid(), item.getPing()));
@@ -163,7 +148,22 @@ public abstract class BungeeTabList extends TrackedTabList<BungeeTabPlayer, Base
}
@Override
public BaseComponent toComponent(@NonNull TabComponent component) {
@Nullable
public Skin getSkin() {
LoginResult loginResult = ((InitialHandler)player.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(TEXTURES_PROPERTY)) {
return new Skin(property.getValue(), property.getSignature());
}
}
return null;
}
@NotNull
protected BaseComponent toComponent(@NonNull TabComponent component) {
return player.getPlatform().transformComponent(component, player.getVersion());
}
}
@@ -1,15 +1,16 @@
package me.neznamy.tab.platforms.bungeecord.tablist;
import lombok.NonNull;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bungeecord.BungeeTabPlayer;
import me.neznamy.tab.shared.ProtocolVersion;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.protocol.packet.PlayerListItem.Item;
import net.md_5.bungee.protocol.packet.PlayerListItemRemove;
import net.md_5.bungee.protocol.packet.PlayerListItemUpdate;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.EnumSet;
import java.util.UUID;
/**
* TabList handler for 1.19.3+ players using the new tab list packets.
@@ -73,9 +74,9 @@ public class BungeeTabList1193 extends BungeeTabList {
}
@Override
public void updateDisplayName(@NonNull UUID entry, @Nullable BaseComponent displayName) {
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
Item item = item(entry);
item.setDisplayName(displayName);
if (displayName != null) item.setDisplayName(toComponent(displayName));
sendPacket(updateDisplayName, item);
}
@@ -117,9 +118,8 @@ public class BungeeTabList1193 extends BungeeTabList {
}
@Override
public void addEntry(@NonNull UUID id, @NonNull String name, @Nullable Skin skin, boolean listed, int latency,
int gameMode, @Nullable BaseComponent displayName, int listOrder, boolean showHat) {
addUuid(id);
public void addEntry0(@NonNull Entry entry) {
addUuid(entry.getUniqueId());
EnumSet<PlayerListItemUpdate.Action> actions;
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_21_4.getNetworkId()) {
actions = addPlayer_1_21_4;
@@ -128,7 +128,7 @@ public class BungeeTabList1193 extends BungeeTabList {
} else {
actions = addPlayer_legacy;
}
sendPacket(actions, entryToItem(id, name, skin, listed, latency, gameMode, displayName, listOrder, showHat));
sendPacket(actions, entryToItem(entry));
}
private void sendPacket(@NonNull EnumSet<PlayerListItemUpdate.Action> actions, @NonNull Item item) {
@@ -1,11 +1,11 @@
package me.neznamy.tab.platforms.bungeecord.tablist;
import lombok.NonNull;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.chat.component.TextComponent;
import me.neznamy.tab.platforms.bungeecord.BungeeTabPlayer;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.chat.component.TabComponent;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.protocol.packet.PlayerListItem;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -30,7 +30,7 @@ public class BungeeTabList17 extends BungeeTabList {
private final Map<UUID, String> userNames = new HashMap<>();
@NotNull
private final Map<UUID, BaseComponent> displayNames = new HashMap<>();
private final Map<UUID, TabComponent> displayNames = new HashMap<>();
/**
* Constructs new instance with given parameters.
@@ -54,10 +54,10 @@ public class BungeeTabList17 extends BungeeTabList {
}
@Override
public void updateDisplayName(@NonNull UUID entry, @Nullable BaseComponent displayName) {
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
if (!displayNames.containsKey(entry)) return; // Entry not tracked by TAB
update(PlayerListItem.Action.REMOVE_PLAYER, createItem(null, displayNames.get(entry), 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
@@ -87,14 +87,14 @@ public class BungeeTabList17 extends BungeeTabList {
}
@Override
public void addEntry(@NonNull UUID id, @NonNull String name, @Nullable Skin skin, boolean listed, int latency,
int gameMode, @Nullable BaseComponent displayName, int listOrder, boolean showHat) {
addUuid(id);
update(PlayerListItem.Action.ADD_PLAYER, createItem(name, displayName == null ? new TextComponent(name) : displayName, latency));
public void addEntry0(@NonNull Entry entry) {
addUuid(entry.getUniqueId());
update(PlayerListItem.Action.ADD_PLAYER, createItem(entry.getName(),
entry.getDisplayName() == null ? new TextComponent(entry.getName()) : entry.getDisplayName(), entry.getLatency()));
// Add to map
userNames.put(id, name);
displayNames.put(id, displayName);
userNames.put(entry.getUniqueId(), entry.getName());
displayNames.put(entry.getUniqueId(), entry.getDisplayName());
}
@Override
@@ -103,6 +103,7 @@ public class BungeeTabList17 extends BungeeTabList {
}
@Override
@NotNull
public BaseComponent toComponent(@NonNull TabComponent component) {
String displayNameString = component.toLegacyText();
if (displayNameString.length() > Limitations.MAX_DISPLAY_NAME_LENGTH_1_7)
@@ -117,11 +118,12 @@ public class BungeeTabList17 extends BungeeTabList {
player.sendPacket(packet);
}
private PlayerListItem.Item createItem(@Nullable String username, @NonNull BaseComponent displayName, int latency) {
@NotNull
private PlayerListItem.Item createItem(@Nullable String username, @Nullable TabComponent displayName, int latency) {
PlayerListItem.Item item = new PlayerListItem.Item();
item.setUsername(username);
item.setPing(latency);
item.setDisplayName(displayName);
if (displayName != null) item.setDisplayName(toComponent(displayName));
return item;
}
}
@@ -1,8 +1,8 @@
package me.neznamy.tab.platforms.bungeecord.tablist;
import lombok.NonNull;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.tab.platforms.bungeecord.BungeeTabPlayer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.protocol.packet.PlayerListItem;
import net.md_5.bungee.protocol.packet.PlayerListItem.Item;
import org.jetbrains.annotations.Nullable;
@@ -32,9 +32,9 @@ public class BungeeTabList18 extends BungeeTabList {
}
@Override
public void updateDisplayName(@NonNull UUID entry, @Nullable BaseComponent displayName) {
public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) {
Item item = item(entry);
item.setDisplayName(displayName);
if (displayName != null) item.setDisplayName(toComponent(displayName));
sendPacket(PlayerListItem.Action.UPDATE_DISPLAY_NAME, item);
}
@@ -68,10 +68,9 @@ public class BungeeTabList18 extends BungeeTabList {
}
@Override
public void addEntry(@NonNull UUID id, @NonNull String name, @Nullable Skin skin, boolean listed, int latency,
int gameMode, @Nullable BaseComponent displayName, int listOrder, boolean showHat) {
addUuid(id);
sendPacket(PlayerListItem.Action.ADD_PLAYER, entryToItem(id, name, skin, listed, latency, gameMode, displayName, listOrder, showHat));
public void addEntry0(@NonNull Entry entry) {
addUuid(entry.getUniqueId());
sendPacket(PlayerListItem.Action.ADD_PLAYER, entryToItem(entry));
}
private void sendPacket(@NonNull PlayerListItem.Action action, @NonNull Item item) {
+2
View File
@@ -1,3 +1,5 @@
dependencies {
compileOnlyApi("net.kyori:adventure-api:4.18.0")
compileOnlyApi("com.viaversion:viaversion-api:5.2.1")
compileOnlyApi("com.viaversion:viaversion-common:5.2.1")
}
@@ -11,35 +11,32 @@ import org.jetbrains.annotations.NotNull;
@AllArgsConstructor
public enum EnumChatFormat {
BLACK('0', 0x000000),
DARK_BLUE('1', 0x0000AA),
DARK_GREEN('2', 0x00AA00),
DARK_AQUA('3', 0x00AAAA),
DARK_RED('4', 0xAA0000),
DARK_PURPLE('5', 0xAA00AA),
GOLD('6', 0xFFAA00),
GRAY('7', 0xAAAAAA),
DARK_GRAY('8', 0x555555),
BLUE('9', 0x5555FF),
GREEN('a', 0x55FF55),
AQUA('b', 0x55FFFF),
RED('c', 0xFF5555),
LIGHT_PURPLE('d', 0xFF55FF),
YELLOW('e', 0xFFFF55),
WHITE('f', 0xFFFFFF),
OBFUSCATED('k', 0),
BOLD('l', 0),
STRIKETHROUGH('m', 0),
UNDERLINE('n', 0),
ITALIC('o', 0),
RESET('r', 0);
BLACK('0'),
DARK_BLUE('1'),
DARK_GREEN('2'),
DARK_AQUA('3'),
DARK_RED('4'),
DARK_PURPLE('5'),
GOLD('6'),
GRAY('7'),
DARK_GRAY('8'),
BLUE('9'),
GREEN('a'),
AQUA('b'),
RED('c'),
LIGHT_PURPLE('d'),
YELLOW('e'),
WHITE('f'),
OBFUSCATED('k'),
BOLD('l'),
STRIKETHROUGH('m'),
UNDERLINE('n'),
ITALIC('o'),
RESET('r');
/** Character representing the color or magic code */
private final char character;
/** Color as a hex code, 0 for magic codes */
private final int rgb;
/**
* Color translation method taken from bukkit, which converts '&amp;' symbol into
* the actual color character if followed by a valid color character.
@@ -59,30 +56,4 @@ public enum EnumChatFormat {
}
return new String(b);
}
/**
* Code taken from bukkit, which returns last color codes used in provided text.
*
* @param input
* text to get last colors from
* @return last colors used in provided text or empty string if nothing was found
*/
public static @NotNull String getLastColors(@NotNull String input) {
StringBuilder result = new StringBuilder();
int length = input.length();
for (int index = length - 1; index > -1; index--) {
char section = input.charAt(index);
if ((section == '§' || section == '&') && (index < length - 1)) {
char c = input.charAt(index + 1);
if ("0123456789AaBbCcDdEeFfKkLlMmNnOoRr".contains(String.valueOf(c))) {
result.insert(0, '§');
result.insert(1, c);
if ("0123456789AaBbCcDdEeFfRr".contains(String.valueOf(c))) {
break;
}
}
}
}
return result.toString();
}
}
@@ -14,28 +14,28 @@ public class TextColor {
/** Map of legacy colors by their character */
public static final Map<Character, TextColor> LEGACY_COLORS = new LinkedHashMap<>();
public static final TextColor BLACK = new TextColor(EnumChatFormat.BLACK);
public static final TextColor DARK_BLUE = new TextColor(EnumChatFormat.DARK_BLUE);
public static final TextColor DARK_GREEN = new TextColor(EnumChatFormat.DARK_GREEN);
public static final TextColor DARK_AQUA = new TextColor(EnumChatFormat.DARK_AQUA);
public static final TextColor DARK_RED = new TextColor(EnumChatFormat.DARK_RED);
public static final TextColor DARK_PURPLE = new TextColor(EnumChatFormat.DARK_PURPLE);
public static final TextColor GOLD = new TextColor(EnumChatFormat.GOLD);
public static final TextColor GRAY = new TextColor(EnumChatFormat.GRAY);
public static final TextColor DARK_GRAY = new TextColor(EnumChatFormat.DARK_GRAY);
public static final TextColor BLUE = new TextColor(EnumChatFormat.BLUE);
public static final TextColor GREEN = new TextColor(EnumChatFormat.GREEN);
public static final TextColor AQUA = new TextColor(EnumChatFormat.AQUA);
public static final TextColor RED = new TextColor(EnumChatFormat.RED);
public static final TextColor LIGHT_PURPLE = new TextColor(EnumChatFormat.LIGHT_PURPLE);
public static final TextColor YELLOW = new TextColor(EnumChatFormat.YELLOW);
public static final TextColor WHITE = new TextColor(EnumChatFormat.WHITE);
public static final TextColor OBFUSCATED = new TextColor(EnumChatFormat.OBFUSCATED);
public static final TextColor BOLD = new TextColor(EnumChatFormat.BOLD);
public static final TextColor STRIKETHROUGH = new TextColor(EnumChatFormat.STRIKETHROUGH);
public static final TextColor UNDERLINE = new TextColor(EnumChatFormat.UNDERLINE);
public static final TextColor ITALIC = new TextColor(EnumChatFormat.ITALIC);
public static final TextColor RESET = new TextColor(EnumChatFormat.RESET);
public static final TextColor BLACK = new TextColor(EnumChatFormat.BLACK, 0x000000);
public static final TextColor DARK_BLUE = new TextColor(EnumChatFormat.DARK_BLUE, 0x0000AA);
public static final TextColor DARK_GREEN = new TextColor(EnumChatFormat.DARK_GREEN, 0x00AA00);
public static final TextColor DARK_AQUA = new TextColor(EnumChatFormat.DARK_AQUA, 0x00AAAA);
public static final TextColor DARK_RED = new TextColor(EnumChatFormat.DARK_RED, 0xAA0000);
public static final TextColor DARK_PURPLE = new TextColor(EnumChatFormat.DARK_PURPLE, 0xAA00AA);
public static final TextColor GOLD = new TextColor(EnumChatFormat.GOLD, 0xFFAA00);
public static final TextColor GRAY = new TextColor(EnumChatFormat.GRAY, 0xAAAAAA);
public static final TextColor DARK_GRAY = new TextColor(EnumChatFormat.DARK_GRAY, 0x555555);
public static final TextColor BLUE = new TextColor(EnumChatFormat.BLUE, 0x5555FF);
public static final TextColor GREEN = new TextColor(EnumChatFormat.GREEN, 0x55FF55);
public static final TextColor AQUA = new TextColor(EnumChatFormat.AQUA, 0x55FFFF);
public static final TextColor RED = new TextColor(EnumChatFormat.RED, 0xFF5555);
public static final TextColor LIGHT_PURPLE = new TextColor(EnumChatFormat.LIGHT_PURPLE, 0xFF55FF);
public static final TextColor YELLOW = new TextColor(EnumChatFormat.YELLOW, 0xFFFF55);
public static final TextColor WHITE = new TextColor(EnumChatFormat.WHITE, 0xFFFFFF);
public static final TextColor OBFUSCATED = new TextColor(EnumChatFormat.OBFUSCATED, 0);
public static final TextColor BOLD = new TextColor(EnumChatFormat.BOLD, 0);
public static final TextColor STRIKETHROUGH = new TextColor(EnumChatFormat.STRIKETHROUGH, 0);
public static final TextColor UNDERLINE = new TextColor(EnumChatFormat.UNDERLINE, 0);
public static final TextColor ITALIC = new TextColor(EnumChatFormat.ITALIC, 0);
public static final TextColor RESET = new TextColor(EnumChatFormat.RESET, 0);
private static final TextColor[] legacyColorArray = LEGACY_COLORS.values().toArray(new TextColor[0]);
@@ -72,10 +72,10 @@ public class TextColor {
* @param legacyColor
* legacy color to construct the instance from
*/
private TextColor(@NotNull EnumChatFormat legacyColor) {
rgb = legacyColor.getRgb();
private TextColor(@NotNull EnumChatFormat legacyColor, int rgb) {
this.rgb = rgb;
this.legacyColor = legacyColor;
hexCode = String.format("%06X", legacyColor.getRgb());
hexCode = String.format("%06X", rgb);
LEGACY_COLORS.put(legacyColor.getCharacter(), this);
}
@@ -1,5 +1,6 @@
package me.neznamy.chat.component;
import lombok.NonNull;
import me.neznamy.chat.TextColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -10,13 +11,29 @@ import org.jetbrains.annotations.Nullable;
*/
public class SimpleTextComponent extends TextComponent {
/** Empty component to avoid recreating one over and over */
public static final SimpleTextComponent EMPTY = new SimpleTextComponent("");
/**
* Creates a new instance with given text.
*
* @param text
* Component text, including color codes
* @return Component with given text
*/
@NotNull
public static SimpleTextComponent text(@NonNull String text) {
if (text.isEmpty()) return EMPTY;
return new SimpleTextComponent(text);
}
/**
* Constructs new instance with given text.
*
* @param text
* Component text
*/
public SimpleTextComponent(@NotNull String text) {
private SimpleTextComponent(@NotNull String text) {
super(text);
}
@@ -1,5 +1,8 @@
package me.neznamy.chat.component;
import com.viaversion.nbt.tag.Tag;
import com.viaversion.viaversion.libs.gson.JsonElement;
import com.viaversion.viaversion.util.ComponentUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
@@ -7,15 +10,14 @@ import me.neznamy.chat.ChatModifier;
import me.neznamy.chat.EnumChatFormat;
import me.neznamy.chat.TextColor;
import me.neznamy.chat.hook.AdventureHook;
import me.neznamy.chat.hook.ViaVersionHook;
import me.neznamy.chat.rgb.RGBUtils;
import me.neznamy.chat.util.TriFunction;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -31,26 +33,61 @@ public abstract class TabComponent {
/** Formatter to convert gradient into TAB's #RRGGBB spam */
private static final TriFunction<TextColor, String, TextColor, String> TABGradientFormatter = (start, text, end) -> {
//lazy support for magic codes in gradients
String magicCodes = EnumChatFormat.getLastColors(text);
String deColorized = text.substring(magicCodes.length());
StringBuilder sb = new StringBuilder();
int length = deColorized.length();
if (length == 1) {
sb.append("#");
sb.append(start.getHexCode());
sb.append(magicCodes);
sb.append(deColorized);
return sb.toString();
if (text.length() == 1) {
return "#" + start.getHexCode() + text;
}
StringBuilder sb = new StringBuilder();
List<Character> characters = new ArrayList<>();
List<ChatModifier> modifiers = new ArrayList<>();
ChatModifier modifier = new ChatModifier();
for (int i=0; i<text.length(); i++) {
char c = text.charAt(i);
if (c == '§' && i < text.length() - 1) {
switch (text.charAt(i+1)) {
case 'l':
modifier.setBold(true);
i++;
break;
case 'o':
modifier.setItalic(true);
i++;
break;
case 'k':
modifier.setObfuscated(true);
i++;
break;
case 'm':
modifier.setStrikethrough(true);
i++;
break;
case 'n':
modifier.setUnderlined(true);
i++;
break;
case 'r':
modifier = new ChatModifier();
i++;
break;
default:
// Invalid code
characters.add('§');
modifiers.add(new ChatModifier(modifier));
break;
}
} else {
characters.add(c);
modifiers.add(new ChatModifier(modifier));
}
}
int length = characters.size();
for (int i=0; i<length; i++) {
int red = (int) (start.getRed() + (float)(end.getRed() - start.getRed())/(length-1)*i);
int green = (int) (start.getGreen() + (float)(end.getGreen() - start.getGreen())/(length-1)*i);
int blue = (int) (start.getBlue() + (float)(end.getBlue() - start.getBlue())/(length-1)*i);
sb.append("#");
sb.append(new TextColor(red, green, blue).getHexCode());
sb.append(magicCodes);
sb.append(deColorized.charAt(i));
sb.append(String.format("#%02X%02X%02X", red, green, blue));
sb.append(modifiers.get(i).getMagicCodes());
sb.append(characters.get(i));
}
return sb.toString();
};
@@ -69,6 +106,15 @@ public abstract class TabComponent {
@Setter
private Component adventureComponent;
/** ViaVersion component from this component */
@Nullable
@Setter
private JsonElement viaComponent;
/** ViaVersion tag from this component */
@Nullable
@Setter
private Tag viaTag;
@Nullable
private Object fixedFormat;
@@ -139,6 +185,26 @@ public abstract class TabComponent {
return adventureComponent;
}
/**
* Converts this component to an ViaVersion component.
* @return Converted component
*/
@NotNull
public JsonElement toViaVersion() {
if (viaComponent == null) viaComponent = ViaVersionHook.convert(this);
return viaComponent;
}
/**
* Converts this component to an ViaVersion tag.
* @return Converted component
*/
@NotNull
public Tag toViaVersionTag() {
if (viaTag == null) viaTag = ComponentUtil.jsonToTag(toViaVersion());
return viaTag;
}
/**
* Creates a FixedFormat using given platform-specific create function.
* If the value is already initialized, it is returned immediately instead.
@@ -262,7 +328,11 @@ public abstract class TabComponent {
break;
}
}
return new TextComponent("", components);
final TextComponent component = new TextComponent("", components);
// Safe check to avoid rare mojang "bug" that display text as italic by default
// This doesn't affect #toLegacyText() method at all
component.modifier.setItalic(false);
return component;
}
@NotNull
@@ -0,0 +1,81 @@
package me.neznamy.chat.hook;
import com.viaversion.viaversion.libs.gson.JsonArray;
import com.viaversion.viaversion.libs.gson.JsonElement;
import com.viaversion.viaversion.libs.gson.JsonObject;
import com.viaversion.viaversion.util.ComponentUtil;
import me.neznamy.chat.ChatModifier;
import me.neznamy.chat.component.KeybindComponent;
import me.neznamy.chat.component.SimpleTextComponent;
import me.neznamy.chat.component.TabComponent;
import me.neznamy.chat.component.TextComponent;
import me.neznamy.chat.component.TranslatableComponent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@ApiStatus.Internal
public class ViaVersionHook {
@NotNull
public static JsonElement convert(@NotNull TabComponent component) {
if (component instanceof SimpleTextComponent) {
return ComponentUtil.legacyToJson(component.toLegacyText());
}
final JsonObject object = new JsonObject();
// Root
if (component instanceof TextComponent) {
object.addProperty("type", "text"); // +1.20.3 optimization
object.addProperty("text", ((TextComponent) component).getText());
} else if (component instanceof TranslatableComponent) {
object.addProperty("type", "translatable"); // +1.20.3 optimization
object.addProperty("translate", ((TranslatableComponent) component).getKey());
} else if (component instanceof KeybindComponent) {
object.addProperty("type", "keybind"); // +1.20.3 optimization
object.addProperty("keybind", ((KeybindComponent) component).getKeybind());
} else {
throw new IllegalStateException("Unknown component type: " + component.getClass().getName());
}
// Color
final ChatModifier modifier = component.getModifier();
if (modifier.getColor() != null) {
object.addProperty("color", "#" + modifier.getColor().getHexCode());
}
if (modifier.getShadowColor() != null) {
object.addProperty("shadow_color", modifier.getShadowColor());
}
// Render
if (Boolean.TRUE.equals(modifier.getBold())) {
object.addProperty("bold", true);
}
if (modifier.getItalic() != null) {
object.addProperty("italic", modifier.getItalic());
}
if (Boolean.TRUE.equals(modifier.getObfuscated())) {
object.addProperty("obfuscated", true);
}
if (Boolean.TRUE.equals(modifier.getStrikethrough())) {
object.addProperty("strikethrough", true);
}
if (Boolean.TRUE.equals(modifier.getUnderlined())) {
object.addProperty("underlined", true);
}
if (modifier.getFont() != null) {
object.addProperty("font", modifier.getFont());
}
// Extra
if (!component.getExtra().isEmpty()) {
final JsonArray extra = new JsonArray();
for (TabComponent sub : component.getExtra()) {
extra.add((JsonElement) convert(sub));
}
object.add("extra", extra);
}
return object;
}
}
@@ -34,7 +34,7 @@ public class RGBUtils {
/** Registered gradient patterns */
private final GradientPattern[] gradients = {
new CMIGradient(), //{#RRGGBB>}Text{#RRGGBB<}
new CommonGradient(Pattern.compile("<#[0-9a-fA-F]{6}>.*?</#[0-9a-fA-F]{6}>"), "<#", 2, 9, 7), //<#RRGGBB>Text</#RRGGBB> // "HTML"
new CommonGradient(Pattern.compile("<#[0-9a-fA-F]{6}>(?:(?!<#[0-9a-fA-F]{6}>).)*?</#[0-9a-fA-F]{6}>"), "<#", 2, 9, 7), //<#RRGGBB>Text</#RRGGBB> // "HTML"
new CommonGradient(Pattern.compile("<\\$#[0-9a-fA-F]{6}>.*?<\\$#[0-9a-fA-F]{6}>"), "<$", 3, 10, 7), //<$#RRGGBB>Text<$#RRGGBB> // ?
new NexEngineGradient() // <gradient:#RRGGBB>Text</gradient:#RRGGBB>
};
+2 -3
View File
@@ -13,7 +13,7 @@ repositories {
dependencies {
api(projects.shared)
minecraft("com.mojang:minecraft:25w07a")
minecraft("com.mojang:minecraft:1.21.5")
mappings(loom.officialMojangMappings())
modImplementation("me.lucko:fabric-permissions-api:0.2-SNAPSHOT")
modImplementation("eu.pb4:placeholder-api:2.5.0+1.21.2")
@@ -22,7 +22,6 @@ dependencies {
modImplementation(fabricApi.module("fabric-lifecycle-events-v1", version))
modImplementation(fabricApi.module("fabric-networking-api-v1", version))
modImplementation(fabricApi.module("fabric-entity-events-v1", version))
modImplementation(fabricApi.module("fabric-command-api-v1", "0.77.0+1.18.2"))
modImplementation(fabricApi.module("fabric-command-api-v2", version))
}
@@ -35,6 +34,6 @@ tasks {
options.release.set(17)
}
validateAccessWidener {
enabled = false
enabled = true
}
}
@@ -28,7 +28,7 @@ public class FabricBossBar extends SafeBossBar<ServerBossEvent> {
BossBarColor.valueOf(color.name()),
BossBarOverlay.valueOf(style.name())
);
bar.setProgress(progress); // Somehow the compiled method name is same despite method being renamed in 1.17
bar.setProgress(progress);
return bar;
}
@@ -44,7 +44,7 @@ public class FabricBossBar extends SafeBossBar<ServerBossEvent> {
@Override
public void updateProgress(@NotNull BossBarInfo bar) {
bar.getBossBar().setProgress(bar.getProgress()); // Somehow the compiled method name is same despite method being renamed in 1.17
bar.getBossBar().setProgress(bar.getProgress());
}
@Override
@@ -3,7 +3,6 @@ package me.neznamy.tab.platforms.fabric;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.EventListener;
import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
@@ -22,17 +21,14 @@ public class FabricEventListener implements EventListener<ServerPlayer> {
ServerPlayConnectionEvents.DISCONNECT.register((connection, $) -> quit(connection.player.getUUID()));
ServerPlayConnectionEvents.JOIN.register((connection, $, $$) -> join(connection.player));
//TODO command preprocess
if (ReflectionUtils.classExists("net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents")) {
// Added in 1.16
ServerPlayerEvents.AFTER_RESPAWN.register(
(oldPlayer, newPlayer, alive) -> {
replacePlayer(newPlayer.getUUID(), newPlayer);
// respawning from death & taking end portal in the end do not call world change event
worldChange(newPlayer.getUUID(), FabricMultiVersion.getLevelName(FabricMultiVersion.getLevel(newPlayer)));
});
ServerEntityWorldChangeEvents.AFTER_PLAYER_CHANGE_WORLD.register(
(player, origin, destination) -> worldChange(player.getUUID(), FabricMultiVersion.getLevelName(destination)));
} // TODO else
ServerPlayerEvents.AFTER_RESPAWN.register(
(oldPlayer, newPlayer, alive) -> {
replacePlayer(newPlayer.getUUID(), newPlayer);
// respawning from death & taking end portal in the end does not call world change event
worldChange(newPlayer.getUUID(), FabricTAB.getLevelName(newPlayer.level()));
});
ServerEntityWorldChangeEvents.AFTER_PLAYER_CHANGE_WORLD.register(
(player, origin, destination) -> worldChange(player.getUUID(), FabricTAB.getLevelName(destination)));
}
@Override
Loaded 100 of 211 files, more files were not shown because too many files have changed in this diff. Show more