Compare commits

..
212 Commits
Author SHA1 Message Date
NEZNAMY 814ad0f9d5 Update README.md 2026-08-06 19:27:30 +02:00
NEZNAMY 147279a8ca [Description] Update badges 2026-08-04 17:48:22 +02:00
NEZNAMY 517ac4403f 6.1.1 2026-08-04 17:27:08 +02:00
NEZNAMY b4e88e570f [Layout] Fix conflict with Bedrock UUID pattern (#1688) 2026-08-03 21:04:32 +02:00
NEZNAMY 9b5385ed61 [Dump command] Add scoreboard (objectives and teams) information to dump output 2026-08-02 13:13:52 +02:00
NEZNAMY 284e3e91f3 [Belowname] Implement view-distance (26.1+) (not working on proxy yet) (#1672) 2026-08-01 16:21:26 +02:00
NEZNAMY 0fb6519617 [Wiki] Monthly wiki backup for changes made in July 2026 2026-08-01 09:46:36 +02:00
NEZNAMY e2452f2780 Merge pull request #1698 from SenseiJu/fix/nametag-registeredteams-leak
[Nametags] Use weak keys for registered team maps to fix disconnected player leak
2026-07-29 20:30:47 +02:00
SenseiJu f1c763861b [Scoreboards] Prevent scoreboard updates for disconnected players 2026-07-29 19:29:45 +01:00
SenseiJu ea99736142 [Nametags] Clear registered teams on player disconnect to prevent memory leaks 2026-07-29 03:58:39 +01:00
NEZNAMY 21d661bd22 [Components] Append dummy char to MiniPlaceholders and then remove it to avoid dropping trailing colors (#1696) 2026-07-23 19:31:27 +02:00
NEZNAMY fc0666fe46 [Commands] Change parse command to use string to component converter (with MM support) instead of just TAB's own converter (#1696) 2026-07-23 17:16:09 +02:00
SenseiJuandClaude Opus 4.8 869c6191b6 [Nametags] Use weak keys for registered team maps to fix disconnected player leak
registeredTeams and registeredProxyTeams held strong references to their
team owners, so a disconnected player that was not explicitly unregistered
(NameTag#unregisterTeam only iterates online viewers) or that got
re-registered by a refresh firing after quit stayed as a map key forever.
Every viewer keeps such a map keyed by every target, so a single missed
removal compounds to O(players^2) retained objects.

Switch both maps to WeakHashMap, matching the sibling per-player maps in
this class (opaqueNameTagViewers, nameTagInvisibilityReasonsRelational).
Online owners stay strongly reachable via TAB#data so their entries are
retained as before; once an owner disconnects and is dereferenced
elsewhere, the weak key lets it be collected and the stale entry is dropped
automatically. Values are Strings that never reference the key, and the maps
are only used via put/remove/containsKey on the feature's single thread.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 21:21:18 +01:00
NEZNAMY a53f41336d [Commands] Disable placeholder check in parse command as the command does more than that (<> placeholders, fonts, heads, MM, ...) (#1696) 2026-07-22 18:17:29 +02:00
NEZNAMY ff4e82d2cc [Placeholders] Fix placeholder detection after MP pull request to preserve order and duplicates (#1696) 2026-07-22 18:12:58 +02:00
NEZNAMY 2de7e3bd7e Merge pull request #1695 from MC20018/codex/align-fand-dependency-packaging
[Fand] Align dependency packaging with universal jar
2026-07-19 19:01:23 +02:00
MC20018 c72e0c5108 [Fand] Align dependency packaging with universal jar
Remove the Fand-only dependency exclusions from the shared platform shadow configuration so the Fand intermediate jar embeds and relocates Redis, Delivery4j, RabbitMQ, Commons Pool, and JSON consistently with the other platform modules.

Delete the Fand libraries.json manifest because the distributed universal jar already includes these dependencies through its merged platform shadow jars. This avoids redundant runtime downloads and keeps Fand on the same packaged dependency path as the artifact users install.
2026-07-20 00:55:08 +08:00
NEZNAMY 11814e0fd2 Merge pull request #1693 from MC20018/codex/update-fand-api-0.8.3
[Fand] Update API dependency to 0.8.3
2026-07-19 07:22:16 +02:00
MC20018 d41d2e74bd [Fand] Update API dependency to 0.8.3
Compile the Fand platform against the latest published API release to keep the integration validated against current server contracts.
2026-07-19 01:50:17 +08:00
NEZNAMY 122b54caee [Components] Update head syntax to match MiniMessage (including TAB's own tag resolvers) 2026-07-12 15:00:59 +02:00
NEZNAMY 3a67785be6 Merge pull request #1690 from MC20018/master
[Fand] Add platform support
2026-07-12 13:26:49 +02:00
NEZNAMY 01e2457611 [Nametags] Create new messages for opaque view after the PR 2026-07-12 11:49:43 +02:00
MC20018 dd13d244c0 Fix Fand platform edge cases 2026-07-12 15:06:45 +08:00
MC20018 fba8983f46 Complete Fand platform integration 2026-07-12 14:12:22 +08:00
MC20018 4a2af1f37a [Fand] Add platform support 2026-07-11 21:25:37 +08:00
NEZNAMY 2b82c45176 Merge pull request #1686 from Nattapat2871/feature/nametag-opaque-view
Add opaque nametag visibility mode
2026-07-10 17:21:39 +02:00
NEZNAMY 3203c64287 [Bukkit] Add update check for Canvas fork due to their refactoring 2026-07-08 21:03:58 +02:00
NEZNAMY 1683e1faf6 [Placeholders] Add coloring after placeholders are parsed, allowing "&" in one element and color code in another 2026-07-08 19:52:41 +02:00
nattapat2871 eb9de8cc3f Add opaque nametag visibility mode 2026-07-07 11:56:36 +07:00
NEZNAMY df6a8281a6 [Wiki] Monthly wiki backup for changes made in June 2026 2026-07-01 18:13:29 +02:00
NEZNAMY 05568296d9 Merge pull request #1677 from Crafting-tunbol/master
Add MiniPlaceholders support for Velocity
2026-06-30 21:04:04 +02:00
Crafting_tunbol c024b28a45 Removed unused methods 2026-06-28 17:13:26 +02:00
Crafting_tunbol 0b0c457cdc Move placeholder identifier handling to PlaceholderConfiguration 2026-06-28 16:30:52 +02:00
Crafting_tunbol 001e26cbb0 Remove unnecessary null check 2026-06-28 16:07:06 +02:00
Crafting_tunbol 3d75520863 Remove missleading minimessage-support config usage for MiniPlaceHolder parsing issues 2026-06-27 17:23:05 +02:00
Crafting_tunbol 63bc73fd98 Fix typo for TAB Bridge 2026-06-27 16:15:30 +02:00
Crafting_tunbol b47e7d2d3b Fix wiki docs for MiniPlaceholders usage on Velocity 2026-06-27 16:11:26 +02:00
Crafting_tunbol cf7b0eed28 Add wiki docs and warning for MiniPlaceholders usage on Velocity 2026-06-27 14:36:39 +02:00
Crafting_tunbol b3b0652222 [Velocity] Use MiniPlaceholders <syntax> for MP placeholders and preserve RGB colors
fix: detect MiniPlaceholders <tag> syntax in placeholder scanning

fix(velocity): support MiniPlaceholders <syntax> alongside %bridge% placeholders

fix: use existing color support for MiniPlaceholder
2026-06-26 18:39:14 +02:00
NEZNAMY 8e78e4e9ce [Fabric] Fix RGB syntax when reading placeholders 2026-06-25 20:01:05 +02:00
Crafting_tunbol f9d60a224a [velocity] Add MiniPlaceholders API support
add missing changes

wip: clean minor issues
2026-06-24 22:21:11 +02:00
NEZNAMY 396678d996 Merge pull request #1675 from JT122406/master
Readd Forge Support
2026-06-24 16:14:51 +02:00
Joseph T. McQuigg fae2d7a532 Readd Forge Support 2026-06-23 22:53:43 -04:00
NEZNAMY 73ab71d7c0 [Dump command] Include MiniMessage information (#1674) 2026-06-23 18:21:13 +02:00
NEZNAMY ec6eb51515 [MiniMessage hook] Add another custom tag for <mineskin:UUID> 2026-06-21 13:00:34 +02:00
NEZNAMY 02a9f25148 6.1.1-SNAPSHOT 2026-06-21 13:00:24 +02:00
NEZNAMY ea4c16a162 Remove unused import 2026-06-21 12:17:40 +02:00
NEZNAMY c7e8c891e2 [Description] Update badges 2026-06-18 18:13:58 +02:00
NEZNAMY 651580078c 6.1.0 2026-06-18 17:50:55 +02:00
NEZNAMY 2c3a7a2256 Fix jar name 2026-06-18 17:50:41 +02:00
NEZNAMY 47b3579fde [Components] Fix last color detection not working for magic codes after using &r 2026-06-18 17:17:08 +02:00
NEZNAMY 8147fd4edf [BungeeCord] Fix magic codes not being sent to <26.2 2026-06-17 20:06:16 +02:00
NEZNAMY 4c6fd3282b [Build] Change how jar for Paper 1.20.5 - 1.21.4 is made and named 2026-06-17 19:09:12 +02:00
NEZNAMY 363f5c7e5e [Bukkit] Add 26.2 support 2026-06-17 18:35:06 +02:00
NEZNAMY 229f7d665b [Fabric / NeoForge] Update to 26.2 2026-06-17 17:52:16 +02:00
NEZNAMY ff2dd5d1d5 [Bukkit] Update error message 2026-06-17 16:58:17 +02:00
NEZNAMY 37f1763d9c [ProxySupport] Implement disable-condition forwarding (#1635) 2026-06-14 17:43:43 +02:00
NEZNAMY 962adc683d [BossBar / Scoreboard] Pass arguments of toggle command to the main command to allow silent toggling and more 2026-06-07 11:05:15 +02:00
NEZNAMY 6f4f6b71ff [Fabric] Update to 26.2-pre4 2026-06-06 14:51:05 +02:00
NEZNAMY 6576c53235 [Bukkit] Avoid modifying final fields in player info packet on 1.17 - 1.19.2 (#1648) 2026-06-06 11:32:20 +02:00
NEZNAMY 3d939c2a39 [Bukkit] Avoid modifying final fields in team packet on 1.17+ (#1648) 2026-06-06 11:07:14 +02:00
NEZNAMY 9564e2cdf8 [Bukkit] Avoid modifying final fields in player info packet on 1.8 - 1.16.5 (#1648) 2026-06-06 09:51:35 +02:00
NEZNAMY 2ed7cb794e [Bukkit] Avoid modifying final fields in team packet on 1.8 - 1.16.5 (#1648) 2026-06-06 09:38:06 +02:00
NEZNAMY f2d4a6bee7 [Per world playerlist] Support "*" wildcard and regex in world names just like in global playerlist 2026-06-06 08:09:39 +02:00
NEZNAMY 63be8824f6 [GitHub] Update badges 2026-06-02 18:07:24 +02:00
NEZNAMY e95a5597fe [Wiki] Monthly wiki backup for changes made in May 2026 2026-06-01 17:32:00 +02:00
NEZNAMY ea1dc94c60 [BungeeCord] Update to build 2068+ 2026-05-31 07:37:34 +02:00
NEZNAMY 2079505a6f [Bukkit] Fix error on shutdown on Canvas 26.1.2 (#1662) 2026-05-27 19:09:45 +02:00
NEZNAMY 1a52f3de21 [Fabric] Update to 26.2-pre-1 2026-05-27 18:55:38 +02:00
NEZNAMY 87321fe6d9 Merge pull request #1660 from NEZNAMY/feature/canvas-compatibility
[Bukkit] Add compatibility with Canvas 26.1.2 (#1658)
2026-05-27 16:48:42 +02:00
NEZNAMY e3afa04f90 [Bukkit] Add compatibility with Canvas 26.1.2 (#1658) 2026-05-26 16:57:33 +02:00
NEZNAMY 4a7fb76011 [Bukkit] Add another bStats chart 2026-05-25 19:52:45 +02:00
NEZNAMY 4c0b79ef25 [MiniMessage hook] Fix error on servers without Adventure library after PR #1657 2026-05-24 18:23:01 +02:00
NEZNAMY 0d6e71ad9b [Bukkit] Update to new bStats chart 2026-05-24 16:06:46 +02:00
NEZNAMY 4025eb5426 [Bukkit] Remove support for 1.9, 1.10, 1.11, 1.13, 1.14 and 1.15 2026-05-24 14:58:58 +02:00
NEZNAMY a26ab401b6 Bump version to 6.1.0-SNAPSHOT 2026-05-24 14:33:13 +02:00
NEZNAMY 1846eab87e [Build] Add comment to repository 2026-05-23 19:48:43 +02:00
NEZNAMY c860fc33fb Merge pull request #1657 from MattiaFioretti/feature/minimessage-head-texture
Register a custom <head_texture:url> MiniMessage
2026-05-23 18:52:30 +02:00
Mattia Fioretti 9a3f5b4322 Use texture hash instead of full URL for <head_texture> tag 2026-05-23 17:26:44 +02:00
Mattia Fioretti fb58ca99ae Fix ClassNotFoundException for <head_texture> tag on versions below 1.21.9 2026-05-23 16:55:20 +02:00
NEZNAMY b490d79929 [BungeeCord] Add back warning if Waterfall's disable tablist rewrite option is enabled 2026-05-23 16:46:08 +02:00
NEZNAMY fb6044c3c1 [BungeeCord] Change compatibility error message logging level from warning to severe 2026-05-23 16:32:19 +02:00
Mattia Fioretti d518e8c463 Register a custom <head_texture:url> MiniMessage tag that builds a player head object component from a Minecraft texture URL. 2026-05-23 15:10:37 +02:00
NEZNAMY 196ab4d61d [Dump command] Include content of animations, config, groups and users in raw format instead of extracting parts from it 2026-05-19 19:24:18 +02:00
NEZNAMY 2e406f3d1c [Plugin messages] Fix placeholders from bridge sometimes not being read on reload with new bridge version 2026-05-10 20:31:12 +02:00
NEZNAMY 8d6c17806c [Global playerlist] Add the feature into dump command output 2026-05-09 17:43:07 +02:00
NEZNAMY d60ffccd2a [Fabric] Correctly read colors in returned placeholder-api components 2026-05-03 16:40:47 +02:00
NEZNAMY 5be3016e90 [Wiki] Monthly wiki backup for changes made in April 2026 2026-05-02 11:18:47 +02:00
NEZNAMY 55befc4d0c [Belowname] Reset score for 26.2+ players if fancy-value is empty thanks to a new MC feature (which hopefully won't be removed) 2026-05-01 17:02:41 +02:00
NEZNAMY 4a4f15ed52 [Fabric / NeoForge] Use access transforming to make fields public and mutable (#1648) 2026-04-28 16:11:59 +02:00
NEZNAMY 0f294a47b0 [Velocity] [Performance] Override method to avoid duplicate check 2026-04-26 13:42:00 +02:00
NEZNAMY 4ee766a6f9 [Tablist] [Performance] Remove unnecessary task queueing 2026-04-26 13:34:34 +02:00
NEZNAMY a3103ea0c9 6.0.3-SNAPSHOT 2026-04-26 10:40:47 +02:00
NEZNAMY bef190ddd7 6.0.2 2026-04-25 18:06:51 +02:00
NEZNAMY cf320b3fa2 [GitHub] Fix compilation 2026-04-25 16:47:32 +02:00
NEZNAMY 34c8745fa9 [Build] Add another shadow jar task for non-modded platforms to avoid error on Paper 1.20.5 - 1.21.4 2026-04-25 16:37:52 +02:00
NEZNAMY 8096ff7f98 Merge pull request #1646 from JT122406/master
Fabric and NeoForge for 26.1.2
2026-04-25 15:48:02 +02:00
NEZNAMY dad3006f84 Refactor TabListEntryTracker into an interface to avoid class loading error on Bukkit 1.7.10 2026-04-23 20:15:50 +02:00
NEZNAMY 0e35f15a86 [Bukkit] Change check to avoid false flag on modded servers 2026-04-23 17:29:08 +02:00
NEZNAMY d423740273 [Nametags] Prefer color codes for team color over magic codes if both are used 2026-04-22 18:11:07 +02:00
NEZNAMY 49f489c4dc [Teams] [Performance] Only send teams to players visible in the tablist, small refactor and a bug fix with disable-condition 2026-04-22 17:48:46 +02:00
NEZNAMY ac71bf2111 [Bossbar] Add more info to dump command output 2026-04-20 19:57:40 +02:00
Joseph T. McQuigg 6d40dd12fa Java 25 in fabric.mod.json 2026-04-19 15:11:46 -04:00
Joseph T. McQuigg a732abcdd6 Refresh 2026-04-19 14:42:25 -04:00
Joseph T. McQuigg c2f90f9daf Fix Credits, Fix Version Numbers 2026-04-19 14:37:29 -04:00
Joseph T. McQuigg 898f56c32a Forge for 26.1.2/1/0 (Non-Working) 2026-04-19 14:17:53 -04:00
Joseph T. McQuigg 2ebc4384f5 NeoForge for 26.1.2/1/0 2026-04-19 13:55:57 -04:00
Joseph T. McQuigg 687b8d5e29 Fabric for 26.1.2/1/0 2026-04-19 13:38:28 -04:00
NEZNAMY d09d95e069 6.0.2-SNAPSHOT 2026-04-19 17:57:05 +02:00
NEZNAMY 4dffc1ffe9 [Bukkit] Update supported version interval in error message 2026-04-16 19:01:30 +02:00
NEZNAMY f3b2cfbb87 [Description] Update badges 2026-04-13 17:02:47 +02:00
NEZNAMY 4af57308d3 6.0.1 2026-04-13 16:44:57 +02:00
NEZNAMY 28ecf8666c [Velocity] Update to VelocityScoreboardAPI 2.0.0 2026-04-13 16:43:08 +02:00
NEZNAMY 1f3d1beb8f Merge pull request #1643 from lokspel/master
26.1.2 is supported
2026-04-12 15:45:57 +02:00
MISHAandNEZNAMY 96d21bc544 Update bukkit/src/main/java/me/neznamy/tab/platforms/bukkit/ServerVersionInfo.java
Co-authored-by: NEZNAMY <martinmiklovicmm@gmail.com>
2026-04-12 14:27:54 +04:00
MISHA f5cb2b5a1d Update ServerVersionInfo.java 2026-04-12 09:32:08 +04:00
MISHA d1d6e73bbc Update ProtocolVersion.java 2026-04-12 09:31:13 +04:00
NEZNAMY 2321ae1c0c [Dump] Include floodgate install status next to player's bedrock status 2026-04-09 19:01:48 +02:00
NEZNAMY 982ee5b6bf [MySQL] Do not relocate driver due to a startup error on NeoForge 2026-04-09 18:11:35 +02:00
NEZNAMY dbe699843b [Dump] Sort plugins/mods alphabetically 2026-04-09 18:09:59 +02:00
NEZNAMY fda19760bd [Bukkit] Compile v26_1 module with Java 21 instead of 25 (#1640) 2026-04-09 17:23:48 +02:00
NEZNAMY 97d42152cd [Configuration] Fix NPE in config conversion 2026-04-07 17:57:21 +02:00
NEZNAMY 5981607783 Fix memory leak 2026-04-06 13:14:58 +02:00
NEZNAMY 7b7927f328 [Code style] Separate methods from class to clean it up 2026-04-05 12:47:07 +02:00
NEZNAMY a8e2730f2f [Performance] Do not send plugin messages about placeholder values if tab expansion is disabled 2026-04-05 12:05:06 +02:00
NEZNAMY 7ae18187bc 6.0.1-SNAPSHOT 2026-04-05 12:03:38 +02:00
NEZNAMY 02634c531f [Wiki] Monthly wiki backup for changes made in March 2026 2026-04-04 16:55:16 +02:00
NEZNAMY 5e8ff89c43 [Description] Update badges 2026-04-04 14:04:35 +02:00
NEZNAMY 2f307c492f 6.0.0 2026-04-04 13:24:48 +02:00
NEZNAMY e4b3e17c14 [Bukkit] Update unsupported server version message 2026-04-04 13:24:32 +02:00
NEZNAMY 66d14a0754 [Bukkit] Refactor server version detection for new Paper format and mark 26.1(.1) as compatible 2026-04-04 13:21:01 +02:00
NEZNAMY cb47dcbe12 [Bukkit] [Code style] Create variable instead of repeatedly calculating it 2026-04-03 18:43:25 +02:00
NEZNAMY ea15b40e05 Acknowledge 26.1.1 2026-04-03 18:01:34 +02:00
NEZNAMY 93cfd74fc0 [Bukkit] Refactor NMS implementation finding 2026-04-03 16:17:09 +02:00
NEZNAMY 79eb6a9078 [Bukkit] Rename classes and add console print 2026-04-03 12:54:07 +02:00
NEZNAMY 2e9e5042e1 Improve handling of IO errors on startup 2026-04-03 12:34:59 +02:00
NEZNAMY bb520edd53 [Bukkit] Catch correct exception and improve error message 2026-03-31 19:13:57 +02:00
NEZNAMY 2beb9563bb [BossBar] Forward UUID to platforms to prepare for 26.1 changes 2026-03-31 17:02:28 +02:00
NEZNAMY 87c15f7611 Bump dependencies 2026-03-31 16:35:52 +02:00
NEZNAMY 655bc176fa [Scoreboard] Fix line score recalculate issue (#1632) 2026-03-30 19:43:59 +02:00
NEZNAMY 66cb30d235 [Commands] Register debug command back to inform users about dump command 2026-03-28 19:32:43 +01:00
NEZNAMY cbe0b25b5f Java 25 and Spigot 26.1 support 2026-03-28 18:22:08 +01:00
NEZNAMY a8ccf4e1c9 Properly initialize minor version for 26.1 2026-03-28 18:17:04 +01:00
NEZNAMY cce29c51e3 Add 26.1 protocol constant 2026-03-25 17:24:49 +01:00
NEZNAMY 242015b0cc [Per world playerlist] Further improve compatibility with vanish plugins (#1621) 2026-03-24 17:41:56 +01:00
NEZNAMY f209e1b0f9 [Per world playerlist] Add vanish check for better compatibility with other plugins (#1621) 2026-03-21 16:16:06 +01:00
NEZNAMY 8a1ab06585 [Bukkit] Update bStats chart 2026-03-21 15:33:21 +01:00
NEZNAMY 50e817a86d [Per world playerlist] Use modern showPlayer/hidePlayer methods with plugin argument for better compatibility with other plugins (#1621) 2026-03-21 10:45:14 +01:00
NEZNAMY dbd596f079 [Belowname / Playerlist objective] Improve parity between the features 2026-03-09 18:43:34 +01:00
NEZNAMY f8e3a07e6f [Belowname / Playerlist objective] Finish vanish support checks after #1629 2026-03-09 17:49:47 +01:00
NEZNAMY ccd4ac3271 Merge pull request #1629 from c0bl/fix/vanished_score_packets
Consider canSee in YellowNumber.java setScore
2026-03-09 17:46:54 +01:00
c m fce1925448 consider canSee when sending scoreboard score packets in yellownumber feature 2026-03-08 21:14:14 -04:00
NEZNAMY 335eb5ef4d [Wiki] Monthly wiki backup for changes made in February 2026 2026-03-08 08:37:45 +01:00
NEZNAMY 654c40b892 [MySQL] Include MySQL driver in the jar for it to work on Velocity and modded platforms (#1620) 2026-03-07 11:46:09 +01:00
NEZNAMY a937243cdf [Dump command] Add proxy players to nametag player table 2026-03-03 17:17:13 +01:00
NEZNAMY 665d6baf1c [Configuration] Add config conversion for the new option (#1626) 2026-03-02 18:41:53 +01:00
NEZNAMY b4d326b52c Merge pull request #1626 from BlvckBytes/placeholders_locale
[Placeholders] Added locale of date-/time-format to config.yml
2026-03-02 18:26:36 +01:00
BlvckBytes 3aacec826b [Placeholders] Added locale of date-/time-format to config.yml 2026-03-01 22:42:27 +01:00
NEZNAMY 3261856385 [Forge/NeoForge] Remove redundant content for easier backporting 2026-02-28 18:05:51 +01:00
NEZNAMY 8cf479bf3b [Forge/NeoForge] Workaround to fix tablist tracking not working properly caused by events being fired too late (#1623) 2026-02-28 11:21:34 +01:00
NEZNAMY 3c3ef636eb [Code style] Remove redundant calls 2026-02-21 21:49:06 +01:00
NEZNAMY 3bd0b13740 [Bukkit] Fix error on Folia 2026-02-15 13:48:34 +01:00
NEZNAMY 92349227a2 Merge pull request #1617 from Arava-0/feat/scoreboard-api-setLines
feat(scoreboard): add setLines method for dynamic line updates
2026-02-13 16:47:01 +01:00
Arava-0 4dfc2a638a feat(scoreboard): add setLines method for dynamic line updates
- Introduces a new method to replace all current scoreboard lines with a provided list.
- Prevents unnecessary updates if the new list is identical to the current one.
2026-02-13 14:58:36 +01:00
NEZNAMY d7c73a8072 [Tablist Formatting] Add information about proxy players to dump output 2026-02-10 17:44:55 +01:00
NEZNAMY 297acc0322 [Belowname & Playerlist objectives] Add information about proxy players to dump output 2026-02-10 17:14:44 +01:00
NEZNAMY 1126b3b471 [Global playerlist] Do not remove players from the same server on quit, let backend handle it (#1611) 2026-02-08 18:07:59 +01:00
NEZNAMY ac59973283 [Tablist] Use ConcurrentHashMap as WeakHashMap was not used the intended way anyway (also may have been causing an issue) 2026-02-08 16:00:15 +01:00
NEZNAMY 4b130ed71d Update Gradle 9.1.0 -> 9.3.1 2026-02-07 18:13:00 +01:00
NEZNAMY ce90a33ce9 [BossBar] Fix unfreezing issue on Velocity and rename some methods (#1610) 2026-02-07 10:09:42 +01:00
NEZNAMY 50fee32218 [Velocity] Bump minimum required Velocity build to 546 (after the bossbar bug was partially fixed) to avoid my head exploding when trying to fix the issue in TAB (and having to make it support the older versions as well) (#1610) 2026-02-06 20:02:51 +01:00
NEZNAMY 39334a4cd9 [Dump command] Add placeholders sent by bridge on proxy installation 2026-02-04 18:26:12 +01:00
NEZNAMY fec2131ba8 [Wiki] Monthly wiki backup for changes made in January 2026 2026-02-02 17:57:02 +01:00
NEZNAMY 7c63436da4 Fix error 2026-02-01 15:05:41 +01:00
NEZNAMY de8fe63bd0 [Components] Partially implement click event and use it in dump command to make the link clickable 2026-01-31 14:40:07 +01:00
NEZNAMY 1c22f2387b [Sorting] Remove redundant variable 2026-01-31 11:16:49 +01:00
NEZNAMY e1b277582a [Sorting] Remove team name note tracking as debug command was deleted and dump already contains this information in a better way 2026-01-31 10:44:11 +01:00
NEZNAMY 94acc4f356 [Placeholders] Create class for holding placeholders to fix issues with re-registering placeholders 2026-01-31 10:39:58 +01:00
NEZNAMY 9afb62a6a4 [Placeholders] Simplify internal placeholder refresh interval handling and make it technically closer to simply pretending they are in the config 2026-01-30 18:58:15 +01:00
NEZNAMY 95ed957d54 [Configuration] Remove comment from default config that is no longer true 2026-01-30 17:10:24 +01:00
NEZNAMY 58814bc2da [API] [Layout] Add method for fixed slot ping as string 2026-01-29 16:07:13 +01:00
NEZNAMY 9e7556c3c1 [Dump Command] Do not show anonymous conditions as they are ugly in the output 2026-01-27 17:42:43 +01:00
NEZNAMY abaf723fb7 [Tablist Formatting] Fix tracked name not being assigned if target player cannot be seen, breaking the feature on server switch 2026-01-26 20:18:51 +01:00
NEZNAMY f52e99c86e [Conditions] Fix conditions not refreshing 2026-01-26 19:05:06 +01:00
NEZNAMY d6e70c35d8 [Dump Command] [Nametags] Dump all display values for all players in a table 2026-01-26 17:55:11 +01:00
NEZNAMY 4c91e73f0e [API] [Layout] Add method for specifying slot count when creating a new layout 2026-01-26 17:16:38 +01:00
NEZNAMY d083fc6e43 [Dump Command] Add table of tablist formatting for all players into dump 2026-01-25 19:47:35 +01:00
NEZNAMY ff2df25e3b [Commands] Delete debug command and update references to dump command 2026-01-25 17:10:40 +01:00
NEZNAMY 86c2342097 Replace most minor version checks with protocol version to prepare for new versioning system. 2026-01-25 16:49:28 +01:00
NEZNAMY 6dbd176683 [Dump Command] Include bridge info about player on proxy installation 2026-01-25 16:06:36 +01:00
NEZNAMY 141aab1c22 [Dump Command] Include list of downloaded PlaceholderAPI expansions 2026-01-25 15:29:10 +01:00
NEZNAMY 126ea4f456 [Dump Command] Print values for all players in belowname and playerlist objectives instead of just the selected player 2026-01-25 15:14:43 +01:00
NEZNAMY a0e5a97594 [Dump Command] Add general config settings to output 2026-01-25 14:54:19 +01:00
NEZNAMY 0420c11937 [Dump Command] Add placeholder-related config sections 2026-01-25 13:46:57 +01:00
NEZNAMY 27472fd387 [Dump Command] Format placeholder values in a table 2026-01-25 12:31:19 +01:00
NEZNAMY 104bdc646f [Dump Command] Improve sorting feature output 2026-01-25 11:44:34 +01:00
NEZNAMY cbc4c275d1 [Description] Update badges 2026-01-24 14:18:17 +01:00
NEZNAMY 372d51f873 Merge branch 'master' of https://github.com/NEZNAMY/TAB
# Conflicts:
#	build.gradle.kts
2026-01-24 14:16:40 +01:00
NEZNAMY 21b7393274 [Dump command] Add more features to output 2026-01-23 20:03:14 +01:00
NEZNAMY dcd5f5a3e6 [Dump command] Improve output and create a class for features with display condition to avoid code duplication 2026-01-23 19:42:29 +01:00
NEZNAMY bf313b3ba1 [Commands] Use better error logging than e.printStackTrace() 2026-01-21 19:50:57 +01:00
NEZNAMY 5b4e3c479c [Commands] Add player info to dump command 2026-01-21 19:40:56 +01:00
NEZNAMY 6dedb05c29 [Commands] Drastically improve the new dump command 2026-01-21 18:18:31 +01:00
NEZNAMY 5f78e01342 [Commands] Initial implementation of /tab dump command 2026-01-18 17:38:07 +01:00
NEZNAMY 881506a3d5 [Tab expansion] Refactor the way data is stored. 2026-01-18 13:59:27 +01:00
NEZNAMY e26fc661b2 [Scoreboard] Execute scoreboard commands in scoreboard thread to avoid error 2026-01-17 20:50:19 +01:00
NEZNAMY f921758ec0 [Placeholders] Small optimization to replacing 2026-01-17 13:21:42 +01:00
NEZNAMY 01d65ba780 [Placeholders] Start rework of placeholder retrieving for better performance and dynamic placeholders 2026-01-17 12:29:55 +01:00
NEZNAMY d77e2d6f45 [Belowname] Clean up and use a different check for vanished players 2026-01-11 18:49:18 +01:00
NEZNAMY 7c30f55f31 [Bukkit] Remove support for several old MC versions 2026-01-11 16:48:47 +01:00
NEZNAMY c2d63865d5 [Bukkit] Repackage paper classes 2026-01-11 15:09:17 +01:00
NEZNAMY 78f8c99f11 6.0.0-SNAPSHOT 2026-01-11 14:03:24 +01:00
481 changed files with 10662 additions and 11591 deletions

No files matched your search

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="Documentation: Wiki"><title>Documentation: Wiki</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="126" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="93" height="20" fill="#555"/><rect x="93" width="33" height="20" fill="#dfb317"/><rect width="126" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="830">Documentation</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="830">Documentation</text><text aria-hidden="true" x="1085" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="230">Wiki</text><text x="1085" y="140" transform="scale(.1)" fill="#fff" textLength="230">Wiki</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="Documentation: Wiki"><title>Documentation: Wiki</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="126" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="93" height="20" fill="#555"/><rect x="93" width="33" height="20" fill="#d8b800"/><rect width="126" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="475" y="150" fill-opacity=".8" filter="url(#blur)" textLength="830">Documentation</text><text x="475" y="150" fill-opacity=".3" textLength="830">Documentation</text></g><text x="475" y="140" textLength="830">Documentation</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="1085" y="150" fill-opacity=".8" filter="url(#blur)" textLength="230">Wiki</text><text x="1085" y="150" fill-opacity=".3" textLength="230">Wiki</text></g><text x="1085" y="140" textLength="230">Wiki</text></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="20" role="img" aria-label="Java: 8+"><title>Java: 8+</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="35" height="20" fill="#555"/><rect x="35" width="25" height="20" fill="#007ec6"/><rect width="60" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="185" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="250">Java</text><text x="185" y="140" transform="scale(.1)" fill="#fff" textLength="250">Java</text><text aria-hidden="true" x="465" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="150">8+</text><text x="465" y="140" transform="scale(.1)" fill="#fff" textLength="150">8+</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="20" role="img" aria-label="Java: 8+"><title>Java: 8+</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="60" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="35" height="20" fill="#555"/><rect x="35" width="25" height="20" fill="#007ec6"/><rect width="60" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="185" y="150" fill-opacity=".8" filter="url(#blur)" textLength="250">Java</text><text x="185" y="150" fill-opacity=".3" textLength="250">Java</text></g><text x="185" y="140" textLength="250">Java</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="465" y="150" fill-opacity=".8" filter="url(#blur)" textLength="150">8+</text><text x="465" y="150" fill-opacity=".3" textLength="150">8+</text></g><text x="465" y="140" textLength="150">8+</text></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="20" role="img" aria-label="Minecraft: 1.7.x - 1.21.11"><title>Minecraft: 1.7.x - 1.21.11</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="156" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="95" height="20" fill="#007ec6"/><rect width="156" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">Minecraft</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">Minecraft</text><text aria-hidden="true" x="1075" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="850">1.7.x - 1.21.11</text><text x="1075" y="140" transform="scale(.1)" fill="#fff" textLength="850">1.7.x - 1.21.11</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20" role="img" aria-label="Release: 5.4.0"><title>Release: 5.4.0</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="92" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="53" height="20" fill="#555"/><rect x="53" width="39" height="20" fill="#007ec6"/><rect width="92" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="275" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">Release</text><text x="275" y="140" transform="scale(.1)" fill="#fff" textLength="430">Release</text><text aria-hidden="true" x="715" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="290">5.4.0</text><text x="715" y="140" transform="scale(.1)" fill="#fff" textLength="290">5.4.0</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20" role="img" aria-label="Release: 6.1.1"><title>Release: 6.1.1</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="92" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="53" height="20" fill="#555"/><rect x="53" width="39" height="20" fill="#007ec6"/><rect width="92" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="275" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">Release</text><text x="275" y="150" fill-opacity=".3" textLength="430">Release</text></g><text x="275" y="140" textLength="430">Release</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="715" y="150" fill-opacity=".8" filter="url(#blur)" textLength="290">6.1.1</text><text x="715" y="150" fill-opacity=".3" textLength="290">6.1.1</text></g><text x="715" y="140" textLength="290">6.1.1</text></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="124" height="20" role="img" aria-label="GitHub: Source code"><title>GitHub: Source code</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="124" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="47" height="20" fill="#555"/><rect x="47" width="77" height="20" fill="#dfb317"/><rect width="124" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="245" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="370">GitHub</text><text x="245" y="140" transform="scale(.1)" fill="#fff" textLength="370">GitHub</text><text aria-hidden="true" x="845" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="670">Source code</text><text x="845" y="140" transform="scale(.1)" fill="#fff" textLength="670">Source code</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="124" height="20" role="img" aria-label="GitHub: Source code"><title>GitHub: Source code</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="124" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="47" height="20" fill="#555"/><rect x="47" width="77" height="20" fill="#d8b800"/><rect width="124" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="245" y="150" fill-opacity=".8" filter="url(#blur)" textLength="370">GitHub</text><text x="245" y="150" fill-opacity=".3" textLength="370">GitHub</text></g><text x="245" y="140" textLength="370">GitHub</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="845" y="150" fill-opacity=".8" filter="url(#blur)" textLength="670">Source code</text><text x="845" y="150" fill-opacity=".3" textLength="670">Source code</text></g><text x="845" y="140" textLength="670">Source code</text></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -4
View File
@@ -274,12 +274,9 @@ if __name__ == "__main__":
]
)
release = "5.4.0"
release = "6.1.1"
download_image(f"https://img.shields.io/badge/Release-{release}-blue.svg", "Badge_Release.svg")
minecraft = "1.7.x - 1.21.11".replace(" ", "%20").replace("-", "--")
download_image(f"https://img.shields.io/badge/Minecraft-{minecraft}-blue.svg", "Badge_Minecraft.svg")
java = "8+"
download_image(f"https://img.shields.io/badge/Java-{java}-blue.svg", "Badge_Java.svg")
+1 -1
View File
@@ -26,12 +26,12 @@
## Functions
* [Animations](https://github.com/NEZNAMY/TAB/wiki/Animations)
* [Component usage (RGB, fonts, sprites)](https://github.com/NEZNAMY/TAB/wiki/How-to-use-Minecraft-components)
* [Conditional placeholders](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders)
* [Error logging](https://github.com/NEZNAMY/TAB/wiki/Error-logging)
* [MySQL](https://github.com/NEZNAMY/TAB/wiki/MySQL)
* [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements)
* [Placeholders](https://github.com/NEZNAMY/TAB/wiki/Placeholders)
* [RGB/font usage](https://github.com/NEZNAMY/TAB/wiki/How-to-use-RGB-color-codes)
## Other
* [Additional information](https://github.com/NEZNAMY/TAB/wiki/Additional-information)
+1
View File
@@ -44,6 +44,7 @@ That includes header/footer, prefix/suffix, belowname, playerlist objective, bos
# Tips & Tricks
* Animations don't need to be fast. You can use them as a changing text in general.
* Animations can also be used as a short alias to a long text. You can do this by creating an animation with one frame.
* Adding to the previous point, animations can be used as a way to wrap a placeholder, having two different identifiers return the same value, allowing to use both original and replaced output from [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements).
# External tools
If you want an animation too complicated to be done manually, you can check out these websites:
+5 -9
View File
@@ -13,7 +13,7 @@ Commands have required permissions associated with them.
* [/tab \<player/group/playeruuid\> \<name\> \<property\> \[value\] \[options\]](#tab-playergroupplayeruuid-name-property-value-options)
* [/tab \<player/group/playeruuid\> \<name\> remove](#tab-playergroupplayeruuid-name-remove)
* [/tab reload](#tab-reload)
* [/tab debug \[player\]](#tab-debug-player)
* [/tab dump \[player\]](#tab-dump-player)
* [/tab group \<group\>](#tab-group-group)
* [/tab groups](#tab-groups)
* [Bossbar](#bossbar)
@@ -65,13 +65,9 @@ Commands have required permissions associated with them.
* **Permission:** `tab.reload`
* **Description:** Unloads the plugin, loads configuration files including changes and loads the plugin.
## /tab debug [player]
* **Permission:** `tab.debug`
* **Description:** Shows the server version, plugin version, permission group choice logic, and sorting type. If player argument is filled, shows info about that player:
* On proxy, shows whether player is connected to the backend server with [Bridge](https://github.com/NEZNAMY/TAB/wiki/TAB-Bridge) plugin or not.
* Sorting value & explanation, useful to see what went wrong if players are not sorted correctly.
* Primary group set using [How to assign players into groups](https://github.com/NEZNAMY/TAB/wiki/How-to-assign-players-into-groups)
* List of all configured properties, their values and source.
## /tab dump \<player\>
* **Permission:** `tab.dump`
* **Description:** Creates a very detailed dump about the specified player to help with troubleshooting, uploads it to a paste site and shows the generated URL.
## /tab group \<group\>
* **Permission:** `tab.groupinfo`
@@ -120,7 +116,7 @@ Commands have required permissions associated with them.
## /tab nametag <showview/hideview/toggleview> \[viewer\] \[options\]
* **Permission:** `tab.nametag.view` (`tab.nametag.view.other` for toggling for other players)
* **Description:** Shows / hides / toggles nametag VIEW a specified player.
* **Description:** Shows / hides / toggles nametag VIEW of a specified player. When hidden, the viewer will not see anyone's nametags.
* **Options:**
* `-s` for silent toggling (no chat message for affected player)
+48 -98
View File
@@ -3,81 +3,53 @@
* [Java](#java)
* [Supported server software and versions](#supported-server-software-and-versions)
* [Supported features per platform](#supported-features-per-platform)
* [Plugin hooks](#plugin-hooks)
* [All platforms](#all-platforms)
* [Bukkit](#bukkit)
* [BungeeCord](#bungeecord)
* [Fabric](#fabric)
* [Forge / NeoForge](#forge--neoforge)
* [Velocity](#velocity)
* [Plugin / mod hooks](#plugin--mod-hooks)
* [Compatibility issues](#compatibility-issues)
# Requirements
TAB does not depend on any other plugins. The experience can however be enhanced by installing other plugins, see below.
## Java
TAB's shared module is compiled with Java 8. For the platforms themselves, TAB does have a higher requirement, however, it does not require a Java version higher than the server software itself does. Specifically:
* Fabric, Forge and NeoForge require Java 21
* Velocity requires Java 17
* Bukkit and BungeeCord are ok with Java 8
TAB does not depend on any other plugins. The experience can however be enhanced by installing other plugins, see below. There is also no additional Java version requirement. If your server runs on your Java version, TAB will too.
# Supported server software and versions
<table>
<thead>
<tr>
<th>Software type</th>
<th>Software name</th>
<th>Supported versions</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Vanilla</td>
<td rowspan=1><a href="https://getbukkit.org/">Bukkit</a> (+forge hybrids)</td>
<td>✔ (1.7.x - 1.21.11)</td>
</tr>
<tr>
<td rowspan=1><a href="https://www.spongepowered.org/">Sponge</a></td>
<td>❌</td>
</tr>
</tbody>
<tbody>
<tr>
<td rowspan=3>Modded</td>
<td rowspan=1><a href="https://fabricmc.net">Fabric</a></td>
<td>✔ (1.21.11)<sup>1</sup></td>
</tr>
<tr>
<td rowspan=1><a href="https://minecraftforge.net">Forge</a></td>
<td>✔ (1.21.9 - 1.21.10)<sup>1</sup></td>
</tr>
<tr>
<td rowspan=1><a href="https://neoforged.net/">NeoForge</a></td>
<td>✔ (1.21.11)<sup>1</sup></td>
</tr>
</tbody>
<tbody>
<tr>
<td rowspan=2>Proxies</td>
<td rowspan=1><a href="https://ci.md-5.net/job/BungeeCord/">BungeeCord</a></td>
<td>✔ (latest only)<sup>2</sup></td>
</tr>
<tr>
<td rowspan=1><a href="https://www.velocitypowered.com/">Velocity</a></td>
<td>✔ (latest only)<sup>2</sup></td>
</tr>
</tbody>
</table>
With the introduction of new Minecraft versions, it is becoming harder and harder to support many versions in the same jar, eventually making it impossible since Minecraft 26.1.
**As a general rule, if you want the latest TAB version for your server please use [Modrinth](https://modrinth.com/plugin/tab-was-taken)'s download filter where you can select your server software and version and you'll get the latest jar.**
<sup>1</sup> Modded platforms only support the latest MC version(s). For older MC versions try [Modrinth](https://modrinth.com/plugin/tab-was-taken)'s version download filter. If TAB is not available for your desired Minecraft version or is very outdated, you may [request a backport](https://github.com/NEZNAMY/TAB/issues/new?template=backport.yml).
Currently, the latest Minecraft version supported by TAB is **26.2**. When new versions come out, TAB is always updated soon after to support them.
<sup>2</sup> Latest only doesn't mean only the latest build will work,
it means the plugin was made to be compatible with the latest version/build.
If your server software and version is reasonably popular and TAB hasn't been backported to that version in a very long time, you may [request a backport](https://github.com/NEZNAMY/TAB/issues/new/choose) (unless backports to that version are no longer offered, see below).
Since TAB 6.0.0, Minecraft 1.x started taking hits with each subsequent versions - Bukkit versions started disappearing and **modded platforms no longer offer backports of TAB 6.0.0+ to MC 1.x** (this is because the build scripts had to be massively changed for MC 26+).
The sections below provide **additional** information for each platform.
## Bukkit
TAB 5.5.0 supports all 1.x versions starting with 1.7. For 1.5.2 - 1.6.4, use TAB v5.2.5.
Since TAB 6.0.0, support for unused 1.x versions will continue to disappear with each subsequent Minecraft release.
## BungeeCord / Velocity
For proxies, the plugin is made to work with the latest build.
Since breaking changes don't happen too often, it means a wide range of versions is usually supported.
When a breaking change occurs, the plugin is updated to support the new version,
automatically making old versions incompatible.
Since proxies support all client versions, there is never a reason to stay outdated,
so you can always safely update to new version/build of your proxy software if the plugin requires it.
so you can always safely update to a new build of your proxy software if the plugin requires it.
## Fabric
Every TAB update only supports the latest Fabric version(s) at the time of release.
Minecraft 1.x - TAB 5.5.0 was backported to 1.20 - 1.21.11 and the jars were uploaded to Modrinth. TAB 5.0.7 supports all Fabric versions from 1.14 to 1.19.4. Backports to 1.x are no longer offered.
## Forge
Every TAB update only supports the latest Forge version(s) at the time of release.
**Currently, Forge 26.x is not supported because the modding tool is broken**.
Minecraft 1.x - TAB 5.5.0 was backported to 1.20 - 1.21.11 and the jars were uploaded to Modrinth, along with a few other random versions people requested. If your version is not available, you may request a backport.
## NeoForge
Every TAB update only supports the latest NeoForge version(s) at the time of release.
Minecraft 1.x - TAB 5.5.0 was backported to all existing NeoForge versions (1.20.2+) and the jars were uploaded to Modrinth. Backports to 1.x are no longer offered.
## Sponge
This platform is no longer supported as no one is using it and it has breaking changes to the API with each Minecraft version. Sponge is supposed to be combined with Forge or NeoForge, both of which TAB supports. You can use Modrinth's download filter to see what is available.
# Supported features per platform
| Feature          <sup>Platform</sup> | Bukkit / Hybrid | Fabric / Forge / NeoForge | BungeeCord | Velocity |
@@ -97,41 +69,20 @@ so you can always safely update to new version/build of your proxy software if t
✔ = Fully functional
❌ = Completely missing
# Plugin hooks
To enhance user experience, TAB hooks into other plugins for better experience.
This set of plugins is different for each platform based on their availability.
# Plugin / mod hooks
To enhance user experience, TAB hooks into other plugins / mods for better experience.
This set of plugins / mods is different for each platform based on their availability.
Some are available on all platforms, some only in a few.
## All platforms
[**Floodgate**](https://github.com/GeyserMC/Floodgate) - For properly detecting bedrock players to adapt features for the best possible user experience.
[**LuckPerms**](https://github.com/LuckPerms/LuckPerms) - Detecting permission groups of players for per-group settings.
[**ViaVersion**](https://github.com/ViaVersion/ViaVersion) - For properly detecting player's version to adapt features for the best possible user experience.
## Bukkit
[**LibsDisguises**](https://github.com/libraryaddict/LibsDisguises) - Detecting disguised players to disable collision to avoid endless push by colliding with own copy created by LibsDisguises.
[**PlaceholderAPI**](https://github.com/PlaceholderAPI/PlaceholderAPI) - Allows users to use its placeholders inside TAB.
[**PremiumVanish**](https://www.spigotmc.org/resources/14404) - Supporting PremiumVanish's vanishing levels instead of using a basic compatibility system.
[**Vault**](https://github.com/milkbowl/Vault) - Detecting permission groups of players for per-group settings.
## BungeeCord
[**PremiumVanish**](https://www.spigotmc.org/resources/14404) -
Supporting PremiumVanish's vanishing levels instead of using a basic compatibility system.
[**RedisBungee**](https://github.com/ProxioDev/RedisBungee) -
Communicating with other proxies to properly display visuals on players on another proxy.
## Fabric
[**fabric-permissions-api**](https://github.com/lucko/fabric-permissions-api) - Supporting permission nodes instead of OP levels.
[**placeholder-api**](https://modrinth.com/mod/placeholder-api) - Displaying placeholders from that mod and offering TAB's placeholders into it.
## Forge / NeoForge
[**LuckPerms**](https://github.com/LuckPerms/LuckPerms) - Permission checks with a string instead of using vanilla OP levels.
## Velocity
[**PremiumVanish**](https://www.spigotmc.org/resources/14404) -
Supporting PremiumVanish's vanishing levels instead of using a basic compatibility system.
[**RedisBungee**](https://github.com/ProxioDev/RedisBungee) -
Communicating with other proxies to properly display visuals on players on another proxy.
[**VelocityScoreboardAPI**](https://github.com/NEZNAMY/VelocityScoreboardAPI) -
Sending scoreboard packets (scoreboard-teams, belowname-objective, playerlist-objective, scoreboard)
| Platform | Plugins / mods |
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| All platforms | [**Floodgate**](https://github.com/GeyserMC/Floodgate) - For properly detecting bedrock players to adapt features for the best possible user experience. <br /> [**LuckPerms**](https://github.com/LuckPerms/LuckPerms) - Detecting permission groups of players for per-group settings. <br /> [**ViaVersion**](https://github.com/ViaVersion/ViaVersion) - For properly detecting player's version to adapt features for the best possible user experience. |
| Bukkit | [**LibsDisguises**](https://github.com/libraryaddict/LibsDisguises) - Detecting disguised players to disable collision to avoid endless push by colliding with own copy created by LibsDisguises. <br />[**PlaceholderAPI**](https://github.com/PlaceholderAPI/PlaceholderAPI) - Allows users to use its placeholders inside TAB. <br />[**PremiumVanish**](https://www.spigotmc.org/resources/14404) - Supporting PremiumVanish's vanishing levels instead of using a basic compatibility system. <br />[**Vault**](https://github.com/milkbowl/Vault) - Detecting permission groups of players for per-group settings. |
| BungeeCord | [**PremiumVanish**](https://www.spigotmc.org/resources/14404) - Supporting PremiumVanish's vanishing levels instead of using a basic compatibility system. <br />[**RedisBungee**](https://github.com/ProxioDev/RedisBungee) - Communicating with other proxies to properly display visuals on players on another proxy. |
| Fabric | [**fabric-permissions-api**](https://github.com/lucko/fabric-permissions-api) - Supporting permission nodes instead of OP levels. <br />[**placeholder-api**](https://modrinth.com/mod/placeholder-api) - Displaying placeholders from that mod and offering TAB's placeholders into it. |
| Forge | [**LuckPerms**](https://github.com/LuckPerms/LuckPerms) - Permission checks with a string instead of using vanilla OP levels. <br /> [**ForgePlaceholderAPI**](https://github.com/EnvyWare/ForgePlaceholderAPI) - Allows users to use its placeholders inside TAB. <br /> [**Vanishmod**](https://modrinth.com/mod/vanishmod) - Getting player's vanish status. |
| NeoForge | [**LuckPerms**](https://github.com/LuckPerms/LuckPerms) - Permission checks with a string instead of using vanilla OP levels. |
| Velocity | [**PremiumVanish**](https://www.spigotmc.org/resources/14404) - Supporting PremiumVanish's vanishing levels instead of using a basic compatibility system. <br />[**RedisBungee**](https://github.com/ProxioDev/RedisBungee) - Communicating with other proxies to properly display visuals on players on another proxy. <br />[**VelocityScoreboardAPI**](https://github.com/NEZNAMY/VelocityScoreboardAPI) - Sending scoreboard packets (scoreboard-teams, belowname-objective, playerlist-objective, scoreboard) |
# Compatibility issues
* **Glow plugins** will fail to apply glow color correctly. Check [How to make the plugin compatible with glow plugins](https://github.com/NEZNAMY/TAB/wiki/How-to-make-TAB-compatible-with-glow-plugins) for more information.
@@ -139,7 +90,6 @@ Sending scoreboard packets (scoreboard-teams, belowname-objective, playerlist-ob
* **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 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.
* **Nexo**'s `hide_scoreboard_numbers` config option (may apply to ItemsAdder and Oraxen as well) makes [Belowname](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Belowname)'s and [Playerlist objective](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Playerlist-Objective)'s `value` and `fancy-value` not visible. When using these features, you'll need to keep the option disabled.
* **LimboAuth** may prevent TAB from applying tablist formatting in a way that is not detectable using Velocity API, resulting in the feature not working properly on join.
* **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
@@ -35,7 +35,7 @@ For Maven users, you can add the API dependency like this:
<dependency>
<groupId>com.github.NEZNAMY</groupId>
<artifactId>TAB-API</artifactId>
<version>5.4.0</version>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
@@ -50,7 +50,7 @@ repositories {
}
dependencies {
compileOnly 'com.github.NEZNAMY:TAB-API:5.4.0'
compileOnly 'com.github.NEZNAMY:TAB-API:6.0.0'
}
```
@@ -61,7 +61,7 @@ repositories {
}
dependencies {
compileOnly("com.github.NEZNAMY", "TAB-API", "5.4.0")
compileOnly("com.github.NEZNAMY", "TAB-API", "6.0.0")
}
```
+68 -11
View File
@@ -1,25 +1,31 @@
# Content
* [About](#about)
* [Configuration](#configuration)
* ~Commands~
* ~Placeholders~
* [Limitations](#limitations)
* [Compatibility with other plugins](#compatibility-with-other-plugins)
* [Additional info](#additional-info)
* [Additional note 1 - Copying nametag visibility rule](#additional-note-1---copying-nametag-visibility-rule)
* [Additional note 2 - Hidden on sneak on 1.8](#additional-note-2---hidden-on-sneak-on-18)
* [Additional note 3 - Visible on NPCs](#additional-note-3---visible-on-npcs)
* [Additional note 2 - [1.8.x] Hidden on sneak](#additional-note-2---18x-hidden-on-sneak)
* [Additional note 3 - [26.1.2-] Visible on NPCs](#additional-note-3---2612--visible-on-npcs)
* [[26.1.x] Visible on all entities](#261x-visible-on-all-entities)
* [Additional note 4 - Compatibility with modified clients](#additional-note-4---compatibility-with-modified-clients)
* [Troubleshooting](#troubleshooting)
* ~API~
* [Examples](#examples)
* [Example 1 - Per-world values](#example-1---per-world-values)
* [Example 2 - Hiding `title` for 1.20.3+ players](#example-2---hiding-title-for-1203-players)
* [Example 3 - Displaying health as 0-10 or in %](#example-3---displaying-health-as-0-10-or-in-)
* [Example 4 - Health bar using hearts](#example-4---health-bar-using-hearts)
* [Example 5 - Conditionally hiding belowname for 26.2+ viewers](#example-5---conditionally-hiding-belowname-for-262-viewers)
* [Tips & Tricks](#tips--tricks)
* [Tip 1 - Heart symbol](#tip-1---heart-symbol)
# About
This features gives you control over Minecraft's scoreboard objective feature with BELOW_NAME slot.
This line is displayed below the nametags of all player entities in game. It is not possible to explicitly disable this feature for NPCs; however, this goal can be achieved (see below for more info).
It is only visible when within an 8-block range of the player (the range is hardcoded in the client and cannot be changed with a plugin).
This features gives you control over Minecraft's scoreboard objective feature with BELOW_NAME slot.
On Minecraft 1.21.11 and below, it is visible on all player entities (including NPCs). On 26.1.x, it is visible on all player entities and all named entities (such as holograms), making this feature unusable. On 26.2 and above, it only appears on entities with a score assigned (the ideal implementation).
It is only visible when the target player is within a 10-block range of the viewer (the value is configurable since Minecraft 26.1).
Example visual effect:
![](https://images-ext-1.discordapp.net/external/YlGPCRDJVeZZI0TPWmVBKyHszxSkjatmclyqUThvTz8/https/image.prntscr.com/image/jcETUzVQQYqectQ2aI4iqQ.png)
@@ -49,8 +55,8 @@ All the options are explained in the following table.
|---------------------|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| enabled | true | Enables / Disables the feature |
| value | %health% | [1.20.2-] An integer from -2147483648 to 2147483647, doesn't support decimal values. The number is always white. Supports placeholders with player-specific output, such as player health. Only visible on 1.20.2 and lower. <br/> **Note**: Even if you only support 1.20.3+, you still need to configure this value to properly evaluate to a number, because the value is still sent to the client (just not displayed). You can set it to `0` for simplicity. |
| fancy-value | &c%health% | [1.20.3+] Any text, supports placeholders with per-player output. Only visible on 1.20.3+, where it completely replaces `value`. |
| fancy-value-default | NPC | [1.20.3+] Default display for all player entities. Plugin uses the value above for every real player, therefore this default value will only appear on player entities which are not actual players, a.k.a. NPCs. Only visible on 1.20.3+. |
| fancy-value | &c%health% | [1.20.3+] Any text, supports placeholders with per-player output. Only visible on 1.20.3+, where it completely replaces `value`. When evaluates to an empty string, the entire belowname is hidden for viewers using MC 26.2+ (when it was made possible). |
| fancy-value-default | NPC | [1.20.3 - 26.1.2] Default number format (`fancy-value`) for all player entities. `fancy-value` is displayed on every real player, therefore this default value will only appear on player entities which are not actual players, a.k.a. NPCs. Only visible on since 1.20.3 (the function did not exist prior) until 26.1.2 (player entities without scores no longer display any belowname since 26.2). |
| title | Health | Shared label shown after the score for every player entity. Player sees the same text on everyone (= placeholders are parsed for the viewer). Use the `value`/`fancy-value` fields for per‑player data; use `title` only for static labels like `Health` or placeholders which are supposed to be parsed for the viewing player. |
| disable-condition | %world%=disabledworld | A [condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) (either name of a condition or a conditional expression) that must be met for disabling the feature for players. Set to empty for not disabling the feature ever. <br/> **Note**: Disabling the feature for a player means sending objective unregister packet to them, which results in player not seeing belowname on anyone anymore. It doesn't work the other way around - you cannot disable this feature on target players, only for viewers. |
@@ -77,10 +83,10 @@ Note that if this was automatically generated by a plugin, it will probably be a
The Minecraft feature is programmed to be affected by nametag visibility rule.
This means that when the nametag is set to invisible, belowname will be invisible as well.
## Additional note 2 - Hidden on sneak on 1.8
## Additional note 2 - [1.8.x] Hidden on sneak
Belowname is not visible on 1.8.x clients when player is sneaking. This is client-sided behavior and cannot be changed by the server.
## Additional note 3 - Visible on NPCs
## Additional note 3 - [26.1.2-] Visible on NPCs
Belowname objective is automatically attached to all entities of player type with the default value of
`0` (<1.20.3) or value configured as `fancy-value-default` (1.20.3+).
This includes player NPCs.
@@ -102,9 +108,39 @@ This is how you can achieve it using the following popular NPC plugins:
* 3 - Change the NPC's name by editing your hologram's line(s): `/hologram edit hi setline 1 <text>`.
You can also add more lines if you want. See [FancyNpcs's wiki](https://docs.fancyinnovations.com/fancyholograms/commands/hologram/#text-hologram-modification) for more commands & info.
# Additional note 4 - Compatibility with modified clients
### [26.1.x] Visible on all entities
In Minecraft version 26.1, Mojang "fixed" [MC-99647](https://bugs.mojang.com/browse/MC/issues/MC-99647), which reported that belowname is not visible on non-player entities. As a result, the belowname is now visible on all entities with a custom name (this even includes invisible armor stands, holograms and more). A new bug report was made for this: [MC-307012](https://bugs.mojang.com/browse/MC/issues/MC-307012).
**This is a client-sided bug. You will experience it on 26.1.x regardless of the server version**.
If your server is on 26.1.x, or you support players with this version through ViaVersion, this might be a dealbreaker for you. If that's the case, either disable this feature entirely, or set
```
belowname-objective:
disable-condition: "%player-version-id%=775"
```
which will make 26.1.x players not see this feature on anyone (remember: disabling this feature disables it for the viewer, not target players).
**This bug was fixed in Minecraft version 26.2.**
## Additional note 4 - Compatibility with modified clients
Sadly, this feature is suffering from bugs introduced by third party clients such as Feather and Lunar. These two completely ignore `fancy-value` as if it was never added into the game, even on 1.20.3+. This is just an example, and it's not limited to these two clients and this one issue. If you experience issues with the feature and believe you configured it correctly, use vanilla client to make sure it's not caused by a broken client.
# Troubleshooting
This is a collection of tips to help you figure out why the feature isn't working as you expect.
To get started, run `/tab dump <any player>` (this feature doesn't contain per-player content in dump output) and open the generated link. Scroll down to `features` -> `BelowName` and check the content:
* If it says `BelowName: Feature is disabled`, it means you disabled the feature. Enable it by setting
```
belowname-objective:
enabled: true
```
* Check the `configuration` section and compare it with your config file. If it's different, you either forgot to reload TAB, or uploaded the config to the wrong server (or did not upload it at all).
* If a player cannot see belowname on anyone, check the table of players and make sure it's not because `Disabled with condition` is `true`.
* Check the table of players for `value`, `fancy-value` and `title` and make sure they match your expectations. Don't forget `value` only supports numbers.
* If you want to see `fancy-value` instead of just `value`'s number:
* Make sure your client is on 1.20.3+.
* Make sure your server is on 1.20.3+ (using TAB on proxy counts as being latest).
* Make sure you are not using a 3rd party client that removed this feature from 1.20.3+, such as Lunar or Feather.
* Keep in mind `title` parses for viewer and is visible on all other players, it does not parse for each target player. Do not use placeholders with strictly per-player results in `title`.
* If you have TAB on Velocity, install [VelocityScoreboardAPI](https://github.com/NEZNAMY/VelocityScoreboardAPI/releases) (and make sure it is not outdated).
# Examples
## Example 1 - Per-world values
This feature doesn't directly support per-world values or similar.
@@ -191,6 +227,7 @@ You can achieve this on `1.20.3+` using `healthbar` expansion from PlaceholderAP
```
belowname-objective:
fancy-value: "%healthbar_healthbar%"
title: ""
```
<img width="130" height="159" alt="image" src="https://github.com/user-attachments/assets/cef217ec-0a39-41ad-8457-f8f67a1e4b02" />
@@ -204,4 +241,24 @@ belowname-objective:
title: "&4❤"
```
> [!IMPORTANT]
> Make sure your config is [saved in UTF-8 encoding](https://github.com/NEZNAMY/TAB/wiki/How-to-save-the-config-in-UTF8-encoding) to properly read the heart symbol.
> Make sure your config is [saved in UTF-8 encoding](https://github.com/NEZNAMY/TAB/wiki/How-to-save-the-config-in-UTF8-encoding) to properly read the heart symbol.
## Example 5 - Conditionally hiding belowname for 26.2+ viewers
Since Minecraft 26.2, belowname only appears on entities with a score. This can be used to disable the belowname on players with a condition. We are trying to achieve a condition like this: "If something, hide belowname for 26.2+ viewers".
TAB is made to reset score (= hide belowname) of target players for 26.2+ viewers (<26.2 viewers are not affected, resetting scores for them would result in `fancy-value-default` to be shown, which is undesirable) if `fancy-value` evaluated to an empty string. Therefore, we will need to make a [relational condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders#relational-conditions) that checks the viewer's version and returns empty string if some condition is met.
Here is an example of hiding it if target player has less than 10 health (5 hearts):
```
conditions:
belowname-health:
conditions:
- "%viewer:player-version-id%>=776" # 26.2 uses 776, check for viewer's version, not target player's
- "%health%<10" # This should check target player's health
type: AND
true: "" # Viewer is on 26.2+ and target has less than 10 health, return empty string to hide it
false: "%health%" # Display health as normal
```
Then, use this condition as a relational condition (because it checks both viewer and target players):
```
belowname-objective:
fancy-value: "%rel_condition:belowname-health%"
```
+22 -10
View File
@@ -3,18 +3,17 @@
* [Configuration](#configuration)
* [Commands](#commands)
* [Placeholders](#placeholders)
* [Internal placeholders](#internal-placeholders)
* [PlaceholderAPI placeholders](#placeholderapi-placeholders)
* [Limitations](#limitations)
* [Compatibility with other plugins](#compatibility-with-other-plugins)
* [Additional info](#additional-info)
* [Additional note 1 - Hiding bar itself](#additional-note-1---hiding-bar-itself)
* [Additional note 1 - Hiding bar itself](#additional-note-1---hiding-bar-itself)
* [Troubleshooting](#troubleshooting)
* [API](#api)
* [Examples](#examples)
* [Example 1 - Changing text](#example-1---changing-text)
* [Example 2 - Switching between bossbars with condition](#example-2---switching-between-bossbars-with-condition)
* [Example 3 - Periodical appearing and disappearing](#example-3---periodical-appearing-and-disappearing)
* [Example 4 - Dynamic progress tracking a relative placeholder value](#example-4---dynamic-progress-tracking-a-relative-placeholder-value)
* [Example 1 - Changing text](#example-1---changing-text)
* [Example 2 - Switching between bossbars with condition](#example-2---switching-between-bossbars-with-condition)
* [Example 3 - Periodical appearing and disappearing](#example-3---periodical-appearing-and-disappearing)
* [Example 4 - Dynamic progress tracking a relative placeholder value](#example-4---dynamic-progress-tracking-a-relative-placeholder-value)
# About
This feature allows you to send modern bossbars to players, which appear on top of the screen.
@@ -81,7 +80,6 @@ Here is an example using all 6 properties:
| /tab bossbar announce \<name\> \<duration\> | `tab.announce.bar` | Shows the bossbar with the given `name` to every player on the server for the given `duration`, in seconds. |
# Placeholders
## Internal placeholders
Here are TAB's internal placeholders you can use when this feature is enabled:
| Placeholder | Description |
|-------------|-------------|
@@ -90,7 +88,6 @@ Here are TAB's internal placeholders you can use when this feature is enabled:
You can further use these placeholders in progress, for example, using Math expansion from PlaceholderAPI, such as `%math_{tab_placeholder_bossbar_announce_time_left_<bossbar>}/{tab_placeholder_bossbar_announce_time_total_<bossbar>}*100%` (remember to replace `<bossbar>` with the actual name of the bossbar).
## PlaceholderAPI placeholders
Here are TAB's PlaceholderAPI placeholders you can use when this feature is enabled:
| Placeholder | Description |
|-------------|-------------|
@@ -98,7 +95,8 @@ Here are TAB's PlaceholderAPI placeholders you can use when this feature is enab
# Limitations
* Does not support newlines. If you want to display more lines, you'll need to create multiple bossbars.
* The bar itself cannot be removed from the server side. However, it can be removed with a resource pack.
* The bar itself cannot be removed from the server side. However, it can be removed with a resource pack (see below for more info).
* While there is no explicit limit to how many bossbars you can configure, the client will only display up to 9 depending on window size and GUI scale in client settings.
# Compatibility with other plugins
Bossbar is a feature that can have multiple instances sent and displayed, so there is no possible conflict like in scoreboard for example. Every plugin manages its own bossbars and doesn't care about others.
@@ -121,6 +119,20 @@ Here is how to do it:
Finally, set your Bossbar color in TAB Config to `WHITE`. Now the line will be invisible!
# Troubleshooting
This is a collection of tips to help you figure out why the feature isn't working as you expect.
To get started, run `/tab dump <affected player>` and open the generated link. Scroll down to `features` -> `BossBar` and check the content:
* If it says `BossBar: Feature is disabled`, it means you disabled the feature. Enable it by setting
```
bossbar:
enabled: true
```
* Check the `configuration` section and compare it with your config file. If it's different, you either forgot to reload TAB, or uploaded the config to the wrong server (or did not upload it at all).
* Check the `bossbar conditions` section and for each bossbar and analyze the `display-condition` options (the value, with placeholders parsed, whether it is met or not) if it matches what you expect. If not, you may have made a mistake in your conditions.
* Check `bossbars visible` value to make sure user did not toggle the bossbar(s).
* Check `currently displayed bossbars` section and compare it with your expectations, as well as the actual in-game result. If it doesn't match your expectations, you didn't configure something correctly. If the output doesn't match the in-game result, it might be a bug.
# API
*To get started with the API, see [Developer API](https://github.com/NEZNAMY/TAB/wiki/Developer-API) page.*
@@ -4,14 +4,14 @@
* [Number comparisons](#number-comparisons)
* [Text operations](#text-operations)
* [Permission](#permission)
* [Multiple condition requirements](#multiple-condition-requirements)
* [Condition output](#condition-output)
* [Configuration](#configuration)
* [Usage](#usage)
* [Displaying text](#displaying-text)
* [Display condition of a feature](#display-condition-of-a-feature)
* [Short format](#short-format)
* [Conditional text](#conditional-text)
* [Using in a field where condition is expected](#using-in-a-field-where-condition-is-expected)
* [Using in a field where condition is expected (short format)](#using-in-a-field-where-condition-is-expected-short-format)
* [Relational conditions](#relational-conditions)
* [Refresh interval](#refresh-interval)
* [Additional info](#additional-info)
* [Additional note 1 - Choosing the correct AND / OR type](#additional-note-1---choosing-the-correct-and--or-type)
* [Examples](#examples)
* [Example 1 - Chaining conditional placeholders](#example-1---chaining-conditional-placeholders)
* [Example 2 - Combining AND and OR](#example-2---combining-and-and-or)
@@ -48,93 +48,79 @@ They have 2 main uses in the plugin:
> For `=` and `!=` you can check for empty output of a placeholder using `%my_placeholder%=` and `%my_placeholder%!=`.
> [!NOTE]
> For string operations, the text must match placeholder's output exactly, including color codes.
> For string operations, the text must match placeholder's output exactly, including color codes, as well as lower/UPPER case letters.
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
> [!WARNING]
> Giving someone OP status will automatically give them all permissions, which will make all the permission checks in conditions pass.
| Operation | Description | Example |
|-----------------------|---------------------------------|----------------------------------------------------------------------------------------------|
| `permission:<value>` | Permission requirement | `permission:my.permission` will pass if player has `my.permission` permission |
| `!permission:<value>` | Negative permission requirement | `!permission:my.permission` will pass if player **does not have** `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.
This value can be found under `type` field. Types are:
* `AND` - all sub-conditions must be met for the final condition to pass
* `OR` - at least one sub-condition must be met for the final condition to pass
If you only defined one subcondition, you don't need to define the type at all, since it's not used for anything.
# Condition output
If using condition as a placeholder, you can specify output in both cases using `true` and `false` values. `true` is used when condition passes, `false` if not. If using condition only as a view requirement, you can leave these values empty / not specify them at all.
# Configuration
Open **config.yml** and find this section:
```
conditions:
health:
conditions:
- '%health%<21'
- '%health%>15'
type: AND
true: Healthy!
false: Damaged!
```
`health` is name of our condition in this case.
`conditions` is a list of subconditions that must be met for this condition to pass.
`type` defines whether all subconditions must be met or at least one.
`true` & `false` define output in both cases.
# Usage
You have 2 ways to use conditions.
There are multiple different ways you can use conditions, all of which are described below.
## Displaying text
The first way is to use conditions to display text.
Configure outputs in `true` and `false` values and then use `%condition:<name>%`,
which will output text defined in `true` or `false` depending on if condition is met or not.
<details>
<summary>Example</summary>
## Conditional text
This way allows you to create conditional text that depends on other placeholders.
First, find the `conditions` section in your config and create a new condition like this one:
```
conditions:
serverName:
lobby:
conditions:
- "%server%=lobby"
true: "You are in the lobby"
- '%server%=lobby'
true: "You are in lobby"
false: "You are not in lobby"
```
This condition checks if player is in server `lobby` and if yes, displays `"You are in lobby"`, otherwise displays `"Your are not in lobby"`.
To check for more than just 1 condition, add more to the list. Additionally, you will now need to specify `type`, which is either `AND` or `OR`.
Use with `%condition:serverName%`
</details>
## Display condition of a feature
The second way is to use condition's name in places where a condition is accepted.
This includes display conditions for bossbar, scoreboard and layout.
In these cases, true/false texts are unused; therefore, they do not need to be defined.
<details>
<summary>Example</summary>
For example,
```
conditions:
MyCondition:
lobby:
conditions:
- "permission:tab.admin"
- '%server%=lobby'
- '%server%=lobby2'
type: OR
true: "You are in lobby"
false: "You are not in lobby"
```
will return the lobby text if player is in server `lobby` **OR** in server `lobby2`. If you want both of those conditions to be required to display text, use `type: AND`.
It is not possible to combine both `AND` and `OR` in a single condition. To do that, create multiple conditions and use one inside another (see Example 2 below).
Now that the condition is created, we can use it using `%condition:lobby%`. This placeholder will return the texts configured in `true` and `false` values based on whether the condition was met or not.
## Using in a field where condition is expected
A lot of the features support either `display-condition` or `disable-condition`. The value configured is expected to be a condition. Let's say we want to use it as a display condition somewhere, for example when player is in server `lobby`. First, create the condition:
```
scoreboards:
MyScoreboard:
display-condition: MyCondition
conditions:
lobby:
conditions:
- '%server%=lobby'
```
As you can see, `true` and `false` were not configured. This is because we are not trying to display any text, we are passing the condition as an object to evaluate.
Then, use `display-condition: lobby` in the feature. This will evaluate the condition named `lobby` and either display or not display the visual based on player's server. To check for 2 servers, use
```
conditions:
lobby:
conditions:
- '%server%=lobby'
- '%server%=lobby2'
type: OR
```
Now, the condition `lobby` will pass if player is in server `lobby` **OR** in server `lobby2`. To require both of the conditions to be met instead of at least 1, use `type: AND`.
It is not possible to combine both `AND` and `OR` in a single condition. To do that, create multiple conditions and use one inside another (see Example 2 below).
In this example, scoreboard will only be displayed to players with `tab.admin` permission.
</details>
## Using in a field where condition is expected (short format)
When using condition as a `display-condition` or `disable-condition`, you don't need to specify `true` / `false` texts. In fact, you only need the list of conditions and separator (AND / OR).
For this purpose, short format was created. Instead of having to create a condition and using it, you can specify the expression directly.
### Short format
If trying to use a condition on place where it's available (bossbar display condition, scoreboard display condition) where you don't need the true/false values, you can use a short format instead.
This can be used by simply creating all subconditions and separating them with `;` for `AND` condition type. For `OR` type, use `|`.
**Single condition example**:
```
display-condition: "%server%=lobby"
@@ -149,6 +135,25 @@ display-condition: "%server%=lobby;%world%=world"
```
display-condition: "%server%=lobby|%server%=lobby2"
```
It is not possible to combine both `AND` (`;`) and `OR` (`|`) in a single condition. To do that, create multiple conditions and use one inside another (see Example 2 below).
## Relational conditions
> [!NOTE]
> This section only describes differences from standard conditions. For more information about those, see sections above.
Relational conditions allow you to check placeholder outputs for the player who is looking instead of only the target player.
To make placeholder check for viewer instead of the target player, use `%viewer:<placeholder>%` instead of just `%placeholder%`.
You can still check for target player using `%placeholder%` or `%target:<placeholder>%` (if you want to make it more clear for the reader).
Example: `%viewer:server%=%server%` will pass if viewer is in the same server as the target player.
There are 2 ways to use it:
* display-condition of layout player groups (which is currently the only feature supporting it as it would not make sense elsewhere)
* A placeholder using `%rel_condition:<condition>%` syntax
Using `%viewer:...%` will upgrade the condition to a relational condition and standard version will no longer be available.
If trying to use relational condition in `%condition:name%` syntax, it will return a warning text instead of evaluating the condition.
Same the other way around - if trying to use standard condition in `%rel_condition:name%` syntax, it will return a warning text.
# Refresh interval
Conditions are just placeholders after all, and, as such, they must be refreshed periodically.
@@ -158,6 +163,13 @@ Permission checks count as 1000ms.
To configure refresh intervals of placeholders,
check out the [Optimization guide](https://github.com/NEZNAMY/TAB/wiki/Optimizing-the-plugin#2---all-platforms-placeholder-refresh-intervals).
# Additional Info
## Additional note 1 - Choosing the correct AND / OR type
Sometimes you want to combine multiple expressions using `AND` or `OR`. When doing this, you must pick the correct one based on your needs.
A common mistake is trying to turn spoken language directly into config, which can be wrong.
For example, you say you want to display scoreboard in servers `server1` **and** `server2`. As such, you may accidentally use `type: AND` or `;` in short format (`%server%=server1;%server%=server2`). This is, however, wrong, because that would mean your server must be `server1` **and** `server2` at the same time, which is obviously impossible. The correct type is `OR` here (`|`). So instead of thinking "I want to display scoreboard in `server1` **and** `server2`", you should think "I want to display scoreboard if player is in `server1` **or** `server2`.
# Examples
## Example 1 - Chaining conditional placeholders
Let's create colored ping by chaining conditions using multiple intervals for ping quality. This specific case would be easier done using [replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements), but this is just an example. Let's say we want <50 for good, <100 for medium and 100+ for bad. Let's create the first condition:
@@ -2,9 +2,17 @@
* [About](#about)
* [Configuration](#configuration)
* [Grouping players only from certain servers](#grouping-players-only-from-certain-servers)
* [Pattern Matching](#pattern-matching)
* [Configuring isolated servers](#configuring-isolated-servers)
* [Seeing all players on the network from some server](#seeing-all-players-on-the-network-from-some-server)
* ~Commands~
* [Placeholders](#placeholders)
* ~Limitations~
* [Compatibility with other plugins](#compatibility-with-other-plugins)
* ~Additional Info~
* [Troubleshooting](#troubleshooting)
* ~API~
* ~Examples~
# About
This feature allows you to see players from other servers, instead of only seeing players on the same server.
@@ -82,8 +90,8 @@ server-groups:
```yaml
server-groups:
lobbies:
- Lobby-* # Matches: Lobby-1, Lobby-2, Lobby-Hub, etc.
- *-lobby # Matches: eu-lobby, us-lobby, etc.
- "Lobby-*" # Matches: Lobby-1, Lobby-2, Lobby-Hub, etc.
- "*-lobby" # Matches: eu-lobby, us-lobby, etc.
```
**Regex Patterns** - Use `regex:` prefix for advanced pattern matching:
@@ -122,5 +130,30 @@ global-playerlist:
```
Now, players connected to "spyserver1" or "spyserver2" will see everyone on the network in tablist, but no one else (unless on another spy server / in the same server group) will see them.
# Placeholders
Here are TAB's internal placeholders you can use when this feature is enabled:
| Placeholder | Description |
|-------------|-------------|
| %playerlist-group_\<group\>% | Amount of online players in specified global playerlist group |
# Compatibility with other plugins
For compatibility with vanish plugins, vanish status must be detected correctly. See [Additional information - Vanish detection](https://github.com/NEZNAMY/TAB/wiki/Additional-information#vanish-detection) for more info.
For compatibility with vanish plugins, vanish status must be detected correctly. See [Additional information - Vanish detection](https://github.com/NEZNAMY/TAB/wiki/Additional-information#vanish-detection) for more info.
# Troubleshooting
This is a collection of tips to help you figure out why the feature isn't working as you expect.
To get started, run `/tab dump <player looking at the tablist>` and open the generated link. Scroll down to `features` -> `GlobalPlayerList` and check the content:
* If it says `GlobalPlayerList: Feature is disabled`, it means you did not enable the feature. Enable it by setting
```
global-playerlist:
enabled: true
```
* Check the `configuration` section and compare it with your config file. If it's different, you either forgot to reload TAB, uploaded the config to the wrong server, or did not upload it at all.
* Check the `player info` section and make sure everything is as expected - server name, server group and other servers in that group. If server name says `N/A`, it means you did not configure `server-name` (TODO create section and link it).
* Check the `visibility from viewer's perspective` table. Find the target player who the viewer should see but doesn't (or vice versa) and check their server, server group and whether they should be visible or not.
<br /> **If "visible" doesn't match the expected result, but matches what is shown in-game:**
* See the full message for a hint why the player is or is not visible
* Make sure server groups are configured as desired
* If the issue involves vanishing, see [Vanish Detection](https://github.com/NEZNAMY/TAB/wiki/Additional-information#vanish-detection). In short, make sure the player has `tab.seevanished` permission to see vanished players and if the vanish plugin is PremiumVanish, its layered vanish system does not block the target player from being seen by the viewer
**If "visible" matches the expected result, but not in-game result:**
* If both players are on the same server, the visibility is managed purely by the server (and plugins there), not by TAB. It means the player was hidden by another plugin or not shown by the server itself, for example when using redis to connect the servers and TAB, but the different servers acting as one do not share tablist information between them.
+24 -8
View File
@@ -3,9 +3,13 @@
* [Configuration](#configuration)
* [Design](#design)
* [Chaining designs](#chaining-designs)
* ~Commands~
* ~Placeholders~
* ~Limitations~
* [Compatibility with other plugins](#compatibility-with-other-plugins)
* [Additional info](#additional-info)
* [Additional note 1 - [1.8 - 1.20.1] Not resetting on server switch](#additional-note-1---18---1201-not-resetting-on-server-switch)
* [Troubleshooting](#troubleshooting)
* [API](#api)
* [Examples](#examples)
* [Example 1 - Per-version header/footer](#example-1---per-version-headerfooter)
@@ -101,8 +105,8 @@ Design `per-world` is checked first. If condition is not met, `default` is displ
If a chaining 3 or more and player meets 2 or more conditions for 2 (or more) different designs, the first one defined in the config will be displayed. You can swap order of designs based on your priority needs.
# Compatibility with other plugins
Header/Footer is a feature that cannot be handled by multiple plugins at once.
To make sure no other plugin sends their own Header/Footer when not disabled in the plugin's config properly, TAB will block header/footer coming from all other plugins if this feature is enabled (and not disabled for a player with a condition).
Header/Footer is a feature that cannot be handled by multiple plugins at once.
To make sure no other plugin sends their own Header/Footer when not disabled in the plugin's config properly, TAB will block header/footer coming from all other plugins if this feature is enabled (and not disabled for a player with a condition).
An exception to this is Velocity installation, where this "anti-override" is not available.
# Additional info
@@ -111,7 +115,21 @@ When under a BungeeCord network and having TAB installed on backend server and s
* Install TAB on BungeeCord and disable header/footer on the server.
* Install a plugin that sends some, or even empty header/footer on join.
Velocity sends reset packet, so it isn't a problem there.
Velocity sends a reset packet on server switch, so it isn't a problem there. It is also not a problem since 1.20.2, where configuration phase was added.
# Troubleshooting
This is a collection of tips to help you figure out why the feature isn't working as you expect.
To get started, run `/tab dump <affected player>` and open the generated link. Scroll down to `features` -> `HeaderFooter` and check the content:
* If it says `HeaderFooter: Feature is disabled`, it means you disabled the feature. Enable it by setting
```
header-footer:
enabled: true
```
* Check the `configuration` section and compare it with your config file. If it's different, you either forgot to reload TAB, or uploaded the config to the wrong server (or did not upload it at all).
* Check the `chain` section and for each design and analyze the `display-condition` options (the value, with placeholders parsed, whether it is met or not) if it matches what you expect. If not, you may have made a mistake in your conditions. If more display conditions for more than 1 design are met, the first design with display condition met is displayed. See [Chaining designs](#chaining-designs) for more info.
* Check `currently displayed design` and compare it with your expectations, as well as the actual in-game result. If it doesn't match your expectations, you didn't configure something correctly.
* Make sure you don't have another plugin sending header/footer, such as CMI. While TAB should block other plugins from applying it, this functionality is not available on Velocity installation.
# API
*To get started with the API, see [Developer API](https://github.com/NEZNAMY/TAB/wiki/Developer-API) page.*
@@ -172,9 +190,8 @@ header-footer:
## Example 3 - Dynamic line count
The entire header is just a single character sequence that allows newline symbol (`\n`) for new lines. It is only being split into multiple lines in config for convenience to make it readable when configuring and editing. Same for footer.
If using a placeholder that returns the newline symbol, it will create a new line. You can use this to your advantage and create a [conditional placeholder](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) or an [animation](https://github.com/NEZNAMY/TAB/wiki/Animations) that includes newline in some frames.
<details>
<summary>Example</summary>
If using a placeholder that returns the newline symbol, it will create a new line. You can use this to your advantage and create a [conditional placeholder](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) or an [animation](https://github.com/NEZNAMY/TAB/wiki/Animations) that includes newline in some frames.
**Example**:
**animations.yml**
```
@@ -191,5 +208,4 @@ header-footer:
DesignName:
header:
- "%animation:MyAnimation%"
```
</details>
```
+70 -20
View File
@@ -6,6 +6,9 @@
* [Player groups](#player-groups)
* [Chaining layouts](#chaining-layouts)
* [Skins](#skins)
* ~Commands~
* ~Placeholders~
* ~Limitations~
* [Compatibility with other plugins](#compatibility-with-other-plugins)
* [Compatibility with other features](#compatibility-with-other-features)
* [Playerlist objective incompatibility](#playerlist-objective-incompatibility)
@@ -15,11 +18,14 @@
* [Additional note 1 - [1.19.3 - 1.21.1] Entries in chat complete](#additional-note-1---1193---1211-entries-in-chat-complete)
* [Additional note 2 - [1.8 - 1.21.3] Second layer of skin missing](#additional-note-2---18---1213-second-layer-of-skin-missing)
* [Additional note 3 - Entry overlap](#additional-note-3---entry-overlap)
* ~Troubleshooting~
* ~API~
* [Examples](#examples)
* [Example 1 - Per-server columns](#example-1---per-server-columns)
* [Example 2 - Per-world playerlist](#example-2---per-world-playerlist)
# About
This feature allows you to customize all 80 tablist slots. Displaying less than 4 columns is currently not supported ([here's why](https://gist.github.com/NEZNAMY/3dfcbf7d44283735d3c18266a2851651)). This feature can be enabled and configured in **config.yml** file under **layout** section.
This feature allows you to customize all 80 tablist slots (possibly less on 1.19.3+). This feature can be enabled and configured in **config.yml** file under **layout** section.
This feature is only available for versions **1.8** and up due to massive tablist changes, which would make 1.7- compatibility require a complete rewrite of the functionality and could still cause all kinds of visual issues, including, but not limited to compatibility with other plugins adding/removing players from the tablist.
@@ -57,7 +63,7 @@ layout:
- '43|&8&m '
groups:
staff:
condition: permission:tab.staff
display-condition: permission:tab.staff
slots:
- 24-40
players:
@@ -80,9 +86,9 @@ All the options are explained in the following table.
A layout consists of the following options:
| Option name | Description |
|-------------|------------------|
| `condition` (optional) | [Condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) (either condition name or a conditional expression) that must be met for the layout to be displayed. If not defined, layout will be displayed without any required condition. If player does not meet condition, another layout may be displayed based on chaining (see below for more info). |
| `display-condition` (optional) | [Condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) (either condition name or a conditional expression) that must be met for the layout to be displayed. If not defined, layout will be displayed without any required condition. If player does not meet condition, another layout may be displayed based on chaining (see below for more info). |
| `default-skin` (optional) | Overrides global `default-skin` setting for this layout. |
| `slot-count` (optional) | Amount of slots to show in this layout. If not specified, defaults to `80`. **This option is coming in TAB v5.5.0**. |
| `slot-count` (optional) | Amount of slots to show in this layout. If not specified, defaults to `80`. **Only works on 1.19.3+**. |
| `fixed-slots` | List of fixed slots, see below for more info. |
| `player-groups` | Map of player groups, see below for more info. |
@@ -112,7 +118,7 @@ Example:
### Player groups
These are groups of players that meet the specified condition. They consist of 2 parts - condition and slots.
**1. Condition**
**1. Display Condition**
The condition players must meet to be displayed in the group.
If a player doesn't meet condition for a group, the next group's condition is checked
(groups are checked in the order they are defined in config).
@@ -127,7 +133,7 @@ Example:
myLayout:
groups:
staff:
condition: permission:tab.staff
display-condition: permission:tab.staff
slots:
- '24-40'
players:
@@ -146,7 +152,7 @@ Example:
```
layouts:
staff:
condition: "permission:tab.staff"
display-condition: "permission:tab.staff"
fixed-slots:
...
groups:
@@ -168,7 +174,7 @@ Currently, TAB supports these skin formats:
|--------|---------|-------------|
| `mineskin:<UUID>` | `mineskin:37e93c8e12cd426cb28fce31969e0674` | Takes UUID from [MineSkin](https://mineskin.org). |
| `player:<name>` | `player:Notch` | Displays skin of specified player. |
| `texture:<texture>` | `texture:469b3bac406b51ba0e76c2c218aa4d45fde9ea7c101c85fbd8106c92c4aa36dd` | Uses skin texture from `textures.minecraft.net`. Skin browsing websites show this value. |
| `texture:<texture>` | `texture:469b3bac406b51ba0e76c2c218aa4d45fde9ea7c101c85fbd8106c92c4aa36dd` | Uses skin texture from `textures.minecraft.net`. Skin browsing websites often show this value (for example `Texture URL` on mineskin.org). |
# Compatibility with other plugins
This feature does not have any compatibility issues with other plugins.
@@ -202,19 +208,19 @@ Each layout only displays players in that world.
```
layouts:
layout1:
condition: "%world%=world1"
display-condition: "%world%=world1"
fixed-slots: []
groups:
players:
condition: "%world%=world1"
display-condition: "%world%=world1"
slots:
- 1-80
layout2:
condition: "%world%=world2"
display-condition: "%world%=world2"
fixed-slots: []
groups:
players:
condition: "%world%=world2"
display-condition: "%world%=world2"
slots:
- 1-80
```
@@ -222,11 +228,11 @@ You can also merge multiple worlds into a group.
```
layouts:
layout1:
condition: "%world%=world1|%world%=world2"
display-condition: "%world%=world1|%world%=world2"
fixed-slots: []
groups:
players:
condition: "%world%=world1|%world%=world2"
display-condition: "%world%=world1|%world%=world2"
slots:
- 1-80
```
@@ -234,7 +240,7 @@ You can also replicate "ignore-effect-in-worlds" by creating a layout with condi
```
layouts:
globalWorld:
condition: "%world%=globalWorld"
display-condition: "%world%=globalWorld"
fixed-slots: []
groups:
players: # No condition here to show all players
@@ -297,21 +303,65 @@ layout:
- "61|&2&lSkyblock"
groups:
lobby:
condition: "%server%=lobby"
display-condition: "%server%=lobby"
slots:
- 2-20
survival:
condition: "%server%=survival"
display-condition: "%server%=survival"
slots:
- 22-40
creative:
condition: "%server%=creative"
display-condition: "%server%=creative"
slots:
- 42-60
skyblock:
condition: "%server%=skyblock"
display-condition: "%server%=skyblock"
slots:
- 62-80
```
You can also remove the last condition (in this case `condition: "%server=skyblock%"`) and the columns will be used for all other servers, instead of just a specified one.
You can also remove the last condition (in this case `display-condition: "%server=skyblock%"`) and the columns will be used for all other servers, instead of just a specified one.
![image](https://github.com/NEZNAMY/TAB/assets/6338394/81016b5a-9b40-445e-8bfc-58204f5457f5)
## Example 2 - Per-world playerlist
If you want to reproduce the per-world-playerlist feature but with layouts, you can use relational conditions to check the viewer's world (`%viewer:world%`) with the target player's world (`%world%`) like so:
```yml
layout:
enabled: true
direction: COLUMNS
default-skin: mineskin:383747683
enable-remaining-players-text: true
remaining-players-text: '... and %s more'
empty-slot-ping-value: 1000
layouts:
default:
slot-count: 80
fixed-slots:
- '1|&3Website&f:'
- '2|&bmyserver.net'
- '3|&8&m '
- '4|&3Name&f:'
- '5|&b%player%'
- '7|&3Rank&f:'
- '8|Rank: %group%'
- '10|&3World&f:'
- '11|&b%world%'
- '13|&3Time&f:'
- '14|&b%time%'
- '21|&3Teamspeak&f:'
- '22|&bts.myserver.net'
- '23|&8&m '
- '41|&3Store&f:'
- '42|&bshop.myserver.net'
- '43|&8&m '
groups:
players:
display-condition: "%viewer:world%=%world%"
slots:
- 24-40
- 44-80
```
> [!NOTE]
> This is just an example, relational conditions are not limited to displaying only players in your world.
> If you want players in your server on proxy, use `%viewer:server%=%server%` in `display-condition`.
> This works for any placeholder offered by TAB or by PlaceholderAPI.
+13 -10
View File
@@ -3,10 +3,12 @@
* [Configuration](#configuration)
* [config.yml](#configyml)
* [groups.yml and users.yml](#groupsyml-and-usersyml)
* [Global settings](#global-settings)
* [Per-world / per-server](#per-world--per-server)
* [Priorities](#priorities)
* [Name color](#name-color)
* [Commands](#commands)
* [Placeholders](#placeholders)
* [PlaceholderAPI placeholders](#placeholderapi-placeholders)
* [Limitations](#limitations)
* [Compatibility with other plugins](#compatibility-with-other-plugins)
* [Additional info](#additional-info)
@@ -16,6 +18,7 @@
* [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)
* ~Troubleshooting~
* [API](#api)
* [Changing prefix and suffix](#changing-prefix-and-suffix)
* [Collision](#collision)
@@ -63,6 +66,7 @@ All of the options are explained in the following table.
## groups.yml and users.yml
This part allows you to configure prefix and suffix. They are called `tagprefix` and `tagsuffix`, where "tag" refers to name tag, to allow separation from tablist settings.
### Global settings
Properties can be applied in 2 ways: groups and users. Users can be defined by both username and their UUID. Values applied to users take priority over groups.
**groups.yml**
@@ -94,6 +98,7 @@ _DEFAULT_:
tagprefix: "&7" # This will be displayed on everyone except admin
```
### Per-world / per-server
Values can also be applied per-world (and per-server on a proxy), where they can be defined per group/user. These values take priority over global settings. Example:
**groups.yml**
```
@@ -120,6 +125,7 @@ per-world:
tagsuffix: "Suffix in all worlds starting with lobby-"
```
### Priorities
The full list of priorities to choose correct prefix/suffix for a player looks like this:
1. value set using the [API](#api)
2. per-world / per-server applied to username
@@ -136,13 +142,13 @@ This list is browsed through until the first match is found. If no match is foun
Values are taken independently of each other.
This means you can set per-world tagprefix, but only keep one global tagsuffix, for example.
You can see the source of a value displayed on player by using `/tab debug <player>` and checking "source"
part of the value you are looking for.
You can see the source of a value displayed on player by using `/tab dump <player>`, opening the link and checking "source"
part of the value you are looking for (tagprefix / tagsuffix).
## Name color
On versions 1.12.2 and below, name color follows the last color of prefix. This also includes magic codes. On 1.13+, this is no longer the case. Instead, a new field called "team color" was created. This is an enum constant, where we need to pick from 22 options - 16 colors, 5 magic codes and reset. **As you can see, it doesn't allow for RGB colors**. This also means name can no longer have color and magic codes simultaneously.
TAB does not have an option for configuring team color to avoid complexity, such as per-version configuration, or having to configure an additional field.
TAB does not have an option for configuring team color to avoid complexity, such as per-version configuration, or having to configure an additional field.
For that reason, TAB detects the last used color code and uses that for team color.
> [!CAUTION]
> An exception is when using MiniMessage syntax / using RGB codes on server which has MiniMessage library. In this scenario, it is not possible to detect reset (`&r`) as last used "color", because in Minecraft components it is not a color, rather, it resets formatting by setting all magic codes to false / color to null. If you use `&r` (`<reset>`) and notice it not working, replace it with `&f` (`<white>`) (or add it after the reset to be safe). There seems to be no difference between setting color to `WHITE` versus `RESET`.
@@ -155,9 +161,6 @@ For that reason, TAB detects the last used color code and uses that for team col
| /tab setcollision \<player\> \<true\|false\> | `tab.setcollision` | Forces collision rule for specified player, overriding configuration. |
# Placeholders
This feature does not offer any internal placeholders, only PlaceholderAPI placeholders.
## PlaceholderAPI placeholders
Here are TAB's PlaceholderAPI placeholders you can use when this feature is enabled:
| Placeholder | Description |
|-------------|-------------|
@@ -168,7 +171,7 @@ Here are TAB's PlaceholderAPI placeholders you can use when this feature is enab
| `%tab_nametag_visibility%` | "Enabled" if player can see nametags, "Disabled" if disabled using `/tab nametag toggleview` |
# Limitations
* [<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.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+] Name color is controlled by a chat format enum constant. This means:
* Name can only have one code. That is either color or magic code (such as &4 or &l), but not both.
* Name does not support RGB codes. Any used RGB colors will be rounded to the nearest legacy code.
@@ -182,7 +185,7 @@ If you want to avoid these at all costs, the only way is to create entities (arm
* [CustomNameplates](https://polymart.org/product/2543/customnameplates)
# Compatibility with other plugins
Scoreboard teams is a feature that cannot be handled by multiple plugins at once.
Scoreboard teams is a feature that cannot be handled by multiple plugins at once.
To make sure no other plugin sends their own teams when not disabled in the plugin's config properly, TAB will block players being added into teams coming from all other plugins (if this feature is enabled and not disabled for a player with a condition).
# Additional info
@@ -222,7 +225,7 @@ In config.yml. Keep in mind, you'll not be able to use any team features if you
## 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 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.
An example 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.
@@ -2,6 +2,15 @@
* [About](#about)
* [Configuration](#configuration)
* [Grouping playerlist from multiple worlds](#grouping-playerlist-from-multiple-worlds)
* [Pattern Matching](#pattern-matching)
* ~Commands~
* ~Placeholders~
* ~Limitations~
* ~Compatibility with other plugins~
* ~Additional Info~
* ~Troubleshooting~
* ~API~
* ~Examples~
# About
This feature allows you to limit players you see in tablist to only players in your world, or in a group of worlds.
@@ -32,4 +41,34 @@ Contains 2 groups, each of them consisting of 2 worlds.
World group name can be anything (in our case, they're called "lobby" and "minigames").
Under group name, list the actual world names.
With this setup, "lobby1" and "lobby2" will share playerlist, as well as "paintball" with "bedwars". All other worlds will have playerlist not shared with any other worlds. Creating a group for 1 world only is completely useless.
With this setup, "lobby1" and "lobby2" will share playerlist, as well as "paintball" with "bedwars". All other worlds will have playerlist not shared with any other worlds. Creating a group for 1 world only is completely useless.
### Pattern Matching
World names in groups support multiple pattern types for flexible configuration:
**Exact Match** - Matches server name exactly:
```yaml
shared-playerlist-world-groups:
worlds:
- world1
- world2
```
**Wildcard Patterns** - Use `*` for prefix or suffix matching:
```yaml
shared-playerlist-world-groups:
worlds:
- "World-*" # Matches: World-1, World-2, World-Hub, etc.
- "*-world" # Matches: eu-world, us-world, etc.
```
**Regex Patterns** - Use `regex:` prefix for advanced pattern matching:
```yaml
shared-playerlist-world-groups:
worlds:
- regex:World-[0-9]+ # Matches: World-1, World-23, but not World-Hub
smp:
- regex:SMP-(EU|US)-[0-9]+ # Matches: SMP-EU-1, SMP-US-2, etc.
```
Note: Wildcard patterns are case-insensitive, while regex patterns are case-sensitive by default (use `(?i)` flag for case-insensitive regex).
@@ -11,6 +11,7 @@
* [Example 1 - Adding brackets to placeholder output if it returns non-empty value](#example-1---adding-brackets-to-placeholder-output-if-it-returns-non-empty-value)
* [Example 2 - Replicating PlaceholderAPI's %player_colored_ping%](#example-2---replicating-placeholderapis-player_colored_ping)
* [Example 3 - Using both original and replaced output](#example-3---using-both-original-and-replaced-output)
* [Example 4 - Replacing identifier with custom text](#example-4---replacing-identifier-with-custom-text)
# About
Most placeholders don't have fully configurable output, making you unable to customize it as you'd like.
@@ -193,4 +194,16 @@ placeholder-output-replacements:
```
Now, you can use `%ping%` for original output and `%animation:ping%` for replaced.
</details>
</details>
## Example 4 - Replacing identifier with custom text
Sometimes a placeholder doesn't parse and instead returns the identifier itself (such as on proxy installation before Bridge responds back with placeholder values), but you want to display some default value to avoid issues (ugly text, warnings in console if used in sorting or conditions).
This can be achieved in the following way:
```
placeholder-output-replacements:
"%my_placeholder%":
"%my_placeholder%": "Loading..."
```
If the placeholder is used in numeric sorting for example, you can make it default to `0` or anything else you need at the time.
@@ -1,12 +1,17 @@
# Content
* [About](#about)
* [Configuration](#configuration)
* ~Commands~
* ~Placeholders~
* [Limitations](#limitations)
* [Compatibility with other plugins](#compatibility-with-other-plugins)
* [Additional info](#additional-info)
* [Additional note 1 - Spectator gamemode](#additional-note-1---spectator-gamemode)
* [Troubleshooting](#troubleshooting)
* ~API~
* [Examples](#examples)
* [Example 1 - Per-world values](#example-1---per-world-values)
* [Example 2 - Displaying hearts](#example-2---displaying-hearts)
# About
This features gives you control over Minecraft's scoreboard objective feature with PLAYER_LIST slot.
@@ -35,10 +40,10 @@ All of the options are explained in the following table.
| Option name | Default value | Description |
|-------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| enabled | true | Enables / Disables the feature |
| value | "%ping%" | [1.20.2-] An integer from -2147483648 to 2147483647, doesn't support decimal values. The number is always yellow. Supports placeholders with player-specific output, such as player health. Only visible on 1.20.2 and lower. <br/> **Note**: Even if you only support 1.20.3+, you still need to configure this value to properly evaluate to a number, because the value is still sent to the client (just not displayed). You can set it to `0` for simplicity. |
| fancy-value | "&7Ping: %ping%" | [1.20.3+] Any text, supports placeholders with per-player output. Only visible on 1.20.3+, where it completely replaces `value`. |
| value | "%ping%" | [1.20.2-] An integer from -2147483648 to 2147483647, doesn't support decimal values. The number is always yellow. Supports placeholders with player-specific output, such as player health. Only visible on 1.20.2 and lower (unless you set `render-type` to `HEARTS`). <br/> **Note**: Even if you only support 1.20.3+, you still need to configure this value to properly evaluate to a number, because the value is still sent to the client (just not displayed). You can set it to `0` for simplicity. |
| fancy-value | "&7Ping: %ping%" | [1.20.3+] Any text, supports placeholders with per-player output. Only visible on 1.20.3+, where it completely replaces `value`, or if `render-type` is set to `HEARTS`. |
| title | "TAB" | Title to send. Only visible on Bedrock Edition. |
| render-type | INTEGER | Render type of the value. Supported values are `INTEGER` and `HEARTS`. |
| render-type | INTEGER | Render type of the value. Supported values are `INTEGER` and `HEARTS`. <br />When set to `HEARTS`, the `value` is used displayed even on 1.20.3+, where `fancy-value` is ignored. |
| disable-condition | %world%=disabledworld | A [condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) (either name of a condition or a conditional expression) that must be met for disabling the feature for players. Set to empty for not disabling the feature ever. <br/> **Note**: Disabling the feature for a player means sending objective unregister packet to them, which results in player not seeing the feature on anyone anymore. It doesn't work the other way around - you cannot disable this feature on target players, only for viewers. |
# Limitations
@@ -59,6 +64,23 @@ Note that if this was automatically generated by a plugin, it will probably be a
## Additional note 1 - Spectator gamemode
The feature will not be visible on players with spectator gamemode. To resolve this, check out [Spectator fix](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Spectator-fix) feature.
# Troubleshooting
This is a collection of tips to help you figure out why the feature isn't working as you expect.
To get started, run `/tab dump <any player>` (this feature doesn't contain per-player content in dump output) and open the generated link. Scroll down to `features` -> `PlayerlistObjective` and check the content:
* If it says `PlayerlistObjective: Feature is disabled`, it means you disabled the feature. Enable it by setting
```
playerlist-objective:
enabled: true
```
* Check the `configuration` section and compare it with your config file. If it's different, you either forgot to reload TAB, or uploaded the config to the wrong server (or did not upload it at all).
* If a player cannot see playerlist objective on anyone, check the table of players and make sure it's not because `Disabled with condition` is `true`.
* Check the table of players for `value`, `fancy-value` and `title` and make sure they match your expectations. Don't forget `value` only supports numbers.
* If you want to see `fancy-value` instead of just `value`'s number:
* Make sure your client is on 1.20.3+.
* Make sure your server is on 1.20.3+ (using TAB on proxy counts as being latest).
* Make sure you are not using a 3rd party client that removed this feature from 1.20.3+, such as Lunar or Feather.
* If you have TAB on Velocity, install [VelocityScoreboardAPI](https://github.com/NEZNAMY/VelocityScoreboardAPI/releases) (and make sure it is not outdated).
# Examples
## Example 1 - Per-world values
This feature doesn't directly support per-world values or similar.
@@ -97,3 +119,16 @@ playerlist-objective:
> This is just an example, the plugin is not limited to displaying different values only per world.
> If you want per server values on proxy, use %server% with server names.
> This works for any placeholder offered by TAB or by PlaceholderAPI.
## Example 2 - Displaying hearts
In order to display the value as hearts, set `render-type` to `HEARTS`.
Then, set `value` to a placeholder that returns player's health. If you have TAB on a backend server, use `%health%`. If you have TAB on a proxy, use PlaceholderAPI's `%player_health_rounded%`.
Example configuration:
```
value: "%health%"
render-type: HEARTS
```
When using `HEARTS` render type, `fancy-value` is not displayed by the client, so it doesn't matter what it is set to.
Final result:
![](https://images-ext-2.discordapp.net/external/RxWu_5hBSLUWqS7vCvSPY9PnNxkYfAMQQXwkbi6GEyU/https/image.prntscr.com/image/edpM4XpOT1q3SsQ5vYNjzQ.png)
+18 -11
View File
@@ -8,12 +8,12 @@
* [Chaining scoreboards](#chaining-scoreboards)
* [Commands](#commands)
* [Placeholders](#placeholders)
* [PlaceholderAPI placeholders](#placeholderapi-placeholders)
* [Limitations](#limitations)
* [Compatibility with other plugins](#compatibility-with-other-plugins)
* [Additional info](#additional-info)
* [Additional note 1 - Text alignment](#additional-note-1---text-alignment)
* [Additional note 2 - Geyser / Bedrock issues](#additional-note-2---geyser--bedrock-issues)
* ~Troubleshooting~
* [API](#api)
* [Creating custom scoreboards](#creating-custom-scoreboards)
* [Showing custom scoreboards](#showing-custom-scoreboards)
@@ -78,11 +78,8 @@ All of the options are explained in the following table.
| toggle-command | /sb | Command that can be used to toggle scoreboard for the player running the command. Players need `tab.scoreboard.toggle` permission to use it. <br />**Note:** Commands are not supposed to be registered / unregistered dynamically at runtime. As such, you may run into small issues when changing the toggle command and reloading. After you modify the command to your liking, reconnect to properly see it in tabcomplete. If you don't use TAB on a proxy, consider restarting the server as well for the old command to actually be unregistered. |
| remember-toggle-choice | false | When enabled, toggling decision is saved into a file to remember it even after reloads/restarts/reconnects |
| hidden-by-default | false | If enabled, scoreboard will automatically be hidden on join until toggle command is used to show it. |
| use-numbers | false | If enabled, numbers 1-15 will be used in the scoreboard. If disabled, `static-number` is shown everywhere. Will not be visible for 1.20.3+ players, instead, you can configure any text to show using `\|\|text` in the lines (scroll up for more info). |
| static-number | 0 | If `use-numbers` is disabled, this is number to be in all lines. |
| delay-on-join-milliseconds | 0 | Delay in milliseconds to send scoreboard after joining. |
| scoreboards | *Map* | Scoreboards to display based on conditions (see below for more info).
|
| scoreboards | *Map* | Scoreboards to display based on conditions (see below for more info). |
## Scoreboard
A scoreboard consists of the following options:
@@ -108,7 +105,21 @@ A scoreboard consists of the following options:
### Line syntax
The lines are specified as a list of strings.
Since 1.20.3, you can also configure the value on the right side, which no longer has to be a red number. To set it, use `||` sequence and put right-side text after it. Example:
Since 1.20.3, you can also configure the value on the right side, which no longer has to be a red number.
On <1.20.3, you can configure red numbers with either a static number, or a placeholder that evaluates to a number.
The final syntax is `<text>||<number format>||<score>`, where:
* `<text>` is the main text displayed on the left side
* `<number format>` is the new 1.20.3+ feature that replaces `score` with any text, not just a number
* `<score>` is the value (red number), only visible on <1.20.3 and must be a number (or a placeholder evaluating to one)
The second and third part are optional. Example syntaxes that configure a line are:
* `<text>`
* `<text>||<number format>`
* `<text>||<number format>||<score>`
* `<text>||||<score>`
For example:
```
lines:
- "Left 1---"
@@ -140,8 +151,7 @@ Additionally, the following configuration:
Will give you
<img width="583" height="131" alt="image" src="https://github.com/user-attachments/assets/15e809c6-4ecd-4234-a30e-744c2d413335" />
The right side text will not be visible at 1.20.2 and lower, and numbers will be displayed instead.
The `number format` will not be visible at 1.20.2 and lower, and `score` will be displayed instead.
### Hiding a line
When a line consists only of a placeholder that returned empty output, the line will be hidden completely. This is intentional to allow dynamic scoreboard size based on placeholder output. This is not the case if empty line is configured (`""`), to allow empty lines in configuration.
@@ -206,9 +216,6 @@ If a player meets both conditions (has defined permission and is in defined worl
| `/tab scoreboard announce <name> <duration>` | `tab.announce.scoreboard` | Shows the scoreboard with the given `name` to every player on the server for the given `duration`, in seconds. |
# Placeholders
This feature does not offer any internal placeholders, only PlaceholderAPI placeholders.
## PlaceholderAPI placeholders
Here are TAB's PlaceholderAPI placeholders you can use when this feature is enabled:
| Placeholder | Description |
|-------------|-------------|
+24 -34
View File
@@ -1,4 +1,5 @@
# Content
* ~About~
* [Enabling](#enabling)
* [Methods of sorting](#methods-of-sorting)
* [GROUPS](#groups)
@@ -11,19 +12,21 @@
* [Multiple elements with the same priority](#multiple-elements-with-the-same-priority)
* [Combining multiple sorting types](#combining-multiple-sorting-types)
* [Additional settings](#additional-settings)
* ~Commands~
* ~Placeholders~
* ~Limitations~
* ~Compatibility with other plugins~
* [Additional info](#additional-info)
* [Additional note 1 - Limitations](#additional-note-1---limitations)
* [Additional note 2 - per-world sorting](#additional-note-2---per-world-sorting)
* [Additional note 3 - Compatibility issues with other plugins](#additional-note-3---compatibility-issues-with-other-plugins)
* [Common mistakes](#common-mistakes)
* [Troubleshooting](#troubleshooting)
* [API](#api)
* ~Examples~
# Enabling
To enable sorting, you must have either [Nametags](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Nametags) or [Layout](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Layout) (or both) enabled.
> [!TIP]
> To verify you enabled sorting, run `/tab debug`. It will say `Sorting type:` followed by anything except `DISABLED`.
# Methods of sorting
Below are all the different methods that you can use to sort players.
@@ -33,10 +36,6 @@ This is the default and recommended method. Players will be sorted by their prim
First, put your players into groups in your permission plugin.
All the ways to do it can be found
at [How to assign players into groups](https://github.com/NEZNAMY/TAB/wiki/How-to-assign-players-into-groups).
> [!TIP]
> Verify player's group using `/tab debug <player>`.
It should say `Primary permission group:` followed by group you configured.
If not, you did not assign players into groups correctly.
Second, place all of your groups in to a comma separated list **in order of priority** into `sorting-types`. Example:
```yaml
@@ -44,15 +43,6 @@ scoreboard-teams:
sorting-types:
- "GROUPS:owner,admin,mod,default"
```
> [!TIP]
> Verify configured sorting priority of players using `/tab debug <player>`.
It should show a message like this:
![image](https://github.com/NEZNAMY/TAB/assets/6338394/095d8523-e8c0-4eff-8935-b0b8e6e0d5ff)
If a group is not in the list, you will get the following:
![image](https://github.com/NEZNAMY/TAB/assets/6338394/9c530873-87c9-4db7-8916-a6121dbeed37)
If that's the case, you forgot to add that group into the list.
## PERMISSIONS
> [!CAUTION]
@@ -71,8 +61,6 @@ scoreboard-teams:
sorting-types:
- "PERMISSIONS:my.permission.1,my.permission.2,my.permission.3"
```
> [!TIP]
> Verify sorting permissions of a player using `/tab debug <player>`
## PLACEHOLDER
This method sorts players using the output of a placeholder and comparing that to pre-defined values.
@@ -220,22 +208,24 @@ To identify such an issue, check if /tab reload fixes your sorting issue.
If it does, it's a compatibility issue of some sort.
If not, it is most likely a misconfiguration issue.
# Common mistakes
Every possible mistake could be called "not reading this wiki page", but that would make this section pointless.
For that reason, let's call it list of mistakes made when following this page.
# Troubleshooting
There are many possible reasons why sorting may not work as desired, let's name them all.
To get started, run `/tab dump <some player>` and open the generated link. Scroll down to `features` -> `Sorting`.
If it says `Sorting: Feature is disabled`, it means you disabled sorting. See [Enabling](#enabling).
Check the table of players in the `Sorting` section. The players are listed in the order they should appear in the tablist. Does it match the in-game result?
**Option 1 - Players are listed in the same order they appear in game** - This means you did not configure sorting correctly. Check the table of all values for all players and see what is different than you expected:
* Your group is wrong (shown in the table as well as on top of the dump):
* You did not configure weights in LuckPerms.
* You accidentally enabled `use-bukkit-permission-manager` option when on a proxy without knowing what it does.
* You installed TAB on a proxy without having any permission plugin on the proxy.
* You enabled `assign-groups-by-permissions` without giving those permissions (or the opposite - giving away OP to users, resulting in the highest group being taken).
* You mistook `primary-group-finding-list` for the sorting list and did not configure the actual group sorting list.
* You combined multiple sorting types, but listed them in the wrong order. See [Combining multiple sorting types](#combining-multiple-sorting-types) (sorting element priorities go from top to bottom, in the table they appear from left to right).
The most common mistakes include:
* Disabling both teams and layout, not realizing it disables sorting as well.
* Mistaking `primary-group-finding-list` for the sorting list, despite that list having nothing to do with sorting and by default even having a comment above it saying it has nothing to do with sorting.
* Not configuring primary groups correctly. This can have multiple reasons, such as
* Not configuring group weights in LuckPerms.
* Accidentally enabling `use-bukkit-permission-manager` option when on a proxy without knowing what it does.
* Installing TAB on a proxy without having any permission plugin on the proxy.
* Enabling `assign-groups-by-permissions` without giving those permissions (or the opposite - giving away OP to users, resulting in the highest group being taken).
[Debug command](https://github.com/NEZNAMY/TAB/wiki/Commands-&-Permissions#tab-debug-player) will help you identify if this is your case.
* Using a plugin that causes TAB to fail to apply teams (such as Tablisknu). This includes installing TAB plugin on both backend and proxy, causing the installations to conflict.
* Using TAB on Velocity without installing [VelocityScoreboardAPI](https://github.com/NEZNAMY/VelocityScoreboardAPI/releases)
**Option 2 - Players are listed in a different order than in-game** - This means teams were not applied properly. Possible reasons include:
* Teams are disabled for the player (`disable-condition` was met or teams were disabled via API).
* You have TAB on Velocity and forgot to install [VelocityScoreboardAPI](https://github.com/NEZNAMY/VelocityScoreboardAPI/releases) (or it's outdated and doesn't support your Velocity / game version).
* You are using a plugin that causes TAB to fail to apply teams (such as Tablisknu). This includes installing TAB plugin on both backend and proxy, causing the installations to conflict (typically proxy taking over and backend not working).
# API
*To get started with the API, see [Developer API](https://github.com/NEZNAMY/TAB/wiki/Developer-API) page.*
@@ -1,14 +1,21 @@
# Content
* [About](#about)
* [Configuration](#configuration)
* [Introduction](#introduction)
* [Groups and users](#groups-and-users)
* [Per-world / per-server](#per-world--per-server)
* [Priority system](#priority-system)
* [Placeholder support](#placeholder-support)
* [Additional settings](#additional-settings)
* [Tips & Tricks](#tips--tricks)
* [config.yml](#configyml)
* [groups.yml and users.yml](#groupsyml-and-usersyml)
* [Format structure](#format-structure)
* [Global settings](#global-settings)
* [Per-world / per-server](#per-world--per-server)
* [Priorities](#priorities)
* ~Commands~
* [Placeholders](#placeholders)
* ~Limitations~
* [Compatibility with other plugins](#compatibility-with-other-plugins)
* ~Additional info~
* ~Troubleshooting~
* [API](#api)
* [Examples](#examples)
* [Example 1 - Taking prefix/suffix from permission plugin](#example-1---taking-prefixsuffix-from-permission-plugin)
# About
This feature allows you to configure player name formats in the tablist.
@@ -20,10 +27,25 @@ take advantage of [this client-sided mechanic](https://github.com/NEZNAMY/TAB/wi
which will appear in the tablist.
![image](https://user-images.githubusercontent.com/6338394/205877929-4df82c78-9773-4614-b4a3-93e1673d6046.png)
Settings of this feature can be configured in **config.yml** under **tablist-name-formatting** section.
# Configuration
## Introduction
This feature is configured in 2 parts based on what you are configuring. Some settings are in the **config.yml**, but prefix/suffix is configured in **groups.yml** and **users.yml**.
## config.yml
The first part can be configured in **config.yml** under **tablist-name-formatting** section. It controls everything except prefix and suffix.
This is how the default configuration looks:
```
tablist-name-formatting:
enabled: true
disable-condition: '%world%=disabledworld'
```
All of the options are explained in the following table.
| Option name | Default value | Description |
|-----------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| enabled | true | Enables / Disables the feature |
| disable-condition | %world%=disabledworld | A [condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) that must be met for disabling the feature for players. Set to empty for not disabling the feature ever. |
## groups.yml and users.yml
### Format structure
The display name is split into 3 parts for convenience:
`tabprefix` - first part of the text, this is where prefix is usually configured
`customtabname` - second part of the text, it will be used as player name. If customtabname is not defined at all (not just setting empty value!), player's original name will be used instead
@@ -33,22 +55,20 @@ These values are then merged and displayed in the tablist.
![image](https://user-images.githubusercontent.com/6338394/205877510-e40f93ce-80df-46f2-84f0-c113e0e813e8.png)
You are free to use the 3 properties in any way you want since they are only split for convenience.
However, it is still a good idea to use them for their designed purpose to not get lost a poor configuration.
However, it is still a good idea to use them for their designed purpose to not get lost in a poor configuration.
This means that the text which should appear before player's name should be put into `tabprefix`,
any placeholder that is supposed to display player's name (such as from a nickname plugin)
should go into `customtabname` and any text that should appear after player's name should be put into `tabsuffix`.
## Groups and users
Properties can be applied in 2 ways: groups and users.
Users can be defined by both username and their UUID.
Values applied to users take priority over groups.
Example:
### Global settings
Properties can be applied in 2 ways: groups and users. Users can be defined by both username and their UUID. Values applied to users take priority over groups.
**groups.yml**
```
admin:
tabprefix: "&4&lAdmin &r"
```
This can also be achieved with commands, in this case `/tab group admin tabprefix "&4&lAdmin &r"`.
Or with an in-game command `/tab group admin tabprefix "&4&lAdmin &r"`.
**users.yml**
```
@@ -59,11 +79,11 @@ _NEZNAMY_:
237d8b55-3f97-4749-aa60-e9fe97b45062:
tabprefix: "&6&lTAB &r"
```
This can also be achieved with commands, in this case `/tab player _NEZNAMY_ tabprefix "&6&lTAB &r"` or `/tab playeruuid _NEZNAMY_ tabprefix "&6&lTAB &r"`.
Or with in-game commands `/tab player _NEZNAMY_ tabprefix "&6&lTAB &r"` and `/tab playeruuid _NEZNAMY_ tabprefix "&6&lTAB &r"` (the player must be online for uuid option to work).
Properties can also be set as "default" for everyone who does not have them defined.
For that purpose, a group keyword `_DEFAULT_` was made.
Example:
For that purpose, a group keyword `_DEFAULT_` was made.
Example:
**groups.yml**
```
admin:
@@ -72,7 +92,7 @@ _DEFAULT_:
tabprefix: "&7" # This will be displayed on everyone except admin
```
## Per-world / per-server
### Per-world / per-server
Values can also be applied per-world (and per-server on a proxy), where they can be defined per group/user. These values take priority over global settings. Example:
**groups.yml**
```
@@ -85,21 +105,22 @@ per-server:
_DEFAULT_:
tabprefix: "&a" # Everyone in server "server1" will have this prefix
```
For multiple worlds/servers to share the same settings, separate them with `;`.
For worlds/servers starting with a specified text, use `*` after shared part. For ending with a shared part, use `*` at the beginning.
For multiple worlds/servers to share the same settings, separate them with a`;`.
For worlds/server starting with a specified text, use `*` after shared part. For ending with a shared part, use `*` at the beginning.
Example:
```
per-world:
world1;world2:
_DEFAULT_:
tabsuffix: "Shared tabsuffix in worlds world1 and world2"
tabsuffix: "Shared tagsuffix in worlds world1 and world2"
lobby-*:
_DEFAULT_:
tabsuffix: "Suffix in all worlds starting with lobby-"
```
## Priority system
The full list of priorities looks like this:
### Priorities
The full list of priorities to choose correct prefix/name/suffix for a player looks like this:
1. value set using the [API](#api)
2. per-world / per-server applied to username
3. value applied to username
@@ -110,32 +131,28 @@ The full list of priorities looks like this:
8. value applied to player's group
9. value applied to group `_DEFAULT_`
This list is browsed through until the first match is found. If no match is found, empty value is used (except `customtabname`, which defaults to player's name).
This list is browsed through until the first match is found. If no match is found, empty value is used.
Values are taken independently of each other.
This means you can set per-world tabprefix, but only keep one global tabsuffix, for example.
You can see the source of a value displayed on player by using `/tab debug <player>` and checking "source"
part of the value you are looking for.
You can see the source of a value displayed on player by using `/tab dump <player>`, opening the link and checking "source"
part of the value you are looking for (tabprefix / customtabname / tabsuffix).
## Placeholder support
All values fully support [TAB's internal placeholders](https://github.com/NEZNAMY/TAB/wiki/Placeholders#internal-placeholders) and [PlaceholderAPI placeholders](https://github.com/PlaceholderAPI/PlaceholderAPI/wiki/Placeholders) including relational placeholders.
The number of placeholders is not limited, and they can be used in combination with static text as well.
# Placeholders
Here are TAB's PlaceholderAPI placeholders you can use when this feature is enabled:
| Placeholder | Description |
|-------------|-------------|
| `%tab_tabprefix%` | Player's current tabprefix with placeholders parsed. |
| `%tab_customtabname%` | Player's current customtabname with placeholders parsed. |
| `%tab_tabsuffix%` | Player's current tabprefix with placeholders parsed. |
| `%tab_tabprefix_raw%` | Player's current raw tabprefix with placeholder identifiers. |
| `%tab_customtabname_raw%` | Player's current raw customtabname with placeholder identifiers. |
| `%tab_tabsuffix_raw%` | Player's current raw tabsuffix with placeholder identifiers. |
## Additional settings
| Option name | Default value | Description |
|-------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| enabled | true | Enables / Disables the feature |
| disable-condition | %world%=disabledworld | A [condition](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders) that must be met for disabling the feature for players. Set to empty for not disabling the feature ever. |
# Tips & Tricks
If you want TAB to only take prefixes/suffixes from your permission plugin,
delete all groups from **groups.yml** and only keep this:
```
_DEFAULT_:
tabprefix: "%luckperms-prefix%"
tabsuffix: "%luckperms-suffix%"
```
# Compatibility with other plugins
Tablist formatting is a feature that cannot be effectively handled by multiple plugins at once.
To make sure no other plugin sets tablist display names when not disabled in the plugin's config properly, TAB will block attempts to modify tablist display names coming from all other plugins (if this feature is enabled and not disabled for a player with a condition).
# API
*To get started with the API, see [Developer API](https://github.com/NEZNAMY/TAB/wiki/Developer-API) page.*
@@ -162,4 +179,18 @@ To get the original value set by the plugin based on configuration:
* `TabListFormatManager#getOriginalReplacedName(TabPlayer)` - Name with all placeholders parsed
* `TabListFormatManager#getOriginalReplacedSuffix(TabPlayer)` - Suffix with all placeholders parsed
**Note**: These values are only temporary, meaning they won't get saved anywhere and will get reset on player quit or plugin reload. If you wish to save these values into file, use [commands](https://github.com/NEZNAMY/TAB/wiki/Commands-&-Permissions#tab-playergroupplayeruuid-name-property-value-options).
**Note**: These values are only temporary, meaning they won't get saved anywhere and will get reset on player quit or plugin reload. If you wish to save these values into file, use [commands](https://github.com/NEZNAMY/TAB/wiki/Commands-&-Permissions#tab-playergroupplayeruuid-name-property-value-options).
# Examples
## Example 1 - Taking prefix/suffix from permission plugin
If you want TAB to only take prefixes/suffixes from the permission plugin,
delete all groups from **groups.yml** and only keep this:
```
_DEFAULT_:
tabprefix: "%luckperms-prefix%"
tabsuffix: "%luckperms-suffix%"
```
Or a PlaceholderAPI placeholder from your permission plugin if you use a different one.
> [!NOTE]
> When doing this, make sure you don't delete something you didn't mean to delete, for example ta**g**prefix and ta**g**suffix from nametag feature.
-1
View File
@@ -2,4 +2,3 @@
A collection of things people have requested that I might look into implementing when I get extremely bored sometime next decade.
* Relational placeholder support in sorting
* 1.21.2+ sorting support
* disable-condition support in multi-server-support
+2 -2
View File
@@ -5,7 +5,7 @@
* [#4 - Why is player sorting not working?](#4---why-is-player-sorting-not-working)
* [#5 - How to add players to %staffonline% placeholder?](#5---how-to-add-players-to-the-staffonline-placeholder)
* [#6 - How to make player heads visible in tablist?](#6---how-to-make-player-heads-visible-in-tablist)
* [#7 - Where can i find current default config files?](#7---where-can-i-find-current-default-config-files)
* [#7 - Where can I find current default config files?](#7---where-can-i-find-current-default-config-files)
* [#8 - How can I split players into multiple columns?](#8---how-can-i-split-players-into-multiple-columns)
* [#9 - How to use space in prefix/suffix command?](#9---how-to-use-spaces-in-prefixsuffix-command)
* [#10 - Can I change/remove the green connection bar in tablist?](#10---can-i-changeremove-the-green-connection-bar-in-tablist)
@@ -54,7 +54,7 @@ The same goes for disabling heads - you cannot disable them if you have online m
Minecraft 1.21.9 has added [object components](https://github.com/NEZNAMY/TAB/wiki/How-to-use-Minecraft-components#object-components-1219), which allow you to display player heads. You can put for example `<head:name:%player%>` at the beginning of tabprefix to simulate this.
## #7 - Where can i find current default config files?
## #7 - Where can I find current default config files?
You can do any of the following:
* Check the [source code](https://github.com/NEZNAMY/TAB/tree/master/shared/src/main/resources/config).
* Delete or rename a file, and the plugin will regenerate it.
+1 -1
View File
@@ -1,5 +1,5 @@
# About the wiki
The wiki explains the functionality of the plugin as of version **5.4.0**.
The wiki explains the functionality of the plugin as of version **6.1.0**.
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.
@@ -22,7 +22,7 @@ There are 2 simple steps to this:
> (for luckperms /lp group \<group\> setweight \<number\>).
> [!TIP]
> Verify player's group using `/tab debug <player>`. It should say `Primary permission group: <your group>`.
> Verify player's group using `/tab dump <player>`, opening the link and check "permissionGroup" in player info.
## Option 2: Permission nodes
Instead of taking whole groups, TAB will check for permission nodes and assign groups based on the highest permission.
@@ -52,14 +52,12 @@ this has lots of side effects.
Most notably, it breaks all name-bound features,
especially [nametags](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Nametags) and [sorting](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Sorting-players-in-tablist).
For that reason, TAB must detect name change.
This is done automatically on Bukkit, BungeeCord and modded platforms.
**This detection is not available on Velocity**.
> [!WARNING]
> If you have TAB installed on backend server and a proxy plugin changes nickname, TAB is not able to detect this change, since the modified packets never actually go through the backend server where TAB is installed.
> [!TIP]
> If automatic detection is not working for you (either you use TAB on Velocity, or use proxy plugin for changing names while TAB is on backend), you can use [this API method](https://github.com/NEZNAMY/TAB/blob/master/api/src/main/java/me/neznamy/tab/api/TabPlayer.java#L88) to let TAB know player's name changed.
> If automatic detection is not working for you (you use proxy plugin for changing names while TAB is on backend), you can use [this API method](https://github.com/NEZNAMY/TAB/blob/master/api/src/main/java/me/neznamy/tab/api/TabPlayer.java#L88) to let TAB know player's name changed.
In order for TAB to properly detect name change, player UUID must remain the same,
otherwise it's a random entry with a random uuid and random name, which TAB isn't able to match with actual players.
@@ -84,7 +82,7 @@ conditions:
```
Then, use `%condition:nick%` in your `customtabname` instead of the placeholder directly.
# Tip 2 - Sorting nicked players with the lowest priority
## Tip 2 - Sorting nicked players with the lowest priority
Let's say your sorting list looks like this:
```
sorting-types:
@@ -104,12 +104,20 @@ where `ATLAS` is the atlas and `SPRITE` is the sprite. Note that atlas can conta
Example:
```
header:
- '&bDiamond helmet: <sprite:"minecraft:blocks":"minecraft:item/diamond_helmet">'
- 'Diamond chestplate: <sprite:"minecraft:blocks":"minecraft:item/diamond_chestplate">'
- 'Diamond leggings: <sprite:"minecraft:blocks":"minecraft:item/diamond_leggings">'
- 'Diamond boots: <sprite:"minecraft:blocks":"minecraft:item/diamond_boots">'
- '&bDiamond helmet: <sprite:"minecraft:items":"minecraft:item/diamond_helmet">'
- 'Diamond chestplate: <sprite:"minecraft:items":"minecraft:item/diamond_chestplate">'
- 'Diamond leggings: <sprite:"minecraft:items":"minecraft:item/diamond_leggings">'
- 'Diamond boots: <sprite:"minecraft:items":"minecraft:item/diamond_boots">'
```
<img width="386" height="101" alt="image" src="https://github.com/user-attachments/assets/e9a66305-2ae4-44f4-9d8e-a227fb7591e4" />
<img width="386" height="101" alt="image" src="https://github.com/user-attachments/assets/e9a66305-2ae4-44f4-9d8e-a227fb7591e4" />
> [!IMPORTANT]
> These values change over MC versions (for example items were in `minecraft:blocks` atlas in 1.21.9 - 1.21.10, but got moved to `minecraft:items` in 1.21.11), so make sure you are using the correct values for your MC version. For proxy installations, you may need to use conditional placeholders and check for player's version. On backend, ViaVersion may or may not automatically translate these.
> [!TIP]
> For full list of available sprites you can check out https://www.gamergeeks.net/apps/minecraft/list-of-atlas-sprites. However, it doesn't contain any information regarding different Minecraft versions.
## Player sprite
@@ -164,10 +172,13 @@ See table below for behavior based on your setup.
# MiniMessage
TAB has [MiniMessage](https://docs.advntr.dev/minimessage/format.html) hook, however, it does not include this library on its own. Including it would cause conflicts, so it has to be included in the server software.
* It **is** included in **Paper 1.18.2+**, **Velocity** and **Sponge**.
* It **is not** included in **Spigot**, any server software for **1.18.1-**, **BungeeCord**, **Fabric**, **Forge** and **NeoForge**.
* It **is** included in **Paper 1.18.2+** and **Velocity**.
* It **is not** included in **Spigot**, any server software for **1.18.1-**, **BungeeCord**, **Fabric**, **Forge** and **NeoForge**. However, you can install [adventure-platform-mod](https://modrinth.com/mod/adventure-platform-mod) on Fabric / NeoForge.
When MiniMessage is detected on your server, it is automatically used. All codes are translated to MiniMessage syntax and then parsed by MiniMessage.
When MiniMessage is detected on your server, it is automatically used unless support is disabled. All codes are translated to MiniMessage syntax and then parsed by MiniMessage.
> [!WARNING]
> TAB's gradient syntax (`<#RRGGBB>Text</#RRGGBB>`) is a compatibility layer that TAB rewrites before parsing, it is more fragile than native [MiniMessage](https://docs.advntr.dev/minimessage/format.html) gradients (`<gradient:#RRGGBB:#RRGGBB>Text</gradient>`). On **Velocity**, this is especially problematic because [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) (see [TAB integration PR #1677](https://github.com/NEZNAMY/TAB/pull/1677)) parses MiniMessage *before* TAB can rewrite its gradient syntax. For example, placeholder output like `<#E0B11E>MyServer</#FF0000>` is interpreted by MiniPlaceholders as a hex color tag (`<#E0B11E>`), but `</#FF0000>` is not recognized as a gradient closing tag, so the formatting breaks. Prefer native MiniMessage syntax in placeholder output (e.g. `<gradient:#E0B11E:#FF0000>MyServer</gradient>`). See [Placeholders - MiniPlaceholders](https://github.com/NEZNAMY/TAB/wiki/Placeholders#miniplaceholders) for more information.
# Config options
| Option name | Default value | Description |
+10 -8
View File
@@ -27,7 +27,7 @@ Install [TAB-Bridge](https://github.com/NEZNAMY/TAB/wiki/TAB-Bridge) on your **b
If you are using **Velocity**,
install [VelocityScoreboardAPI](https://github.com/NEZNAMY/VelocityScoreboardAPI/) plugin **on the proxy** for scoreboard-related features to work.
> [!NOTE]
> [!NOTE]
> With proxy installation, use **/btab** command instead of **/tab**.
Advantages of proxy installation compared to installing on all backend servers instead:
@@ -35,6 +35,7 @@ Advantages of proxy installation compared to installing on all backend servers i
* [Layout](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Layout) can work with all players connected to the proxy
* Avoid [header/footer](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Header-&-Footer#additional-note-1---not-resetting-on-server-switch) and [bossbar](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Bossbar#additional-note-2---not-hiding-on-server-switch) not disappearing on server switch
* Configuration files in a single place for easier editing / no need to use MySQL for syncing data
* Can use new features for new players even if backend is using an older version (RGB, 1.20.3+ NumberFormat and more).
Disadvantages compared to installing on all backend servers instead:
@@ -44,11 +45,12 @@ Disadvantages compared to installing on all backend servers instead:
Just like when not running a network, install the plugin on all servers where you want the plugin. However, this setup is not as good (see advantages of proxy installation).
## Mixed installation
Mixed installation is absolutely not recommended and should be avoided.
It requires you to disable features to make only 1 instance handle a single feature at a time,
which most people fail miserably to do,
resulting in issues such as header/footer flashing between proxy and backend configuration.
With bridge, you are able to achieve almost anything with proxy installation.
Mixed installation is absolutely not supported and should be avoided.
There is no sort of communication between backend and proxy TAB installations (this is only done by TAB-Bridge, which doesn't do anything on its own). Having TAB on both proxy and backend is effectively installing 2 plugins to do the same thing, which will naturally cause issues (although TAB's anti-override for key features should help with this).
If you really wish to proceed (for example having feature X handled by proxy and feature Y on backend), make sure to properly disable features (`enabled: false`) to avoid problems. However, the only case where this makes any sense is per world playerlist feature, which is not available on proxies. Everything else can be done from proxy side.
# Updating the plugin
Updating the plugin is always as simple as replacing old jar with the new one.
@@ -59,5 +61,5 @@ TAB will automatically convert your existing configuration files to the latest f
(most notably 2.9.2 → 3.0.0, but any other changes as well).
Downgrading is completely unsupported, as an old plugin version cannot know what the future config format is / will be.
Just like Minecraft itself.
There is no reason for downgrading anyway, as every version is superior to older versions released before it.
Just like Minecraft itself, although TAB won't fail with an error message like Minecraft does, but you will get warnings and / or features not working correctly.
There is no reason for downgrading anyway, since in general, every version is typically superior to older versions released before it.
+29 -1
View File
@@ -9,6 +9,7 @@ Combine the knowledge gathered by reading the wiki to get interesting results, m
* [Showing combined online player count from multiple servers](#showing-combined-online-player-count-from-multiple-servers)
* [Option 1 - Using global playerlist](#option-1---using-global-playerlist)
* [Option 2 - Using PlaceholderAPI](#option-2---using-placeholderapi)
* [Showing head components only to 1.21.9+ players](#showing-head-components-only-to-1219-players)
# Displaying kill / death counter in tablist
This example uses PlaceholderAPI placeholders.
@@ -203,4 +204,31 @@ Unlike the first option, this one offers much more freedom, but also requires an
First, you need placeholders for online count on specific servers. Bungee expansion does exactly that. The format is `%bungee_<server>%`. To merge the online counts, use math expansion. The syntax will be `%math_0_<expression>%`, where `expression` uses online counts from specific servers from bungee expansion. Finally, you'll end up with a placeholder like `%math_0_{bungee_lobby1}+{bungee_lobby2}%`. This placeholder will merge online counts from `lobby1` and `lobby2`.
Please note that by default, bungee expansion updates values only every 30 seconds. This can be changed in PlaceholderAPI's config. If the numbers still don't show correct value, check if the placeholder works correctly using `/papi parse me <placeholder>`. If not, this is not a TAB issue.
Advantage of this is no requirement for a specific TAB feature to be enabled, which also includes no need to have TAB installed on the proxy. The downside is relying on 3rd party software. If TAB is on proxy, you'll also need [bridge plugin](https://github.com/NEZNAMY/TAB/wiki/TAB-Bridge).
Advantage of this is no requirement for a specific TAB feature to be enabled, which also includes no need to have TAB installed on the proxy. The downside is relying on 3rd party software. If TAB is on proxy, you'll also need [bridge plugin](https://github.com/NEZNAMY/TAB/wiki/TAB-Bridge).
# Showing head components only to 1.21.9+ players
Object components were added in 1.21.9, which means they cannot be shown to 1.21.8- players. [If you try to do so, you'll either get an ugly text (BungeeCord / backend), or disconnect with a packet decode error (on Velocity)](https://github.com/NEZNAMY/TAB/wiki/How-to-use-Minecraft-components#compatibility-with--1219).
To achieve this goal, we will use [relational conditions](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Conditional-placeholders#relational-conditions) and check if viewer is 1.21.9+:
```
conditions:
head:
conditions:
- "%viewer:player-version-id%>=773"
true: "<head:name:%player%> "
false: "" # Show nothing
```
The `773` is the protocol version of 1.21.9, so we can use a simple math operation instead of comparing strings. You can check [TAB's source code](https://github.com/NEZNAMY/TAB/blob/master/shared/src/main/java/me/neznamy/tab/shared/ProtocolVersion.java) for the full list. This condition will return head component of the **target** player if the **viewer** is 1.21.9+, empty value otherwise.
[If you want to use this head syntax, you'll need to disable MiniMessage support](https://github.com/NEZNAMY/TAB/wiki/How-to-use-Minecraft-components#player-sprite), or use a syntax supported by MiniMessage (which is currently extremely limited).
Use this condition with `%rel_condition:head%`. The rel is important, so TAB knows the placeholder is a relational placeholder. If you forget to, the condition will return a warning message instead of configured value.
To showcase it, I have this in **groups.yml** (to show player versions as well):
```
_DEFAULT_:
tabprefix: "%rel_condition:head%"
tabsuffix: " %player-version% (%player-version-id%)"
```
1.21.11 player's view:
<img width="634" height="103" alt="image" src="https://github.com/user-attachments/assets/4c762fae-3166-4247-865f-c87d1916afd7" />
1.21.8 player's view:
<img width="581" height="106" alt="image" src="https://github.com/user-attachments/assets/cb7ab93d-0db5-4968-bcfe-60004125ddf1" />
-13
View File
@@ -1,5 +1,4 @@
* [About](#about)
* [Availability](#availability)
* [Enabling](#enabling)
* [Configuration changes](#configuration-changes)
* [Data conversion](#data-conversion)
@@ -17,18 +16,6 @@ It may be added in the future.
All tables from TAB are prefixed with `tab_`, currently `tab_groups` for groups and `tab_users` for users.
# Availability
Before attempting to use the function, make sure your server software has an integrated MySQL driver.
TAB does not contain it by itself.
See the compatibility table below for current information about compatibility:
| Platform | Available |
|------------|-----------|
| Bukkit | ✔ |
| BungeeCord | ✔ |
| Modded | ❌ |
| Velocity | ❌ |
# Enabling
Find this section in **config.yml**:
```
+80 -19
View File
@@ -1,24 +1,33 @@
# Content
* [About](#about)
* [Ways of optimizing](#ways-of-optimizing)
* [#1 - [All platforms] Static text instead of placeholders](#1---all-platforms-static-text-instead-of-placeholders)
* [#2 - [All platforms] Placeholder refresh intervals](#2---all-platforms-placeholder-refresh-intervals)
* [#3 - [All platforms] Don't use RGB](#3---all-platforms-dont-use-rgb)
* [#4 - [All platforms] Don't use animations](#4---all-platforms-dont-use-animations)
* [#5 - [All platforms] Disable tablist name formatting](#5---all-platforms-disable-tablist-name-formatting)
* [#6 - [All platforms] Increase permission refresh interval](#6---all-platforms-increase-permission-refresh-interval)
* [#7 - [Proxies] Disable TAB expansion](#7---proxies-disable-tab-expansion)
* [#8 - [Proxies] Disable update-latency option in global playerlist](#8---proxies-disable-update-latency-option-in-global-playerlist)
* [Netty false positive in spark reports](#netty-false-positive-in-spark-reports)
* [Why there is a false positive](#why-there-is-a-false-positive)
* [How to read the real value](#how-to-read-the-real-value)
* [Packet listening usage](#packet-listening-usage)
* [Sending packets](#sending-packets)
# About
This guide will explain all primary ways you can reduce plugin's CPU usage besides the obvious things like "don't spam animations" or "disable features you don't want".
The usage can be checked at any time using [**/tab cpu**](https://github.com/NEZNAMY/TAB/wiki/Commands-&-Permissions#tab-cpu) command. If the numbers appear to be higher than they should be, check this guide for various ways to optimize the plugin. Most of these apply for all platforms, but some of them are only specific for some platforms.
# Content
* [#1 - [All platforms] Static text instead of placeholders](#1---all-platforms-static-text-instead-of-placeholders)
* [#2 - [All platforms] Placeholder refresh intervals](#2---all-platforms-placeholder-refresh-intervals)
* [#3 - [All platforms] Don't use RGB](#3---all-platforms-dont-use-rgb)
* [#4 - [All platforms] Don't use animations](#4---all-platforms-dont-use-animations)
* [#5 - [All platforms] Disable tablist name formatting](#5---all-platforms-disable-tablist-name-formatting)
* [#6 - [All platforms] Increase permission refresh interval](#6---all-platforms-increase-permission-refresh-interval)
* [#7 - [Proxies] Disable TAB expansion](#7---proxies-disable-tab-expansion)
* [#8 - [Proxies] Disable update-latency option in global playerlist](#8---proxies-disable-update-latency-option-in-global-playerlist)
# #1 - [All platforms] Static text instead of placeholders
# Ways of optimizing
## #1 - [All platforms] Static text instead of placeholders
Static text doesn't need to be refreshed periodically, resulting in better performance.
Some placeholders can be replaced with static text, such as placeholder for max player counts,
since that value doesn't change at runtime.
Another option is to configure prefixes per-group in config
instead of using placeholders to take those values from your permission plugin.
# #2 - [All platforms] Placeholder refresh intervals
## #2 - [All platforms] Placeholder refresh intervals
Some placeholders take a long time to process and result in high cpu usage. However, this can be optimized by configuring refresh interval of placeholders.
Internal placeholders use a value hardcoded into the plugin by default. PlaceholderAPI placeholders use `default-refresh-interval`.
@@ -45,16 +54,16 @@ making it look like the CPU is perfect, while on the next refresh it may be high
For this reason, if you want to cleanly track usage, keep the interval at 10000 at most.
Using a higher interval will help if needed, but may lead to misleading info in cpu output.
# #3 - [All platforms] Don't use RGB
## #3 - [All platforms] Don't use RGB
Though RGB colors look good, they require the text to be split into several components with `color` field used instead of the whole text pasted into the `text` field. This is a complicated process that takes quite some time. Sticking to legacy colors results in better performance.
# #4 - [All platforms] Don't use animations
## #4 - [All platforms] Don't use animations
While animations look cool, they also have a high resource usage.
This is because they work by spamming updates every time an animation switches to a new frame.
When animations are not used, no updates need to be sent (unless another placeholder changes value),
not causing extra CPU usage.
# #5 - [All platforms] Disable tablist name formatting
## #5 - [All platforms] Disable tablist name formatting
As we already know from [Client-sided mechanics](https://github.com/NEZNAMY/TAB/wiki/Client%E2%80%90sided-mechanics#nametag-format-in-tablist) page,
the nametag format appears in tablist if tablist name is not defined.
You can take advantage of this to effectively disable one entire feature and its cpu usage,
@@ -65,7 +74,7 @@ However, you can only do this if:
Whether you have another plugin attempting to handle them or not may not be easy to identify with a lot of plugins and not enough time spent configuring them. In that case, your best choice is to try it and see. If tablist names break, it means you have another plugin and cannot take advantage of this optimization option.
# #6 - [All platforms] Increase permission refresh interval
## #6 - [All platforms] Increase permission refresh interval
Plugin needs to constantly check permissions for:
* Permission checks in conditions
* Permission checks if sorting by permissions
@@ -79,7 +88,7 @@ Refreshes them every second.
If you don't need any permission/group changes to take effect within a second,
you can increase this value for better performance.
# #7 - [Proxies] Disable TAB expansion
## #7 - [Proxies] Disable TAB expansion
To maximize the performance of TAB's response to PlaceholderAPI request,
values are tracked in advance and then quickly returned.
This process takes resources and is especially heavy on proxy installation,
@@ -90,5 +99,57 @@ Disabling TAB's PlaceholderAPI expansion if you don't use it improves performanc
placeholders:
register-tab-expansion: false
```
# #8 - [Proxies] Disable update-latency option in global playerlist
When [Global playerlist](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Global-playerlist) is enabled, you can enable / disable [update-latency](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Global-playerlist#:~:text=other%20unlisted%20servers.-,update%2Dlatency,-false) option (open the link for detailed description of the option). Disabling this option massively improves performance.
## #8 - [Proxies] Disable update-latency option in global playerlist
When [Global playerlist](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Global-playerlist) is enabled, you can enable / disable [update-latency](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Global-playerlist#:~:text=other%20unlisted%20servers.-,update%2Dlatency,-false) option (open the link for detailed description of the option). Disabling this option massively improves performance.
# Netty false positive in spark reports
When taking a spark report with all threads monitored instead of just the server thread using `--thread *` argument (or on a proxy where this is enabled by default since there is no main thread to monitor by default), you may notice high usage (both CPU and RAM) in netty thread when grouping threads by plugin. **Vast majority of that usage is false positive.** Let's go over why this is the case and how to read the real value.
## Why there is a false positive
When listening to an event, the server fires the events in a loop, where each event is the end point. As such, measuring every event handler individually is accurate. Here is how the hierarchy looks with example numbers:
```
Event processing in loop (6ms total)
↳ Plugin 1 (1ms)
↳ Plugin 2 (2ms)
↳ Plugin 3 (3ms)
End of loop
```
As such, the measured values are accurate and spark is able to separate each event handler based on plugin. Command executions and repeating tasks are even simpler.
The issue is with netty injection. The purpose is to listen to outgoing packets, react to them and possibly modify them. Unlike with events, these are not processed in parallel, but every handler forwards the packet to the next one (or doesn't if it should be cancelled). As such, the hierarchy looks like this (simplified version):
```
Beginning (12ms total)
↳ Plugin 1 (1ms self, 12ms total)
↳ Plugin 2 (1ms self, 11ms total)
↳ Plugin 3 (1ms self, 10ms total)
↳ Minecraft encoder (10ms self)
```
As such, spark will show 12ms total for Plugin 1, even though its own usage is only 1ms. This is due to a limitation of the design of Spark in combination with how netty handlers work. This is only the case for plugins with their own netty injection, it does not apply to plugins that use ProtocolLib, because that one offers event-like functionality explained above. In this case, the usage shown in spark report is 100% accurate.
## How to read the real value
An actual usage of the netty threads can be split into 2 parts:
* Listening to outgoing packets
* Sending packets, which causes it to go though the pipeline and then encode
### Packet listening usage
This one is simple and accurate. Take the shown number, expand the call stack and subtract `io.netty.channel.ChannelDuplexHandler.write()` usage, which is forwarding the packet to the next handler and would be there even if that plugin wasn't injected.
Let's take a look at a practical example:
<img width="1091" height="266" alt="image" src="https://github.com/user-attachments/assets/6d9e8c25-8375-413b-ab9b-31b52ff9262c" />
Here, spark reports `31.71%` usage by TAB. However, `30.87%` of it is forwarding it to the next handler (`io.netty.channel.ChannelDuplexHandler.write()`), which forwards it to the next one, until eventually the server encodes the packet. Therefore, TAB's real usage by listening to outgoing packets in this report is `31.71%` - `30.87%` = `0.84%`. Much less than what you originally see.
### Sending packets
Sadly, this one is impossible to calculate directly. When a packet is scheduled for sending, it is only added to a queue, then consumed by a netty thread, not the caller thread. At that point we are losing information about where a packet comes from.
There is some good news though.
First, vast majority of plugins barely send any packets. For example PlaceholderAPI is just an interface between plugins, chat plugins only modify chat message packets that would be sent anyway, ViaVersion same thing, and so on...
Second, plugins that send a lot of packets usually send the kind that the server either doesn't send at all, or not much. Expanding the data from the previously shown report gives us this:
<img width="1083" height="496" alt="image" src="https://github.com/user-attachments/assets/c6157e78-ef6a-4d9c-b4b7-7c7b32dd14f8" />
Here we can see that encoding team packets took 0.99%. Since TAB is the only team plugin you need for managing players, it is safe to assume most, if not all of it comes from TAB.
Same here - 0.13% from header and footer.
<img width="1085" height="93" alt="image" src="https://github.com/user-attachments/assets/559dce56-83d3-49b2-9f4b-29b6124f944c" />
Sadly there are other operations needed than just encoding packets, such as allocating buffers and other, so this part is not that simple anymore.
+41 -27
View File
@@ -5,6 +5,7 @@
* [Backend only](#backend-only)
* [Proxy only](#proxy-only)
* [PlaceholderAPI](#placeholderapi)
* [MiniPlaceholders](#miniplaceholders)
* [Relational placeholders](#relational-placeholders)
* [About](#about-1)
* [Usage](#usage)
@@ -65,12 +66,13 @@ These placeholders are available on all platforms.
| %bedrock% | -1 | Returns `true`/`false` based on whether player is using Bedrock edition or not (Java) |
| %bossbar_announce_time_total_\<bossbar\>% | -1 | Total time (inputted value) of an announced bossbar. |
| %bossbar_announce_time_left_\<bossbar\>% | 100 | Remaining time of an announced bossbar. |
| %online_\<server name\>% | 1000 | Amount of online players on specified server (excluding vanished players) |
| %% | -1 | Returns the `%` symbol. Useful to display `%` symbol without breaking all placeholders after it |
## Backend only
These placeholders only work when TAB is installed on a backend server, they don't work when TAB is on a proxy. If you have TAB installed on proxy and wish to use these, use their [PlaceholderAPI](https://github.com/NEZNAMY/TAB/wiki/Quick-PlaceholderAPI-startup-guide) alternative with [Bridge plugin](https://github.com/NEZNAMY/TAB/wiki/TAB-Bridge) installed for PlaceholderAPI support on proxy.
| Identifier | Refresh interval | Description | PlaceholderAPI alternative |
| ------------- | ------------- | ------------- | ------------- |
|---------------|---------------| ------------- | ------------- |
| %health% | 100 | Player's health, rounded up to match Minecraft's heart display. | %player_health_rounded% |
| %displayname% | 500 | Player's display name value set by permission plugin. | %player_displayname% |
| %tps% | 1000 | TPS of server from the last minute. Measured by the server. Not directly available on modded platforms - primitive approximation using MSPT (TPS = 1000/MSPT, max 20) is used there. | %server_tps_1% |
@@ -82,7 +84,6 @@ These placeholders only work when TAB is installed on a backend server, they don
|------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| %server% | -1 | Name of server where player is, defined in proxy's config. |
| %serveronline% | 1000 | Amount of online players on server where the player is (excluding vanished players) |
| %online_\<server name\>% | 1000 | Amount of online players on specified server (excluding vanished players) |
| %playerlist-group_\<group\>% | 1000 | Amount of online players in specified [global playerlist](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Global-playerlist) group (therefore the feature must be enabled) |
# PlaceholderAPI
@@ -103,40 +104,53 @@ in **config.yml**.
Full list of placeholders (general + feature specific):
**General**:
| Identifier | Description |
| ------------- | ------------- |
| `%tab_replace_<placeholder>%` | Applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to a PlaceholderAPI placeholder (for example `%tab_replace_essentials_vanished%`) |
| `%tab_placeholder_<placeholder>%` | returns value of tab's internal placeholder (such as `%tab_placeholder_animation:name%` for `%animation:name%`) |
| Identifier | Description |
|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `%tab_replace_<placeholder>%` | Applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to a PlaceholderAPI placeholder (for example `%tab_replace_essentials_vanished%`) |
| `%tab_placeholder_<placeholder>%` | Returns value of tab's internal placeholder (this also applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to them if configured). <br />Example: `%tab_placeholder_animation:name%` for `%animation:name%` |
| `%rel_tab_replace_<rel_placeholder>%` | Applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to a **relationnal** PlaceholderAPI placeholder |
| `%rel_tab_placeholder_<rel_placeholder>%` | Returns value of tab's internal **relational** placeholder (this also applies [Placeholder output replacements](https://github.com/NEZNAMY/TAB/wiki/Feature-guide:-Placeholder-output-replacements) to them if configured). It may look strange that the `rel` is twice in there, but there's no other way. The first one tells PlaceholderAPI that this is a relational placeholder, the second one is part of the TAB's identifier, which also uses it to mark the placeholder as relational. <br />Example: `%rel_tab_placeholder_rel_condition:name%` for `%rel_condition:name%` |
**BossBar**:
| Identifier | Description |
| ------------- | ------------- |
| Identifier | Description |
|-------------------------|-----------------------------------------|
| `%tab_bossbar_visible%` | "Enabled" if visible, "Disabled" if not |
**NameTags**:
| Identifier | Description |
| ------------- | ------------- |
| `%tab_tagprefix%` | Player's current tagprefix with placeholders parsed. |
| `%tab_tagsuffix%` | Player's current tagprefix with placeholders parsed. |
| `%tab_tagprefix_raw%` | Player's current raw tagprefix with placeholder identifiers. |
| `%tab_tagsuffix_raw%` | Player's current raw tagsuffix with placeholder identifiers. |
| Identifier | Description |
|----------------------------|----------------------------------------------------------------------------------------------|
| `%tab_tagprefix%` | Player's current tagprefix with placeholders parsed. |
| `%tab_tagsuffix%` | Player's current tagprefix with placeholders parsed. |
| `%tab_tagprefix_raw%` | Player's current raw tagprefix with placeholder identifiers. |
| `%tab_tagsuffix_raw%` | Player's current raw tagsuffix with placeholder identifiers. |
| `%tab_nametag_visibility%` | "Enabled" if player can see nametags, "Disabled" if disabled using `/tab nametag toggleview` |
**Scoreboard**:
| Identifier | Description |
| ------------- | ------------- |
| `%tab_scoreboard_name%` | Returns name of player's currently displayed scoreboard or empty string if none is displayed due to no display condition being met. |
| `%tab_scoreboard_visible%` | "Enabled" if visible, "Disabled" if not (toggled with a command) |
**Scoreboard**:
| Identifier | Description |
|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| `%tab_scoreboard_name%` | Returns name of player's currently displayed scoreboard or empty string if none is displayed due to no display condition being met. |
| `%tab_scoreboard_visible%` | "Enabled" if visible, "Disabled" if not (toggled with a command) |
**Tablist name formatting**:
| Identifier | Description |
| ------------- | ------------- |
| `%tab_tabprefix%` | Player's current tabprefix with placeholders parsed. |
| `%tab_customtabname%` | Player's current customtabname with placeholders parsed. |
| `%tab_tabsuffix%` | Player's current tabprefix with placeholders parsed. |
| `%tab_tabprefix_raw%` | Player's current raw tabprefix with placeholder identifiers. |
| Identifier | Description |
|---------------------------|------------------------------------------------------------------|
| `%tab_tabprefix%` | Player's current tabprefix with placeholders parsed. |
| `%tab_customtabname%` | Player's current customtabname with placeholders parsed. |
| `%tab_tabsuffix%` | Player's current tabprefix with placeholders parsed. |
| `%tab_tabprefix_raw%` | Player's current raw tabprefix with placeholder identifiers. |
| `%tab_customtabname_raw%` | Player's current raw customtabname with placeholder identifiers. |
| `%tab_tabsuffix_raw%` | Player's current raw tabsuffix with placeholder identifiers. |
| `%tab_tabsuffix_raw%` | Player's current raw tabsuffix with placeholder identifiers. |
# MiniPlaceholders
On **Velocity**, TAB integrates with [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) when the plugin is installed. TAB automatically detects and resolves MiniPlaceholders placeholders in config text, including global, per-player, and relational ones. Placeholders use angle brackets, with the expansion and name separated by an underscore (for example `<luckperms_prefix>`), and arguments added after a colon (for example `<luckperms_meta:key>`). See the [MiniPlaceholders placeholder list](https://miniplaceholders.github.io/docs/user-guide/Placeholders) for all available placeholders. TAB also registers its own expansion, offering placeholders such as `<tab_tabprefix>` and `<tab_placeholder:name>`. The `%placeholder%` syntax remains reserved for [PlaceholderAPI](https://github.com/PlaceholderAPI/PlaceholderAPI) (via TAB Bridge on Velocity).
> [!WARNING]
> TAB's gradient syntax (`<#RRGGBB>Text</#RRGGBB>`) is a compatibility layer that TAB rewrites before parsing, it is more fragile than native [MiniMessage](https://docs.advntr.dev/minimessage/format.html) gradients (`<gradient:#RRGGBB:#RRGGBB>Text</gradient>`). On **Velocity**, this is especially problematic because [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) parses MiniMessage *before* TAB can rewrite its gradient syntax. For example, placeholder output like `<#E0B11E>MyServer</#FF0000>` is interpreted by MiniPlaceholders as a hex color tag (`<#E0B11E>`), but `</#FF0000>` is not recognized as a gradient closing tag, so the formatting breaks. Prefer native MiniMessage syntax in placeholder output (e.g. `<gradient:#E0B11E:#FF0000>MyServer</gradient>`). See [How to use Minecraft components - MiniMessage](https://github.com/NEZNAMY/TAB/wiki/How-to-use-Minecraft-components#minimessage) for more information.
# Relational placeholders
## About
@@ -163,7 +177,7 @@ To properly distinguish viewer/target (if needed), your relational placeholder e
# Refreshing
## Refresh intervals
TAB is refreshing placeholders in intervals. For internal placeholders it's using values I found the most optimal and set directly in the plugin. Refresh intervals for PlaceholderAPI placeholders can be [configured](https://github.com/NEZNAMY/TAB/wiki/Optimizing-the-plugin#2---placeholder-refresh-intervals).
TAB is refreshing placeholders in intervals. For internal placeholders it's using values I found the most optimal and set directly in the plugin. Refresh intervals for PlaceholderAPI placeholders can be [configured](https://github.com/NEZNAMY/TAB/wiki/Optimizing-the-plugin#2---all-platforms-placeholder-refresh-intervals).
## Sync refreshing
By default, all placeholders are refreshed asynchronously,
+1 -6
View File
@@ -25,9 +25,4 @@ older software will not be compatible with the new one.
You should always be running the latest versions of both TAB and Bridge.
# Confirming it works
You have many ways to verify TAB and Bridge are properly connected. Besides checking if the features above work, the easiest one is to run `/btab debug <player>`.
If it's properly connected, you'll get a message saying this:
![image](https://github.com/NEZNAMY/TAB/assets/6338394/3f8dd978-a8bd-4613-9ea1-a56340194859)
If it's not properly connected, you'll get this message:
![image](https://github.com/NEZNAMY/TAB/assets/6338394/a90018bc-2c3b-4bf5-b54c-56621178ce80)
If this is the case, it means Bridge is either not installed or versions are not compatible. Update both to the latest version.
You have many ways to verify TAB and Bridge are properly connected. Besides checking if the features above work, the easiest one is to run `/btab dump <player>` and check if bridge connection says connected. If not, it means Bridge is either not installed or versions are not compatible. Update both to the latest version.
+14 -10
View File
@@ -8,16 +8,16 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # https://github.com/actions/checkout/releases
- uses: actions/checkout@v6 # https://github.com/actions/checkout/releases
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 21
uses: actions/setup-java@v4 # https://github.com/actions/setup-java/releases
- name: Set up JDK 25
uses: actions/setup-java@v5 # https://github.com/actions/setup-java/releases
with:
distribution: 'temurin'
java-version: 21
java-version: 25
- name: Cache Gradle
uses: actions/cache@v4.2.0 # https://github.com/actions/cache/releases
uses: actions/cache@v6 # https://github.com/actions/cache/releases
with:
path: |
~/.gradle/caches
@@ -31,11 +31,15 @@ jobs:
run: ./gradlew clean build
- name: Rename JAR with Build Number
run: |
original_jar=$(find jar/build/libs -type f -name "TAB-*.jar")
new_name=$(echo "$original_jar" | sed "s/TAB-\(.*\)\.jar/TAB-\1+${{ github.run_number }}.jar/")
mv "$original_jar" "$new_name"
find jar/build/libs -type f -name "TAB v*.jar" -print0 | while IFS= read -r -d '' original; do
dir=$(dirname "$original")
base=$(basename "$original")
new_base="${base%.jar}+${{ github.run_number }}.jar"
new_path="$dir/$new_base"
mv "$original" "$new_path"
done
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4.3.3 # https://github.com/actions/upload-artifact/releases
uses: actions/upload-artifact@v7 # https://github.com/actions/upload-artifact/releases
with:
name: Click here to download
path: jar/build/libs/TAB-*+${{ github.run_number }}.jar
path: "jar/build/libs/TAB v*+${{ github.run_number }}.jar"
+1 -55
View File
@@ -1,55 +1 @@
# About
TAB aims to be a superior all-in-one minecraft plugin for displaying information that outperforms all
similar plugins in terms of features, performance and compatibility.
More information can be found at [Why TAB?](https://github.com/NEZNAMY/TAB/wiki/Why-TAB%3F) wiki page.
# Download
### Releases
You can download full releases from [GitHub releases](https://github.com/NEZNAMY/TAB/releases),
[SpigotMC](https://www.spigotmc.org/resources/57806/), [Modrinth](https://modrinth.com/plugin/tab-was-taken), [BuiltByBit](https://builtbybit.com/resources/20631) or [CurseForge](https://www.curseforge.com/minecraft/mc-mods/tab).
### Dev builds
Plugin's development is continuous and contains lots of small changes/fixes not worthy of being called a full
update, therefore development builds are being created and eventually a full update is released when enough changes are made.
If you want to benefit from a recent change in the code before official release, you can download development builds
from [GitHub actions](https://github.com/NEZNAMY/TAB/actions) (you must be logged into GitHub to be able to download it).
# Compiling
Compilation requires JDK 21 and up.
To compile the plugin, run `./gradlew build` from the terminal.
Once the plugin compiles, grab the jar from `/jar/build/libs/` folder.
The universal jar contains all modules for all supported platforms.
# Documentation
You can find everything about the plugin on the [Wiki](https://github.com/NEZNAMY/TAB/wiki). This includes a detailed description
of all features, as well as information regarding compatibility or limitations of each feature.
# Issues
The [Issues](https://github.com/NEZNAMY/TAB/issues) section is an issue tracker for all the kind of problems covered in issue templates.
Please refrain from attempting to use it to receive help with your server, such issues will be closed immediately.
When opening an issue, provide as much information as possible. The difference between a bug getting fixed in 1 week vs 1 hour
is in quality of the report. Bug reports where steps to reproduce are "idk" have no value and will not help to get the bug fixed at all.
# Contributing
See [CONTRIBUTING.md](https://github.com/NEZNAMY/TAB/blob/master/CONTRIBUTING.md)
# Provided services
The plugin is being used by a large amount of servers, which also means the demand for my attention is high. Unfortunately,
I am no longer able to keep up with the demand since I don't want to dedicate my whole existence to a free plugin.
In order to provide the best quality of all provided services, the amount of services provided may vary over time.
Here is the full list of potential services and their status:
| Service type | Availability |
|-------------------------------|--------------|
| Updates for new MC versions | ✔ |
| Bug fixes | ✔ |
| Wiki improvements | ✔ |
| Performance optimizations | ✔ |
| Feature requests | ❌ |
| Customer support<sup>1</sup> | ✔ |
| Free user support<sup>2</sup> | ❌* |
<sup>1</sup> - Unrestricted assistance with the plugin for those, who bought a copy of the plugin when it was purchasable.
<sup>2</sup> - Unlimited access to my free time for everyone in the world.
*Instead, you can use [this fresh new community discord](https://discord.gg/YPqXt63YQj) made by a nice person who will provide this service.
This is a backup of the repository at the state of 6.1.1 release.
@@ -1,48 +0,0 @@
package me.neznamy.tab.api.event.plugin;
import lombok.NonNull;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.event.TabEvent;
import org.jetbrains.annotations.NotNull;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Called when an unknown placeholder is about to be registered.
* It defaults to a PlaceholderAPI implementation, but can be overridden.
*/
public interface PlaceholderRegisterEvent extends TabEvent {
/**
* Returns placeholder identifier starting and ending with {@code %}
*
* @return placeholder identifier starting and ending with {@code %}
*/
@NotNull String getIdentifier();
/**
* Sets placeholder to specified server placeholder implementation.
*
* @param supplier
* Placeholder replacer
*/
void setServerPlaceholder(@NonNull Supplier<String> supplier);
/**
* Sets placeholder to specified player placeholder implementation.
*
* @param function
* Placeholder replacer
*/
void setPlayerPlaceholder(@NonNull Function<TabPlayer, String> function);
/**
* Sets placeholder to specified relational placeholder implementation.
*
* @param function
* Placeholder replacer
*/
void setRelationalPlaceholder(@NonNull BiFunction<TabPlayer, TabPlayer, String> function);
}
@@ -8,6 +8,8 @@ import org.jetbrains.annotations.NotNull;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An interface allowing placeholder registration. Instance can be
@@ -65,6 +67,14 @@ public interface PlaceholderManager {
*/
@NotNull RelationalPlaceholder registerRelationalPlaceholder(@NonNull String identifier, int refresh, @NonNull BiFunction<TabPlayer, TabPlayer, String> function);
void registerServerPlaceholder(@NonNull Pattern identifier, int refresh, @NonNull Function<Matcher, Supplier<String>> function);
void registerPlayerPlaceholder(@NonNull Pattern identifier, int refresh,
@NonNull Function<Matcher, Function<me.neznamy.tab.api.TabPlayer, String>> function);
void registerRelationalPlaceholder(@NonNull Pattern identifier, int refresh,
@NonNull Function<Matcher, BiFunction<me.neznamy.tab.api.TabPlayer, me.neznamy.tab.api.TabPlayer, String>> function);
/**
* Returns placeholder from specified identifier. If it does not exist, it is registered
* as a PlaceholderAPI placeholder and returned.
@@ -84,8 +84,24 @@ public interface Scoreboard {
*/
void removeLine(int index);
/**
* Replaces all current lines with the provided list.
* Supports placeholders, which will automatically be refreshed.
* Client can only see up to 15 lines at a time.
* If the new list is identical to the current one, nothing happens.
*
* @param lines
* New list of lines to display
* @throws NullPointerException
* if {@code lines} is {@code null}
* @see #getLines()
* @see #addLine(String)
* @see #removeLine(int)
*/
void setLines(@NonNull List<String> lines);
/**
* Unregisters this scoreboard from all players who can see it.
*/
void unregister();
}
}
@@ -65,6 +65,20 @@ public interface Layout {
*/
void addFixedSlot(int slot, @NonNull String text, @NonNull String skin, int ping);
/**
* Adds fixed slot with specified parameters
*
* @param slot
* Slot (1-80)
* @param text
* Text to display
* @param skin
* Skin definition like in config
* @param ping
* Slot's ping, accepts placeholders that evaluate to a number
*/
void addFixedSlot(int slot, @NonNull String text, @NonNull String skin, @NonNull String ping);
/**
* Adds a player group.
*
@@ -24,6 +24,18 @@ public interface LayoutManager {
@NotNull
Layout createNewLayout(@NonNull String name);
/**
* Creates new layout with given name and slot count (1.19.3+ only).
*
* @param name
* Unique layout name
* @param slotCount
* Amount of slots to use for 1.19.3+ players
* @return Created layout
*/
@NotNull
Layout createNewLayout(@NonNull String name, int slotCount);
/**
* Returns defined layout from config by name. If no such layout is defined in config,
* {@code null} is returned.
+3 -3
View File
@@ -3,7 +3,7 @@ plugins {
}
dependencies {
implementation("com.gradleup.shadow:shadow-gradle-plugin:9.2.2")
implementation("io.freefair.gradle:lombok-plugin:9.0.0")
implementation("dev.architectury.loom:dev.architectury.loom.gradle.plugin:1.13-SNAPSHOT")
implementation("com.gradleup.shadow:shadow-gradle-plugin:9.4.2")
implementation("io.freefair.gradle:lombok-plugin:9.5.0")
implementation("dev.architectury.loom-no-remap:dev.architectury.loom-no-remap.gradle.plugin:1.17-SNAPSHOT")
}
@@ -13,7 +13,8 @@ tasks {
"version" to project.version,
"description" to project.description,
"website" to rootProject.ext.get("website")!!.toString(),
"author" to rootProject.ext.get("author")!!.toString()
"author" to rootProject.ext.get("author")!!.toString(),
"credits" to rootProject.ext.get("credits")!!.toString()
))
}
javadoc {
@@ -31,6 +32,6 @@ tasks {
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
languageVersion.set(JavaLanguageVersion.of(25))
}
}
+7 -19
View File
@@ -4,7 +4,7 @@ plugins {
allprojects {
group = "me.neznamy"
version = "5.5.0"
version = "6.1.1"
description = "An all-in-one solution that works"
ext.set("id", "tab")
@@ -20,27 +20,12 @@ val platformPaths = setOf(
":bukkit:paper_1_21_4",
":bukkit:paper_1_21_9",
":bukkit:paper_1_21_11",
":bukkit:v1_7_R1",
":bukkit:v1_7_R2",
":bukkit:v1_7_R3",
":bukkit:paper_26_2",
":bukkit:v1_7_R4",
":bukkit:v1_8_R1",
":bukkit:v1_8_R2",
":bukkit:v1_8_R3",
":bukkit:v1_9_R1",
":bukkit:v1_9_R2",
":bukkit:v1_10_R1",
":bukkit:v1_11_R1",
":bukkit:v1_12_R1",
":bukkit:v1_13_R1",
":bukkit:v1_13_R2",
":bukkit:v1_14_R1",
":bukkit:v1_15_R1",
":bukkit:v1_16_R1",
":bukkit:v1_16_R2",
":bukkit:v1_16_R3",
":bukkit:v1_17_R1",
":bukkit:v1_18_R1",
":bukkit:v1_18_R2",
":bukkit:v1_19_R1",
":bukkit:v1_19_R2",
@@ -56,11 +41,14 @@ val platformPaths = setOf(
":bukkit:v1_21_R5",
":bukkit:v1_21_R6",
":bukkit:v1_21_R7",
":bukkit:v26_1",
":bukkit:v26_2",
":bungeecord",
":velocity",
":fabric",
":neoforge",
":forge"
":fand",
":neoforge"
// ":forge"
)
val specialPaths = setOf(
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
id("io.papermc.paperweight.userdev") version "2.0.0-beta.19"
id("io.papermc.paperweight.userdev") version "2.0.0-beta.21"
}
repositories {
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_20_5;
package me.neznamy.tab.platforms.bukkit.paper_1_20_5;
import me.neznamy.tab.shared.chat.TabStyle;
import me.neznamy.tab.shared.chat.component.object.TabAtlasSprite;
@@ -12,7 +12,7 @@ import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct mojang-mapped code.
*/
public class PaperComponentConverter extends ComponentConverter<Component> {
public class NMSComponentConverter extends ComponentConverter<Component> {
@Override
@NotNull
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.bukkit.v1_18_R1;
package me.neznamy.tab.platforms.bukkit.paper_1_20_5;
import io.netty.channel.Channel;
import lombok.Getter;
@@ -8,12 +8,12 @@ import me.neznamy.tab.platforms.bukkit.provider.ImplementationProvider;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.TabListEntryTracker;
import org.bukkit.craftbukkit.v1_18_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Implementation provider using direct NMS code for 1.18 - 1.18.1.
* Implementation provider using direct Mojang-mapped NMS code for paper 1.20.5 - 1.21.1.
*/
@Getter
public class NMSImplementationProvider implements ImplementationProvider {
@@ -36,13 +36,13 @@ public class NMSImplementationProvider implements ImplementationProvider {
@Override
@NotNull
public Channel getChannel(@NotNull Player player) {
return ((CraftPlayer)player).getHandle().b.a.k;
return ((CraftPlayer)player).getHandle().connection.connection.channel;
}
@Override
@NotNull
public TabListEntryTracker newTabListEntryTracker() {
return new NMSTabListEntryTracker();
public TabListEntryTracker newTabListEntryTracker(@NotNull Player player) {
return new NMSTabListEntryTracker(getChannel(player));
}
@Override
@@ -1,11 +1,10 @@
package me.neznamy.tab.platforms.paper_1_20_5;
package me.neznamy.tab.platforms.bukkit.paper_1_20_5;
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;
@@ -17,29 +16,36 @@ 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.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
/**
* Scoreboard implementation using direct mojang-mapped code.
*/
public class PaperPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
public class NMSPacketScoreboard 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 players = ReflectionUtils.getOnlyField(ClientboundSetPlayerTeamPacket.class, Collection.class);
private static final Constructor<ClientboundSetPlayerTeamPacket> teamConstructor;
static {
try {
teamConstructor = ClientboundSetPlayerTeamPacket.class.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class);
teamConstructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public PaperPacketScoreboard(@NotNull BukkitTabPlayer player) {
public NMSPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
@@ -144,7 +150,15 @@ public class PaperPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
if (packet instanceof ClientboundSetPlayerTeamPacket team) {
int action = getMethod(team);
if (action != TeamAction.UPDATE) {
players.set(team, onTeamPacket(action, team.getName(), team.getPlayers() == null ? Collections.emptyList() : team.getPlayers()));
Collection<String> players = team.getPlayers();
if (players != null) {
return teamConstructor.newInstance(
team.getName(),
action,
team.getParameters(),
onTeamPacket(action, team.getName(), players)
);
}
}
}
return packet;
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_20_5;
package me.neznamy.tab.platforms.bukkit.paper_1_20_5;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
@@ -6,6 +6,7 @@ import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import net.minecraft.network.chat.Component;
@@ -23,7 +24,7 @@ import java.util.*;
/**
* TabList implementation using direct mojang-mapped code.
*/
public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
public class NMSPacketTabList 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);
@@ -37,7 +38,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
* @param player
* Player this tablist will belong to
*/
public PaperPacketTabList(@NotNull BukkitTabPlayer player) {
public NMSPacketTabList(@NotNull BukkitTabPlayer player) {
super(player);
}
@@ -135,7 +136,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
listed = false;
rewriteEntry = rewritePacket = true;
}
@@ -1,6 +1,7 @@
package me.neznamy.tab.platforms.paper_1_21_2;
package me.neznamy.tab.platforms.bukkit.paper_1_20_5;
import me.neznamy.tab.shared.platform.TabListEntryTracker;
import io.netty.channel.Channel;
import me.neznamy.tab.shared.platform.NettyTabListEntryTracker;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import org.jetbrains.annotations.NotNull;
@@ -10,7 +11,11 @@ import java.util.UUID;
/**
* Implementation of TabListEntryTracker.
*/
public class PaperTabListEntryTracker extends TabListEntryTracker {
public class NMSTabListEntryTracker extends NettyTabListEntryTracker {
public NMSTabListEntryTracker(@NotNull Channel channel) {
super(channel);
}
@Override
public void onPacketSend(@NotNull Object packet) {
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
id("io.papermc.paperweight.userdev") version "2.0.0-beta.19"
id("io.papermc.paperweight.userdev") version "2.0.0-beta.21"
}
repositories {
@@ -0,0 +1,63 @@
package me.neznamy.tab.platforms.bukkit.paper_1_21_11;
import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import net.minecraft.network.chat.numbers.FixedFormat;
import net.minecraft.network.protocol.game.ClientboundSetObjectivePacket;
import net.minecraft.world.scores.criteria.ObjectiveCriteria;
import org.jetbrains.annotations.NotNull;
/**
* Scoreboard override for Canvas (Folia fork) that implements scoreboards with
* new checks in NMS setters since 26.1.2.
*/
public class CanvasPacketScoreboard extends NMSPacketScoreboard {
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public CanvasPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
/**
* Updates the objective by creating a new one instead of updating the existing one,
* which would require sync access on Canvas.
*
* @param objective
* Objective to update
*/
@Override
public void updateObjective(@NonNull Objective objective) {
net.minecraft.world.scores.Objective obj = new net.minecraft.world.scores.Objective(
dummyScoreboard,
objective.getName(),
ObjectiveCriteria.DUMMY,
objective.getTitle().convert(),
ObjectiveCriteria.RenderType.values()[objective.getHealthDisplay().ordinal()],
false,
objective.getNumberFormat() == null ? null : objective.getNumberFormat().toFixedFormat(FixedFormat::new)
);
objective.setPlatformObjective(obj);
sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.UPDATE));
}
@Override
public void registerTeam(@NonNull Team team) {
player.getPlatform().runSyncGlobal(() -> super.registerTeam(team));
}
@Override
public void unregisterTeam(@NonNull Team team) {
// This one too, to avoid register+unregister causing unregister to run first
player.getPlatform().runSyncGlobal(() -> super.unregisterTeam(team));
}
@Override
public void updateTeam(@NonNull Team team) {
player.getPlatform().runSyncGlobal(() -> super.updateTeam(team));
}
}
@@ -0,0 +1,84 @@
package me.neznamy.tab.platforms.bukkit.paper_1_21_11;
import com.google.common.collect.ImmutableMultimap;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
import me.neznamy.tab.platforms.bukkit.provider.ComponentConverter;
import me.neznamy.tab.shared.chat.TabStyle;
import me.neznamy.tab.shared.chat.component.object.TabAtlasSprite;
import me.neznamy.tab.shared.chat.component.object.TabPlayerSprite;
import me.neznamy.tab.shared.platform.TabList;
import net.minecraft.network.chat.*;
import net.minecraft.network.chat.contents.objects.AtlasSprite;
import net.minecraft.network.chat.contents.objects.PlayerSprite;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.component.ResolvableProfile;
import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct mojang-mapped code.
*/
public class NMSComponentConverter extends ComponentConverter<Component> {
@Override
@NotNull
public Component newTextComponent(@NotNull String text) {
return Component.literal(text);
}
@Override
@NotNull
public Component newTranslatableComponent(@NotNull String key) {
return Component.translatable(key);
}
@Override
@NotNull
public Component newKeybindComponent(@NotNull String keybind) {
return Component.keybind(keybind);
}
@Override
@NotNull
public Component newObjectComponent(@NotNull TabAtlasSprite sprite) {
return Component.object(new AtlasSprite(Identifier.parse(sprite.getAtlas()), Identifier.parse(sprite.getSprite())));
}
@Override
@NotNull
public Component newObjectComponent(@NotNull TabPlayerSprite sprite) {
ResolvableProfile profile;
if (sprite.getId() != null) {
profile = ResolvableProfile.createUnresolved(sprite.getId());
} else if (sprite.getName() != null) {
profile = ResolvableProfile.createUnresolved(sprite.getName());
} else if (sprite.getSkin() != null) {
ImmutableMultimap.Builder<String, Property> builder = ImmutableMultimap.builder();
builder.put(TabList.TEXTURES_PROPERTY, new Property(TabList.TEXTURES_PROPERTY, sprite.getSkin().getValue(), sprite.getSkin().getSignature()));
profile = ResolvableProfile.createResolved(new GameProfile(NIL_UUID, "", new PropertyMap(builder.build())));
} else {
throw new IllegalStateException("Player head component does not have id, name or skin set");
}
return Component.object(new PlayerSprite(profile, sprite.isShowHat()));
}
@Override
public void applyStyle(@NotNull Component nmsComponent, @NotNull TabStyle 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 : new FontDescription.Resource(Identifier.parse(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 Component parent, @NotNull Component child) {
((MutableComponent)parent).append(child);
}
}
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_2;
package me.neznamy.tab.platforms.bukkit.paper_1_21_11;
import io.netty.channel.Channel;
import lombok.Getter;
@@ -8,30 +8,37 @@ import me.neznamy.tab.platforms.bukkit.provider.ImplementationProvider;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.TabListEntryTracker;
import me.neznamy.tab.shared.util.ReflectionUtils;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Implementation provider using direct Mojang-mapped NMS code for versions 1.21.2 - 1.21.3.
* Implementation provider using direct Mojang-mapped NMS code for Paper 1.21.11 - 26.1.2.
*/
@Getter
public class PaperImplementationProvider implements ImplementationProvider {
public class NMSImplementationProvider implements ImplementationProvider {
/** Flag tracking if this server is Canvas 26.1.2+ with new scoreboard checks */
private final boolean isCanvas = ReflectionUtils.classExists("io.canvasmc.canvas.world.scores.TeamData");
@NotNull
private final ComponentConverter<?> componentConverter = new PaperComponentConverter();
private final ComponentConverter<?> componentConverter = new NMSComponentConverter();
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
return new PaperPacketScoreboard(player);
if (isCanvas) {
return new CanvasPacketScoreboard(player);
} else {
return new NMSPacketScoreboard(player);
}
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return new PaperPacketTabList(player);
return new NMSPacketTabList(player);
}
@Override
@@ -42,8 +49,8 @@ public class PaperImplementationProvider implements ImplementationProvider {
@Override
@NotNull
public TabListEntryTracker newTabListEntryTracker() {
return new PaperTabListEntryTracker();
public TabListEntryTracker newTabListEntryTracker(@NotNull Player player) {
return new NMSTabListEntryTracker(getChannel(player));
}
@Override
@@ -0,0 +1,190 @@
package me.neznamy.tab.platforms.bukkit.paper_1_21_11;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import net.minecraft.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.Constructor;
import java.util.Collection;
import java.util.Optional;
/**
* Scoreboard implementation using direct mojang-mapped code.
*/
public class NMSPacketScoreboard 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();
protected static final Scoreboard dummyScoreboard = new Scoreboard();
private static final Constructor<ClientboundSetPlayerTeamPacket> teamConstructor;
static {
try {
teamConstructor = ClientboundSetPlayerTeamPacket.class.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class);
teamConstructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public NMSPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
@Override
public void registerObjective(@NonNull Objective objective) {
net.minecraft.world.scores.Objective obj = new net.minecraft.world.scores.Objective(
dummyScoreboard,
objective.getName(),
ObjectiveCriteria.DUMMY,
objective.getTitle().convert(),
RenderType.values()[objective.getHealthDisplay().ordinal()],
false,
objective.getNumberFormat() == null ? null : objective.getNumberFormat().toFixedFormat(FixedFormat::new)
);
objective.setPlatformObjective(obj);
sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.REGISTER));
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
sendPacket(new ClientboundSetDisplayObjectivePacket(
net.minecraft.world.scores.DisplaySlot.values()[objective.getDisplaySlot().ordinal()],
(net.minecraft.world.scores.Objective) objective.getPlatformObjective()
));
}
@Override
public void unregisterObjective(@NonNull Objective objective) {
sendPacket(new ClientboundSetObjectivePacket((net.minecraft.world.scores.Objective) objective.getPlatformObjective(), ObjectiveAction.UNREGISTER));
}
@Override
public void updateObjective(@NonNull Objective objective) {
net.minecraft.world.scores.Objective obj = (net.minecraft.world.scores.Objective) objective.getPlatformObjective();
obj.setDisplayName(objective.getTitle().convert());
obj.setRenderType(RenderType.values()[objective.getHealthDisplay().ordinal()]);
sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.UPDATE));
}
@Override
public void setScore(@NonNull Score score) {
sendPacket(new ClientboundSetScorePacket(
score.getHolder(),
score.getObjective().getName(),
score.getValue(),
Optional.ofNullable(score.getDisplayName() == null ? null : score.getDisplayName().convert()),
Optional.ofNullable(score.getNumberFormat() == null ? null : score.getNumberFormat().toFixedFormat(FixedFormat::new))
));
}
@Override
public void removeScore(@NonNull Score score) {
sendPacket(new ClientboundResetScorePacket(score.getHolder(), score.getObjective().getName()));
}
@Override
@NotNull
public Object createTeam(@NonNull String name) {
return new PlayerTeam(dummyScoreboard, name);
}
@Override
public void registerTeam(@NonNull Team team) {
updateTeamProperties(team);
PlayerTeam t = (PlayerTeam) team.getPlatformTeam();
t.getPlayers().addAll(team.getPlayers());
sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(t, true));
}
@Override
public void unregisterTeam(@NonNull Team team) {
sendPacket(ClientboundSetPlayerTeamPacket.createRemovePacket((PlayerTeam) team.getPlatformTeam()));
}
@Override
public void updateTeam(@NonNull Team team) {
updateTeamProperties(team);
sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket((PlayerTeam) team.getPlatformTeam(), false));
}
private void updateTeamProperties(@NonNull Team team) {
PlayerTeam t = (PlayerTeam) team.getPlatformTeam();
t.setAllowFriendlyFire((team.getOptions() & 0x01) != 0);
t.setSeeFriendlyInvisibles((team.getOptions() & 0x02) != 0);
t.setColor(formats[team.getColor().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
@NotNull
public Object onPacketSend(@NonNull Object packet) {
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 (packet instanceof ClientboundSetPlayerTeamPacket team) {
int action = getMethod(team);
if (action != TeamAction.UPDATE) {
Collection<String> players = team.getPlayers();
if (players != null) {
return teamConstructor.newInstance(
team.getName(),
action,
team.getParameters(),
onTeamPacket(action, team.getName(), players)
);
}
}
}
return packet;
}
private int getMethod(@NonNull ClientboundSetPlayerTeamPacket team) {
if (team.getTeamAction() == ClientboundSetPlayerTeamPacket.Action.ADD) {
return 0;
} else if (team.getTeamAction() == ClientboundSetPlayerTeamPacket.Action.REMOVE) {
return 1;
} else if (team.getPlayerAction() == ClientboundSetPlayerTeamPacket.Action.ADD) {
return 3;
} else if (team.getPlayerAction() == ClientboundSetPlayerTeamPacket.Action.REMOVE) {
return 4;
} else {
return 2;
}
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
protected void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().connection.send(packet);
}
}
@@ -0,0 +1,205 @@
package me.neznamy.tab.platforms.bukkit.paper_1_21_11;
import com.google.common.collect.ImmutableMultimap;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
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.util.*;
/**
* TabList implementation using direct mojang-mapped code.
*/
public class NMSPacketTabList 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);
/**
* Constructs new instance.
*
* @param player
* Player this tablist will belong to
*/
public NMSPacketTabList(@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 setPlayerListHeaderFooter0(@NonNull TabComponent header, @NonNull TabComponent footer) {
sendPacket(new ClientboundTabListPacket(header.convert(), footer.convert()));
}
@Override
@Nullable
public Skin getSkin() {
Collection<Property> properties = ((CraftPlayer)player.getPlayer()).getProfile().properties().get(TEXTURES_PROPERTY);
if (properties.isEmpty()) return null; // Offline mode
Property property = properties.iterator().next();
return new Skin(property.value(), property.signature());
}
@Override
@NotNull
public Object onPacketSend(@NonNull Object packet) {
if (packet instanceof ClientboundTabListPacket tablist) {
if (header == null || footer == null) return packet;
if (tablist.header() != header.convert() || tablist.footer() != footer.convert()) {
return new ClientboundTabListPacket(header.convert(), footer.convert());
}
}
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();
int gameMode = nmsData.gameMode().getId();
boolean listed = nmsData.listed();
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME)) {
TabComponent forcedDisplayName = getForcedDisplayNames().get(nmsData.profileId());
if (forcedDisplayName != null && forcedDisplayName.convert() != displayName) {
displayName = forcedDisplayName.convert();
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE)) {
if (getBlockedSpectators().contains(nmsData.profileId()) && gameMode == 3) {
gameMode = 0;
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY)) {
if (getForcedLatency() != null) {
latency = getForcedLatency();
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
listed = false;
rewriteEntry = rewritePacket = true;
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER)) {
TAB.getInstance().getFeatureManager().onEntryAdd(player, nmsData.profileId(), nmsData.profile().name());
}
updatedList.add(rewriteEntry ? new ClientboundPlayerInfoUpdatePacket.Entry(
nmsData.profileId(), nmsData.profile(), listed, latency, GameType.byId(gameMode), displayName,
nmsData.showHat(), nmsData.listOrder(), nmsData.chatSession()
) : nmsData);
}
if (rewritePacket) return new ClientboundPlayerInfoUpdatePacket(actions, updatedList);
}
return packet;
}
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) {
ImmutableMultimap.Builder<String, Property> builder = ImmutableMultimap.builder();
if (skin != null) {
builder.put(TEXTURES_PROPERTY, new Property(TEXTURES_PROPERTY, skin.getValue(), skin.getSignature()));
}
return new GameProfile(id, name, new PropertyMap(builder.build()));
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
private void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().connection.send(packet);
}
}
@@ -0,0 +1,35 @@
package me.neznamy.tab.platforms.bukkit.paper_1_21_11;
import io.netty.channel.Channel;
import me.neznamy.tab.shared.platform.NettyTabListEntryTracker;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* Implementation of TabListEntryTracker.
*/
public class NMSTabListEntryTracker extends NettyTabListEntryTracker {
public NMSTabListEntryTracker(@NotNull Channel channel) {
super(channel);
}
@Override
public void onPacketSend(@NotNull Object packet) {
if (packet instanceof ClientboundPlayerInfoRemovePacket remove) {
for (UUID id : remove.profileIds()) {
tablistEntries.remove(id);
}
}
if (packet instanceof ClientboundPlayerInfoUpdatePacket update) {
if (update.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER)) {
for (ClientboundPlayerInfoUpdatePacket.Entry nmsData : update.entries()) {
tablistEntries.add(nmsData.profileId());
}
}
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
id("io.papermc.paperweight.userdev") version "2.0.0-beta.19"
id("io.papermc.paperweight.userdev") version "2.0.0-beta.21"
}
repositories {
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_2;
package me.neznamy.tab.platforms.bukkit.paper_1_21_2;
import me.neznamy.tab.shared.chat.TabStyle;
import me.neznamy.tab.shared.chat.component.object.TabAtlasSprite;
@@ -15,7 +15,7 @@ import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct mojang-mapped code.
*/
public class PaperComponentConverter extends ComponentConverter<Component> {
public class NMSComponentConverter extends ComponentConverter<Component> {
@Override
@NotNull
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_11;
package me.neznamy.tab.platforms.bukkit.paper_1_21_2;
import io.netty.channel.Channel;
import lombok.Getter;
@@ -11,27 +11,26 @@ import me.neznamy.tab.shared.platform.TabListEntryTracker;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Implementation provider using direct Mojang-mapped NMS code for version 1.21.11.
* Implementation provider using direct Mojang-mapped NMS code for paper 1.21.2 - 1.21.3.
*/
@Getter
public class PaperImplementationProvider implements ImplementationProvider {
public class NMSImplementationProvider implements ImplementationProvider {
@NotNull
private final ComponentConverter<?> componentConverter = new PaperComponentConverter();
private final ComponentConverter<?> componentConverter = new NMSComponentConverter();
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
return new PaperPacketScoreboard(player);
return new NMSPacketScoreboard(player);
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return new PaperPacketTabList(player);
return new NMSPacketTabList(player);
}
@Override
@@ -42,8 +41,8 @@ public class PaperImplementationProvider implements ImplementationProvider {
@Override
@NotNull
public TabListEntryTracker newTabListEntryTracker() {
return new PaperTabListEntryTracker();
public TabListEntryTracker newTabListEntryTracker(@NotNull Player player) {
return new NMSTabListEntryTracker(getChannel(player));
}
@Override
@@ -1,11 +1,10 @@
package me.neznamy.tab.platforms.paper_1_21_2;
package me.neznamy.tab.platforms.bukkit.paper_1_21_2;
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;
@@ -17,29 +16,36 @@ 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.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
/**
* Scoreboard implementation using direct mojang-mapped code.
*/
public class PaperPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
public class NMSPacketScoreboard 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 players = ReflectionUtils.getOnlyField(ClientboundSetPlayerTeamPacket.class, Collection.class);
private static final Constructor<ClientboundSetPlayerTeamPacket> teamConstructor;
static {
try {
teamConstructor = ClientboundSetPlayerTeamPacket.class.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class);
teamConstructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public PaperPacketScoreboard(@NotNull BukkitTabPlayer player) {
public NMSPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
@@ -144,7 +150,15 @@ public class PaperPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
if (packet instanceof ClientboundSetPlayerTeamPacket team) {
int action = getMethod(team);
if (action != TeamAction.UPDATE) {
players.set(team, onTeamPacket(action, team.getName(), team.getPlayers() == null ? Collections.emptyList() : team.getPlayers()));
Collection<String> players = team.getPlayers();
if (players != null) {
return teamConstructor.newInstance(
team.getName(),
action,
team.getParameters(),
onTeamPacket(action, team.getName(), players)
);
}
}
}
return packet;
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_2;
package me.neznamy.tab.platforms.bukkit.paper_1_21_2;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
@@ -6,6 +6,7 @@ import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import net.minecraft.network.chat.Component;
@@ -23,7 +24,7 @@ import java.util.*;
/**
* TabList implementation using direct mojang-mapped code.
*/
public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
public class NMSPacketTabList 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);
@@ -38,7 +39,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
* @param player
* Player this tablist will belong to
*/
public PaperPacketTabList(@NotNull BukkitTabPlayer player) {
public NMSPacketTabList(@NotNull BukkitTabPlayer player) {
super(player);
}
@@ -136,7 +137,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
listed = false;
rewriteEntry = rewritePacket = true;
}
@@ -0,0 +1,35 @@
package me.neznamy.tab.platforms.bukkit.paper_1_21_2;
import io.netty.channel.Channel;
import me.neznamy.tab.shared.platform.NettyTabListEntryTracker;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* Implementation of TabListEntryTracker.
*/
public class NMSTabListEntryTracker extends NettyTabListEntryTracker {
public NMSTabListEntryTracker(@NotNull Channel channel) {
super(channel);
}
@Override
public void onPacketSend(@NotNull Object packet) {
if (packet instanceof ClientboundPlayerInfoRemovePacket remove) {
for (UUID id : remove.profileIds()) {
tablistEntries.remove(id);
}
}
if (packet instanceof ClientboundPlayerInfoUpdatePacket update) {
if (update.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER)) {
for (ClientboundPlayerInfoUpdatePacket.Entry nmsData : update.entries()) {
tablistEntries.add(nmsData.profileId());
}
}
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
id("io.papermc.paperweight.userdev") version "2.0.0-beta.19"
id("io.papermc.paperweight.userdev") version "2.0.0-beta.21"
}
repositories {
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_4;
package me.neznamy.tab.platforms.bukkit.paper_1_21_4;
import me.neznamy.tab.shared.chat.TabStyle;
import me.neznamy.tab.shared.chat.component.object.TabAtlasSprite;
@@ -12,7 +12,7 @@ import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct mojang-mapped code.
*/
public class PaperComponentConverter extends ComponentConverter<Component> {
public class NMSComponentConverter extends ComponentConverter<Component> {
@Override
@NotNull
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_4;
package me.neznamy.tab.platforms.bukkit.paper_1_21_4;
import io.netty.channel.Channel;
import lombok.Getter;
@@ -11,27 +11,26 @@ import me.neznamy.tab.shared.platform.TabListEntryTracker;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Implementation provider using direct Mojang-mapped NMS code for versions 1.21.4 - 1.21.8.
* Implementation provider using direct Mojang-mapped NMS code for paper 1.21.4 - 1.21.8.
*/
@Getter
public class PaperImplementationProvider implements ImplementationProvider {
public class NMSImplementationProvider implements ImplementationProvider {
@NotNull
private final ComponentConverter<?> componentConverter = new PaperComponentConverter();
private final ComponentConverter<?> componentConverter = new NMSComponentConverter();
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
return new PaperPacketScoreboard(player);
return new NMSPacketScoreboard(player);
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return new PaperPacketTabList(player);
return new NMSPacketTabList(player);
}
@Override
@@ -42,8 +41,8 @@ public class PaperImplementationProvider implements ImplementationProvider {
@Override
@NotNull
public TabListEntryTracker newTabListEntryTracker() {
return new PaperTabListEntryTracker();
public TabListEntryTracker newTabListEntryTracker(@NotNull Player player) {
return new NMSTabListEntryTracker(getChannel(player));
}
@Override
@@ -1,11 +1,10 @@
package me.neznamy.tab.platforms.paper_1_21_11;
package me.neznamy.tab.platforms.bukkit.paper_1_21_4;
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;
@@ -17,29 +16,36 @@ 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.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
/**
* Scoreboard implementation using direct mojang-mapped code.
*/
public class PaperPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
public class NMSPacketScoreboard 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 players = ReflectionUtils.getOnlyField(ClientboundSetPlayerTeamPacket.class, Collection.class);
private static final Constructor<ClientboundSetPlayerTeamPacket> teamConstructor;
static {
try {
teamConstructor = ClientboundSetPlayerTeamPacket.class.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class);
teamConstructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public PaperPacketScoreboard(@NotNull BukkitTabPlayer player) {
public NMSPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
@@ -144,7 +150,15 @@ public class PaperPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
if (packet instanceof ClientboundSetPlayerTeamPacket team) {
int action = getMethod(team);
if (action != TeamAction.UPDATE) {
players.set(team, onTeamPacket(action, team.getName(), team.getPlayers() == null ? Collections.emptyList() : team.getPlayers()));
Collection<String> players = team.getPlayers();
if (players != null) {
return teamConstructor.newInstance(
team.getName(),
action,
team.getParameters(),
onTeamPacket(action, team.getName(), players)
);
}
}
}
return packet;
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_4;
package me.neznamy.tab.platforms.bukkit.paper_1_21_4;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
@@ -6,6 +6,7 @@ import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import net.minecraft.network.chat.Component;
@@ -23,7 +24,7 @@ import java.util.*;
/**
* TabList implementation using direct mojang-mapped code.
*/
public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
public class NMSPacketTabList 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);
@@ -39,7 +40,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
* @param player
* Player this tablist will belong to
*/
public PaperPacketTabList(@NotNull BukkitTabPlayer player) {
public NMSPacketTabList(@NotNull BukkitTabPlayer player) {
super(player);
}
@@ -137,7 +138,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
listed = false;
rewriteEntry = rewritePacket = true;
}
@@ -0,0 +1,35 @@
package me.neznamy.tab.platforms.bukkit.paper_1_21_4;
import io.netty.channel.Channel;
import me.neznamy.tab.shared.platform.NettyTabListEntryTracker;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* Implementation of TabListEntryTracker.
*/
public class NMSTabListEntryTracker extends NettyTabListEntryTracker {
public NMSTabListEntryTracker(@NotNull Channel channel) {
super(channel);
}
@Override
public void onPacketSend(@NotNull Object packet) {
if (packet instanceof ClientboundPlayerInfoRemovePacket remove) {
for (UUID id : remove.profileIds()) {
tablistEntries.remove(id);
}
}
if (packet instanceof ClientboundPlayerInfoUpdatePacket update) {
if (update.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER)) {
for (ClientboundPlayerInfoUpdatePacket.Entry nmsData : update.entries()) {
tablistEntries.add(nmsData.profileId());
}
}
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
id("io.papermc.paperweight.userdev") version "2.0.0-beta.19"
id("io.papermc.paperweight.userdev") version "2.0.0-beta.21"
}
repositories {
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_9;
package me.neznamy.tab.platforms.bukkit.paper_1_21_9;
import com.google.common.collect.ImmutableMultimap;
import com.mojang.authlib.GameProfile;
@@ -19,7 +19,7 @@ import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct mojang-mapped code.
*/
public class PaperComponentConverter extends ComponentConverter<Component> {
public class NMSComponentConverter extends ComponentConverter<Component> {
@Override
@NotNull
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_20_5;
package me.neznamy.tab.platforms.bukkit.paper_1_21_9;
import io.netty.channel.Channel;
import lombok.Getter;
@@ -11,27 +11,26 @@ import me.neznamy.tab.shared.platform.TabListEntryTracker;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Implementation provider using direct Mojang-mapped NMS code for versions 1.20.5 - 1.21.1.
* Implementation provider using direct Mojang-mapped NMS code for paper 1.21.9 - 1.21.10.
*/
@Getter
public class PaperImplementationProvider implements ImplementationProvider {
public class NMSImplementationProvider implements ImplementationProvider {
@NotNull
private final ComponentConverter<?> componentConverter = new PaperComponentConverter();
private final ComponentConverter<?> componentConverter = new NMSComponentConverter();
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
return new PaperPacketScoreboard(player);
return new NMSPacketScoreboard(player);
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return new PaperPacketTabList(player);
return new NMSPacketTabList(player);
}
@Override
@@ -42,8 +41,8 @@ public class PaperImplementationProvider implements ImplementationProvider {
@Override
@NotNull
public TabListEntryTracker newTabListEntryTracker() {
return new PaperTabListEntryTracker();
public TabListEntryTracker newTabListEntryTracker(@NotNull Player player) {
return new NMSTabListEntryTracker(getChannel(player));
}
@Override
@@ -1,11 +1,10 @@
package me.neznamy.tab.platforms.paper_1_21_9;
package me.neznamy.tab.platforms.bukkit.paper_1_21_9;
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;
@@ -17,29 +16,36 @@ 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.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
/**
* Scoreboard implementation using direct mojang-mapped code.
*/
public class PaperPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
public class NMSPacketScoreboard 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 players = ReflectionUtils.getOnlyField(ClientboundSetPlayerTeamPacket.class, Collection.class);
private static final Constructor<ClientboundSetPlayerTeamPacket> teamConstructor;
static {
try {
teamConstructor = ClientboundSetPlayerTeamPacket.class.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class);
teamConstructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public PaperPacketScoreboard(@NotNull BukkitTabPlayer player) {
public NMSPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
@@ -144,7 +150,15 @@ public class PaperPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
if (packet instanceof ClientboundSetPlayerTeamPacket team) {
int action = getMethod(team);
if (action != TeamAction.UPDATE) {
players.set(team, onTeamPacket(action, team.getName(), team.getPlayers() == null ? Collections.emptyList() : team.getPlayers()));
Collection<String> players = team.getPlayers();
if (players != null) {
return teamConstructor.newInstance(
team.getName(),
action,
team.getParameters(),
onTeamPacket(action, team.getName(), players)
);
}
}
}
return packet;
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_9;
package me.neznamy.tab.platforms.bukkit.paper_1_21_9;
import com.google.common.collect.ImmutableMultimap;
import com.mojang.authlib.GameProfile;
@@ -8,6 +8,7 @@ import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import net.minecraft.network.chat.Component;
@@ -25,7 +26,7 @@ import java.util.*;
/**
* TabList implementation using direct mojang-mapped code.
*/
public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
public class NMSPacketTabList 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);
@@ -41,7 +42,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
* @param player
* Player this tablist will belong to
*/
public PaperPacketTabList(@NotNull BukkitTabPlayer player) {
public NMSPacketTabList(@NotNull BukkitTabPlayer player) {
super(player);
}
@@ -139,7 +140,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
listed = false;
rewriteEntry = rewritePacket = true;
}
@@ -0,0 +1,35 @@
package me.neznamy.tab.platforms.bukkit.paper_1_21_9;
import io.netty.channel.Channel;
import me.neznamy.tab.shared.platform.NettyTabListEntryTracker;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* Implementation of TabListEntryTracker.
*/
public class NMSTabListEntryTracker extends NettyTabListEntryTracker {
public NMSTabListEntryTracker(@NotNull Channel channel) {
super(channel);
}
@Override
public void onPacketSend(@NotNull Object packet) {
if (packet instanceof ClientboundPlayerInfoRemovePacket remove) {
for (UUID id : remove.profileIds()) {
tablistEntries.remove(id);
}
}
if (packet instanceof ClientboundPlayerInfoUpdatePacket update) {
if (update.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER)) {
for (ClientboundPlayerInfoUpdatePacket.Entry nmsData : update.entries()) {
tablistEntries.add(nmsData.profileId());
}
}
}
}
}
@@ -1,53 +0,0 @@
package me.neznamy.tab.platforms.paper_1_21_9;
import io.netty.channel.Channel;
import lombok.Getter;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.provider.ComponentConverter;
import me.neznamy.tab.platforms.bukkit.provider.ImplementationProvider;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.TabListEntryTracker;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Implementation provider using direct Mojang-mapped NMS code for versions 1.21.9 - 1.21.10.
*/
@Getter
public class PaperImplementationProvider implements ImplementationProvider {
@NotNull
private final ComponentConverter<?> componentConverter = new PaperComponentConverter();
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
return new PaperPacketScoreboard(player);
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return new PaperPacketTabList(player);
}
@Override
@NotNull
public Channel getChannel(@NotNull Player player) {
return ((CraftPlayer)player).getHandle().connection.connection.channel;
}
@Override
@NotNull
public TabListEntryTracker newTabListEntryTracker() {
return new PaperTabListEntryTracker();
}
@Override
public int getPing(@NotNull BukkitTabPlayer player) {
return player.getPlayer().getPing();
}
}
@@ -1,30 +0,0 @@
package me.neznamy.tab.platforms.paper_1_21_9;
import me.neznamy.tab.shared.platform.TabListEntryTracker;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* Implementation of TabListEntryTracker.
*/
public class PaperTabListEntryTracker extends TabListEntryTracker {
@Override
public void onPacketSend(@NotNull Object packet) {
if (packet instanceof ClientboundPlayerInfoRemovePacket remove) {
for (UUID id : remove.profileIds()) {
tablistEntries.remove(id);
}
}
if (packet instanceof ClientboundPlayerInfoUpdatePacket update) {
if (update.actions().contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER)) {
for (ClientboundPlayerInfoUpdatePacket.Entry nmsData : update.entries()) {
tablistEntries.add(nmsData.profileId());
}
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
plugins {
id("io.papermc.paperweight.userdev") version "2.0.0-beta.21"
}
repositories {
maven("https://jitpack.io") // YamlAssist
maven("https://repo.opencollab.dev/maven-snapshots/")
maven("https://repo.viaversion.com/")
maven {
name = "luck-repo"
url = uri("https://repo.lucko.me/")
content {
includeModule("me.lucko", "spark-api")
}
}
}
val version = "26.2.build.+"
dependencies {
implementation(projects.bukkit)
paperweight.paperDevBundle(version)
compileOnly("io.papermc.paper:paper-api:${version}")
}
tasks.compileJava {
options.release.set(25)
}
@@ -0,0 +1,63 @@
package me.neznamy.tab.platforms.bukkit.paper_26_2;
import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import net.minecraft.network.chat.numbers.FixedFormat;
import net.minecraft.network.protocol.game.ClientboundSetObjectivePacket;
import net.minecraft.world.scores.criteria.ObjectiveCriteria;
import org.jetbrains.annotations.NotNull;
/**
* Scoreboard override for Canvas (Folia fork) that implements scoreboards with
* new checks in NMS setters since 26.1.2.
*/
public class CanvasPacketScoreboard extends NMSPacketScoreboard {
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public CanvasPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
/**
* Updates the objective by creating a new one instead of updating the existing one,
* which would require sync access on Canvas.
*
* @param objective
* Objective to update
*/
@Override
public void updateObjective(@NonNull Objective objective) {
net.minecraft.world.scores.Objective obj = new net.minecraft.world.scores.Objective(
dummyScoreboard,
objective.getName(),
ObjectiveCriteria.DUMMY,
objective.getTitle().convert(),
ObjectiveCriteria.RenderType.values()[objective.getHealthDisplay().ordinal()],
false,
objective.getNumberFormat() == null ? null : objective.getNumberFormat().toFixedFormat(FixedFormat::new)
);
objective.setPlatformObjective(obj);
sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.UPDATE));
}
@Override
public void registerTeam(@NonNull Team team) {
player.getPlatform().runSyncGlobal(() -> super.registerTeam(team));
}
@Override
public void unregisterTeam(@NonNull Team team) {
// This one too, to avoid register+unregister causing unregister to run first
player.getPlatform().runSyncGlobal(() -> super.unregisterTeam(team));
}
@Override
public void updateTeam(@NonNull Team team) {
player.getPlatform().runSyncGlobal(() -> super.updateTeam(team));
}
}
@@ -0,0 +1,84 @@
package me.neznamy.tab.platforms.bukkit.paper_26_2;
import com.google.common.collect.ImmutableMultimap;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
import me.neznamy.tab.platforms.bukkit.provider.ComponentConverter;
import me.neznamy.tab.shared.chat.TabStyle;
import me.neznamy.tab.shared.chat.component.object.TabAtlasSprite;
import me.neznamy.tab.shared.chat.component.object.TabPlayerSprite;
import me.neznamy.tab.shared.platform.TabList;
import net.minecraft.network.chat.*;
import net.minecraft.network.chat.contents.objects.AtlasSprite;
import net.minecraft.network.chat.contents.objects.PlayerSprite;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.component.ResolvableProfile;
import org.jetbrains.annotations.NotNull;
/**
* Component converter using direct mojang-mapped code.
*/
public class NMSComponentConverter extends ComponentConverter<Component> {
@Override
@NotNull
public Component newTextComponent(@NotNull String text) {
return Component.literal(text);
}
@Override
@NotNull
public Component newTranslatableComponent(@NotNull String key) {
return Component.translatable(key);
}
@Override
@NotNull
public Component newKeybindComponent(@NotNull String keybind) {
return Component.keybind(keybind);
}
@Override
@NotNull
public Component newObjectComponent(@NotNull TabAtlasSprite sprite) {
return Component.object(new AtlasSprite(Identifier.parse(sprite.getAtlas()), Identifier.parse(sprite.getSprite())));
}
@Override
@NotNull
public Component newObjectComponent(@NotNull TabPlayerSprite sprite) {
ResolvableProfile profile;
if (sprite.getId() != null) {
profile = ResolvableProfile.createUnresolved(sprite.getId());
} else if (sprite.getName() != null) {
profile = ResolvableProfile.createUnresolved(sprite.getName());
} else if (sprite.getSkin() != null) {
ImmutableMultimap.Builder<String, Property> builder = ImmutableMultimap.builder();
builder.put(TabList.TEXTURES_PROPERTY, new Property(TabList.TEXTURES_PROPERTY, sprite.getSkin().getValue(), sprite.getSkin().getSignature()));
profile = ResolvableProfile.createResolved(new GameProfile(NIL_UUID, "", new PropertyMap(builder.build())));
} else {
throw new IllegalStateException("Player head component does not have id, name or skin set");
}
return Component.object(new PlayerSprite(profile, sprite.isShowHat()));
}
@Override
public void applyStyle(@NotNull Component nmsComponent, @NotNull TabStyle 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 : new FontDescription.Resource(Identifier.parse(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 Component parent, @NotNull Component child) {
((MutableComponent)parent).append(child);
}
}
@@ -0,0 +1,61 @@
package me.neznamy.tab.platforms.bukkit.paper_26_2;
import io.netty.channel.Channel;
import lombok.Getter;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.provider.ComponentConverter;
import me.neznamy.tab.platforms.bukkit.provider.ImplementationProvider;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.TabListEntryTracker;
import me.neznamy.tab.shared.util.ReflectionUtils;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* Implementation provider using direct Mojang-mapped NMS code for Paper 26.2.
*/
@Getter
public class NMSImplementationProvider implements ImplementationProvider {
/** Flag tracking if this server is Canvas 26.1.2+ with new scoreboard checks */
private final boolean isCanvas = ReflectionUtils.classExists("io.canvasmc.canvas.world.scores.TeamData") ||
ReflectionUtils.classExists("io.canvasmc.canvas.threadedregions.scores.TeamData");
@NotNull
private final ComponentConverter<?> componentConverter = new NMSComponentConverter();
@Override
@NotNull
public Scoreboard newScoreboard(@NotNull BukkitTabPlayer player) {
if (isCanvas) {
return new CanvasPacketScoreboard(player);
} else {
return new NMSPacketScoreboard(player);
}
}
@Override
@NotNull
public TabList newTabList(@NotNull BukkitTabPlayer player) {
return new NMSPacketTabList(player);
}
@Override
@NotNull
public Channel getChannel(@NotNull Player player) {
return ((CraftPlayer)player).getHandle().connection.connection.channel;
}
@Override
@NotNull
public TabListEntryTracker newTabListEntryTracker(@NotNull Player player) {
return new NMSTabListEntryTracker(getChannel(player));
}
@Override
public int getPing(@NotNull BukkitTabPlayer player) {
return player.getPlayer().getPing();
}
}
@@ -0,0 +1,191 @@
package me.neznamy.tab.platforms.bukkit.paper_26_2;
import lombok.NonNull;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.decorators.SafeScoreboard;
import net.minecraft.network.chat.numbers.FixedFormat;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.*;
import net.minecraft.world.scores.PlayerTeam;
import net.minecraft.world.scores.Scoreboard;
import net.minecraft.world.scores.TeamColor;
import net.minecraft.world.scores.criteria.ObjectiveCriteria;
import net.minecraft.world.scores.criteria.ObjectiveCriteria.RenderType;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
/**
* Scoreboard implementation using direct mojang-mapped code.
*/
public class NMSPacketScoreboard extends SafeScoreboard<BukkitTabPlayer> {
private static final Optional<TeamColor>[] colors = Arrays.stream(TeamColor.values()).map(Optional::of).toArray(Optional[]::new);
private static final net.minecraft.world.scores.Team.CollisionRule[] collisions = net.minecraft.world.scores.Team.CollisionRule.values();
private static final net.minecraft.world.scores.Team.Visibility[] visibilities = net.minecraft.world.scores.Team.Visibility.values();
protected static final Scoreboard dummyScoreboard = new Scoreboard();
private static final Constructor<ClientboundSetPlayerTeamPacket> teamConstructor;
static {
try {
teamConstructor = ClientboundSetPlayerTeamPacket.class.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class);
teamConstructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/**
* Constructs new instance with given player.
*
* @param player
* Player this scoreboard will belong to
*/
public NMSPacketScoreboard(@NotNull BukkitTabPlayer player) {
super(player);
}
@Override
public void registerObjective(@NonNull Objective objective) {
net.minecraft.world.scores.Objective obj = new net.minecraft.world.scores.Objective(
dummyScoreboard,
objective.getName(),
ObjectiveCriteria.DUMMY,
objective.getTitle().convert(),
RenderType.values()[objective.getHealthDisplay().ordinal()],
false,
objective.getNumberFormat() == null ? null : objective.getNumberFormat().toFixedFormat(FixedFormat::new)
);
objective.setPlatformObjective(obj);
sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.REGISTER));
}
@Override
public void setDisplaySlot(@NonNull Objective objective) {
sendPacket(new ClientboundSetDisplayObjectivePacket(
net.minecraft.world.scores.DisplaySlot.values()[objective.getDisplaySlot().ordinal()],
(net.minecraft.world.scores.Objective) objective.getPlatformObjective()
));
}
@Override
public void unregisterObjective(@NonNull Objective objective) {
sendPacket(new ClientboundSetObjectivePacket((net.minecraft.world.scores.Objective) objective.getPlatformObjective(), ObjectiveAction.UNREGISTER));
}
@Override
public void updateObjective(@NonNull Objective objective) {
net.minecraft.world.scores.Objective obj = (net.minecraft.world.scores.Objective) objective.getPlatformObjective();
obj.setDisplayName(objective.getTitle().convert());
obj.setRenderType(RenderType.values()[objective.getHealthDisplay().ordinal()]);
sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.UPDATE));
}
@Override
public void setScore(@NonNull Score score) {
sendPacket(new ClientboundSetScorePacket(
score.getHolder(),
score.getObjective().getName(),
score.getValue(),
Optional.ofNullable(score.getDisplayName() == null ? null : score.getDisplayName().convert()),
Optional.ofNullable(score.getNumberFormat() == null ? null : score.getNumberFormat().toFixedFormat(FixedFormat::new))
));
}
@Override
public void removeScore(@NonNull Score score) {
sendPacket(new ClientboundResetScorePacket(score.getHolder(), score.getObjective().getName()));
}
@Override
@NotNull
public Object createTeam(@NonNull String name) {
return new PlayerTeam(dummyScoreboard, name);
}
@Override
public void registerTeam(@NonNull Team team) {
updateTeamProperties(team);
PlayerTeam t = (PlayerTeam) team.getPlatformTeam();
t.getPlayers().addAll(team.getPlayers());
sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(t, true));
}
@Override
public void unregisterTeam(@NonNull Team team) {
sendPacket(ClientboundSetPlayerTeamPacket.createRemovePacket((PlayerTeam) team.getPlatformTeam()));
}
@Override
public void updateTeam(@NonNull Team team) {
updateTeamProperties(team);
sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket((PlayerTeam) team.getPlatformTeam(), false));
}
private void updateTeamProperties(@NonNull Team team) {
PlayerTeam t = (PlayerTeam) team.getPlatformTeam();
t.setAllowFriendlyFire((team.getOptions() & 0x01) != 0);
t.setSeeFriendlyInvisibles((team.getOptions() & 0x02) != 0);
t.setColor(colors[Math.min(team.getColor().ordinal(), colors.length - 1)]);
t.setCollisionRule(collisions[team.getCollision().ordinal()]);
t.setNameTagVisibility(visibilities[team.getVisibility().ordinal()]);
t.setPlayerPrefix(team.getPrefix().convert());
t.setPlayerSuffix(team.getSuffix().convert());
}
@Override
@SneakyThrows
@NotNull
public Object onPacketSend(@NonNull Object packet) {
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 (packet instanceof ClientboundSetPlayerTeamPacket team) {
int action = getMethod(team);
if (action != TeamAction.UPDATE) {
Collection<String> players = team.getPlayers();
if (players != null) {
return teamConstructor.newInstance(
team.getName(),
action,
team.getParameters(),
onTeamPacket(action, team.getName(), players)
);
}
}
}
return packet;
}
private int getMethod(@NonNull ClientboundSetPlayerTeamPacket team) {
if (team.getTeamAction() == ClientboundSetPlayerTeamPacket.Action.ADD) {
return 0;
} else if (team.getTeamAction() == ClientboundSetPlayerTeamPacket.Action.REMOVE) {
return 1;
} else if (team.getPlayerAction() == ClientboundSetPlayerTeamPacket.Action.ADD) {
return 3;
} else if (team.getPlayerAction() == ClientboundSetPlayerTeamPacket.Action.REMOVE) {
return 4;
} else {
return 2;
}
}
/**
* Sends the packet to the player.
*
* @param packet
* Packet to send
*/
protected void sendPacket(@NotNull Packet<?> packet) {
((CraftPlayer)player.getPlayer()).getHandle().connection.send(packet);
}
}
@@ -1,4 +1,4 @@
package me.neznamy.tab.platforms.paper_1_21_11;
package me.neznamy.tab.platforms.bukkit.paper_26_2;
import com.google.common.collect.ImmutableMultimap;
import com.mojang.authlib.GameProfile;
@@ -8,6 +8,7 @@ import lombok.NonNull;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.features.layout.LayoutManagerImpl;
import me.neznamy.tab.shared.platform.decorators.TrackedTabList;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
@@ -24,7 +25,7 @@ import java.util.*;
/**
* TabList implementation using direct mojang-mapped code.
*/
public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
public class NMSPacketTabList 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);
@@ -40,7 +41,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
* @param player
* Player this tablist will belong to
*/
public PaperPacketTabList(@NotNull BukkitTabPlayer player) {
public NMSPacketTabList(@NotNull BukkitTabPlayer player) {
super(player);
}
@@ -138,7 +139,7 @@ public class PaperPacketTabList extends TrackedTabList<BukkitTabPlayer> {
}
}
if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)) {
if (allPlayersHidden && nmsData.profileId().getMostSignificantBits() != 0) { // Filter out layout entries
if (allPlayersHidden && !LayoutManagerImpl.UUIDS_SET.contains(nmsData.profileId())) { // Filter out layout entries
listed = false;
rewriteEntry = rewritePacket = true;
}
@@ -1,6 +1,7 @@
package me.neznamy.tab.platforms.paper_1_21_4;
package me.neznamy.tab.platforms.bukkit.paper_26_2;
import me.neznamy.tab.shared.platform.TabListEntryTracker;
import io.netty.channel.Channel;
import me.neznamy.tab.shared.platform.NettyTabListEntryTracker;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import org.jetbrains.annotations.NotNull;
@@ -10,7 +11,11 @@ import java.util.UUID;
/**
* Implementation of TabListEntryTracker.
*/
public class PaperTabListEntryTracker extends TabListEntryTracker {
public class NMSTabListEntryTracker extends NettyTabListEntryTracker {
public NMSTabListEntryTracker(@NotNull Channel channel) {
super(channel);
}
@Override
public void onPacketSend(@NotNull Object packet) {
@@ -44,8 +44,7 @@ public class BukkitEventListener implements EventListener<Player>, Listener {
public void onJoin(PlayerJoinEvent e) {
TAB.getInstance().addTablistTracker(
e.getPlayer().getUniqueId(),
platform.getImplementationProvider().getChannel(e.getPlayer()),
platform.getImplementationProvider().newTabListEntryTracker()
platform.getServerVersionInfo().getImplementationProvider().newTabListEntryTracker(e.getPlayer())
);
join(e.getPlayer());
}
@@ -21,6 +21,6 @@ public class BukkitPipelineInjector extends NettyPipelineInjector {
@Override
@NotNull
protected Channel getChannel(@NotNull TabPlayer player) {
return ((BukkitPlatform)player.getPlatform()).getImplementationProvider().getChannel(((BukkitTabPlayer) player).getPlayer());
return ((BukkitPlatform)player.getPlatform()).getServerVersionInfo().getImplementationProvider().getChannel(((BukkitTabPlayer) player).getPlayer());
}
}
}
@@ -17,10 +17,13 @@ public class BukkitTAB extends JavaPlugin {
boolean folia = ReflectionUtils.classExists("io.papermc.paper.threadedregions.RegionizedServer");
try {
TAB.create(folia ? new FoliaPlatform(this) : new BukkitPlatform(this));
} catch (UnsupportedOperationException e) {
} catch (IllegalStateException e) {
Bukkit.getConsoleSender().sendMessage("§c[TAB] ================================================================================");
Bukkit.getConsoleSender().sendMessage("§c[TAB] Your server version (" + Bukkit.getBukkitVersion().split("-")[0] + ") is not supported. " +
"This jar only supports 1.7 - 1.21.11. If you just updated to a new Minecraft version, check for TAB updates.");
Bukkit.getConsoleSender().sendMessage("§c[TAB] Your server version (" + Bukkit.getBukkitVersion() + ") is not supported.");
Bukkit.getConsoleSender().sendMessage("§c[TAB] This jar only supports 1.7.10, 1.8.8, 1.12.2, 1.16.5, 1.17.1 and 1.18.2 - 26.2");
Bukkit.getConsoleSender().sendMessage("§c[TAB] If you just updated to a new Minecraft version, check for TAB updates.");
Bukkit.getConsoleSender().sendMessage("§c[TAB] If you are using an unsupported 1.x version, use an older version of TAB (latest TAB 5.x supports all MC 1.x versions).");
Bukkit.getConsoleSender().sendMessage("§c[TAB] Thrown error message: " + e.getMessage());
Bukkit.getConsoleSender().sendMessage("§c[TAB] ================================================================================");
}
}
@@ -5,10 +5,12 @@ import me.neznamy.tab.platforms.bukkit.platform.BukkitPlatform;
import me.neznamy.tab.shared.backend.BackendTabPlayer;
import me.neznamy.tab.shared.chat.component.TabComponent;
import org.bukkit.Statistic;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.Player;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* TabPlayer implementation for Bukkit platform
@@ -16,6 +18,18 @@ import org.jetbrains.annotations.NotNull;
@SuppressWarnings("deprecation")
public class BukkitTabPlayer extends BackendTabPlayer {
/** Below name distance attribute (26.1+) */
@Nullable
private static Attribute BELOW_NAME_DISTANCE;
static {
try {
BELOW_NAME_DISTANCE = (Attribute) Attribute.class.getDeclaredField("BELOW_NAME_DISTANCE").get(null);
} catch (ReflectiveOperationException e) {
// 1.21.11 and lower
}
}
/**
* Constructs new instance with given bukkit player
*
@@ -25,7 +39,7 @@ public class BukkitTabPlayer extends BackendTabPlayer {
* bukkit player
*/
public BukkitTabPlayer(@NotNull BukkitPlatform platform, @NotNull Player p) {
super(platform, p, p.getUniqueId(), p.getName(), p.getWorld().getName(), platform.getServerVersion().getNetworkId());
super(platform, p, p.getUniqueId(), p.getName(), p.getWorld().getName(), platform.getServerVersionInfo().getServerVersion().getNetworkId());
}
@Override
@@ -35,7 +49,7 @@ public class BukkitTabPlayer extends BackendTabPlayer {
@Override
public int getPing() {
return getPlatform().getImplementationProvider().getPing(this);
return getPlatform().getServerVersionInfo().getImplementationProvider().getPing(this);
}
@Override
@@ -64,6 +78,13 @@ public class BukkitTabPlayer extends BackendTabPlayer {
return (BukkitPlatform) platform;
}
@Override
public void setBelowNameDistance(double distance) {
if (BELOW_NAME_DISTANCE != null) {
getPlayer().getAttribute(BELOW_NAME_DISTANCE).setBaseValue(distance);
}
}
@Override
public boolean isVanished0() {
for (MetadataValue v : getPlayer().getMetadata("vanished")) {
@@ -0,0 +1,148 @@
package me.neznamy.tab.platforms.bukkit;
import lombok.Getter;
import lombok.Setter;
import me.neznamy.tab.platforms.bukkit.provider.ImplementationProvider;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.util.ReflectionUtils;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Class for detecting server software and version and finding available NMS implementation(s).
*/
@Getter
public class ServerVersionInfo {
/** Package name of the server implementation, null on Paper 1.20.5+ / Spigot 26+ */
@Nullable
private final String serverPackage;
/** Minecraft version as returned by the platform */
@NotNull
private final String minecraftVersion;
/** Name of the server software, either "Paper" or "Spigot" */
@NotNull
private final String serverName;
/** Server version */
private final ProtocolVersion serverVersion;
/** Implementation for creating new instances using content available on the server */
@NotNull
@Setter
private ImplementationProvider implementationProvider;
/** Name of the implementation package (such as v1_7_R4 or paper_1_20_5) */
@Nullable
private String implementationPackage;
/**
* Constructs new instance and detects server version info.
*/
public ServerVersionInfo() {
// Server package
String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackage().getName();
String[] array = CRAFTBUKKIT_PACKAGE.split("\\.");
serverPackage = array.length > 3 ? array[3] : null;
// Minecraft version
if (ReflectionUtils.methodExists(Bukkit.class, "getMinecraftVersion")) {
serverName = "Paper";
minecraftVersion = Bukkit.getMinecraftVersion();
} else {
serverName = "Spigot";
minecraftVersion = Bukkit.getBukkitVersion().split("-")[0];
}
// Minecraft version to server version
serverVersion = ProtocolVersion.fromFriendlyName(minecraftVersion);
// Find implementation
implementationProvider = findImplementationProvider();
}
/**
* Finds implementation provider for current server software and version.
*
* @return Implementation provider for current server
* @throws IllegalStateException
* If no implementation was found
*/
@NotNull
private ImplementationProvider findImplementationProvider() {
if (serverPackage != null) {
// Paper <1.20.5 or Spigot 1.x
try {
// Does not actually support flat 1.19, but whatever, no one is using it anyway
implementationPackage = serverPackage;
return (ImplementationProvider) Class.forName("me.neznamy.tab.platforms.bukkit." + serverPackage + ".NMSImplementationProvider").getConstructor().newInstance();
} catch (ReflectiveOperationException ignored) {
throw new IllegalStateException(String.format(
"Your server version (%s - %s) is no longer supported. Please use an older version of TAB.",
minecraftVersion, serverPackage
));
}
}
// Paper 1.20.5+ or Spigot 26+
Map<ProtocolVersion, String> spigotVersions = new LinkedHashMap<>();
spigotVersions.put(ProtocolVersion.V26_1, "v26_1");
spigotVersions.put(ProtocolVersion.V26_1_1, "v26_1");
spigotVersions.put(ProtocolVersion.V26_1_2, "v26_1");
spigotVersions.put(ProtocolVersion.V26_2, "v26_2");
Map<ProtocolVersion, String> paperVersions = new LinkedHashMap<>();
paperVersions.put(ProtocolVersion.V1_20_5, "paper_1_20_5");
paperVersions.put(ProtocolVersion.V1_20_6, "paper_1_20_5");
paperVersions.put(ProtocolVersion.V1_21, "paper_1_20_5");
paperVersions.put(ProtocolVersion.V1_21_1, "paper_1_20_5");
paperVersions.put(ProtocolVersion.V1_21_2, "paper_1_21_2");
paperVersions.put(ProtocolVersion.V1_21_3, "paper_1_21_2");
paperVersions.put(ProtocolVersion.V1_21_4, "paper_1_21_4");
paperVersions.put(ProtocolVersion.V1_21_5, "paper_1_21_4");
paperVersions.put(ProtocolVersion.V1_21_6, "paper_1_21_4");
paperVersions.put(ProtocolVersion.V1_21_7, "paper_1_21_4");
paperVersions.put(ProtocolVersion.V1_21_8, "paper_1_21_4");
paperVersions.put(ProtocolVersion.V1_21_9, "paper_1_21_9");
paperVersions.put(ProtocolVersion.V1_21_10, "paper_1_21_9");
paperVersions.put(ProtocolVersion.V1_21_11, "paper_1_21_11");
paperVersions.put(ProtocolVersion.V26_1, "paper_1_21_11"); // v26_1 works too
paperVersions.put(ProtocolVersion.V26_1_1, "paper_1_21_11"); // v26_1 works too
paperVersions.put(ProtocolVersion.V26_1_2, "paper_1_21_11"); // v26_1 works too
paperVersions.put(ProtocolVersion.V26_2, "paper_26_2"); // v26_2 works too
if (serverVersion == ProtocolVersion.UNKNOWN) {
throw new IllegalStateException(String.format(
"Unknown server version (%s %s), cannot find implementation.",
serverName, minecraftVersion
));
}
String implementation = (serverName.equals("Paper") ? paperVersions : spigotVersions).get(serverVersion);
if (implementation == null) {
throw new IllegalStateException(String.format(
"Your server version (%s %s) is no longer supported. Please use an older version of TAB.",
serverName, minecraftVersion
));
}
try {
implementationPackage = implementation;
return (ImplementationProvider) Class.forName("me.neznamy.tab.platforms.bukkit." + implementation + ".NMSImplementationProvider").getConstructor().newInstance();
} catch (ClassNotFoundException e) {
throw new IllegalStateException(String.format(
"Your server version (%s %s) is marked as compatible, but the implementation does not exist. This is probably a bug.",
serverName, minecraftVersion
), e);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(String.format(
"Failed to initialize implementation for %s %s. This is probably a bug.",
serverName, minecraftVersion
), e);
}
}
}
@@ -12,6 +12,8 @@ import org.bukkit.Bukkit;
import org.bukkit.boss.BossBar;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* BossBar for 1.9+ servers where Bukkit API is used. If ViaVersion is used
* to allow 1.8 players, it will handle the entity and teleporting by itself.
@@ -32,7 +34,7 @@ public class BukkitBossBar extends SafeBossBar<BossBar> {
@Override
@NotNull
public BossBar constructBossBar(@NotNull TabComponent title, float progress, @NotNull BarColor color, @NotNull BarStyle style) {
public BossBar constructBossBar(@NotNull UUID id, @NotNull TabComponent title, float progress, @NotNull BarColor color, @NotNull BarStyle style) {
BossBar bar = Bukkit.createBossBar(
player.getPlatform().toBukkitFormat(title),
org.bukkit.boss.BarColor.valueOf(color.name()),
@@ -43,7 +45,7 @@ public class BukkitBossBar extends SafeBossBar<BossBar> {
}
@Override
public void create(SafeBossBar<BossBar>.@NotNull BossBarInfo bar) {
public void show(SafeBossBar<BossBar>.@NotNull BossBarInfo bar) {
bar.getBossBar().addPlayer(player.getPlayer());
}
@@ -68,7 +70,7 @@ public class BukkitBossBar extends SafeBossBar<BossBar> {
}
@Override
public void remove(SafeBossBar<BossBar>.@NotNull BossBarInfo bar) {
public void hide(SafeBossBar<BossBar>.@NotNull BossBarInfo bar) {
bar.getBossBar().removePlayer(player.getPlayer());
}
}
@@ -12,6 +12,8 @@ import me.neznamy.tab.shared.chat.component.TabComponent;
import me.neznamy.tab.shared.platform.decorators.SafeBossBar;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* Handler for 1.9+ players on 1.8 server using ViaVersion API.
*/
@@ -27,7 +29,7 @@ public class ViaBossBar extends SafeBossBar<BossBar> {
@Override
@NotNull
public BossBar constructBossBar(@NotNull TabComponent title, float progress, @NotNull BarColor color, @NotNull BarStyle style) {
public BossBar constructBossBar(@NotNull UUID id, @NotNull TabComponent title, float progress, @NotNull BarColor color, @NotNull BarStyle style) {
return Via.getAPI().legacyAPI().createLegacyBossBar(
title.toLegacyText(),
progress,
@@ -37,7 +39,7 @@ public class ViaBossBar extends SafeBossBar<BossBar> {
}
@Override
public void create(@NotNull BossBarInfo bar) {
public void show(@NotNull BossBarInfo bar) {
bar.getBossBar().addPlayer(player.getPlayer().getUniqueId());
}
@@ -62,7 +64,7 @@ public class ViaBossBar extends SafeBossBar<BossBar> {
}
@Override
public void remove(@NotNull BossBarInfo bar) {
public void hide(@NotNull BossBarInfo bar) {
bar.getBossBar().removePlayer(player.getPlayer().getUniqueId());
}
}
@@ -98,9 +98,9 @@ public class BukkitTabExpansion extends PlaceholderExpansion implements TabExpan
String requestedPlaceholder = "%" + identifier.substring("placeholder_".length()) + "%";
PlaceholderManagerImpl pm = TAB.getInstance().getPlaceholderManager();
pm.addUsedPlaceholder(requestedPlaceholder, pm);
return pm.getPlaceholder(requestedPlaceholder).set(requestedPlaceholder, p);
return pm.getPlaceholder(requestedPlaceholder).parse(p);
}
return p.expansionValues.get(identifier);
return p.expansionData.getValue(identifier);
}
@Override
@@ -132,17 +132,7 @@ public class BukkitTabExpansion extends PlaceholderExpansion implements TabExpan
return "<Not a relational placeholder: " + requestedPlaceholder + ">";
}
}
return "<Unknown identifier: " + identifier + ">";
}
@Override
public void setPlaceholderValue(@NotNull TabPlayer player, @NotNull String placeholder, @NotNull String value) {
// Do not unnecessarily do all placeholders, just retrieve them on request to save resources by not using string builder all the time
}
@Override
public void setValue(@NotNull TabPlayer player, @NotNull String key, @NotNull String value) {
player.expansionValues.put(key, value);
return "<Unknown identifier: \"" + identifier + "\">";
}
@Override
@@ -7,6 +7,9 @@ import me.neznamy.tab.shared.features.PerWorldPlayerListConfiguration;
import me.neznamy.tab.shared.features.types.Loadable;
import me.neznamy.tab.shared.features.types.TabFeature;
import me.neznamy.tab.shared.features.types.UnLoadable;
import me.neznamy.tab.shared.features.types.VanishListener;
import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.util.ReflectionUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -14,6 +17,8 @@ import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
@@ -24,7 +29,7 @@ import java.util.Map.Entry;
* Per-world-PlayerList feature handler
*/
@SuppressWarnings("deprecation")
public class PerWorldPlayerList extends TabFeature implements Listener, Loadable, UnLoadable {
public class PerWorldPlayerList extends TabFeature implements Listener, Loadable, UnLoadable, VanishListener {
/** Reference to platform */
@NotNull
@@ -34,6 +39,9 @@ public class PerWorldPlayerList extends TabFeature implements Listener, Loadable
@NotNull
private final PerWorldPlayerListConfiguration configuration;
/** Check for presence of modern (1.12.2+) methods that take plugin as argument to avoid conflict */
private final boolean modernMethodsAvailable = ReflectionUtils.methodExists(Player.class, "hidePlayer", Plugin.class, Player.class);
/**
* Constructs new instance and registers events.
*
@@ -61,7 +69,7 @@ public class PerWorldPlayerList extends TabFeature implements Listener, Loadable
public void unload() {
for (Player p : platform.getOnlinePlayers()) {
for (Player pl : platform.getOnlinePlayers()) {
p.showPlayer(pl);
showPlayer(p, pl);
}
}
HandlerList.unregisterAll(this);
@@ -104,10 +112,32 @@ public class PerWorldPlayerList extends TabFeature implements Listener, Loadable
private void checkPlayer(@NotNull Player p) {
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);
if (!shouldSee(all, p) && all.canSee(p)) all.hidePlayer(p);
if (shouldSee(all, p) && !all.canSee(p)) all.showPlayer(p);
if (shouldSee(p, all)) {
showPlayer(p, all);
} else {
hidePlayer(p, all);
}
if (shouldSee(all, p)) {
showPlayer(all, p);
} else {
hidePlayer(all, p);
}
}
}
private void hidePlayer(@NotNull Player viewer, @NotNull Player target) {
if (modernMethodsAvailable) {
viewer.hidePlayer(platform.getPlugin(), target);
} else {
viewer.hidePlayer(target);
}
}
private void showPlayer(@NotNull Player viewer, @NotNull Player target) {
if (modernMethodsAvailable) {
viewer.showPlayer(platform.getPlugin(), target);
} else {
viewer.showPlayer(target);
}
}
@@ -121,16 +151,25 @@ public class PerWorldPlayerList extends TabFeature implements Listener, Loadable
*/
private boolean shouldSee(@NotNull Player viewer, @NotNull Player target) {
if (target == viewer) return true;
for (MetadataValue v : target.getMetadata("vanished")) {
if (v.asBoolean() && !viewer.hasPermission(TabConstants.Permission.SEE_VANISHED)) return false;
}
if ((configuration.isAllowBypassPermission() && viewer.hasPermission(TabConstants.Permission.PER_WORLD_PLAYERLIST_BYPASS)) || configuration.getIgnoredWorlds().contains(viewer.getWorld().getName())) return true;
String viewerWorldGroup = viewer.getWorld().getName() + "-default"; //preventing unwanted behavior when some group is called exactly like a world
String targetWorldGroup = target.getWorld().getName() + "-default";
return computeWorldGroup(viewer).equals(computeWorldGroup(target));
}
@NotNull
private String computeWorldGroup(@NotNull Player p) {
for (Entry<String, List<String>> group : configuration.getSharedWorlds().entrySet()) {
if (group.getValue() != null) {
if (group.getValue().contains(viewer.getWorld().getName())) viewerWorldGroup = group.getKey();
if (group.getValue().contains(target.getWorld().getName())) targetWorldGroup = group.getKey();
List<String> worlds = group.getValue();
if (worlds == null) continue;
for (String world : worlds) {
if (TAB.getInstance().getDataManager().matchesPattern(p.getWorld().getName(), world)) {
return group.getKey();
}
}
}
return viewerWorldGroup.equals(targetWorldGroup);
return p.getWorld().getName() + "-default"; //preventing unwanted behavior when some group is called exactly like a world
}
@NotNull
@@ -138,4 +177,9 @@ public class PerWorldPlayerList extends TabFeature implements Listener, Loadable
public String getFeatureName() {
return "Per world PlayerList";
}
@Override
public void onVanishStatusChange(@NotNull TabPlayer player) {
platform.runSync((Player) player.getPlayer(), () -> checkPlayer((Player) player.getPlayer()));
}
}
@@ -1,23 +1,17 @@
package me.neznamy.tab.platforms.bukkit.platform;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import me.clip.placeholderapi.PlaceholderAPI;
import me.neznamy.tab.platforms.bukkit.BukkitEventListener;
import me.neznamy.tab.platforms.bukkit.BukkitPipelineInjector;
import me.neznamy.tab.platforms.bukkit.BukkitTabCommand;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import me.neznamy.tab.platforms.bukkit.*;
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.hook.BukkitPremiumVanishHook;
import me.neznamy.tab.platforms.bukkit.provider.ImplementationProvider;
import me.neznamy.tab.shared.GroupManager;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.*;
import me.neznamy.tab.shared.backend.BackendPlatform;
import me.neznamy.tab.shared.chat.TabTextColor;
import me.neznamy.tab.shared.chat.component.TabComponent;
@@ -53,6 +47,7 @@ import org.bukkit.command.SimpleCommandMap;
import org.bukkit.command.defaults.BukkitCommand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
@@ -72,11 +67,12 @@ public class BukkitPlatform implements BackendPlatform {
@NotNull
private final JavaPlugin plugin;
/** Server version */
private final ProtocolVersion serverVersion = ProtocolVersion.fromFriendlyName(Bukkit.getBukkitVersion().split("-")[0]);
/** Information about server version */
@NotNull
private final ServerVersionInfo serverVersionInfo = new ServerVersionInfo();
/** Variables checking presence of other plugins to hook into */
private final boolean placeholderAPI = ReflectionUtils.classExists("me.clip.placeholderapi.PlaceholderAPI");
private final boolean placeholderAPI = Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI");
/** Spigot field for tracking TPS, the array is final and only being modified instead of re-instantiated */
private double[] recentTps;
@@ -87,15 +83,6 @@ public class BukkitPlatform implements BackendPlatform {
/** Detection for presence of Paper's MSPT getter */
private final boolean paperMspt = ReflectionUtils.methodExists(Bukkit.class, "getAverageTickTime");
/** Package name of the server implementation, null on Paper 1.20.5+ */
@Nullable
private final String serverPackage;
/** Implementation for creating new instances using content available on the server */
@NotNull
@Setter
private ImplementationProvider implementationProvider;
private final boolean modernOnlinePlayers;
/** Command map for dynamic command registering */
@@ -116,10 +103,7 @@ public class BukkitPlatform implements BackendPlatform {
public BukkitPlatform(@NotNull JavaPlugin plugin) {
this.plugin = plugin;
modernOnlinePlayers = Bukkit.class.getMethod("getOnlinePlayers").getReturnType() == Collection.class;
String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackage().getName();
String[] array = CRAFTBUKKIT_PACKAGE.split("\\.");
serverPackage = array.length > 3 ? array[3] : null;
implementationProvider = findImplementationProvider();
logInfo(new TabTextComponent("Found NMS implementation: " + serverVersionInfo.getImplementationProvider().getClass().getName(), TabTextColor.GRAY));
try {
Object server = Bukkit.getServer().getClass().getMethod("getServer").invoke(Bukkit.getServer());
recentTps = ((double[]) server.getClass().getField("recentTps").get(server));
@@ -133,63 +117,6 @@ public class BukkitPlatform implements BackendPlatform {
knownCommands = (Map<String, Command>) ReflectionUtils.getField(SimpleCommandMap.class, "knownCommands").get(commandMap);
}
@NotNull
@SneakyThrows
private ImplementationProvider findImplementationProvider() {
if (serverPackage == null) {
// Paper 1.20.5+, check for available module
String paperModule = getPaperModule();
if (paperModule != null) {
return (ImplementationProvider) Class.forName("me.neznamy.tab.platforms.paper_" + paperModule + ".PaperImplementationProvider").getConstructor().newInstance();
} else {
throw new UnsupportedOperationException();
}
} else {
// Paper <1.20.5 or Spigot
try {
// Does not actually support flat 1.19, but whatever, no one is using it anyway
return (ImplementationProvider) Class.forName("me.neznamy.tab.platforms.bukkit." + serverPackage + ".NMSImplementationProvider").getConstructor().newInstance();
} catch (ClassNotFoundException ignored) {
throw new UnsupportedOperationException();
}
}
}
/**
* Returns name of the paper module that can be used on this server.
* If this server is not using paper or no module is available for any other reason,
* {@code null} is returned.
*
* @return Name of the available paper module or {@code null} if not available
*/
@Nullable
private String getPaperModule() {
if (!ReflectionUtils.classExists("org.bukkit.craftbukkit.CraftServer")) return null;
switch (serverVersion) {
case V1_20_5:
case V1_20_6:
case V1_21:
case V1_21_1:
return "1_20_5";
case V1_21_2:
case V1_21_3:
return "1_21_2";
case V1_21_4:
case V1_21_5:
case V1_21_6:
case V1_21_7:
case V1_21_8:
return "1_21_4";
case V1_21_9:
case V1_21_10:
return "1_21_9";
case V1_21_11:
return "1_21_11";
default:
return null;
}
}
@Override
public void loadPlayers() {
for (Player p : getOnlinePlayers()) {
@@ -200,14 +127,14 @@ public class BukkitPlatform implements BackendPlatform {
@Override
public void registerPlaceholders() {
PlaceholderManagerImpl manager = TAB.getInstance().getPlaceholderManager();
manager.registerInternalServerPlaceholder("%vault-prefix%", -1, () -> "");
manager.registerInternalServerPlaceholder("%vault-suffix%", -1, () -> "");
manager.registerServerPlaceholder("%vault-prefix%", -1, () -> "");
manager.registerServerPlaceholder("%vault-suffix%", -1, () -> "");
if (Bukkit.getPluginManager().isPluginEnabled("Vault")) {
RegisteredServiceProvider<Chat> rspChat = Bukkit.getServicesManager().getRegistration(Chat.class);
if (rspChat != null) {
Chat chat = rspChat.getProvider();
manager.registerInternalPlayerPlaceholder("%vault-prefix%", 1000, p -> chat.getPlayerPrefix((Player) p.getPlayer()));
manager.registerInternalPlayerPlaceholder("%vault-suffix%", 1000, p -> chat.getPlayerSuffix((Player) p.getPlayer()));
manager.registerPlayerPlaceholder("%vault-prefix%", p -> chat.getPlayerPrefix((Player) p.getPlayer()));
manager.registerPlayerPlaceholder("%vault-suffix%", p -> chat.getPlayerSuffix((Player) p.getPlayer()));
}
}
BackendPlatform.super.registerPlaceholders();
@@ -216,7 +143,8 @@ public class BukkitPlatform implements BackendPlatform {
@Override
@Nullable
public PipelineInjector createPipelineInjector() {
return serverVersion.getMinorVersion() >= 8 ? new BukkitPipelineInjector() : null;
return serverVersionInfo.getServerVersion().getNetworkId() >= ProtocolVersion.V1_8.getNetworkId()
? new BukkitPipelineInjector() : null;
}
@Override
@@ -288,12 +216,6 @@ public class BukkitPlatform implements BackendPlatform {
Bukkit.getConsoleSender().sendMessage("§c[TAB] [WARN] " + toBukkitFormat(message));
}
@Override
@NotNull
public String getServerVersionInfo() {
return "[Bukkit] " + Bukkit.getName() + " - " + Bukkit.getBukkitVersion().split("-")[0] + " (" + serverPackage + ")";
}
@Override
public void registerListener() {
Bukkit.getPluginManager().registerEvents(new BukkitEventListener(this), plugin);
@@ -316,9 +238,9 @@ public class BukkitPlatform implements BackendPlatform {
Metrics metrics = new Metrics(plugin, TabConstants.BSTATS_PLUGIN_ID_BUKKIT);
metrics.addCustomChart(new SimplePie(TabConstants.MetricsChart.PERMISSION_SYSTEM,
() -> TAB.getInstance().getGroupManager().getPermissionPlugin()));
String version = serverVersion == ProtocolVersion.UNKNOWN ? "Unknown" : "1." + serverVersion.getMinorVersion() + ".x";
metrics.addCustomChart(new SimplePie(TabConstants.MetricsChart.SERVER_VERSION, () -> version));
metrics.addCustomChart(new SimplePie("tab_5_5_0_servers", serverVersion::getFriendlyName));
metrics.addCustomChart(new SimplePie("tab_6_1_0_servers",
() -> serverVersionInfo.getServerName() + " " + serverVersionInfo.getServerVersion().getFriendlyName()));
metrics.addCustomChart(new SimplePie("tab_6_1_0_package", serverVersionInfo::getImplementationPackage));
}
@Override
@@ -330,13 +252,13 @@ public class BukkitPlatform implements BackendPlatform {
@Override
@NotNull
public Object convertComponent(@NotNull TabComponent component) {
return implementationProvider.getComponentConverter().convert(component);
return serverVersionInfo.getImplementationProvider().getComponentConverter().convert(component);
}
@Override
@NotNull
public Scoreboard createScoreboard(@NotNull TabPlayer player) {
return implementationProvider.newScoreboard((BukkitTabPlayer) player);
return serverVersionInfo.getImplementationProvider().newScoreboard((BukkitTabPlayer) player);
}
@Override
@@ -349,7 +271,7 @@ public class BukkitPlatform implements BackendPlatform {
if (BukkitBossBar.isAvailable()) return new BukkitBossBar((BukkitTabPlayer) player);
// 1.9+ player on 1.8 server, handle using ViaVersion API
if (player.getVersion().getMinorVersion() >= 9) return new ViaBossBar((BukkitTabPlayer) player);
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_9.getNetworkId()) return new ViaBossBar((BukkitTabPlayer) player);
// 1.8- server and player, no implementation
return new DummyBossBar();
@@ -358,7 +280,7 @@ public class BukkitPlatform implements BackendPlatform {
@Override
@NotNull
public TabList createTabList(@NotNull TabPlayer player) {
return implementationProvider.newTabList((BukkitTabPlayer) player);
return serverVersionInfo.getImplementationProvider().newTabList((BukkitTabPlayer) player);
}
@Override
@@ -368,17 +290,17 @@ public class BukkitPlatform implements BackendPlatform {
@Override
public boolean supportsListed() {
return serverVersion.getNetworkId() >= ProtocolVersion.V1_19_3.getNetworkId();
return serverVersionInfo.getServerVersion().getNetworkId() >= ProtocolVersion.V1_19_3.getNetworkId();
}
@Override
public boolean supportsListOrder() {
return serverVersion.getNetworkId() >= ProtocolVersion.V1_21_2.getNetworkId();
return serverVersionInfo.getServerVersion().getNetworkId() >= ProtocolVersion.V1_21_2.getNetworkId();
}
@Override
public boolean isSafeFromPacketEventsBug() {
return serverVersion.getMinorVersion() >= 13;
return serverVersionInfo.getServerVersion().getMinorVersion() >= 13;
}
@Override
@@ -410,6 +332,7 @@ public class BukkitPlatform implements BackendPlatform {
knownCommands.remove(command.getName() + ":" + command.getName());
command.unregister(commandMap);
}
customCommands.clear();
}
@Override
@@ -448,7 +371,7 @@ public class BukkitPlatform implements BackendPlatform {
* Runs task in the main thread for given entity.
*
* @param entity
* Entity's main thread
* Entity to run the task for
* @param task
* Task to run
*/
@@ -456,6 +379,21 @@ public class BukkitPlatform implements BackendPlatform {
Bukkit.getScheduler().runTask(plugin, task);
}
/**
* Runs task in the global tick thread.
*
* @param task
* Task to run
*/
public void runSyncGlobal(@NotNull Runnable task) {
Bukkit.getScheduler().runTask(plugin, task);
}
@Override
public boolean hasLineOfSight(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
return ((Player) viewer.getPlayer()).hasLineOfSight((Player) target.getPlayer());
}
/**
* Converts component to string using bukkit RGB format if supported by the server.
* If not, closest legacy color is used instead.
@@ -468,7 +406,7 @@ public class BukkitPlatform implements BackendPlatform {
public String toBukkitFormat(@NotNull TabComponent component) {
StringBuilder sb = new StringBuilder();
if (component.getModifier().getColor() != null) {
if (serverVersion.supportsRGB()) {
if (serverVersionInfo.getServerVersion().getNetworkId() >= ProtocolVersion.V1_16.getNetworkId()) {
String hexCode = component.getModifier().getColor().getHexCode();
sb.append('§').append("x").append('§').append(hexCode.charAt(0)).append('§').append(hexCode.charAt(1))
.append('§').append(hexCode.charAt(2)).append('§').append(hexCode.charAt(3))
@@ -510,4 +448,33 @@ public class BukkitPlatform implements BackendPlatform {
}
return Arrays.asList((Player[]) Bukkit.class.getMethod("getOnlinePlayers").invoke(null));
}
}
@Override
@NotNull
public Object dump() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("server-type", "Bukkit");
map.put("server-name", Bukkit.getName());
map.put("server-version", serverVersionInfo.getMinecraftVersion());
map.put("craftbukkit-package", serverVersionInfo.getServerPackage());
map.put("nms-implementation", serverVersionInfo.getImplementationProvider().getClass().getName());
map.put("tab-version", ProjectVariables.PLUGIN_VERSION);
Map<String, Object> plugins = new LinkedHashMap<>();
Plugin[] pluginArray = Bukkit.getPluginManager().getPlugins();
Arrays.sort(pluginArray, Comparator.comparing(p -> p.getDescription().getName(), String.CASE_INSENSITIVE_ORDER));
for (Plugin p : pluginArray) {
plugins.put(p.getDescription().getName(), p.getDescription().getVersion());
}
map.put("plugins", plugins);
if (placeholderAPI) {
Map<String, String> expansions = new LinkedHashMap<>();
PlaceholderExpansion[] expansionArray = PlaceholderAPIPlugin.getInstance().getLocalExpansionManager().getExpansions().toArray(new PlaceholderExpansion[0]);
Arrays.sort(expansionArray, Comparator.comparing(PlaceholderExpansion::getIdentifier, String.CASE_INSENSITIVE_ORDER));
for (PlaceholderExpansion p : expansionArray) {
expansions.put(p.getIdentifier(), p.getVersion());
}
map.put("placeholderapi-expansions", expansions);
}
return map;
}
}
@@ -18,6 +18,7 @@ import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Method;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.function.Consumer;
@@ -28,14 +29,25 @@ import java.util.function.Function;
*/
public class FoliaPlatform extends BukkitPlatform {
/** Global tick thread scheduler */
@NotNull
private final Object globalScheduler;
/** FoliaGlobalRegionScheduler#execute(Plugin, Runnable) method */
private final Method globalScheduler_execute;
/**
* Constructs new instance with given plugin.
*
* @param plugin
* Plugin
*/
@SneakyThrows
@SuppressWarnings("JavaReflectionMemberAccess")
public FoliaPlatform(@NotNull JavaPlugin plugin) {
super(plugin);
globalScheduler = Bukkit.class.getMethod("getGlobalRegionScheduler").invoke(null);
globalScheduler_execute = globalScheduler.getClass().getMethod("execute", Plugin.class, Runnable.class);
}
@Override
@@ -64,8 +76,8 @@ public class FoliaPlatform extends BukkitPlatform {
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setDecimalSeparator('.');
decimal2 = new DecimalFormat("#.##", symbols);
registerInternalSyncPlaceholder(TabConstants.Placeholder.MSPT, 1000, p -> decimal2.format(Bukkit.getAverageTickTime()));
registerInternalSyncPlaceholder(TabConstants.Placeholder.TPS, 1000, p -> decimal2.format(Math.min(20, Bukkit.getTPS()[0])));
registerInternalSyncPlaceholder(TabConstants.Placeholder.MSPT, p -> decimal2.format(Bukkit.getAverageTickTime()));
registerInternalSyncPlaceholder(TabConstants.Placeholder.TPS, p -> decimal2.format(Math.min(20, Bukkit.getTPS()[0])));
}
@Override
@@ -85,9 +97,9 @@ public class FoliaPlatform extends BukkitPlatform {
});
}
private void registerInternalSyncPlaceholder(@NonNull String identifier, int refresh, @NonNull Function<TabPlayer, String> function) {
private void registerInternalSyncPlaceholder(@NonNull String identifier, @NonNull Function<TabPlayer, String> function) {
PlayerPlaceholderImpl[] ppl = new PlayerPlaceholderImpl[1];
ppl[0] = TAB.getInstance().getPlaceholderManager().registerInternalPlayerPlaceholder(identifier, refresh, p -> {
ppl[0] = TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier, p -> {
runSync((Entity) p.getPlayer(), () -> {
long time = System.nanoTime();
String output = function.apply((TabPlayer) p);
@@ -112,6 +124,18 @@ public class FoliaPlatform extends BukkitPlatform {
return -1;
}
/**
* Overriding the method to fix initial error caused by ServerPlaceholder implementation trying to
* retrieve the value in constructor, but folia does not support that. Overriding this function to avoid the
* TPS function being called in the wrong thread.
*
* @return -1
*/
@Override
public double getTPS() {
return -1;
}
/**
* Runs task using player's entity scheduler. It's using reflection, because
* Folia uses Java 17 while TAB maintains Java 8 compatibility for compatibility
@@ -126,9 +150,30 @@ public class FoliaPlatform extends BukkitPlatform {
@SneakyThrows
@SuppressWarnings("JavaReflectionMemberAccess")
public void runSync(@NotNull Entity entity, @NotNull Runnable task) {
if (!getPlugin().isEnabled()) return; // Server shutdown, no one cares anymore, everyone is about to be kicked
Object entityScheduler = Entity.class.getMethod("getScheduler").invoke(entity);
Consumer<?> consumer = $ -> task.run(); // Reflection and lambdas don't go together
entityScheduler.getClass().getMethod("run", Plugin.class, Consumer.class, Runnable.class)
.invoke(entityScheduler, getPlugin(), consumer, null);
}
/**
* Runs task in global tick thread. It's using reflection, because
* Folia uses Java 17 while TAB maintains Java 8 compatibility for compatibility
* with MC versions older than their player base.
*
* @param task
* Task to run
*/
@Override
@SneakyThrows
public void runSyncGlobal(@NotNull Runnable task) {
if (!getPlugin().isEnabled()) return; // Server shutdown, no one cares anymore, everyone is about to be kicked
globalScheduler_execute.invoke(globalScheduler, getPlugin(), task);
}
@Override
public boolean hasLineOfSight(@NotNull TabPlayer viewer, @NotNull TabPlayer target) {
return true; // Cross-entity line-of-sight checks are not safe from Folia's global scheduler.
}
}
@@ -55,10 +55,12 @@ public interface ImplementationProvider {
/**
* Creates a new instance of tab list entry tracker.
*
* @param player
* Player to create tab list entry tracker for
* @return Newly created tab list entry tracker
*/
@NotNull
TabListEntryTracker newTabListEntryTracker();
TabListEntryTracker newTabListEntryTracker(@NotNull Player player);
/**
* Returns ping of the given player.
+2 -2
View File
@@ -25,8 +25,8 @@ permissions:
tab.cpu:
description: Allows player to run /tab cpu
default: op
tab.debug:
description: Allows player to run /tab debug [player]
tab.dump:
description: Allows player to run /tab dump <player>
default: op
tab.groupinfo:
description: Allows player to run /tab group <group>
-4
View File
@@ -1,4 +0,0 @@
dependencies {
implementation(projects.bukkit)
compileOnly("org.bukkit:craftbukkit:1.10.2-R0.1-SNAPSHOT")
}
Loaded 100 of 481 files, more files were not shown because too many files have changed in this diff. Show more