Compare commits

...
407 Commits
Author SHA1 Message Date
NEZNAMY 4510491177 3.2.0 2022-12-12 09:46:21 +01:00
NEZNAMY 01b3a12377 1.19.3 support 2022-12-12 09:46:12 +01:00
NEZNAMY 64ae9863fb Bukkit - Drop old event API system 2022-12-10 12:27:51 +01:00
NEZNAMY ecd315a407 Sorting - Fix error from previous commit (.toLowerCase made string longer) 2022-12-08 11:20:58 +01:00
NEZNAMY 678eed288e Conditions - Add OR type for anonymous conditions using "|" symbol 2022-12-06 10:32:41 +01:00
NEZNAMY 9510455cb1 Conditions - Fix anonymous conditions not updating from previous commit 2022-12-06 10:31:29 +01:00
NEZNAMY e925ce23d0 Conditions - Fix nested conditions not working if they are defined below the condition using them 2022-12-06 09:51:16 +01:00
NEZNAMY c68cad6a1e Global PlayerList - Fix feature breaking nick/skin changes (minecraft mechanic changed in 1.19) (#752) 2022-12-06 09:50:00 +01:00
NEZNAMY 01d6eda4fb Configuration - Use luckperms placeholders instead of vault in default configuration (and make them return empty value if LP is not found) 2022-12-04 14:19:21 +01:00
NEZNAMY 73e7119bbc Configuration - add hidden server-name option for %server% value (useful with MySQL) 2022-12-04 11:49:59 +01:00
NEZNAMY cea781382a Sorting - Fix wrong team name being unregistered (#727) 2022-12-03 21:18:38 +01:00
NEZNAMY cf25b84e86 Commands - Fix debug command showing sorting is disabled even if layout is enabled for sorting 2022-12-03 21:16:29 +01:00
NEZNAMY 86e1d22390 Sorting - Only use 1 character for sorting by groups, permissions and predefined values; Improve debug output of sorting results 2022-12-03 12:16:38 +01:00
NEZNAMY 1625be111f Global PlayerList - Make server names in server groups and spy-servers not case-sensitive 2022-12-02 23:18:40 +01:00
NEZNAMY 00cda3b2aa Placeholders - Load all used placeholders on join in placeholder manager to make cpu usage appear under that "feature" instead of a feature actually using the placeholders 2022-12-01 22:46:40 +01:00
NEZNAMY 85a00a4436 Code style - Extract minimum placeholder refresh interval into an editable constant 2022-12-01 22:36:33 +01:00
NEZNAMY 37aec1a497 Global PlayerList - Add %playerlist-group_<group>% placeholder for online count in each group 2022-12-01 22:32:43 +01:00
NEZNAMY ef637616ee Placeholders - Improve error message 2022-11-27 17:13:24 +01:00
NEZNAMY af3b5067fb BossBar - Ignore players in disabled worlds/servers in announce command (#748) 2022-11-25 11:23:48 +01:00
NEZNAMY 53e5b15f96 Tab Expansion - Fix error in nested placeholder parsing 2022-11-24 11:01:55 +01:00
NEZNAMY 6a3afae607 Implement empty method 2022-11-16 12:24:03 +01:00
NEZNAMY 2e05c7d9d1 Another attempt for #727 2022-11-15 19:15:49 +01:00
NEZNAMY fb698a04b7 Configuration - Merge default config.yml files into one 2022-11-12 19:11:36 +01:00
NEZNAMY 52bdf30ddb Merge remote-tracking branch 'origin/master' 2022-11-09 22:34:22 +01:00
NEZNAMY a92f07aa40 Tab Expansion - Resend values to bridge on server switch (#740) 2022-11-09 22:34:09 +01:00
NEZNAMY 003ff7f99d Merge pull request #739 from CoachYT1/patch-1
bump org.yaml:snakeyaml to 1.33
2022-11-09 21:59:08 +01:00
NEZNAMY 47b9806ae7 Tab Expansion - Parse nested placeholders in replace function 2022-11-07 22:26:17 +01:00
CoachYT d2a7c65e08 bump org.yaml:snakeyaml to 1.33
snakeyaml 1.30 is affected by 5 vulnerabilities and should be upgraded to the latest version
2022-11-07 17:53:24 +01:00
NEZNAMY e232336a5f Add check for modifying non-existing teams (#727) 2022-11-06 22:42:23 +01:00
NEZNAMY 8816c75212 BungeeCord - Fix crash on 1.7 caused by unregistering non-existing objectives (login packet cleared them) (#727) 2022-11-04 13:31:34 +01:00
NEZNAMY f9d54751ba Fix rare ConcurrentModificationException 2022-11-04 12:53:00 +01:00
NEZNAMY 43d41ee761 BungeeCord - Improve unsupported BungeeCord build message 2022-11-01 17:55:43 +01:00
NEZNAMY 4512ba8d5e NameTags - Print a console warn if both unlimited nametags and invisible nametags are enabled 2022-10-30 17:56:00 +01:00
NEZNAMY 8b55bca564 Remove redundant variable 2022-10-30 13:40:32 +01:00
NEZNAMY d8d54970d3 Code style - Clean up vanish check code 2022-10-30 13:29:45 +01:00
NEZNAMY 15ea4482f9 Bukkit - Drop direct Essentials hook for vanishing as it supports metadata 2022-10-30 13:27:24 +01:00
NEZNAMY 87f9f38eac Update issue templates 2022-10-29 18:35:11 +02:00
NEZNAMY dba559f041 Layout - Fix startup error caused by invalid number as slot 2022-10-29 12:17:04 +02:00
NEZNAMY c610b6f323 PlayerList - Fix name applying on server switch when pipeline injection is disabled 2022-10-26 17:36:08 +02:00
NEZNAMY a5fb900e19 API - Add back TabPlayer#getTeamName() 2022-10-24 14:05:39 +02:00
NEZNAMY 09d6e455f5 Layout - Implement a fix for compatibility with plugins which add empty player name into their 2022-10-21 19:55:59 +02:00
NEZNAMY e68097b0f4 Layout - Fix startup error if slots is a single number instead of interval 2022-10-16 15:32:26 +02:00
NEZNAMY 0744fed953 Merge branch 'master' of https://github.com/NEZNAMY/TAB 2022-10-16 14:40:42 +02:00
NEZNAMY de0adf4067 MySQL - Attempt to fix startup error (#717) 2022-10-16 13:12:54 +02:00
NEZNAMY 76bf2b3efc Update links in README 2022-10-16 13:11:56 +02:00
NEZNAMY c089badeb4 Configuration - Remove owner and admin settings from default configuration 2022-10-15 19:15:38 +02:00
NEZNAMY b3b99d366a Placeholders - Fix relational placeholders updating causing normal placeholders to not refresh 2022-10-14 19:28:05 +02:00
NEZNAMY 4a36486f65 Bukkit - Fix startup on 1.8 2022-10-08 12:41:05 +02:00
NEZNAMY 75126d00d9 BossBar - Fix countdown placeholder not activating on usage in PlaceholderAPI 2022-10-06 16:43:06 +02:00
NEZNAMY 17ebf95d5b BossBar - Fix announcement bars disappearing on refresh 2022-10-06 16:42:32 +02:00
NEZNAMY c764420cfe Code Style - Simplify field value assignment 2022-10-06 15:33:50 +02:00
Callum Seabrook 28f05fc2af Changed publishing artifact IDs to begin with "tab-" 2022-10-06 12:02:28 +01:00
Callum Seabrook a420892887 Fixed publishing ID 2022-10-06 11:54:06 +01:00
NEZNAMY d910dc5913 Header / Footer - Prioritize world over server in config.yml as well 2022-10-06 11:11:00 +02:00
NEZNAMY c9c1cbc349 Layout - Fix anti-override for playerlist names being disabled even when no layout is visible 2022-09-26 11:02:14 +02:00
NEZNAMY fd4704f75e 3.1.5 2022-09-20 15:46:28 +02:00
NEZNAMY 5400ca637f Global PlayerList - Do not fill display name for 1.7 players 2022-09-19 18:23:16 +02:00
NEZNAMY 79d2610117 Global PlayerList - Fix server switch when pipeline injection is disabled 2022-09-17 18:38:06 +02:00
NEZNAMY 6a51f4cb9d BungeeCord - Fix 1.7 players not being processed fully on server switch (reapplying scoreboards) 2022-09-13 18:33:52 +02:00
NEZNAMY c190776665 Scoreboard - Fix show command enabling scoreboard for players who disabled it using toggle command 2022-09-13 18:32:54 +02:00
NEZNAMY 3765a1232e Merge pull request #697 from JT122406/master
Updates and Velocity Deprecation Fix
2022-08-27 19:48:00 +02:00
J.T. McQuigg 28866eba21 More Updates and Build Fix 2022-08-27 13:07:28 -04:00
J.T. McQuigg 08adee3dc3 Recomment this out for PR 2022-08-27 11:57:34 -04:00
J.T. McQuigg 8a1adaf2cd More Updates 2022-08-27 11:57:08 -04:00
J.T. McQuigg fdb9accaa6 Updates and don't use Deprecation in Velocity module 2022-08-27 11:42:15 -04:00
NEZNAMY 4f7656748b Merge pull request #695 from Tanguygab/master
added StartWith `|-` and EndsWith `-|` condition types
2022-08-25 22:25:48 +02:00
Tanguygab 03e4495ac7 added |- and -| conditions 2022-08-25 15:15:21 +02:00
NEZNAMY 01af5cb9e1 Placeholders - Fix nested placeholders not being applied when updating from nested?!! (#694) 2022-08-24 16:52:01 +02:00
NEZNAMY 8d49f32df2 Code style - Change the way API instance is set 2022-08-22 20:30:16 +02:00
NEZNAMY 8a955b3bc8 Javadoc - Further improve documentation 2022-08-22 20:27:20 +02:00
NEZNAMY ead40cb413 Merge pull request #691 from Tanguygab/master
Commands - Added target and -s arguments for /tab nametag preview/toggle (#690)
2022-08-22 16:22:32 +02:00
Tanguygab 5e07f78d4f updated messages.yml 2022-08-22 16:20:43 +02:00
Tanguygab c7d98795bc Revert "* for all players in /tab scoreboard show"
This reverts commit e4f33b58f7.
2022-08-22 16:13:46 +02:00
Tanguygab 9f637e3d6a added target and -s arguments for /tab nametag preview/toggle 2022-08-22 16:09:23 +02:00
Tanguygab 2d5f074c78 Merge remote-tracking branch 'upstream/master' 2022-08-22 15:35:23 +02:00
NEZNAMY 3be6ba484d API - Fix show_item hover event on 1.16+ 2022-08-18 22:36:43 +02:00
NEZNAMY d9d74e7a32 Placeholders - Fix NPE in %essentialsnick% event listener due to Essentials bug (#684) 2022-08-18 12:46:54 +02:00
NEZNAMY 887306a5ed Bukkit - Include 1.19.2 in supported version range 2022-08-16 01:25:15 +02:00
NEZNAMY 4b2542eb4f Global Playerlist - Add config option to disable (and enable) profile key filling (#680) 2022-08-15 13:28:56 +02:00
NEZNAMY 66bd3baa3e Layout - Add config option to show vanished players in layout 2022-08-14 15:41:19 +02:00
NEZNAMY bf32004f8f Layout - Do not format original entries in any way (can cause spaces) 2022-08-13 17:50:28 +02:00
NEZNAMY bfb2df9c95 Commands - Show storage type in debug output 2022-08-13 17:44:46 +02:00
Callum Seabrook ddeb05e408 Merge pull request #662 from JT122406/Updates
Update Gradle Wrapper to 7.5
2022-08-12 16:58:38 +01:00
Callum Seabrook 9312d61258 Fix some more BS 2022-08-12 16:49:30 +01:00
Callum Seabrook d44909bc42 Fixed some BS 2022-08-12 16:41:26 +01:00
Callum Seabrook 1d77df335c Switched to Indra for publishing and improved code reuse in build scripts 2022-08-12 16:29:12 +01:00
NEZNAMY a268dc25f4 BungeeCord - Use direct code for proxy module again 2022-08-12 15:46:14 +02:00
NEZNAMY 8c5696faa0 Bukkit - Register universal placeholders after bukkit ones to fix reference issue with nested placeholders 2022-08-12 15:05:40 +02:00
NEZNAMY 94f3ccfb7d Javadoc - Further improve documentation 2022-08-12 15:04:49 +02:00
NEZNAMY 576329214d Add 1.19.2 to known versions 2022-08-11 17:57:49 +02:00
NEZNAMY 5c13c2e225 Velocity - Remove join processing delay since the required delay is already included elsewhere 2022-08-11 17:49:42 +02:00
NEZNAMY 1070f85617 Placeholders - Retry updateValue if player is not loaded yet (might fix #564) 2022-08-11 17:48:26 +02:00
NEZNAMY 7c9dc87fcb NameTags - Make canSeeFriendlyInvisibles flag configurable 2022-08-09 14:12:14 +02:00
NEZNAMY 363e58bf91 API - Expose another method 2022-08-04 17:06:30 +02:00
NEZNAMY daca3f5b71 3.1.4 2022-08-04 15:43:53 +02:00
NEZNAMY a7d2ec084b Use profile key in player info packet (#673) 2022-08-03 12:57:48 +02:00
NEZNAMY 167f8cd0ff BungeeCord - Remove Waterfall tablist rewrite hook since the option doesn't work as I expected 2022-08-03 11:14:10 +02:00
NEZNAMY 170307f45f Catch everything 2022-07-29 15:49:59 +02:00
NEZNAMY 2f36377780 Revert "Placeholder output replacements - Apply again after applying nested placeholders"
This reverts commit 2706cd5881.
2022-07-29 12:48:13 +02:00
NEZNAMY 48c565d85d 3.1.3 2022-07-28 17:59:15 +02:00
NEZNAMY 86b3cc4044 Sorting - Fix startup error caused by misconfiguration (#664) 2022-07-28 11:40:42 +02:00
NEZNAMY 6a2d5091b8 Bukkit - Add 1.19.1 support 2022-07-28 11:30:34 +02:00
J.T. McQuigg dc09e77190 Update Gradle Wrapper to 7.5 2022-07-26 17:16:56 -04:00
NEZNAMY 4fc139c0c6 Remove synchronized lock from sendCustomPacket method 2022-07-26 21:57:36 +02:00
NEZNAMY e5d800f469 API - Expose a few more methods 2022-07-26 21:56:16 +02:00
NEZNAMY 4879ac28b5 PingSpoof - Add Layout compatibility 2022-07-25 15:34:33 +02:00
NEZNAMY bd0badab48 Feature management - Don't deserialize objective packets if no feature is listening to them 2022-07-24 21:20:55 +02:00
NEZNAMY 5575a26d3b Placeholders - Use event instead of refresh task for %essentialsnick% placeholder 2022-07-24 20:27:43 +02:00
NEZNAMY d8cbe96949 BossBar - Fix issues with hidden-by-default enabled 2022-07-24 17:55:04 +02:00
NEZNAMY cfb506a7ff BossBar - Make Wither's size (nearly) zero on 1.8 (#654) 2022-07-24 13:28:18 +02:00
Tanguygab d5b0ff7f40 Merge remote-tracking branch 'upstream/master' 2022-07-24 13:11:11 +02:00
NEZNAMY 31d259210c Bukkit - Add support for mojang mapped jars 2022-07-21 12:56:23 +02:00
NEZNAMY 39112b8d41 Tab expansion - Add %tab_nametag_visibility% and rename %tab_ntpreview% to %tab_nametag_preview% 2022-07-20 00:14:38 +02:00
NEZNAMY 4c92286ebb Thread management - Increase max thread pool size to 20 2022-07-19 14:02:44 +02:00
NEZNAMY a317aeba85 Plugin message handling - Fix ClassCastException when a placeholder is externally registered as a server placeholder 2022-07-19 13:30:32 +02:00
NEZNAMY dcaf17294e Aligned PlayerList - Fix commit ebd67dd - Do not save config if no changes were made 2022-07-19 12:00:16 +02:00
NEZNAMY 0bc734689a Unlimited Nametags - Fix per-world formatting not applying on world switch 2022-07-19 00:22:11 +02:00
NEZNAMY d0c2c00cc9 BungeeCord - Respect Waterfall's tablist rewrite config option (#652) 2022-07-18 19:25:07 +02:00
NEZNAMY fd02328af9 Aligned PlayerList - Update widths.txt for 1.19 2022-07-18 14:55:05 +02:00
NEZNAMY 3b7de31324 Aligned PlayerList - Add a subcommand (disabled by default) for mass-verifying character widths 2022-07-18 14:52:44 +02:00
NEZNAMY ebd67dd025 Aligned PlayerList - Remove custom widths from config that match default value 2022-07-17 21:30:56 +02:00
NEZNAMY 4557a39ae2 RGB - Fix &r getting removed 2022-07-17 21:29:59 +02:00
NEZNAMY ce1eb299d3 Conditions - Fix conditions being 1 loop behind nested placeholders 2022-07-16 22:58:37 +02:00
Callum Seabrook 20ab3f0a72 Disabled JavaDoc generation for all but API and added property to enable for API 2022-07-16 20:11:58 +01:00
NEZNAMY d61f90c25c Code style - use api module instead of shared where possible 2022-07-16 20:56:59 +02:00
NEZNAMY 90474e7592 Layout - Fix startup error when a layout is missing groups section 2022-07-16 19:15:18 +02:00
NEZNAMY a2e28003bc Code style - Move TabConstants class to api module 2022-07-16 19:13:31 +02:00
NEZNAMY 6e409c3dcd Thread management - Use scheduled executor for repeating and delayed tasks 2022-07-16 18:39:13 +02:00
NEZNAMY 85a699c8de Commands - Only show announcement bars in auto-complete for bossbar announcement command 2022-07-16 18:33:57 +02:00
NEZNAMY a788d1fb23 Commands - Add missing permission check when toggling scoreboard using player argument 2022-07-16 14:14:35 +02:00
NEZNAMY ab5b72375c Code style - Remove redundant suppression and cast 2022-07-13 16:42:36 +02:00
NEZNAMY 3a023c51b5 Catch NoClassDefFoundError which is rarely thrown on reloads and leads to memory leaks 2022-07-13 16:24:17 +02:00
NEZNAMY 0ad91bcafe Sorting - Fix space in sorting types causing it to not work 2022-07-13 15:25:30 +02:00
NEZNAMY f41ea3bcab Unlimited Nametags - Improve heights to match vanilla & remove config option for space between lines, since original value was found 2022-07-13 14:57:07 +02:00
NEZNAMY 327dc4b416 Unlimited Nametags - Fix sneaking not being detected immediately on reload 2022-07-13 14:55:39 +02:00
NEZNAMY 27e7779456 Unlimited Nametags - Do not spawn for <1.8 players 2022-07-13 14:02:33 +02:00
NEZNAMY a7b2f5bb22 BungeeCord - Fix NPE on <1.8 clients 2022-07-13 13:32:53 +02:00
NEZNAMY 19fcb7c946 Layout - Do not send latency update packets to <1.8 clients 2022-07-13 13:32:23 +02:00
NEZNAMY 2706cd5881 Placeholder output replacements - Apply again after applying nested placeholders 2022-07-13 12:17:39 +02:00
NEZNAMY 4e2abd9c5b Bukkit - Fix startup on 1.7.x and 1.8.0 2022-07-13 12:04:21 +02:00
NEZNAMY 51ba2fa80a Placeholder output replacements - Fix exact match detection when input is not colored (new placeholderapi version) 2022-07-11 00:28:14 +02:00
NEZNAMY fd60cd0a21 Change the way the module is disabled 2022-07-06 22:00:52 +02:00
NEZNAMY 5025372c05 Revert "Code style - Remove unused method" 2022-07-06 21:15:54 +02:00
NEZNAMY 4806bd075f Configuration - Fix NPE when disabled world/server is null 2022-07-05 19:09:56 +02:00
NEZNAMY 370d17757a Add poor and unreliable MiniMessage support 2022-07-04 18:06:19 +02:00
NEZNAMY a648d7e75f Scoreboard - Fix NPE when using show command from console without player argument (#639) 2022-07-04 14:05:44 +02:00
NEZNAMY 518e1c5310 AlignedPlayerList - Fix rare NPE caused by multi-thread access 2022-07-04 13:46:29 +02:00
NEZNAMY 11539d8290 Commands - Allow removing per-world/server values 2022-07-04 13:44:08 +02:00
Tanguygab e4f33b58f7 * for all players in /tab scoreboard show 2022-07-04 13:34:04 +02:00
NEZNAMY 073caa9515 Compilation - Bump Authlib version 2022-07-04 13:33:00 +02:00
NEZNAMY 2b06b11916 Scoreboard - Fix same lines of text not appearing with use-numbers true 2022-06-24 21:16:45 +02:00
NEZNAMY 032b0f8a41 Configuration - Fix NPE when copying bukkit config to bungeecord 2022-06-19 14:05:59 +02:00
NEZNAMY 9391363f9b Unlimited Nametags - Disable on boat with chest as well (#625) 2022-06-19 12:36:35 +02:00
NEZNAMY 467fb2e9c7 Bukkit - Use reflection for LibsDisguises hook 2022-06-18 16:12:53 +02:00
NEZNAMY 257893b8de Downgrade dependency to fix compilation 2022-06-18 16:12:09 +02:00
NEZNAMY e919839de5 Merge pull request #623 from JT122406/Updates
Repo Bumps
2022-06-18 14:12:41 +02:00
J.T. McQuigg 77d372cbee Use new RedisBungee 2022-06-18 07:42:38 -04:00
J.T. McQuigg 03e9facaf7 Repo Bumps 2022-06-18 07:35:45 -04:00
NEZNAMY f3e459d911 Revert "BungeeCord - Retry plugin message processing if player isn't loaded for any reason (#564)"
This reverts commit dffac5deb8.
2022-06-17 14:15:39 +02:00
NEZNAMY 36feff0e04 Sorting - Fix off-by-1 possibly causing sorting to not work correctly for players without sorting group/permission 2022-06-16 18:53:42 +02:00
NEZNAMY 0320140a1b BungeeCord - Fix vanish/invisible status not resetting on server switch 2022-06-12 14:16:20 +02:00
NEZNAMY 4fb1b1c9f9 3.1.2 2022-06-11 18:01:17 +02:00
NEZNAMY 37f3896a41 Update to 1.19 2022-06-11 18:01:08 +02:00
NEZNAMY e1c152d95d Add missing change in previous commit 2022-06-11 18:00:08 +02:00
NEZNAMY dffac5deb8 BungeeCord - Retry plugin message processing if player isn't loaded for any reason (#564) 2022-06-11 17:57:18 +02:00
NEZNAMY 8aa3bbfad3 Scoreboard - Fix RGB codes not translating into legacy correctly in long lines 2022-06-09 23:10:44 +02:00
NEZNAMY 5bb4006e55 Scoreboard - Fix additional empty lines when use-numbers is enabled 2022-06-08 00:46:32 +02:00
NEZNAMY e06a406682 Clean up repositories 2022-06-08 00:45:21 +02:00
NEZNAMY a19719be4b Configuration - Remove deleted placeholder from default config 2022-06-03 23:47:41 +02:00
NEZNAMY 35856a9f3d Merge branch 'master' of https://github.com/NEZNAMY/TAB 2022-06-02 15:50:46 +02:00
NEZNAMY cf1d5d5ee2 Code style - Remove unused method 2022-06-01 20:23:16 +02:00
NEZNAMY f69a845e51 Bukkit - Implement chat message sending on <1.7 2022-05-31 12:21:07 +02:00
NEZNAMY db67e93bb8 Disable compilation of a module 2022-05-31 11:51:21 +02:00
NEZNAMY 313f089222 Aligned Suffix - Fix memory leak 2022-05-20 01:10:00 +02:00
NEZNAMY 458c08ded7 Tab expansion - Slightly optimize placeholder retrieving 2022-05-18 01:39:20 +02:00
NEZNAMY fe6cc7417f Placeholders - Fix refresh interval forwarding to bridge (#597) (2nd attempt) 2022-05-17 19:00:23 +02:00
NEZNAMY 181c94a808 Code style - Extract strings into constants 2022-05-13 00:42:30 +02:00
NEZNAMY 9ff6f02d8d Code style - Remove unused code and other minor changes 2022-05-12 22:49:41 +02:00
NEZNAMY 3cf74516d9 Bukkit - Remove unused data watcher registries 2022-05-12 22:25:18 +02:00
NEZNAMY 31f5502c16 BungeeCord - Delete TAB's bungeecord events (to make users switch to universal ones) 2022-05-12 22:04:37 +02:00
NEZNAMY 671d60bd72 API - Throw user-friendly exception if player is not loaded yet (#585) 2022-05-10 18:32:09 +02:00
NEZNAMY 294e866501 Placeholders - Fix refresh interval forwarding to bridge (#597) 2022-05-10 18:21:13 +02:00
NEZNAMY 96789100e3 Bukkit - Wrap incoming components instead of deserializing and then serializing them 2022-05-10 13:22:27 +02:00
NEZNAMY ea934fc7fb GroupManager - Add null check for player group from permission plugin 2022-05-08 12:48:06 +02:00
NEZNAMY d529b7b340 Sorting - Allow placeholders with ":" inside to work properly in PLACEHOLDER sorting type 2022-05-08 12:47:40 +02:00
NEZNAMY 092ca7f9f8 Aligned Suffix - Assume players can see themselves when vanished 2022-05-03 18:03:29 +02:00
NEZNAMY 79f4bdb869 Unlimited Nametags - Do not delay respawn by 1 tick anymore to fix OOM 2022-05-01 13:07:42 +02:00
NEZNAMY b0d2d0f7dc NickCompatibility - Clean up code 2022-04-30 20:01:40 +02:00
NEZNAMY cdf58c1dfe Teams - set canSeeFriendlyInvisibles to true 2022-04-30 18:21:46 +02:00
NEZNAMY 4680de6022 Feature management - Add try/catch for each feature in join processing (#560) 2022-04-30 17:22:03 +02:00
NEZNAMY e8f61181b6 Error logging - Log (critical) errors into console even when error file is full 2022-04-30 17:20:24 +02:00
NEZNAMY 75e9e007d0 Fix disabled worlds not loading correctly (from #6f07cf4) 2022-04-28 17:27:23 +02:00
NEZNAMY 3574ef7c0b Belowname - Do not update score when entering disabled world 2022-04-28 12:20:54 +02:00
NEZNAMY 86158036d2 Commands - Make permission group assigning more obvious in debug output 2022-04-28 11:55:21 +02:00
NEZNAMY e73beb47a3 API - Fix scoreboard Line#setText not working 2022-04-26 23:23:42 +02:00
NEZNAMY 0db5f1dd73 Scoreboard - Fix NPE when a line is null 2022-04-23 14:18:30 +02:00
NEZNAMY 6dcdc2aa58 PetFix - Fix attack packet cancellation 2022-04-20 10:46:08 +02:00
NEZNAMY 64601137e8 Scoreboard - Show empty lines when defined as empty, only hide when placeholder output is empty 2022-04-15 00:07:44 +02:00
NEZNAMY af7697b670 BungeeCord - Fixed NPE from moving to reflection 2022-04-14 23:24:45 +02:00
NEZNAMY 9e4f46088b Commands - Do not force refresh players who are not loaded yet (#560) 2022-04-12 11:21:06 +02:00
NEZNAMY 91e71a9b9c Layout - Fixed NPE when player had no layout visible 2022-04-10 23:32:13 +02:00
NEZNAMY 1198ffb847 BungeeCord - Use reflection for proxy module access due to the repository no longer being available 2022-04-10 23:31:51 +02:00
NEZNAMY 72116ecd0c Javadoc - Further improve documentation 2022-04-10 21:08:19 +02:00
NEZNAMY 04ebb7123d Code style - Move NMS object declaration and initialization 2022-04-10 20:28:29 +02:00
NEZNAMY 43096beafe Bukkit - Use ConsoleCommandSender for colored messages (#555) 2022-04-09 18:27:37 +02:00
NEZNAMY 2a08c62456 Tab expansion - Fix server placeholders not loading on join 2022-04-07 15:44:58 +02:00
NEZNAMY b62d74c3fa Code style - Remove unnecessary reflection 2022-04-06 20:38:03 +02:00
NEZNAMY 652fb5a6ad Bukkit - Switch from Paper to Purpur API 2022-04-06 19:11:19 +02:00
NEZNAMY d1f25cc19f Bukkit - Disable checking for plugins with compatibility issue 2022-04-06 18:25:12 +02:00
NEZNAMY dc6a9d9761 Fix layout startup 2022-04-06 13:44:16 +02:00
NEZNAMY 838d725715 Code style - Removed redundant static modifiers 2022-04-06 13:31:31 +02:00
NEZNAMY c45acb39ca Code style - Clean up variable initialization 2022-04-06 13:10:11 +02:00
NEZNAMY 957edf2a52 Javadoc - Fixed compilation warn 2022-04-06 12:44:54 +02:00
NEZNAMY d969c6ed0e Code style - Change way of checking if server is proxy 2022-04-06 12:41:20 +02:00
NEZNAMY dd6fca961c Code style - Clean up configuration loading 2022-04-06 12:34:39 +02:00
NEZNAMY e88a6df456 Config - remove hidden unregister-before-register option and remove the function entirely. Hopefully with improved checks this should not be necessary anymore. It caused client errors, as well as #549 2022-04-06 11:35:03 +02:00
NEZNAMY 3a4530c999 RGB - Finish #552 - Only replace clean gradients in property initialization 2022-04-05 23:24:37 +02:00
NEZNAMY b623dba40a Merge branch 'master' of https://github.com/NEZNAMY/TAB 2022-04-05 23:20:13 +02:00
NEZNAMY e2fd945109 Merge pull request #552 from lucypoulton/feat/kyori-rgb-format
Add support for Kyori MiniMessage RGB format
2022-04-05 23:19:52 +02:00
NEZNAMY e6dc3cda31 Unlimited nametags - Manually destroy armor stands on world switch 2022-04-05 20:21:36 +02:00
Lucy Poulton 18d8ff127a add support for kyori minimessage rgb format 2022-04-05 14:25:22 +01:00
NEZNAMY 4a9526451f Code style - Remove redundant "triggerMode" flag from Placeholder class due being redundant. Setting refresh to -1 has the same effect. 2022-04-04 19:03:51 +02:00
NEZNAMY 669d89c870 Code style - Move tab expansion code to interface 2022-04-04 18:15:06 +02:00
NEZNAMY 39161f2af5 Nametags - Reduced CPU usage of anti-override 2022-04-04 16:02:32 +02:00
NEZNAMY be8c33e3fc Velocity - Delay join event processing to avoid unformatted tablist names 2022-04-03 14:08:19 +02:00
NEZNAMY 7c1a27f9df Javadoc - Further improve documentation 2022-04-02 19:07:18 +02:00
NEZNAMY 342cc93dbf Code style - Clean up code 2022-04-02 13:17:15 +02:00
NEZNAMY 6f07cf439b Code style - Clean up loading of disabled servers/worlds 2022-04-01 23:40:04 +02:00
NEZNAMY 1d4d00b69b Code style - Clean up code with usage of @Inject 2022-04-01 23:21:30 +02:00
NEZNAMY 54cb3e4d54 3.1.1 2022-04-01 10:02:26 +02:00
NEZNAMY b0fc2caef9 RedisBungee - Detect player name change from other proxies 2022-03-31 21:54:51 +02:00
NEZNAMY 1221509d24 MySQL - Do not lowercase _DEFAULT_ group name, which caused it to not load (#546) (for real this time) 2022-03-31 20:52:12 +02:00
NEZNAMY 17f5f60504 MySQL - Do not lowercase _DEFAULT_ group name, which caused it to not load (#546) 2022-03-31 17:02:12 +02:00
NEZNAMY a8b0b488c6 Layout - Implement latency updating for player slots (#545) 2022-03-31 15:45:14 +02:00
NEZNAMY 6d922d5b70 Unlimited nametags - Fix NPE when player moves too quickly after joining 2022-03-31 14:50:54 +02:00
NEZNAMY 1ab62a33e3 Placeholders - Improve refresh task interval adaptation (#543) 2022-03-31 10:01:10 +02:00
NEZNAMY f9540a1e78 Tab Expansion - Fix placeholders not loading on player join 2022-03-29 11:17:04 +02:00
NEZNAMY a720bf2c8b Unlimited nametags - Do not allow preview command when armor stands are disabled for player 2022-03-28 21:25:42 +02:00
NEZNAMY c143eab6fb Global playerlist - Fix player possibly being removed from playerlist after reconnecting rapidly (#539) 2022-03-28 20:57:47 +02:00
NEZNAMY 1afb20af8b Code style - Avoid reassigning parameters 2022-03-28 12:57:31 +02:00
NEZNAMY c6f27c839d Code style - Document empty method body 2022-03-28 12:49:37 +02:00
NEZNAMY 742f1708c5 Code style - Remove unused import 2022-03-28 12:47:19 +02:00
NEZNAMY ac9601a48b Unlimited nametags - Fix nametag toggle command not hiding vanilla name on players with disabled armor stands via API 2022-03-28 12:13:23 +02:00
NEZNAMY 4cd109a5fc Javadoc - Update style and fix alignment after replacing tabs with spaces 2022-03-26 23:13:19 +01:00
NEZNAMY 078554a8b0 RedisBungee - Fix /btab reload removing players from tablist when global playerlist is disabled 2022-03-25 14:46:59 +01:00
NEZNAMY 464e2f4f64 RedisBungee - Fix duplicated team names through proxies 2022-03-25 14:46:22 +01:00
NEZNAMY b84f55aa18 RedisBungee - Fix errors and issues when unlimited nametag mode is enabled 2022-03-25 14:45:55 +01:00
NEZNAMY be2f780bfa Drop LuckPerms event hook and go back to periodic task. Too many issues related to events. 2022-03-25 10:27:36 +01:00
NEZNAMY 262d8493a6 fix NPE 2022-03-25 09:33:32 +01:00
NEZNAMY 24e12e871b finish #534 2022-03-24 22:33:18 +01:00
NEZNAMY a2ee75339c 3.1.1-pre2 2022-03-24 22:20:25 +01:00
NEZNAMY 9c28c4d63c add auto complete for group names in group command 2022-03-22 22:12:55 +01:00
NEZNAMY 7d57da1424 fix disableArmorStands method not destroying armor stands 2022-03-22 13:06:28 +01:00
NEZNAMY b77b20ba4d expose placeholder output getters in the API 2022-03-22 11:54:30 +01:00
NEZNAMY 57ae58f093 call super.onQuit as it should 2022-03-21 20:26:56 +01:00
NEZNAMY b29c878688 Merge remote-tracking branch 'origin/master' 2022-03-21 08:51:52 +01:00
NEZNAMY 5a5a6c76c1 another banger that took a week to fix 2022-03-21 08:51:32 +01:00
NEZNAMY f6a53f537c fix NPE 2022-03-21 08:46:02 +01:00
NEZNAMY d86ed37208 allow negative values in replacement intervals using ~ 2022-03-20 17:55:17 +01:00
NEZNAMY 92b945f28d Merge pull request #534 from itsmemac/master
fix nametag preview help text
2022-03-20 15:03:07 +01:00
Mac cb164f0055 fix nametag preview help text
it seems new update `/tab ntpreview` has been replaced with `/tab nametag preview`
Help text was not updated
2022-03-20 19:31:38 +05:30
NEZNAMY acfe37c1b5 fix NPE 2022-03-20 12:50:30 +01:00
NEZNAMY 44ded26df4 change method call order in which groups are refreshed 2022-03-19 16:13:39 +01:00
NEZNAMY 6dbd92a010 remove players from map on quit 2022-03-19 16:12:06 +01:00
NEZNAMY f729ca0ad2 fix NPE 2022-03-19 16:11:45 +01:00
NEZNAMY 065490c077 Merge branch 'master' of https://github.com/NEZNAMY/TAB 2022-03-18 17:12:20 +01:00
NEZNAMY da6425dd93 Merge pull request #530 from CoachYT1/master
Update dependencies & GitHub actions
2022-03-15 08:25:12 +01:00
CoachYT 250dcbc676 Update build.yml 2022-03-14 12:18:41 +01:00
CoachYT de4ca4a88f Update libs.versions.toml 2022-03-14 12:15:58 +01:00
NEZNAMY 723a0c890e fix NPE 2022-03-13 22:36:37 +01:00
NEZNAMY 7085e7df20 do not forward group if assigning by permissions 2022-03-13 22:36:25 +01:00
NEZNAMY 09496d60f3 add argument check 2022-03-13 13:19:05 +01:00
NEZNAMY bf6bc26a5c add check into API for all the users who shade API into their projects 2022-03-13 13:16:20 +01:00
NEZNAMY a7217eae15 fix placeholders used only in tab expansion not refreshing 2022-03-13 13:12:37 +01:00
NEZNAMY 2a532ef9b7 update README.md 2022-03-13 13:11:37 +01:00
NEZNAMY c304ded924 avoid thread spam from LP events 2022-03-11 15:06:13 +01:00
NEZNAMY 4d8c47a505 #517 - make group names not case-sensitive when connected to MySQL 2022-03-09 10:22:51 +01:00
NEZNAMY d3e0ccbf34 forward nametag preview on server switch 2022-03-09 09:08:49 +01:00
NEZNAMY 3c82b46148 fix updating of invalid score on server switch when pipeline injection is disabled 2022-03-08 23:09:29 +01:00
NEZNAMY 07d7f2cc17 catch StackOverflowError as well 2022-03-08 21:50:10 +01:00
NEZNAMY 54d4f09ab1 fix StackOverflowError when using placeholder identifier inside its own replacements (will act the same as %value%) 2022-03-08 21:45:11 +01:00
NEZNAMY e8c698b5e8 #517 - fix mysql header/footer download/upload 2022-03-07 22:14:49 +01:00
NEZNAMY b096906ffd 3.1.1-pre1 2022-03-07 18:59:40 +01:00
NEZNAMY 5a9cec12fa WeakHashMap isn't so weak after all (fixed memory leak) 2022-03-07 18:59:22 +01:00
NEZNAMY a2ed71751f fix NPE 2022-03-07 18:58:10 +01:00
NEZNAMY 5a3a2df920 simplify 2022-03-07 15:50:24 +01:00
NEZNAMY 9d07e669ea merge ifs 2022-03-07 15:50:12 +01:00
NEZNAMY aa18f1cc35 remove unnecessary full path 2022-03-07 14:03:04 +01:00
NEZNAMY 668d78ea64 remove redundant constructor 2022-03-07 13:59:38 +01:00
NEZNAMY 6f62581d6e print OOM into file instead of throwing into console 2022-03-07 12:53:12 +01:00
NEZNAMY 06686ada5c add synchronized lock to printError method 2022-03-07 12:21:47 +01:00
NEZNAMY 4380482606 remove direct reference to player channel on bukkit 2022-03-07 12:21:24 +01:00
NEZNAMY d5112b3d32 fix group refreshing introduced in previous commit which in the end didn't even fix the issue it was supposed to 2022-03-06 20:26:49 +01:00
NEZNAMY ef67506640 fix error caused by concurrent access 2022-03-06 12:01:50 +01:00
NEZNAMY c2ea4d2793 #516 - skin signature can be null 2022-03-05 16:29:16 +01:00
NEZNAMY f0d8d479bd 3.1.0 2022-03-04 22:46:16 +01:00
NEZNAMY 2d46d70c92 Fix super rare permission group detecting issue when using TAB on bungeecord with assigning by permissions with using backend permissions with many online players. Jesus. 2022-03-04 22:38:43 +01:00
NEZNAMY 4320bb7cb4 try to fix rare error that doesn't make sense 2022-03-04 22:36:53 +01:00
NEZNAMY 398e8ed5ff remove redundant space 2022-03-04 12:31:43 +01:00
NEZNAMY 4e8b804337 prefer per-world settings over per-server 2022-03-04 10:39:45 +01:00
NEZNAMY de7c664e0d remove redundant check 2022-03-02 14:17:05 +01:00
NEZNAMY c7600a2e3d fix issues for players with unknown version 2022-03-02 13:34:29 +01:00
NEZNAMY 8b34e82aab simplify 2022-03-02 12:49:26 +01:00
NEZNAMY c96efe8802 use Logger for console messages 2022-03-02 12:46:33 +01:00
NEZNAMY 0a1bfd3a2e 3.1.0-pre3 2022-03-02 10:57:23 +01:00
NEZNAMY 755cf9ce43 1.18.2 2022-03-02 10:56:46 +01:00
NEZNAMY c6aca74396 fix NPE when assigning groups by permissions on bungeecord 2022-03-01 22:37:37 +01:00
NEZNAMY b5b7f3c16c add try/catch for PremiumVanish hook 2022-02-28 23:26:53 +01:00
NEZNAMY 293162b484 show name of file with syntax error correctly in error message 2022-02-28 15:27:17 +01:00
NEZNAMY bd543b7b9d fix rare NPE (#504) 2022-02-25 20:49:40 +01:00
NEZNAMY 994fe9f482 fix conversion error when bossbar names are numbers (#506) 2022-02-25 20:48:28 +01:00
NEZNAMY f657bdb14c update README.md 2022-02-22 23:42:16 +01:00
NEZNAMY a4238e1191 compensate for 1.8.0 client sided bug (#501) 2022-02-22 08:05:45 +01:00
NEZNAMY daf08423b4 3.1.0-pre2 2022-02-21 19:01:40 +01:00
NEZNAMY 91367f3d3a fix wrong text displayed 2022-02-21 16:33:21 +01:00
NEZNAMY 9dbd52b638 replace tabs with 4 spaces 2022-02-21 16:15:52 +01:00
NEZNAMY 22c6be2770 fix names not updating when new max player joins 2022-02-21 16:05:51 +01:00
NEZNAMY fb261c9f90 cleanup 2022-02-21 12:43:23 +01:00
NEZNAMY 638e35bf90 more abstraction 2022-02-21 12:40:15 +01:00
NEZNAMY 25d13862ae initialize server version & data folder constants instead of having per-platform getters 2022-02-21 12:30:02 +01:00
NEZNAMY 1c8f483138 split content of plugin message handler classes into other classes 2022-02-21 11:57:43 +01:00
NEZNAMY 92ad215873 more code abstraction 2022-02-21 11:06:02 +01:00
NEZNAMY 7ed3cfbb7c fix vanish status not refreshing correctly on bungeecord 2022-02-20 23:42:43 +01:00
NEZNAMY 53e75a27be avoid bytebuf instance check entirely if deserialization is disabled 2022-02-20 23:08:15 +01:00
NEZNAMY 1b67819c3c only forward relevant properties to expansion and fix values not updating 2022-02-20 15:16:12 +01:00
NEZNAMY 07f8020f79 fix syntax error in default file 2022-02-20 14:53:31 +01:00
NEZNAMY b12d0680d8 massively optimize some API methods 2022-02-20 14:50:05 +01:00
NEZNAMY 64247d913e implement "/tab nametag toggle" command hiding nametags of all players for command sender 2022-02-19 21:29:22 +01:00
NEZNAMY 3575cdae85 move code & respawn armor stands on world switch if preview is enabled 2022-02-19 14:27:04 +01:00
NEZNAMY f71a542247 add check for directory existence 2022-02-19 13:56:59 +01:00
NEZNAMY 46cc59138b skip action if list is empty 2022-02-19 13:31:00 +01:00
NEZNAMY 46bfa5112e for some reason this check is needed as well 2022-02-19 13:29:32 +01:00
NEZNAMY fcdff51783 use pre-allocated enum values array (again) 2022-02-19 13:29:18 +01:00
NEZNAMY a4be4ae419 fix check not working properly 2022-02-19 13:28:05 +01:00
NEZNAMY 7083352b9a cache enum values to reduce memory allocations 2022-02-18 23:37:27 +01:00
NEZNAMY 956a0f46c0 name the second thread pool as well 2022-02-18 23:26:26 +01:00
NEZNAMY aea1864627 iterate over arrays to reduce allocations 2022-02-18 23:01:29 +01:00
NEZNAMY 803b87111a make packet deserializing on BungeeCord use 30x less CPU 2022-02-18 22:40:36 +01:00
NEZNAMY f64a641b63 fix NPE caused by a dumb mistake 2022-02-18 12:49:28 +01:00
NEZNAMY 7f9bb354d7 remove wrong null checks 2022-02-18 12:48:08 +01:00
NEZNAMY 4b7b6284f8 move code to avoid duplication 2022-02-18 12:38:55 +01:00
NEZNAMY bbccd26415 remove %maxplayers% placeholder 2022-02-18 12:35:31 +01:00
NEZNAMY bab7c737b3 add more precondition checks 2022-02-18 12:31:06 +01:00
NEZNAMY 3169787b41 fix NPE on /btab parse 2022-02-18 11:13:34 +01:00
NEZNAMY f29bbc6657 move config option loading 2022-02-17 23:04:21 +01:00
NEZNAMY f53cd5ae11 fix armor stands heights not updating 2022-02-17 22:39:59 +01:00
NEZNAMY 38d36b5ab5 remove unused softdependency 2022-02-17 21:33:54 +01:00
NEZNAMY 8f8f596abc drop direct UltraPermissions hook (still supported via Vault) 2022-02-17 16:28:43 +01:00
NEZNAMY 2afc8abdb2 rename fields from "method" to "action" 2022-02-17 16:19:57 +01:00
NEZNAMY 38dd948306 cleanup 2022-02-17 11:37:28 +01:00
NEZNAMY e393cf0a10 make code more universal 2022-02-17 11:35:36 +01:00
NEZNAMY 4f451017e3 make PacketBuilder not abstract and add default implementation to delete a redundant class 2022-02-17 11:17:27 +01:00
NEZNAMY 9cb31af48a class cleanup using consumer 2022-02-17 10:56:45 +01:00
NEZNAMY 2c17041b27 universal registration of %online_<server>% on proxies 2022-02-17 10:34:55 +01:00
NEZNAMY 72c75a443e more abstraction of RedisBungee feature 2022-02-16 20:04:06 +01:00
NEZNAMY 1ba6384647 update README.md 2022-02-16 18:57:40 +01:00
NEZNAMY e2edf9a37c delete unused file 2022-02-16 17:35:23 +01:00
NEZNAMY cb0bfd095d 3.1.0-pre1 2022-02-15 13:05:53 +01:00
NEZNAMY ec03dd4755 fix long line creating empty line above it 2022-02-15 13:05:21 +01:00
NEZNAMY de44f5ae42 update issue templates 2022-02-14 21:34:22 +01:00
NEZNAMY 5ee6796172 implement #347 and probably fix original issue in #475 2022-02-14 21:13:38 +01:00
NEZNAMY d034fca513 add MySQL data conversion commands 2022-02-14 15:58:28 +01:00
NEZNAMY f65c3fb29a remove unused code 2022-02-14 00:02:38 +01:00
NEZNAMY 7192bc9d69 resend objectives/teams on server switch even when pipeline injection is disabled 2022-02-14 00:02:06 +01:00
NEZNAMY d7636a05f4 drastically reduce amount of permission checks by the feature 2022-02-13 23:05:47 +01:00
NEZNAMY bd93301634 send missing packets on that thing through bridge 2022-02-13 21:58:32 +01:00
NEZNAMY b0558ba0a2 just one \t 2022-02-13 21:57:53 +01:00
NEZNAMY 251934bb4b fix NPE on group info when mysql is enabled 2022-02-13 17:01:13 +01:00
NEZNAMY f2c9ef5d30 implement tab expansion on BungeeCord through bridge 2022-02-13 16:43:17 +01:00
NEZNAMY 15a072bbaa fix weird NPE 2022-02-13 13:52:07 +01:00
NEZNAMY 8f1c303862 implement /tab groups and /tab group <name> subcommands 2022-02-12 14:07:23 +01:00
NEZNAMY c79d2e3e6e fix BossBar placeholders not updating when changing value while it was hidden 2022-02-11 19:41:59 +01:00
NEZNAMY f45d8d860b default condition type to AND instead of OR 2022-02-11 19:40:54 +01:00
NEZNAMY 88f31184a0 add back \t for stack traces 2022-02-11 17:54:11 +01:00
NEZNAMY d5368baab4 add final modifier 2022-02-11 17:22:14 +01:00
NEZNAMY 10f645226c fix bridge placeholder refreshing from replacements 2022-02-11 17:12:54 +01:00
NEZNAMY 322c572cd6 add new issue template for wiki changes 2022-02-11 14:19:24 +01:00
NEZNAMY fdfeb178b5 update error.yml 2022-02-10 21:04:37 +01:00
NEZNAMY 1c63cdba91 update error.yml 2022-02-10 21:03:31 +01:00
NEZNAMY 289d0db8e3 create error template 2022-02-10 21:02:26 +01:00
NEZNAMY 9adacca77f upgrade bug report template 2022-02-10 20:59:12 +01:00
NEZNAMY 7dc6813274 avoid NegativeArraySizeException when copying collection by avoiding that operation 2022-02-10 17:53:30 +01:00
NEZNAMY 77bf1cfbb9 #494 2022-02-10 15:44:07 +01:00
NEZNAMY 8f28350579 initial unlimited nametags on bungee support 2022-02-10 15:33:35 +01:00
NEZNAMY 2d06d0c1d3 fix compatibility issue with other plugins 2022-02-06 13:21:35 +01:00
NEZNAMY 7e8301afcd goodbye 3 days of my life 2022-02-05 13:59:12 +01:00
NEZNAMY 2600e8f856 send empty player slots in a single packet 2022-02-03 22:58:35 +01:00
NEZNAMY 914748e14e add toString override to Skin 2022-02-03 22:32:54 +01:00
NEZNAMY 21a7b633e7 simplify 2022-02-01 19:15:57 +01:00
NEZNAMY a77d4352e2 convert classes into anonymous ones 2022-02-01 18:54:01 +01:00
NEZNAMY a7a21e0e51 fix another config conversion bug 2022-02-01 14:49:53 +01:00
NEZNAMY a0a14cd285 fix NPE 2022-02-01 14:18:03 +01:00
NEZNAMY 44b7bedd53 fix bossbar never showing when joining disabled world/server 2022-02-01 14:16:04 +01:00
NEZNAMY 409a0808c1 fix NumberFormatException with Placeholder sorting type 2022-02-01 14:08:22 +01:00
NEZNAMY 4214d02fad send join plugin message on server switch as well 2022-02-01 12:52:02 +01:00
NEZNAMY bc1abb1b58 more javadocs and moved some variables around 2022-01-31 21:37:10 +01:00
NEZNAMY 2f2f53ba4d #488 2022-01-31 20:26:19 +01:00
NEZNAMY cbeb4c151d remove errorDescription parameter from submitted tasks 2022-01-30 19:01:22 +01:00
NEZNAMY 1d5ca70790 remove bridge placeholder usage from cpu command since it's no longer measured 2022-01-30 18:38:03 +01:00
NEZNAMY 1f619ff4d3 allow enabling pet fix feature on proxy and forward status to bridge 2022-01-30 00:43:55 +01:00
NEZNAMY f5acdebe60 bridge recode to drastically reduce packet flow & allow relational placeholder support 2022-01-30 00:34:00 +01:00
NEZNAMY bd59922204 more docs, general cleanup 2022-01-29 15:49:55 +01:00
NEZNAMY c4d9482c79 remove unused code 2022-01-28 21:12:31 +01:00
NEZNAMY 8faef8f017 class cleanup 2022-01-28 20:56:02 +01:00
NEZNAMY 284161db77 create and use Skin class instead of forwarding Objects 2022-01-28 20:54:33 +01:00
NEZNAMY 78db2b66ef placeholder cleanup, unification and performance improvements 2022-01-28 17:18:52 +01:00
NEZNAMY 0d71643ecb make ping value of layout slots configurable 2022-01-28 16:25:39 +01:00
300 changed files with 26562 additions and 22452 deletions

No files matched your search

-28
View File
@@ -1,28 +0,0 @@
---
name: Bug report
about: Report a bug
title: ''
labels: ''
assignees: ''
---
**Server version**
**Plugin version** ("latest" is not a version)
**Describe the bug**
**How to reproduce**
**Expected behavior**
**Actual behavior**
**Additional info** (errors, screenshots, ...)
+63
View File
@@ -0,0 +1,63 @@
name: Bug Report
description: Unwanted behavior that does not match configuration.
labels: ["Bug"]
body:
- type: input
attributes:
label: Server version
description: Version and type of used server software where TAB is installed.
placeholder: Spigot 1.18.1
validations:
required: true
- type: input
attributes:
label: TAB version
description: Full version in numbers, "latest" is not a version.
placeholder: 3.0.2
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: How the plugin behaves
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: How the plugin is supposed to behave instead with your configuration.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: What to do in order to reproduce this issue
placeholder: |
Step 1: ..
Step 2: ..
validations:
required: true
- type: textarea
attributes:
label: Additional info
description: Any other info you want to provide, such as screenshots.
validations:
required: false
- type: checkboxes
attributes:
label: Checklist
description: Let's make sure this report is valid
options:
- label: I am running latest version of the plugin
required: true
- label: I have read the wiki to make sure it's not an issue with configuration
required: true
- label: I ticked all of the boxes without actually reading them
required: false
- label: I have read the Compatibility wiki page and am not trying to run the plugin on an unsupported server version / platform
required: true
+57
View File
@@ -0,0 +1,57 @@
name: Error
description: Error message with a stack trace in console or error file.
labels: ["Error"]
body:
- type: input
attributes:
label: Server version
description: Version and type of used server software where TAB is installed.
placeholder: Spigot 1.18.1
validations:
required: true
- type: input
attributes:
label: TAB version
description: Full version in numbers, "latest" is not a version.
placeholder: 3.0.2
validations:
required: true
- type: textarea
attributes:
label: Stack trace
description: The error message and stack trace you received in console or errors.log file.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce (if known)
description: What to do in order to reproduce this issue
placeholder: |
Step 1: ..
Step 2: ..
validations:
required: false
- type: textarea
attributes:
label: Additional info
description: Any other info you want to provide.
validations:
required: false
- type: checkboxes
attributes:
label: Checklist
description: Let's make sure this report is valid
options:
- label: I am running latest version of the plugin
required: true
- label: I have included a paste of the error
required: true
- label: I ticked all of the boxes without actually reading them
required: false
- label: I have read the Compatibility wiki page and am not trying to run the plugin on an unsupported server version / platform
required: true
+29
View File
@@ -0,0 +1,29 @@
name: Wiki change
description: Change to text on a wiki page or adding a new section.
labels: ["Wiki"]
body:
- type: input
attributes:
label: Page link
description: Link to the page that should receive the change.
placeholder: https://github.com/NEZNAMY/TAB/wiki/Installation
validations:
required: true
- type: textarea
attributes:
label: Current text (if applicable)
description: Current version of the text (if modifying), ignore in case of new section.
placeholder: |
Installation on Bukkit/Spigot/Paper is as simple as it can be. Just put the plugin into plugins folder and restart the server. You can also run `/plugman load tab` to load the plugin while server is running.
validations:
required: false
- type: textarea
attributes:
label: New text
description: What the text should be changed to / how should new section look
placeholder: |
Put the plugin into plugins folder and restart the server. In case you don't want to restart your server, loading with plugins like PlugMan is supported.
validations:
required: true
+5 -4
View File
@@ -11,15 +11,16 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 11
- name: Cache SonarCloud packages
uses: actions/cache@v1
uses: actions/cache@v2
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
@@ -38,7 +39,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
run: ./gradlew clean build
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3
with:
name: Jar
path: jar/build/libs/TAB-**.jar
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+27 -21
View File
@@ -15,27 +15,33 @@ To compile the plugin, run `./gradlew build` from the terminal.
Once the plugin compiles, grab the jar from `/jar/build/libs/` folder.
# Links
The precompiled JAR can be downloaded for free from
[github releases](https://github.com/NEZNAMY/TAB/releases)
and [MC-Market](https://www.mc-market.org/resources/20631/).
If you want to show your appreciation for my work, you can purchase the plugin on
[MC-Market](https://www.mc-market.org/resources/14009/),
[Polymart](https://polymart.org/resource/484) or just [send a donation](https://paypal.me/neznamy1).
Support is provided to buyers on [discord](https://discord.gg/EaSvdk6).
The precompiled JAR can be downloaded for free from:
* GitHub releases: https://github.com/NEZNAMY/TAB/releases
* BuiltByBit: https://builtbybit.com/resources/20631/
A detailed explanation of all the plugin's functions can be found on the
[wiki](https://github.com/NEZNAMY/TAB/wiki).
Support my work & receive official assistance with configuration:
* BuiltByBit: https://builtbybit.com/resources/14009/
* Polymart: https://polymart.org/resource/484
Other links:
* Discord support for buyers: https://discord.gg/EaSvdk6
* Wiki: https://github.com/NEZNAMY/TAB/wiki
# Issues
The [Issues](https://github.com/NEZNAMY/TAB/issues) section is **only** available for **bug reports**
or other issues which are not directly a bug (performance issues, errors, etc.).
Support is provided on discord and feature requests are currently not accepted due to high demand and
being busy with bug reports, which take priority over new features.
We are not looking to add any new major features anymore.
We would rather spend time improving existing features instead.
When opening a bug report, make sure you provide all necessary information to reproduce the bug,
otherwise we will not able to fix it. Saying "it only occurs randomly" or "I don't know how to
reproduce it" has no value and cannot be converted into actual code.
The [Issues](https://github.com/NEZNAMY/TAB/issues) section is **only** available for cases included in templates.
In other words, they **are not available for**:
* **Support** - support is provided on discord for buyers, where roles are given to verified users
allowing us to know who bought it and who didn't. It also serves as a much faster way of communication.
* **Feature requests** - closed indefinitely.
# Contributing
If this plugin has helped you in any way, and you would like to return something back
to make the plugin even better, there is a lot of ways to contribute:
* Open a **pull request** containing a new feature or a bug fix, which you believe many users will benefit from.
* Make detailed high-quality **bug reports**. The difference between a bug getting fixed in 1 week vs 1 hour
is in quality of the report (typically providing correct steps to reproduce that actually work).
* Test **experimental builds**, which can be found in [Github Actions](https://github.com/NEZNAMY/TAB/actions) or in a form of pre-releases on discord
for problems of any kind including bugs. This way you will help make the actual releases more stable / help get stable releases sooner.
* Help improve the wiki by opening a [Wiki change issue](https://github.com/NEZNAMY/TAB/issues/new?assignees=&labels=Wiki&template=wiki_change.yml),
where you can improve existing descriptions, add information you found missing, fix typos / grammar mistakes or add more examples of usage of functions.
* [Buy a copy](https://builtbybit.com/resources/14009/) of the plugin or [send a donation](https://paypal.me/neznamy1) to keep the plugin active.
+16
View File
@@ -1,3 +1,7 @@
plugins {
id("net.kyori.blossom") version "1.2.0"
}
dependencies {
compileOnlyApi(libs.netty)
api(libs.jsonSimple) {
@@ -6,4 +10,16 @@ dependencies {
compileOnlyApi(libs.snakeyaml)
api(libs.yamlAssist)
compileOnlyApi(libs.gson)
compileOnlyApi(libs.adventureMiniMessage)
compileOnlyApi(libs.adventureApi)
compileOnlyApi(libs.adventureLegacy)
}
blossom {
replaceToken("@plugin_version@", project.version)
replaceTokenIn("src/main/java/me/neznamy/tab/api/TabConstants.java")
}
tasks.javadoc {
enabled = project.hasProperty("enable-javadoc")
}
@@ -1,82 +1,108 @@
package me.neznamy.tab.api;
/**
* Interface representing an armor stand attached to player
* for displaying text in game.
*/
public interface ArmorStand {
/**
* Returns true if offset is static, false if not
* @return true if static, false if dynamic
*/
boolean hasStaticOffset();
/**
* Changes offset of the armor stand
* @param offset - new offset
*/
void setOffset(double offset);
/**
* Returns true if offset is static, false if not
*
* @return true if static, false if dynamic
*/
boolean hasStaticOffset();
/**
* Changes offset of the armor stand
*
* @param offset
* new offset
*/
void setOffset(double offset);
/**
* Returns offset of the armor stand
* @return offset from base
*/
double getOffset();
/**
* Returns property for armor stand's name
* @return property for armor stand's name
*/
Property getProperty();
/**
* Teleports armor stand to owner's current location for everyone in range
*/
void teleport();
/**
* Teleports armor stand to owner's current location for specified player
* @param viewer - player to send packet to
*/
void teleport(TabPlayer viewer);
/**
* Changes sneaking flag and sends packets to everyone in range
* @param sneaking
* new sneaking status
*/
void sneak(boolean sneaking);
/**
* DeSpawns armor stand for everyone
*/
void destroy();
/**
* DeSpawns armor stand for specified player
* @param viewer - player to send packet to
*/
void destroy(TabPlayer viewer);
/**
* Updates armor stand's name if needed
*/
void refresh();
/**
* Updates visibility if needed
* @param force - if refresh should be forced
*/
void updateVisibility(boolean force);
/**
* Returns entity ID of this armor stand
* @return entity ID of this armor stand
*/
int getEntityId();
/**
* Spawns armor stand for specified player
* @param viewer - player to spawn for
*/
void spawn(TabPlayer viewer);
void respawn(TabPlayer viewer);
/**
* Returns offset of the armor stand
*
* @return offset from base
*/
double getOffset();
/**
* Returns property for armor stand's name
*
* @return property for armor stand's name
*/
Property getProperty();
/**
* Teleports armor stand to owner's current location for everyone in range
*/
void teleport();
/**
* Teleports armor stand to owner's current location for specified player
*
* @param viewer
* player to send packet to
*/
void teleport(TabPlayer viewer);
/**
* Changes sneaking flag and sends packets to everyone in range
*
* @param sneaking
* new sneaking status
*/
void sneak(boolean sneaking);
/**
* DeSpawns armor stand for everyone
*/
void destroy();
/**
* DeSpawns armor stand for specified player
*
* @param viewer
* player to send packet to
*/
void destroy(TabPlayer viewer);
/**
* Updates armor stand's name if needed
*/
void refresh();
/**
* Updates visibility if needed
*
* @param force
* if refresh should be forced
*/
void updateVisibility(boolean force);
/**
* Returns entity ID of this armor stand
*
* @return entity ID of this armor stand
*/
int getEntityId();
/**
* Spawns armor stand for specified player
*
* @param viewer
* player to spawn for
*/
void spawn(TabPlayer viewer);
/**
* Sends respawn packets to specified player to make position
* change instant instead of teleport animation
*
* @param viewer
* Player to send armor stand respawn packets to
*/
void respawn(TabPlayer viewer);
}
@@ -1,170 +1,20 @@
package me.neznamy.tab.api;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* A helper class for easy management of armor stands of a player
*/
public class ArmorStandManager {
public interface ArmorStandManager {
//map of registered armor stands
private final Map<String, ArmorStand> armorStands = new LinkedHashMap<>();
//players in entity tracking range
private final List<TabPlayer> nearbyPlayers = new ArrayList<>();
//array to iterate over to avoid concurrent modification and slightly boost performance & memory
private ArmorStand[] armorStandArray = new ArmorStand[0];
private TabPlayer[] nearbyPlayerArray = new TabPlayer[0];
/**
* Adds armor stand into list
* @param name - key name of the armor stand
* @param as - armor stand to add
*/
public void addArmorStand(String name, ArmorStand as) {
armorStands.put(name, as);
armorStandArray = armorStands.values().toArray(new ArmorStand[0]);
for (TabPlayer p : nearbyPlayerArray) as.spawn(p);
}
/**
* Returns armor stand with given name or null if not present
* @param name - name of armor stand registered with
* @return armor stand with given name
*/
public ArmorStand getArmorStand(String name) {
return armorStands.get(name);
}
/**
* Sends destroy packet of all armor stands to everyone and clears nearby players list
*/
void destroy();
/**
* Removes armor stand from list. It is not automatically destroyed for players
* @param name - name of armor stand to remove
*/
public void removeArmorStand(String name) {
armorStands.get(name).destroy();
armorStands.remove(name);
armorStandArray = armorStands.values().toArray(new ArmorStand[0]);
}
/**
* Spawns all armor stands for specified viewer and adds them into nearby players
* @param viewer - player to spawn armor stands for
*/
public void spawn(TabPlayer viewer) {
nearbyPlayers.add(viewer);
nearbyPlayerArray = nearbyPlayers.toArray(new TabPlayer[0]);
if (viewer.getVersion().getMinorVersion() < 8) return;
for (ArmorStand a : armorStandArray) a.spawn(viewer);
}
/**
* Sets sneak value of armor stands to specified value
* @param sneaking - new sneaking status
*/
public void sneak(boolean sneaking) {
for (ArmorStand a : armorStandArray) a.sneak(sneaking);
}
/**
* Teleports armor stands to player's current location for all nearby players
*/
public void teleport() {
for (ArmorStand a : armorStandArray) a.teleport();
}
/**
* Teleports armor stands to player's current location for specified viewer
* @param viewer - player to teleport armor stands for
*/
public void teleport(TabPlayer viewer) {
for (ArmorStand a : armorStandArray) a.teleport(viewer);
}
/**
* Refreshes name of all armor stands for all nearby players
*/
public void refresh() {
for (ArmorStand a : armorStandArray) a.refresh();
}
/**
* Updates armor stand visibility for all armor stands
* @param force - true if packets should be sent despite seemingly not needed
*/
public void updateVisibility(boolean force) {
for (ArmorStand a : armorStandArray) a.updateVisibility(force);
}
/**
* Removes specified player from list of nearby players
* @param viewer - player to remove
*/
public void unregisterPlayer(TabPlayer viewer) {
if (nearbyPlayers.remove(viewer)) nearbyPlayerArray = nearbyPlayers.toArray(new TabPlayer[0]);
}
/**
* Sends destroy packet of all armor stands to everyone and clears nearby players list
*/
public void destroy() {
for (ArmorStand a : armorStandArray) a.destroy();
nearbyPlayers.clear();
nearbyPlayerArray = new TabPlayer[0];
}
/**
* Sends destroy packet of all armor stands to specified player and removes them from nearby players list
* @param viewer - player to destroy armor stands for
*/
public void destroy(TabPlayer viewer) {
for (ArmorStand a : armorStandArray) a.destroy(viewer);
unregisterPlayer(viewer);
}
/**
* Returns true if manager contains armor stand with specified entity id, false if not
* @param entityId - entity id
* @return true if armor stand with specified entity id exists, false if not
*/
public boolean hasArmorStandWithID(int entityId) {
for (ArmorStand a : armorStandArray) {
if (a.getEntityId() == entityId) {
return true;
}
}
return false;
}
/**
* Returns array of all registered armor stands
* @return armor stand array
*/
public ArmorStand[] getArmorStands(){
return armorStandArray;
}
/**
* Returns array of nearby players
* @return array of nearby players
*/
public TabPlayer[] getNearbyPlayers(){
return nearbyPlayerArray;
}
public boolean isNearby(TabPlayer viewer) {
return nearbyPlayers.contains(viewer);
}
public void respawn() {
for (ArmorStand a : armorStandArray) {
for (TabPlayer viewer : nearbyPlayerArray) {
a.respawn(viewer);
}
}
}
/**
* Refreshes text of all armor stands for all nearby players
*
* @param force
* If refresh should be force despite no update seemingly being needed
*/
void refresh(boolean force);
}
@@ -1,35 +1,116 @@
package me.neznamy.tab.api;
import java.util.UUID;
/**
* Feature manager allows registration and work with features.
*/
public interface FeatureManager {
/**
* Registers a feature, which will start receiving events
*
* @param name - name of feature
* @param feature - the handler
*/
void registerFeature(String name, TabFeature feature);
/**
* Unregisters feature making it no longer receive events. This does not run unload method nor cancel
* tasks created by the feature
* @param name - feature name defined in registerFeature
*/
void unregisterFeature(String name);
/**
* Returns whether a feature with said name is registered or not
*
* @param name - name of feature defined in registerFeature method
* @return true if feature exists, false if not
*/
boolean isFeatureEnabled(String name);
/**
* Registers a feature, which will start receiving events
*
* @param name
* name of feature
* @param feature
* the handler
*/
void registerFeature(String name, TabFeature feature);
/**
* Unregisters feature making it no longer receive events. This does not run unload method nor cancel
* tasks created by the feature
*
* @param name
* feature name defined in registerFeature
*/
void unregisterFeature(String name);
/**
* Returns whether a feature with said name is registered or not
*
* @param name
* name of feature defined in registerFeature method
* @return true if feature exists, false if not
*/
boolean isFeatureEnabled(String name);
/**
* Returns feature handler by its name
*
* @param name - name of feature defined in registerFeature method
* @return the feature or null if feature does not exist
*/
TabFeature getFeature(String name);
/**
* Returns feature handler by its name
*
* @param name
* name of feature defined in registerFeature method
* @return the feature or null if feature does not exist
*/
TabFeature getFeature(String name);
/**
* Calls onQuit(TabPlayer) to all enabled features
*
* @param disconnectedPlayer
* player who disconnected
*/
void onQuit(TabPlayer disconnectedPlayer);
/**
* Calls onJoin(TabPlayer) to all enabled features
*
* @param connectedPlayer
* player who connected
*/
void onJoin(TabPlayer connectedPlayer);
/**
* Calls onServerChange(TabPlayer, String, String) to all enabled features
*
* @param playerUUID
* player who switched server
* @param to
* name of the new server
*/
void onServerChange(UUID playerUUID, String to);
/**
* Calls onWorldChange(TabPlayer, String, String) to all enabled features
*
* @param playerUUID
* player who switched world
* @param to
* name of the new world
*/
void onWorldChange(UUID playerUUID, String to);
/**
* Calls onCommand(TabPlayer, String) to all enabled features
*
* @param sender
* command sender
* @param command
* command line including /
* @return {@code true} if some feature marked the command for cancel, {@code false} if not
*/
boolean onCommand(TabPlayer sender, String command);
/**
* Internal function that marks objective packet deserialization
* when at least 1 feature uses it.
* <p>
* Internal use only.
*/
void markObjective();
/**
* Internal function that marks display objective packet deserialization
* when at least 1 feature uses it.
* <p>
* Internal use only.
*/
void markDisplayObjective();
/**
* Returns array of all currently enabled features
*
* @return array of all currently enabled features
*/
TabFeature[] getValues();
}
@@ -1,16 +1,82 @@
package me.neznamy.tab.api;
/**
* Interface allowing to send header/footer to players.
* <p>
* Instance can be obtained using {@link TabAPI#getHeaderFooterManager()}.
* This requires the Header/footer feature to be enabled in config, otherwise
* the method will return {@code null}.
*/
public interface HeaderFooterManager {
void setHeader(TabPlayer player, String header);
void setFooter(TabPlayer player, String footer);
void setHeaderAndFooter(TabPlayer player, String header, String footer);
void resetHeader(TabPlayer player);
void resetFooter(TabPlayer player);
void resetHeaderAndFooter(TabPlayer player);
/**
* Sets raw value of header to specified value. This overrides plugin
* configuration until reset is called. Has full placeholder support
* and will send update packet whenever a placeholder used inside
* changes its value.
*
* @param player
* Player to set header for
* @param header
* Raw header value
*/
void setHeader(TabPlayer player, String header);
/**
* Sets raw value of footer to specified value. This overrides plugin
* configuration until reset is called. Has full placeholder support
* and will send update packet whenever a placeholder used inside
* changes its value.
*
* @param player
* Player to set header for
* @param footer
* Raw footer value
*/
void setFooter(TabPlayer player, String footer);
/**
* Sets raw value of header and footer to specified values. This overrides plugin
* configuration until reset is called. Has full placeholder support
* and will send update packet whenever a placeholder used inside
* changes its value.
*
* @param player
* Player to set header for
* @param header
* Raw header value
* @param footer
* Raw footer value
*/
void setHeaderAndFooter(TabPlayer player, String header, String footer);
/**
* Resets header value previously set using {@link #setHeader(TabPlayer, String)}
* or {@link #setHeaderAndFooter(TabPlayer, String, String)} and restores internal
* logic using configuration files.
*
* @param player
* Player to reset header for
*/
void resetHeader(TabPlayer player);
/**
* Resets footer value previously set using {@link #setFooter(TabPlayer, String)}
* or {@link #setHeaderAndFooter(TabPlayer, String, String)} and restores internal
* logic using configuration files.
*
* @param player
* Player to reset header for
*/
void resetFooter(TabPlayer player);
/**
* Resets values previously set using {@link #setHeader(TabPlayer, String)},
* {@link #setFooter(TabPlayer, String)} or {@link #setHeaderAndFooter(TabPlayer, String, String)}
* and restores internal logic using configuration files.
*
* @param player
* Player to reset header and footer for
*/
void resetHeaderAndFooter(TabPlayer player);
}
@@ -1,60 +1,68 @@
package me.neznamy.tab.api;
/**
* An interface representing a property assigned to a player,
* which may contain placeholders and can be changed using the API.
*/
public interface Property {
/**
* Changes raw value to new one
* @param rawValue - new value to be used
*/
void changeRawValue(String rawValue);
/**
* Returns original raw value ignoring API calls
*
* @return original raw value
*/
String getOriginalRawValue();
/**
* Returns original raw value ignoring API calls
* @return original raw value
*/
String getOriginalRawValue();
/**
* Temporarily overrides current raw value with an API call
*
* @param temporaryValue
* temporary value to be assigned
*/
void setTemporaryValue(String temporaryValue);
/**
* Temporarily overrides current raw value with an API call
* @param temporaryValue - temporary value to be assigned
*/
void setTemporaryValue(String temporaryValue);
/**
* Returns current temporary value
* @return temporary value or null if not set
*/
String getTemporaryValue();
/**
* Returns current temporary value
*
* @return temporary value or null if not set
*/
String getTemporaryValue();
/**
* Returns temporary value (via API) if present, raw value otherwise
* @return current raw value
*/
String getCurrentRawValue();
/**
* Returns temporary value (via API) if present, raw value otherwise
*
* @return current raw value
*/
String getCurrentRawValue();
/**
* Returns last known value
* @return last known value
*/
String get();
/**
* Returns last known value
*
* @return last known value
*/
String get();
/**
* Replaces all placeholders in current raw value, colorizes it, removes remove-strings and returns whether value changed or not
* @return if updating changed value or not
*/
boolean update();
/**
* Replaces all placeholders in current raw value, colorizes it, removes remove-strings and returns it.
* Equal to calling update() and then get().
* @return updated value
*/
String updateAndGet();
/**
* Replaces all placeholders in current raw value, colorizes it, removes remove-strings and returns whether value changed or not
*
* @return if updating changed value or not
*/
boolean update();
/**
* Returns value for defined viewer by applying relational placeholders to last known value
* @param viewer - the viewer
* @return format for the viewer
*/
String getFormat(TabPlayer viewer);
/**
* Replaces all placeholders in current raw value, colorizes it, removes remove-strings and returns it.
* Equal to calling update() and then get().
*
* @return updated value
*/
String updateAndGet();
/**
* Returns value for defined viewer by applying relational placeholders to last known value
*
* @param viewer
* the viewer
* @return format for the viewer
*/
String getFormat(TabPlayer viewer);
}
@@ -1,10 +1,147 @@
package me.neznamy.tab.api;
import java.util.*;
import java.util.stream.Collectors;
/**
* Interface representing user or group configuration.
* Implementation is either using file or MySQL, if it's enabled.
*/
public interface PropertyConfiguration {
void setProperty(String name, String property, String server, String world, String value);
String[] getProperty(String name, String property, String server, String world);
void remove(String name);
/**
* Sets property value of group or user to specified value. If {@code world} or
* {@code server} is not {@code null}, value will be inserted as a per-world or
* per-server setting. If both {@code world} and {@code server} are {@code null},
* value is inserted as a global setting.
*
* @param groupOrUser
* Name of group or user, depending on what this instance handles
* @param property
* Name of property to set
* @param server
* Name of server to apply setting to, {@code null} for global setting
* @param world
* Name of world to apply setting to, {@code null} for global setting
* @param value
* Value of the property
*/
void setProperty(String groupOrUser, String property, String server, String world, String value);
/**
* Gets property of group or user. If {@code server} or {@code world}
* are not {@code null}, per-server / per-world settings are checked first
* to try to find a match. If no match is found, global value is returned.
* Returned value is an array with 2 elements, with first argument being value
* and second being source. If nothing is found, empty array is returned.
*
* @param groupOrUser
* Name of group or user, depending on what this instance handles
* @param property
* Name of property to get
* @param server
* Server the player is currently in, to allow check for per-server settings
* @param world
* World the player is currently in, to allow check for per-world settings
* @return Array with 2 elements with value being first, source second if found,
* empty array if nothing was found.
*/
String[] getProperty(String groupOrUser, String property, String server, String world);
/**
* Removes all data applied to specified group or user.
*
* @param groupOrUser
* Name of group or user, depending on what this instance handles
*/
void remove(String groupOrUser);
/**
* Returns map of global settings applied to specified group or user.
* Map key is property name, value is property value.
*
* @param groupOrUser
* Name of group or user, depending on what this instance handles
* @return Map of global settings of specified group or user
*/
Map<String, Object> getGlobalSettings(String groupOrUser);
/**
* Returns map of per-world settings of specified group or user.
* Map key is world name, value is property name-value map in that world.
*
* @param groupOrUser
* Name of group or user, depending on what this instance handles
* @return Map of per-world settings of specified group or user
*/
Map<String, Map<String, Object>> getPerWorldSettings(String groupOrUser);
/**
* Returns map of per-server settings of specified group or user.
* Map key is server name, value is property name-value map in that server.
*
* @param groupOrUser
* Name of group or user, depending on what this instance handles
* @return Map of per-server settings of specified group or user
*/
Map<String, Map<String, Object>> getPerServerSettings(String groupOrUser);
/**
* Returns set of all groups or users that have anything configured,
* either globally or in some world or server.
*
* @return Set of all entries with something configured
*/
Set<String> getAllEntries();
/**
* Converts per-world or per-server map into a per-world or per-server map
* with values only belonging to entered group or user. In other words, extracts
* settings belonging to group or user from the map.
*
* @param map
* Original per-world or per-server setting map
* @param groupOrUser
* Name of group or user, depending on what this instance handles
* @return Converted map only containing data of specified group or user
*/
default Map<String, Map<String, Object>> convertMap(Map<String, Map<String, Map<String, Object>>> map, String groupOrUser) {
Map<String, Map<String, Object>> converted = new HashMap<>();
for (Map.Entry<String, Map<String, Map<String, Object>>> entry : map.entrySet()) {
converted.put(entry.getKey(), entry.getValue().get(groupOrUser));
}
return converted;
}
/**
* Converts object into string. For most objects, {@link Object#toString()} is called.
* For lists, entries are connected with {@code "\n"}.
*
* @param obj
* Object to convert
* @return Converted string
*/
@SuppressWarnings("unchecked")
default String toString(Object obj) {
if (obj instanceof List) {
return ((List<Object>)obj).stream().map(Object::toString).collect(Collectors.joining("\n"));
}
return obj.toString();
}
/**
* Converts string into object. If string contains {@code "\n"},
* a list is created using {@code "\n"} as a separator. Otherwise,
* inserted string is returned.
*
* @param string
* String to convert
* @return Converted object
*/
default Object fromString(String string) {
if (string != null && string.contains("\n")) {
return Arrays.asList(string.split("\n"));
}
return string;
}
}
@@ -5,148 +5,169 @@ package me.neznamy.tab.api;
*/
public enum ProtocolVersion {
UNKNOWN ("Unknown"),
PROXY ("Proxy"),
V1_18_1 (757),
V1_18 (757),
V1_17_1 (756),
V1_17 (755),
V1_16_5 (754),
V1_16_4 (754),
V1_16_3 (753),
V1_16_2 (751),
V1_16_1 (736),
V1_16 (735),
V1_15_2 (578),
V1_15_1 (575),
V1_15 (573),
V1_14_4 (498),
V1_14_3 (490),
V1_14_2 (485),
V1_14_1 (480),
V1_14 (477),
V1_13_2 (404),
V1_13_1 (401),
V1_13 (393),
V1_12_2 (340),
V1_12_1 (338),
V1_12 (335),
V1_11_2 (316),
V1_11_1 (316),
V1_11 (315),
V1_10_2 (210),
V1_10_1 (210),
V1_10 (210),
V1_9_4 (110),
V1_9_3 (110),
V1_9_2 (109),
V1_9_1 (108),
V1_9 (107),
V1_8 (47),
V1_7_10 (5),
V1_7_9 (5),
V1_7_8 (5),
V1_7_7 (5),
V1_7_6 (5),
V1_7_5 (4),
V1_7_4 (4),
V1_7_2 (4),
V1_6_4 (78),
V1_6_2 (74),
V1_6_1 (73),
V1_5_2 (61),
V1_5_1 (60),
V1_5 (60),
V1_4_7 (51),
V1_4_6 (51);
UNKNOWN_SERVER_VERSION ("Unknown"),
UNKNOWN_CLIENT_VERSION ("Unknown"),
PROXY ("Proxy"),
V1_19_3 (761),
V1_19_2 (760),
V1_19_1 (760),
V1_19 (759),
V1_18_2 (758),
V1_18_1 (757),
V1_18 (757),
V1_17_1 (756),
V1_17 (755),
V1_16_5 (754),
V1_16_4 (754),
V1_16_3 (753),
V1_16_2 (751),
V1_16_1 (736),
V1_16 (735),
V1_15_2 (578),
V1_15_1 (575),
V1_15 (573),
V1_14_4 (498),
V1_14_3 (490),
V1_14_2 (485),
V1_14_1 (480),
V1_14 (477),
V1_13_2 (404),
V1_13_1 (401),
V1_13 (393),
V1_12_2 (340),
V1_12_1 (338),
V1_12 (335),
V1_11_2 (316),
V1_11_1 (316),
V1_11 (315),
V1_10_2 (210),
V1_10_1 (210),
V1_10 (210),
V1_9_4 (110),
V1_9_3 (110),
V1_9_2 (109),
V1_9_1 (108),
V1_9 (107),
V1_8 (47),
V1_7_10 (5),
V1_7_9 (5),
V1_7_8 (5),
V1_7_7 (5),
V1_7_6 (5),
V1_7_5 (4),
V1_7_4 (4),
V1_7_2 (4),
V1_6_4 (78),
V1_6_2 (74),
V1_6_1 (73),
V1_5_2 (61),
V1_5_1 (60),
V1_5 (60),
V1_4_7 (51),
V1_4_6 (51);
//version's network id found at https://wiki.vg/Protocol_version_numbers
private final int networkId;
//minor version, such as 16
private int minorVersion;
//friendly name displayed in %player-version% placeholder
private final String friendlyName;
/** Value array to iterate over to avoid array creations on each call */
public static final ProtocolVersion[] VALUES = values();
/**
* Constructs new instance with given network id
* @param networkId - network id of this version
*/
ProtocolVersion(int networkId){
this.networkId = networkId;
this.minorVersion = Integer.parseInt(toString().split("_")[1]);
this.friendlyName = toString().substring(1).replace("_", ".");
}
/**
* Constructs new instance with given friendly name
* @param friendlyName - friendly name to display
*/
ProtocolVersion(String friendlyName){
this.networkId = 999;
this.minorVersion = 18;
this.friendlyName = friendlyName;
}
/** Version's network id found at https://wiki.vg/Protocol_version_numbers */
private final int networkId;
/**
* Returns the version's network id
* @return version's network id
*/
public int getNetworkId() {
return networkId;
}
/** Version's minor version, such as 16 for 1.16.x */
private int minorVersion;
/**
* Returns user-friendly name of the version (such as 1.16.2 instead of V1_16_2)
* @return user-friendly name of the version
*/
public String getFriendlyName() {
return friendlyName;
}
/** Version's friendly name displayed in %player-version% placeholder */
private final String friendlyName;
/**
* Returns minor version of this release, such as 8 for 1.8.x or 13 for 1.13.x
* @return version's minor version
*/
public int getMinorVersion() {
return minorVersion;
}
/**
* Constructs new instance with given network id
*
* @param networkId
* network id of this version
*/
ProtocolVersion(int networkId){
this.networkId = networkId;
this.minorVersion = Integer.parseInt(toString().split("_")[1]);
this.friendlyName = toString().substring(1).replace("_", ".");
}
/**
* Overrides minor version of the version instance. This is used internally to
* set minor version of UNKNOWN value to value from package to fix compatibility
* with server forks that set bukkit version field value to "Unknown".
* @param minorVersion
* minor version to change value to
*/
public void setMinorVersion(int minorVersion) {
this.minorVersion = minorVersion;
}
/**
* Constructs new instance with given friendly name
*
* @param friendlyName
* friendly name to display
*/
ProtocolVersion(String friendlyName){
this.networkId = 999;
this.minorVersion = 18;
this.friendlyName = friendlyName;
}
/**
* Returns enum constant of entered version or UNKNOWN if unknown version
* @param friendlyName - friendly name of the version
* @return version or UNKNOWN if version is unknown
*/
public static ProtocolVersion fromFriendlyName(String friendlyName) {
if (friendlyName.startsWith("1.8")) return V1_8;
try {
return valueOf("V" + friendlyName.replace(".", "_"));
} catch (IllegalArgumentException e) {
return UNKNOWN;
}
}
/**
* Returns the version's network id
*
* @return version's network id
*/
public int getNetworkId() {
return networkId;
}
/**
* Returns version from given network id
* @param networkId - network id of protocol version
* @return version from given network id
*/
public static ProtocolVersion fromNetworkId(int networkId) {
for (ProtocolVersion v : values()) {
if (networkId == v.getNetworkId()) return v;
}
return UNKNOWN;
}
/**
* Returns user-friendly name of the version (such as 1.16.2 instead of V1_16_2)
*
* @return user-friendly name of the version
*/
public String getFriendlyName() {
return friendlyName;
}
/**
* Returns minor version of this release, such as 8 for 1.8.x or 13 for 1.13.x
*
* @return version's minor version
*/
public int getMinorVersion() {
return minorVersion;
}
/**
* Overrides minor version of the version instance. This is used internally to
* set minor version of UNKNOWN_SERVER_VERSION value to value from package to fix compatibility
* with server forks that set bukkit version field value to "Unknown".
*
* @param minorVersion
* minor version to change value to
*/
public void setMinorVersion(int minorVersion) {
this.minorVersion = minorVersion;
}
/**
* Returns enum constant of entered version or UNKNOWN_SERVER_VERSION if unknown version
*
* @param friendlyName
* friendly name of the version
* @return version or UNKNOWN_SERVER_VERSION if version is unknown
*/
public static ProtocolVersion fromFriendlyName(String friendlyName) {
if (friendlyName.startsWith("1.8")) return V1_8;
try {
return valueOf("V" + friendlyName.replace(".", "_"));
} catch (IllegalArgumentException e) {
return UNKNOWN_SERVER_VERSION;
}
}
/**
* Returns version from given network id
*
* @param networkId
* network id of protocol version
* @return version from given network id
*/
public static ProtocolVersion fromNetworkId(int networkId) {
for (ProtocolVersion v : VALUES) {
if (networkId == v.getNetworkId()) return v;
}
return UNKNOWN_CLIENT_VERSION;
}
}
+189 -91
View File
@@ -15,101 +15,199 @@ import me.neznamy.tab.api.team.TeamManager;
*/
public abstract class TabAPI {
private static TabAPI instance;
public static TabAPI getInstance() {
return instance;
}
public static void setInstance(TabAPI instance) {
TabAPI.instance = instance;
}
/** Instance of the API */
private static TabAPI instance;
/**
* Returns player object from given UUID
* @return player object from given UUID
* @param id - Player UUID
*/
public abstract TabPlayer getPlayer(UUID id);
/**
* Constructs new instance and sets it as new API instance
*/
public TabAPI() {
instance = this;
}
/**
* Returns player object from given name
* @return player object from given name
* @param name - Player name
*/
public abstract TabPlayer getPlayer(String name);
/**
* Returns collection of all online players. Will return empty list if plugin is disabled (due to a broken configuration file for example).
* @return collection of online players
*/
public abstract TabPlayer[] getOnlinePlayers();
/**
* Returns API instance. If instance was not set by the plugin, throws
* {@code IllegalStateException}. This is usually caused by shading the API
* into own project, which is not allowed. Another option is calling the method
* before plugin was able to load.
*
* @return API instance
* @throws IllegalStateException
* If instance is {@code null}
*/
public static TabAPI getInstance() {
if (instance == null) throw new IllegalStateException("API instance is null. This likely means you shaded TAB's API into your project" +
" instead of only using it, which is not allowed.");
return instance;
}
/**
* Return BossBar manager instance if the feature is enabled. Returns null otherwise.
* @return BossBar manager
*/
public abstract BossBarManager getBossBarManager();
/**
* Returns scoreboard manager instance if the feature is enabled. Returns null otherwise.
* @return scoreboard manager
*/
public abstract ScoreboardManager getScoreboardManager();
/**
* Returns team manager instance if the feature is enabled, false otherwise
* @return team manager
*/
public abstract TeamManager getTeamManager();
public abstract HeaderFooterManager getHeaderFooterManager();
/**
* Returns PlaceholderManager instance
* @return PlaceholderManager instance
*/
public abstract PlaceholderManager getPlaceholderManager();
/**
* Returns player object from given UUID
*
* @param id
* Player UUID
* @return player object from given UUID
*/
public abstract TabPlayer getPlayer(UUID id);
/**
* Returns feature manager instance
* @return feature manager instance
*/
public abstract FeatureManager getFeatureManager();
public abstract TablistFormatManager getTablistFormatManager();
/**
* Returns player object from given name
*
* @param name
* Player name
* @return player object from given name
*/
public abstract TabPlayer getPlayer(String name);
/**
* Gets the event bus for registering listeners for TAB events.
*
* @return the event bus
*/
public abstract EventBus getEventBus();
/**
* Returns collection of all online players. Will return empty list if plugin is disabled (due to a broken configuration file for example).
*
* @return collection of online players
*/
public abstract TabPlayer[] getOnlinePlayers();
/**
* Returns server version. On proxy installation returns PROXY.
* @return server version
*/
public abstract ProtocolVersion getServerVersion();
/**
* Prints message into console
* @param message - message to print
* @param translateColors - true if colors should be translated, false if not
*/
public abstract void sendConsoleMessage(String message, boolean translateColors);
public abstract ThreadManager getThreadManager();
public abstract ConfigurationFile getPlayerCache();
public abstract ConfigurationFile getConfig();
public abstract PropertyConfiguration getGroups();
public abstract PropertyConfiguration getUsers();
public abstract void debug(String message);
public abstract void logError(String message, Throwable t);
/**
* Return BossBar manager instance if the feature is enabled. If not, returns {@code null}.
*
* @return BossBar manager
*/
public abstract BossBarManager getBossBarManager();
/**
* Returns scoreboard manager instance if the feature is enabled. If not, returns {@code null}.
*
* @return scoreboard manager
*/
public abstract ScoreboardManager getScoreboardManager();
/**
* Returns team manager instance if the feature is enabled. If not, returns {@code null}.
*
* @return team manager
*/
public abstract TeamManager getTeamManager();
/**
* Returns header/footer manager instance if the feature is enabled. If not, returns {@code null}.
*
* @return Header/footer manager
*/
public abstract HeaderFooterManager getHeaderFooterManager();
/**
* Returns PlaceholderManager instance
*
* @return PlaceholderManager instance
*/
public abstract PlaceholderManager getPlaceholderManager();
/**
* Returns feature manager instance
*
* @return feature manager instance
*/
public abstract FeatureManager getFeatureManager();
/**
* Returns Tablist name format manager instance if the feature is enabled. If not, returns {@code null}.
*
* @return Tablist name format manager
*/
public abstract TablistFormatManager getTablistFormatManager();
/**
* Gets the event bus for registering listeners for TAB events.
*
* @return the event bus
*/
public abstract EventBus getEventBus();
/**
* Returns server version. On proxy installation returns PROXY.
*
* @return server version
*/
public abstract ProtocolVersion getServerVersion();
/**
* Prints message into console
*
* @param message
* message to print
* @param translateColors
* {@code true} if colors should be translated, {@code false} if not
*/
public abstract void sendConsoleMessage(String message, boolean translateColors);
/**
* Returns TAB's Thread manager, which allows task submitting
*
* @return ThreadManager instance
*/
public abstract ThreadManager getThreadManager();
/**
* Returns TAB's cache file used to store player toggle data
*
* @return TAB's player cache file
*/
public abstract ConfigurationFile getPlayerCache();
/**
* Returns TAB's config.yml file
*
* @return config.yml file
*/
public abstract ConfigurationFile getConfig();
/**
* Returns TAB's group configuration
*
* @return TAB's group configuration
*/
public abstract PropertyConfiguration getGroups();
/**
* Returns TAB's user configuration
*
* @return TAB's user configuration
*/
public abstract PropertyConfiguration getUsers();
/**
* Sends a debug message into console if the option
* is enabled in config.
*
* @param message
* Message to send
*/
public abstract void debug(String message);
/**
* Logs an error into errors.log file
*
* @param message
* Error message
* @param t
* Thrown error
*/
public abstract void logError(String message, Throwable t);
/**
* Sets name of file with syntax error, which prevented
* the plugin from enabling. Internal use only.
*
* @param file
* Name of file with syntax error
*/
public abstract void setBrokenFile(String file);
/**
* Returns {@code true} if the plugin is currently disabled due to an error,
* either a yaml syntax error or an internal startup error. Returns {@code false}
* if the plugin is properly running.
*
* @return {@code true} if disabled, {@code false} if running correctly
*/
public abstract boolean isPluginDisabled();
}
@@ -0,0 +1,382 @@
package me.neznamy.tab.api;
/**
* A class containing various constants used everywhere in the plugin
* to allow easier overview and modification to prevent inconsistencies.
*/
public class TabConstants {
public static final String PLUGIN_VERSION = "@plugin_version@";
public static final String NO_GROUP = "NONE";
public static final String DEFAULT_GROUP = "_DEFAULT_";
public static final String PLUGIN_MESSAGE_CHANNEL_NAME = "tab:bridge-2";
public static final String REDIS_CHANNEL_NAME = "TAB";
/**
* Private constructor to prevent the class from being instantiated
*/
private TabConstants() {}
/**
* Feature sub-category explaining why / when a certain feature
* needed CPU time to process tasks.
*/
public static class CpuUsageCategory {
public static final String PLAYER_JOIN = "Player Join";
public static final String PLAYER_QUIT = "Player Quit";
public static final String WORLD_SWITCH = "World Switch";
public static final String SERVER_SWITCH = "Server Switch";
public static final String COMMAND_PREPROCESS = "Command Preprocess";
public static final String PLAYER_SNEAK = "Player Sneak";
public static final String PLAYER_RESPAWN = "Player Respawn";
public static final String PLUGIN_MESSAGE = "PluginMessageEvent";
public static final String REDIS_BUNGEE_MESSAGE = "PubSubMessageEvent";
public static final String VANISH_CHANGE = "Vanish status change";
public static final String ANTI_OVERRIDE = "Anti override";
public static final String BYTE_BUF = "ByteBuf";
public static final String PACKET_PLAYER_INFO = "PacketPlayOutPlayerInfo";
public static final String PACKET_DISPLAY_OBJECTIVE = "PacketPlayOutScoreboardDisplayObjective";
public static final String PACKET_OBJECTIVE = "PacketPlayOutScoreboardObjective";
public static final String PACKET_JOIN_GAME = "Login Packet";
public static final String PACKET_ENTITY_MOVE = "PacketPlayOutEntity";
public static final String PACKET_ENTITY_MOVE_PASSENGER = "PacketPlayOutEntity (passenger)";
public static final String PACKET_ENTITY_SPAWN = "PacketPlayOutNamedEntitySpawn";
public static final String PACKET_ENTITY_DESTROY = "PacketPlayOutEntityDestroy";
public static final String RAW_PACKET_IN = "Packet reading (in)";
public static final String RAW_PACKET_OUT = "Packet reading (out)";
public static final String PLACEHOLDER_REFRESHING = "Refreshing placeholders";
public static final String GROUP_REFRESHING = "Refreshing groups";
public static final String REFRESHING_NAME_TAG_VISIBILITY = "Refreshing NameTag visibility";
public static final String SCOREBOARD_PACKET_CHECK = "Checking for other plugins";
public static final String PROCESSING_PLAYER_MOVEMENT = "Processing player movement";
public static final String TELEPORTING_WITHER = "Teleporting wither";
/**
* Private constructor to prevent the class from being instantiated
*/
private CpuUsageCategory() {}
}
/**
* Information why a specific feature sent a packet
* and what the packet sent is supposed to do.
*/
public static class PacketCategory {
public static final String BOSSBAR_COLOR_STYLE = "BossBar (Color and style)";
public static final String BOSSBAR_PROGRESS = "BossBar (Progress)";
public static final String BOSSBAR_TEXT = "BossBar (Text)";
public static final String BOSSBAR_WITHER_TELEPORT = "BossBar (Teleporting Wither)";
public static final String GLOBAL_PLAYERLIST_LATENCY = "Global PlayerList (Updating latency)";
public static final String GLOBAL_PLAYERLIST_VANISH = "Global PlayerList (Vanishing)";
public static final String LAYOUT_FIXED_SLOTS = "Layout (Fixed slots)";
public static final String LAYOUT_PLAYER_SLOTS = "Layout (Player slots)";
public static final String LAYOUT_LATENCY = "Layout (Updating latency)";
public static final String NAMETAGS_TEAM_REGISTER = "NameTags (Team register)";
public static final String NAMETAGS_TEAM_UNREGISTER = "NameTags (Team unregister)";
public static final String NAMETAGS_TEAM_UPDATE = "NameTags (Team update)";
public static final String SORTING_CHANGING_TEAM_NAME = "Sorting (Changing team name)";
public static final String SCOREBOARD_TITLE = "Scoreboard (Title)";
public static final String SCOREBOARD_LINES = "Scoreboard (Lines)";
public static final String UNLIMITED_NAMETAGS_OFFSET_CHANGE = "Unlimited NameTags (Changing offset)";
public static final String UNLIMITED_NAMETAGS_SPAWN = "Unlimited NameTags (Spawning)";
public static final String UNLIMITED_NAMETAGS_DESPAWN = "Unlimited NameTags (Despawning)";
public static final String UNLIMITED_NAMETAGS_TELEPORT = "Unlimited NameTags (Teleporting)";
public static final String UNLIMITED_NAMETAGS_SNEAK = "Unlimited NameTags (Sneaking)";
public static final String UNLIMITED_NAMETAGS_METADATA = "Unlimited NameTags (Metadata)";
/**
* Private constructor to prevent the class from being instantiated
*/
private PacketCategory() {}
}
/**
* Permission nodes used by the plugin
*/
public static class Permission {
public static final String COMMAND_ALL = "tab.admin";
public static final String COMMAND_RELOAD = "tab.reload";
public static final String COMMAND_BOSSBAR_ANNOUNCE = "tab.announce.bar";
public static final String COMMAND_BOSSBAR_TOGGLE = "tab.bossbar.toggle";
public static final String COMMAND_BOSSBAR_SEND = "tab.send.bar";
public static final String COMMAND_CPU = "tab.cpu";
public static final String COMMAND_DEBUG = "tab.debug";
public static final String COMMAND_GROUP_INFO = "tab.groupinfo";
public static final String COMMAND_GROUP_LIST = "tab.grouplist";
public static final String COMMAND_MYSQL_DOWNLOAD = "tab.mysql.download";
public static final String COMMAND_MYSQL_UPLOAD = "tab.mysql.upload";
public static final String COMMAND_NAMETAG_TOGGLE = "tab.nametag.toggle";
public static final String COMMAND_NAMETAG_TOGGLE_OTHER = "tab.nametag.toggle.other";
public static final String COMMAND_NAMETAG_PREVIEW = "tab.nametag.preview";
public static final String COMMAND_NAMETAG_PREVIEW_OTHER = "tab.nametag.preview.other";
public static final String COMMAND_PARSE = "tab.parse";
public static final String COMMAND_SCOREBOARD_ANNOUNCE = "tab.announce.scoreboard";
public static final String COMMAND_SCOREBOARD_TOGGLE = "tab.scoreboard.toggle";
public static final String COMMAND_SCOREBOARD_TOGGLE_OTHER = "tab.scoreboard.toggle.other";
public static final String COMMAND_SCOREBOARD_SHOW = "tab.scoreboard.show";
public static final String COMMAND_SCOREBOARD_SHOW_OTHER = "tab.scoreboard.show.other";
public static final String COMMAND_SETCOLLISION = "tab.setcollision";
public static final String COMMAND_AUTOCOMPLETE = "tab.tabcomplete";
public static final String COMMAND_DATA_REMOVE = "tab.remove";
public static final String COMMAND_PROPERTY_CHANGE_PREFIX = "tab.change.";
public static final String COMMAND_WIDTH = "tab.width";
public static final String STAFF = "tab.staff";
public static final String PER_WORLD_PLAYERLIST_BYPASS = "tab.bypass";
public static final String SEE_VANISHED = "tab.seevanished";
public static final String SPECTATOR_BYPASS = "tab.spectatorbypass";
public static final String TEST_PERMISSION = "tab.testpermission";
public static final String GROUP_PREFIX = "tab.group.";
/**
* Private constructor to prevent the class from being instantiated
*/
private Permission(){}
}
/**
* Internal property names used to store text
* under a specific key
*/
public static class Property {
public static final String HEADER = "header";
public static final String FOOTER = "footer";
public static final String TABPREFIX = "tabprefix";
public static final String CUSTOMTABNAME = "customtabname";
public static final String TABSUFFIX = "tabsuffix";
public static final String TAGPREFIX = "tagprefix";
public static final String CUSTOMTAGNAME = "customtagname";
public static final String TAGSUFFIX = "tagsuffix";
public static final String ABOVENAME = "abovename";
public static final String NAMETAG = "nametag";
public static final String BELOWNAME = "belowname";
public static final String SCOREBOARD_TITLE = "scoreboard-title";
public static final String BELOWNAME_NUMBER = "belowname-number";
public static final String BELOWNAME_TEXT = "belowname-text";
public static final String YELLOW_NUMBER = "yellow-number";
/**
* Private constructor to prevent the class from being instantiated
*/
private Property() {}
public static String bossbarTitle(String name) {
return "bossbar-title-" + name;
}
public static String bossbarProgress(String name) {
return "bossbar-progress-" + name;
}
public static String bossbarColor(String name) {
return "bossbar-color-" + name;
}
public static String bossbarStyle(String name) {
return "bossbar-style-" + name;
}
public static String scoreboardPrefix(String scoreboard, int lineNumber) {
return scoreboard + "-" + lineNumber + "-prefix";
}
public static String scoreboardName(String scoreboard, int lineNumber) {
return scoreboard + "-" + lineNumber + "-name";
}
public static String scoreboardSuffix(String scoreboard, int lineNumber) {
return scoreboard + "-" + lineNumber + "-suffix";
}
}
/**
* Feature names used to register features under in the
* feature manager which they can be retrieved by.
*/
public static class Feature {
//universal features
public static final String HEADER_FOOTER = "HeaderFooter";
public static final String GHOST_PLAYER_FIX = "GhostPlayerFix";
public static final String PLAYER_LIST = "PlayerList";
public static final String SPECTATOR_FIX = "SpectatorFix";
public static final String YELLOW_NUMBER = "YellowNumber";
public static final String BELOW_NAME = "BelowName";
public static final String BELOW_NAME_TEXT = "BelowNameText";
public static final String SCOREBOARD = "ScoreBoard";
public static final String PING_SPOOF = "PingSpoof";
public static final String SORTING = "sorting";
public static final String LAYOUT = "layout";
public static final String LAYOUT_LATENCY = "layout-latency";
public static final String NICK_COMPATIBILITY = "nick";
public static final String PIPELINE_INJECTION = "injection";
public static final String BOSS_BAR = "BossBar";
public static final String NAME_TAGS = "NameTag16";
public static final String NAME_TAGS_COLLISION = "NameTagCollision";
public static final String NAME_TAGS_VISIBILITY = "NameTagVisibility";
public static final String GROUP_MANAGER = "GroupManager";
public static final String PLACEHOLDER_MANAGER = "PlaceholderManager";
public static final String PET_FIX = "PetFix";
public static final String UNLIMITED_NAME_TAGS = "NameTagX";
public static final String UNLIMITED_NAME_TAGS_PACKET_LISTENER = "nametagx-packet";
public static final String UNLIMITED_NAME_TAGS_VEHICLE_REFRESHER = "nametagx-vehicle";
//Bukkit only
public static final String PER_WORLD_PLAYER_LIST = "PerWorldPlayerList";
//BungeeCord only
public static final String REDIS_BUNGEE = "RedisBungee";
public static final String GLOBAL_PLAYER_LIST = "GlobalPlayerList";
public static final String GLOBAL_PLAYER_LIST_LATENCY = "GlobalPlayerList-Latency";
//additional info displayed in cpu command
public static final String PACKET_SERIALIZING = "Packet serializing";
public static final String PACKET_DESERIALIZING = "Packet deserializing";
/**
* Private constructor to prevent the class from being instantiated
*/
private Feature(){}
public static String scoreboardLine(String line) {
return "scoreboard-" + line;
}
public static String scoreboardLine(String scoreboard, int index) {
return "scoreboard-score-" + scoreboard + "-" + index;
}
public static String layout(String name) {
return "layout-" + name;
}
public static String layoutSlot(String layout, int slot) {
return "layout-" + layout + "-slot-" + slot;
}
public static String bossBarTitle(String name) {
return "BossBar-title-" + name;
}
public static String bossBarProgress(String name) {
return "BossBar-progress-" + name;
}
public static String bossBarColorStyle(String name) {
return "BossBar-color-style-" + name;
}
}
/**
* All internal placeholders offered by the plugin
*/
public static class Placeholder {
// Universal
public static final String DISPLAY_NAME = "%displayname%";
public static final String GROUP = "%group%";
public static final String VANISHED = "%vanished%";
public static final String WORLD = "%world%";
public static final String WORLD_ONLINE = "%worldonline%";
public static final String SERVER = "%server%";
public static final String SERVER_ONLINE = "%serveronline%";
public static final String PLAYER = "%player%";
public static final String TIME = "%time%";
public static final String DATE = "%date%";
public static final String PING = "%ping%";
public static final String PLAYER_VERSION = "%player-version%";
public static final String PLAYER_VERSION_ID = "%player-version-id%";
public static final String MEMORY_USED = "%memory-used%";
public static final String MEMORY_MAX = "%memory-max%";
public static final String MEMORY_USED_GB = "%memory-used-gb%";
public static final String MEMORY_MAX_GB = "%memory-max-gb%";
public static final String ONLINE = "%online%";
public static final String STAFF_ONLINE = "%staffonline%";
public static final String NON_STAFF_ONLINE = "%nonstaffonline%";
public static final String LUCKPERMS_PREFIX = "%luckperms-prefix%";
public static final String LUCKPERMS_SUFFIX = "%luckperms-suffix%";
public static final String COUNTDOWN = "%countdown%";
// Placeholders for internal use
public static final String COLLISION = "%collision%";
public static final String INVISIBLE = "%invisible%";
public static final String VEHICLE = "%vehicle%";
// Bukkit only
public static final String TPS = "%tps%";
public static final String MSPT = "%mspt%";
public static final String AFK = "%afk%";
public static final String ESSENTIALS_NICK = "%essentialsnick%";
public static final String VAULT_PREFIX = "%vault-prefix%";
public static final String VAULT_SUFFIX = "%vault-suffix%";
public static final String HEALTH = "%health%";
public static final int MINIMUM_REFRESH_INTERVAL = 50;
public static String condition(String name) {
return "%condition:" + name + "%";
}
public static String animation(String name) {
return "%animation:" + name + "%";
}
public static String globalPlayerListGroup(String group) {
return "%playerlist-group_" + group + "%";
}
}
/**
* All plugins TAB hooks into
*/
public static class Plugin {
public static final String LUCKPERMS = "LuckPerms";
public static final String VAULT = "Vault";
public static final String PLACEHOLDER_API = "PlaceholderAPI";
public static final String LIBS_DISGUISES = "LibsDisguises";
public static final String VIAVERSION = "ViaVersion";
public static final String PROTOCOL_SUPPORT = "ProtocolSupport";
public static final String ESSENTIALS = "Essentials";
public static final String REDIS_BUNGEE = "RedisBungee";
public static final String PREMIUM_VANISH = "PremiumVanish";
}
/**
* TAB's custom metrics charts
*/
public static class MetricsChart {
public static final String PERMISSION_SYSTEM = "permission_system";
public static final String GLOBAL_PLAYER_LIST_ENABLED = "global_playerlist_enabled";
public static final String PLACEHOLDER_API = "placeholderapi";
public static final String SERVER_VERSION = "server_version";
public static final String UNLIMITED_NAME_TAG_MODE_ENABLED = "unlimited_nametag_mode_enabled";
}
}
@@ -6,256 +6,403 @@ import me.neznamy.tab.api.protocol.PacketPlayOutPlayerInfo;
import me.neznamy.tab.api.protocol.PacketPlayOutScoreboardDisplayObjective;
import me.neznamy.tab.api.protocol.PacketPlayOutScoreboardObjective;
/**
* Abstract class representing a core feature of the plugin.
* <p>
* It receives all kinds of events and can react to them.
*/
public abstract class TabFeature {
private final String featureName;
private final String refreshDisplayName;
protected final String[] disabledServers;
private final boolean serverWhitelistMode;
protected final String[] disabledWorlds;
private final boolean worldWhitelistMode;
private final Set<TabPlayer> disabledPlayers = Collections.newSetFromMap(new WeakHashMap<>());
private final List<String> methodOverrides = new ArrayList<>();
protected TabFeature(String featureName, String refreshDisplayName) {
this(featureName, refreshDisplayName, null, null);
}
protected TabFeature(String featureName, String refreshDisplayName, List<String> disabledServers, List<String> disabledWorlds) {
this.featureName = featureName;
this.refreshDisplayName = refreshDisplayName;
if (disabledServers != null) {
this.disabledServers = disabledServers.toArray(new String[0]);
serverWhitelistMode = disabledServers.contains("WHITELIST");
} else {
this.disabledServers = new String[0];
serverWhitelistMode = false;
}
if (disabledWorlds != null) {
this.disabledWorlds = disabledWorlds.toArray(new String[0]);
worldWhitelistMode = disabledWorlds.contains("WHITELIST");
} else {
this.disabledWorlds = new String[0];
worldWhitelistMode = false;
}
try {
if (getClass().getMethod("onCommand", TabPlayer.class, String.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onCommand");
if (getClass().getMethod("onJoin", TabPlayer.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onJoin");
if (getClass().getMethod("onQuit", TabPlayer.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onQuit");
if (getClass().getMethod("onWorldChange", TabPlayer.class, String.class, String.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onWorldChange");
if (getClass().getMethod("onServerChange", TabPlayer.class, String.class, String.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onServerChange");
if (getClass().getMethod("onDisplayObjective", TabPlayer.class, PacketPlayOutScoreboardDisplayObjective.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onDisplayObjective");
if (getClass().getMethod("onLoginPacket", TabPlayer.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onLoginPacket");
if (getClass().getMethod("onObjective", TabPlayer.class, PacketPlayOutScoreboardObjective.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onObjective");
if (getClass().getMethod("onPlayerInfo", TabPlayer.class, PacketPlayOutPlayerInfo.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onPlayerInfo");
if (getClass().getMethod("onPacketReceive", TabPlayer.class, Object.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onPacketReceive");
if (getClass().getMethod("onPacketSend", TabPlayer.class, Object.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onPacketSend");
if (getClass().getMethod("refresh", TabPlayer.class, boolean.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("refresh");
} catch (NoSuchMethodException e) {
//this will never happen
}
}
/**
* Loads all players and sends packets
*/
public void load() {
//empty by default
}
/**
* Unloads all players and sends clear packets
*/
public void unload() {
//empty by default
}
/**
* Processes command from player
* @param sender - command sender
* @param message - command line
* @return true if event should be cancelled, false if not
*/
public boolean onCommand(TabPlayer sender, String message) {
return false;
}
/**
* Processes join event
* @param connectedPlayer - player who connected
*/
public void onJoin(TabPlayer connectedPlayer) {
//empty by default
}
/**
* Processes quit event
* @param disconnectedPlayer - player who disconnected
*/
public void onQuit(TabPlayer disconnectedPlayer) {
}
/**
* Processes world switch
* @param changed - player who switched world
* @param from - world player changed from
* @param to - world player changed to
*/
public void onWorldChange(TabPlayer changed, String from, String to) {
//empty by default
}
/**
* Processes server switch
* @param changed - player who switched server
* @param from - server player changed from
* @param to - server player changed to
*/
public void onServerChange(TabPlayer changed, String from, String to) {
//empty by default
}
/**
* Processes the packet send and returns true if packet should be cancelled
* @param receiver - player receiving packet
* @param packet - received packet
*/
public void onDisplayObjective(TabPlayer receiver, PacketPlayOutScoreboardDisplayObjective packet) {
//empty by default
}
/**
* Processes login packet, only available on BungeeCord
* @param packetReceiver - player receiving client reset packet
*/
public void onLoginPacket(TabPlayer packetReceiver) {
//empty by default
}
/**
* Processes the packet send
* @param receiver - player receiving packet
* @param packet - received packet
*/
public void onObjective(TabPlayer receiver, PacketPlayOutScoreboardObjective packet) {
//empty by default
}
/**
* Processes the packet send and possibly modifies it
* @param receiver - player receiving packet
* @param info - received packet
*/
public void onPlayerInfo(TabPlayer receiver, PacketPlayOutPlayerInfo info) {
//empty by default
}
/**
* Processes raw packet sent by client
* @param sender - packet sender
* @param packet - packet received
* @return true if false should be cancelled, false if not
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public boolean onPacketReceive(TabPlayer sender, Object packet) throws ReflectiveOperationException {
return false;
}
/**
* Processes raw packet sent to client
* @param receiver - packet receiver
* @param packet - the packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public void onPacketSend(TabPlayer receiver, Object packet) throws ReflectiveOperationException {
//empty by default
}
/** Feature's name displayed in /tab cpu */
private final String featureName;
/**
* Performs refresh of specified player
* @param refreshed - player to refresh
* @param force - if refresh should be forced despite refresh seemingly not needed
*/
public void refresh(TabPlayer refreshed, boolean force) {
//empty by default
}
/** Feature's function name displayed in place of refreshing in /tab cpu */
private final String refreshDisplayName;
/**
* Registers this feature as one using specified placeholders
* @param placeholders - placeholders to add as used in this feature
*/
public void addUsedPlaceholders(Collection<String> placeholders) {
if (placeholders.isEmpty()) return;
placeholders.forEach(p -> TabAPI.getInstance().getPlaceholderManager().addUsedPlaceholder(p, this));
}
/**
* Returns true if world or server is disabled, false if not
* @param world - world to check
* @param server - server to check
* @return true if feature should be disabled, false if not
*/
public boolean isDisabled(String server, String world) {
boolean contains = contains(disabledWorlds, world);
if (worldWhitelistMode) contains = !contains;
if (contains) return true;
contains = contains(disabledServers, server);
if (serverWhitelistMode) contains = !contains;
return contains;
}
protected boolean contains(String[] list, String element) {
if (element == null) return false;
for (String s : list) {
if (s.endsWith("*")) {
if (element.toLowerCase().startsWith(s.substring(0, s.length()-1).toLowerCase())) return true;
} else {
if (element.equalsIgnoreCase(s)) return true;
}
}
return false;
}
/**
* Returns name of the feature displayed in /tab cpu.
* @return name of the feature displayed in /tab cpu
*/
public String getFeatureName() {
return featureName;
}
/** Servers where the feature is disabled (or enabled if using whitelist mode) */
protected String[] disabledServers = new String[0];
public boolean overridesMethod(String method) {
return methodOverrides.contains(method);
}
/** Flag tracking whether disabled server list is a whitelist or blacklist */
private boolean serverWhitelistMode = false;
public boolean isDisabledPlayer(TabPlayer p) {
return disabledPlayers.contains(p);
}
public void addDisabledPlayer(TabPlayer p) {
if (disabledPlayers.contains(p)) return;
disabledPlayers.add(p);
}
public void removeDisabledPlayer(TabPlayer p) {
disabledPlayers.remove(p);
}
/** Worlds where the feature is disabled (or enabled if using whitelist mode) */
protected String[] disabledWorlds = new String[0];
public String getRefreshDisplayName() {
return refreshDisplayName;
}
/** Flag tracking whether disabled world list is a whitelist or blacklist */
private boolean worldWhitelistMode = false;
/** Players located in currently disabled worlds / servers */
private final Set<TabPlayer> disabledPlayers = Collections.newSetFromMap(new WeakHashMap<>());
/**
* List of methods overridden (implemented) by each feature.
* If a method is not overridden, it is not called at all.
* This avoids a massive spam for every single method
* in every single feature in /tab cpu output
*/
private final List<String> methodOverrides = new ArrayList<>();
/**
* Constructs new instance with given parameters and loads method overrides
*
* @param featureName
* Feature's name in /tab cpu
* @param refreshDisplayName
* "refreshing" cpu display type name of the feature
*/
protected TabFeature(String featureName, String refreshDisplayName) {
this.featureName = featureName;
this.refreshDisplayName = refreshDisplayName;
try {
if (getClass().getMethod("onCommand", TabPlayer.class, String.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onCommand");
if (getClass().getMethod("onJoin", TabPlayer.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onJoin");
if (getClass().getMethod("onQuit", TabPlayer.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onQuit");
if (getClass().getMethod("onWorldChange", TabPlayer.class, String.class, String.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onWorldChange");
if (getClass().getMethod("onServerChange", TabPlayer.class, String.class, String.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onServerChange");
if (getClass().getMethod("onDisplayObjective", TabPlayer.class, PacketPlayOutScoreboardDisplayObjective.class).getDeclaringClass() != TabFeature.class) {
methodOverrides.add("onDisplayObjective");
TabAPI.getInstance().getFeatureManager().markDisplayObjective();
}
if (getClass().getMethod("onLoginPacket", TabPlayer.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onLoginPacket");
if (getClass().getMethod("onObjective", TabPlayer.class, PacketPlayOutScoreboardObjective.class).getDeclaringClass() != TabFeature.class) {
methodOverrides.add("onObjective");
TabAPI.getInstance().getFeatureManager().markObjective();
}
if (getClass().getMethod("onPlayerInfo", TabPlayer.class, PacketPlayOutPlayerInfo.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onPlayerInfo");
if (getClass().getMethod("onPacketReceive", TabPlayer.class, Object.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onPacketReceive");
if (getClass().getMethod("onPacketSend", TabPlayer.class, Object.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onPacketSend");
if (getClass().getMethod("refresh", TabPlayer.class, boolean.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("refresh");
if (getClass().getMethod("onVanishStatusChange", TabPlayer.class).getDeclaringClass() != TabFeature.class)
methodOverrides.add("onVanishStatusChange");
} catch (NoSuchMethodException e) {
//this will never happen
}
}
/**
* Constructs new instance with given parameters and loads method overrides.
* Also loads lists of disabled worlds and servers of this feature with config
* section path specified with {@code configSection} parameter.
*
* @param featureName
* Feature's name in /tab cpu
* @param refreshDisplayName
* "refreshing" cpu display type name of the feature
* @param configSection
* Configuration section of the feature to load disabled
* servers / worlds from
*/
protected TabFeature(String featureName, String refreshDisplayName, String configSection) {
this(featureName, refreshDisplayName);
List<String> disabledServers = TabAPI.getInstance().getConfig().getStringList(configSection + ".disable-in-servers");
List<String> disabledWorlds = TabAPI.getInstance().getConfig().getStringList(configSection + ".disable-in-worlds");
if (disabledServers != null) {
this.disabledServers = disabledServers.toArray(new String[0]);
serverWhitelistMode = disabledServers.contains("WHITELIST");
}
if (disabledWorlds != null) {
this.disabledWorlds = disabledWorlds.toArray(new String[0]);
worldWhitelistMode = disabledWorlds.contains("WHITELIST");
}
}
/**
* Loads all online players and sends packets
*/
public void load() {
//empty by default
}
/**
* Unloads all players and sends clear packets
*/
public void unload() {
//empty by default
}
/**
* Processes command from player. This is typically a toggle command.
*
* @param sender
* command sender
* @param message
* command line
* @return {@code true} if event should be cancelled, {@code true} if not
*/
public boolean onCommand(TabPlayer sender, String message) {
return false;
}
/**
* Processes join event
*
* @param connectedPlayer
* player who connected
*/
public void onJoin(TabPlayer connectedPlayer) {
//empty by default
}
/**
* Processes quit event
*
* @param disconnectedPlayer
* player who disconnected
*/
public void onQuit(TabPlayer disconnectedPlayer) {
//empty by default
}
/**
* Processes world switch
*
* @param changed
* player who switched world
* @param from
* world player changed from
* @param to
* world player changed to
*/
public void onWorldChange(TabPlayer changed, String from, String to) {
//empty by default
}
/**
* Processes server switch
*
* @param changed
* player who switched server
* @param from
* server player changed from
* @param to
* server player changed to
*/
public void onServerChange(TabPlayer changed, String from, String to) {
//empty by default
}
/**
* Processes the packet send and returns true if packet should be cancelled
*
* @param receiver
* player receiving packet
* @param packet
* received packet
*/
public void onDisplayObjective(TabPlayer receiver, PacketPlayOutScoreboardDisplayObjective packet) {
//empty by default
}
/**
* Processes login packet, only available on BungeeCord
*
* @param packetReceiver
* player receiving client reset packet
*/
public void onLoginPacket(TabPlayer packetReceiver) {
//empty by default
}
/**
* Processes the packet send
*
* @param receiver
* player receiving packet
* @param packet
* received packet
*/
public void onObjective(TabPlayer receiver, PacketPlayOutScoreboardObjective packet) {
//empty by default
}
/**
* Processes the packet send and possibly modifies it
*
* @param receiver
* player receiving packet
* @param info
* received packet
*/
public void onPlayerInfo(TabPlayer receiver, PacketPlayOutPlayerInfo info) {
//empty by default
}
/**
* Processes raw packet sent by client
*
* @param sender
* packet sender
* @param packet
* packet received
* @return true if false should be cancelled, false if not
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public boolean onPacketReceive(TabPlayer sender, Object packet) throws ReflectiveOperationException {
return false;
}
/**
* Processes raw packet sent to client
*
* @param receiver
* packet receiver
* @param packet
* the packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public void onPacketSend(TabPlayer receiver, Object packet) throws ReflectiveOperationException {
//empty by default
}
/**
* Performs refresh of specified player
*
* @param refreshed
* player to refresh
* @param force
* if refresh should be forced despite refresh seemingly not needed
*/
public void refresh(TabPlayer refreshed, boolean force) {
//empty by default
}
/**
* Processes vanish status change of player
*
* @param player
* Player who changed vanish status
*/
public void onVanishStatusChange(TabPlayer player){
//empty by default
}
/**
* Registers this feature as one using specified placeholders
*
* @param placeholders
* placeholders to add as used in this feature
*/
public void addUsedPlaceholders(Collection<String> placeholders) {
if (placeholders.isEmpty()) return;
placeholders.forEach(p -> TabAPI.getInstance().getPlaceholderManager().addUsedPlaceholder(p, this));
}
/**
* Returns {@code true} if world or server is disabled, {@code false} if not
*
* @param server
* server to check
* @param world
* world to check
* @return {@code true} if feature should be disabled, {@code false} if not
*/
public boolean isDisabled(String server, String world) {
boolean contains = contains(disabledWorlds, world);
if (worldWhitelistMode) contains = !contains;
if (contains) return true;
contains = contains(disabledServers, server);
if (serverWhitelistMode) contains = !contains;
return contains;
}
/**
* Returns {@code true} if list contains the specified element or element
* ends with {@code "*"} and element meeting that requirement is present,
* {@code false} otherwise.
*
* @param list
* List to check
* @param element
* Element to find
* @return {@code true} if element was found, {@code false} if not
*/
protected boolean contains(String[] list, String element) {
if (element == null) return false;
for (String s : list) {
if (s.endsWith("*")) {
if (element.toLowerCase().startsWith(s.substring(0, s.length()-1).toLowerCase())) return true;
} else {
if (element.equalsIgnoreCase(s)) return true;
}
}
return false;
}
/**
* Returns name of the feature displayed in /tab cpu.
*
* @return name of the feature displayed in /tab cpu
*/
public String getFeatureName() {
return featureName;
}
/**
* Returns text to display in /tab cpu of this feature refreshing visuals
*
* @return Text to display as refreshing of this feature
*/
public String getRefreshDisplayName() {
return refreshDisplayName;
}
/**
* Returns {@code true} if method with specified name is overridden
* in the implementation, {@code false} if not
*
* @param method
* Method to check
* @return {@code true} if overridden, {@code false} if not
*/
public boolean overridesMethod(String method) {
return methodOverrides.contains(method);
}
/**
* Returns {@code true} if player is currently located in a server
* or world, which is marked as disabled, {@code false} if not
*
* @param p
* Player to check
* @return {@code true} if player is in disabled server / world,
* {@code false} if not
*/
public boolean isDisabledPlayer(TabPlayer p) {
return disabledPlayers.contains(p);
}
/**
* Adds specified player into list of players in disabled
* servers / worlds.
*
* @param p
* Player to add
*/
public void addDisabledPlayer(TabPlayer p) {
if (disabledPlayers.contains(p)) return;
disabledPlayers.add(p);
}
/**
* Removes specified player from list of players in disabled
* servers / worlds
*
* @param p
* Player to check
*/
public void removeDisabledPlayer(TabPlayer p) {
disabledPlayers.remove(p);
}
}
@@ -4,6 +4,7 @@ import java.util.UUID;
import io.netty.channel.Channel;
import me.neznamy.tab.api.chat.IChatBaseComponent;
import me.neznamy.tab.api.protocol.Skin;
import me.neznamy.tab.api.protocol.TabPacket;
/**
@@ -11,253 +12,309 @@ import me.neznamy.tab.api.protocol.TabPacket;
*/
public interface TabPlayer {
/**
* Refreshes all visuals on the player
*/
void forceRefresh();
/**
* Refreshes all visuals on the player
*/
void forceRefresh();
/**
* Returns player's name
* @return Player's name
*/
String getName();
/**
* Returns player's name
*
* @return Player's name
*/
String getName();
/**
* Returns player's UUID
* @return Player's UUID
*/
UUID getUniqueId();
/**
* Returns player's UUID
*
* @return Player's UUID
*/
UUID getUniqueId();
/**
* Returns player's uuid used in TabList. This may only be different from real uuid if
* TAB is installed on velocity with some specific velocity setup
* @return player's uuid in TabList
*/
UUID getTablistUUID();
/**
* Returns player's uuid used in TabList. This may only be different from real uuid if
* TAB is installed on velocity with some specific velocity setup
*
* @return player's uuid in TabList
*/
UUID getTablistUUID();
/**
* Returns player's protocol version
* @return player's protocol version
*/
ProtocolVersion getVersion();
/**
* Returns player's protocol version
*
* @return player's protocol version
*/
ProtocolVersion getVersion();
/**
* Returns platform-specific entity
* @return an instance of bukkit/bungee player depending on platform
*/
Object getPlayer();
/**
* Returns platform-specific entity
*
* @return an instance of bukkit/bungee player depending on platform
*/
Object getPlayer();
/**
* Returns player's current world name (on BungeeCord this requires bridge installed)
* @return name of world where player is currently in, null on BungeeCord if bridge is not installed
*/
String getWorld();
/**
* Returns player's current server name on BungeeCord (on bukkit this returns null)
* @return name of server where player is currently in, null on bukkit
*/
String getServer();
/**
* Returns player's current world name (on BungeeCord this requires bridge installed)
*
* @return name of world where player is currently in, "N/A" on BungeeCord if bridge is not installed
*/
String getWorld();
/**
* Returns player's current server name on BungeeCord (on bukkit this returns "N/A")
*
* @return name of server where player is currently in, "N/A" on bukkit
*/
String getServer();
/**
* Performs platform-specific API call to check for permission and returns the result
* @param permission - the permission to check for
* @return - true if player has permission, false if not
*/
boolean hasPermission(String permission);
/**
* Performs platform-specific API call to check for permission and returns the result
*
* @param permission
* the permission to check for
* @return true if player has permission, false if not
*/
boolean hasPermission(String permission);
/**
* Sends the player a custom universal packet
* @param packet - packet to send
*/
void sendCustomPacket(TabPacket packet);
/**
* Sends the player a custom universal packet
*
* @param packet
* packet to send
*/
void sendCustomPacket(TabPacket packet);
/**
* Sends the player a custom universal packet and adds that packet into counter that
* is displayed in /tab cpu
* @param packet - packet to send
* @param feature - feature to increment sent packet counter of
*/
void sendCustomPacket(TabPacket packet, TabFeature feature);
/**
* Sends the player a custom universal packet and adds that packet into counter that
* is displayed in /tab cpu
* @param packet - packet to send
* @param feature - feature to increment sent packet counter of
*/
void sendCustomPacket(TabPacket packet, String feature);
/**
* Sends the player a custom universal packet and adds that packet into counter that
* is displayed in /tab cpu
*
* @param packet
* packet to send
* @param feature
* feature to increment sent packet counter of
*/
void sendCustomPacket(TabPacket packet, TabFeature feature);
/**
* Sends the player a custom universal packet and adds that packet into counter that
* is displayed in /tab cpu
*
* @param packet
* packet to send
* @param feature
* feature to increment sent packet counter of
*/
void sendCustomPacket(TabPacket packet, String feature);
/**
* Sends the player a platform-specific packet
* @param packet - an instance of packet depending on platform
*/
void sendPacket(Object packet);
/**
* Sends the player a platform-specific packet
*
* @param packet
* an instance of packet depending on platform
*/
void sendPacket(Object packet);
/**
* Sends the player a platform-specific packet and adds that packet into counter that
* is displayed in /tab cpu
* @param packet - an instance of packet depending on platform
* @param feature - feature to increment sent packet counter of
*/
void sendPacket(Object packet, TabFeature feature);
/**
* Returns player's property by name
*
* @param name
* name of property
* @return the property or {@code null} if not found
*/
Property getProperty(String name);
/**
* Sends the player a platform-specific packet and adds that packet into counter that
* is displayed in /tab cpu
* @param packet - an instance of packet depending on platform
* @param feature - feature to increment sent packet counter of
*/
void sendPacket(Object packet, String feature);
/**
* Sends a message to the player
*
* @param message
* message to be sent
* @param translateColors
* whether colors should be translated or not
*/
void sendMessage(String message, boolean translateColors);
/**
* Returns player's property by name
* @param name - name of property
* @return the property or null if not found
*/
Property getProperty(String name);
/**
* Sends specified component as a chat message
*
* @param message
* message to send
*/
void sendMessage(IChatBaseComponent message);
/**
* Sends a message to the player
* @param message - message to be sent
* @param translateColors - whether colors should be translated or not
*/
void sendMessage(String message, boolean translateColors);
/**
* Returns player's netty channel. On servers running version 1.7 or older returns {@code null}.
*
* @return player's channel
*/
Channel getChannel();
/**
* Sends specified component as a chat message
* @param message - message to send
*/
void sendMessage(IChatBaseComponent message);
/**
* Returns player's ping calculated by server
*
* @return player's ping
*/
int getPing();
/**
* Returns player's primary permission group
* @return player's group depending on configuration
*/
String getGroup();
/**
* Returns player's skin data
*
* @return player's skin
*/
Skin getSkin();
/**
* Toggles armor stands preview mode for the player
*/
void toggleNametagPreview();
/**
* Returns true once the player is successfully loaded (onJoin method ran through all methods)
*
* @return {@code true} if player is fully loaded, {@code false} otherwise
*/
boolean isLoaded();
/**
* Returns whether player is previewing NameTag or not
* @return true if previewing, false if not
*/
boolean isPreviewingNametag();
/**
* Sets property with specified name to new value. If property did not exist before, it is
* created and {@code true} is returned. If it existed, it is overridden and {@code true} is returned.
* {@code false} is returned otherwise.
*
* @param feature
* feature using this property to get placeholders registered
* @param identifier
* property name
* @param rawValue
* new raw value
* @return {@code true} if value changed / did not exist, {@code false} if value did not change
*/
boolean setProperty(TabFeature feature, String identifier, String rawValue);
/**
* Returns player's channel or null if server is 1.7 or older
* @return player's channel
*/
Channel getChannel();
/**
* Loads property from config using standard property loading algorithm
*
* @param property
* property name to load
* @return {@code true} if value did not exist or changed, {@code false} otherwise
*/
boolean loadPropertyFromConfig(TabFeature feature, String property);
/**
* Returns player's ping calculated by server
* @return player's ping
*/
int getPing();
/**
* Loads property from config using standard property loading algorithm. If the property is
* not set in config, {@code ifNotSet} value is used.
*
* @param property
* property name to load
* @param ifNotSet
* value to use if property is not defined in config
* @return {@code true} if value did not exist or changed, {@code false} otherwise
*/
boolean loadPropertyFromConfig(TabFeature feature, String property, String ifNotSet);
/**
* Returns player's platform-specific skin data
* @return player's skin
*/
Object getSkin();
/**
* Returns name of player's scoreboard team or {@code null} if NameTag feature is disabled
*
* @return name of player's team
*/
String getTeamName();
/**
* Returns true once the player is successfully loaded (onJoin method ran through all methods)
* @return true if player is fully loaded, false otherwise
*/
boolean isLoaded();
/**
* Returns {@code true} if player is disguised using LibsDisguises, {@code false} if not
*
* @return {@code true} if player is disguised, {@code false} if not
*/
boolean isDisguised();
/**
* Sets property with specified name to new value. If property did not exist before, it is
* created and true is returned. If it existed, it is overridden and true is returned. False is returned otherwise.
* @param feature - feature using this property to get placeholders registered
* @param identifier - property name
* @param rawValue - new raw value
* @return true if value changed / did not exist, false if value did not change
*/
boolean setProperty(TabFeature feature, String identifier, String rawValue);
/**
* Returns {@code true} if player has invisibility potion, {@code false} if not.
* For bukkit, API is used, for BungeeCord, bridge is used.
*
* @return {@code true} if player has invisibility potion, {@code false} if not
*/
boolean hasInvisibilityPotion();
/**
* Loads property from config using standard property loading algorithm
* @param property - property name to load
* @return true if value did not exist or changed, false otherwise
*/
boolean loadPropertyFromConfig(TabFeature feature, String property);
/**
* Checks if player is vanished using any supported vanish plugin and returns the result.
*
* @return {@code true} if player is vanished, {@code false} if not
*/
boolean isVanished();
/**
* Loads property from config using standard property loading algorithm. If the property is
* not set in config, sets it to ifNotSet value
* @param property - property name to load
* @param ifNotSet - value to use if property is not defined in config
* @return true if value did not exist or changed, false otherwise
*/
boolean loadPropertyFromConfig(TabFeature feature, String property, String ifNotSet);
/**
* Calls platform-specific method and returns the result
*
* @return {@code true} if player is online, {@code false} if not
*/
boolean isOnline();
/**
* Returns GameMode of the player (0 for survival, 1 creative, 2 adventure, 3 spectator)
*
* @return GameMode of the player
*/
int getGamemode();
/**
* Returns name of player's scoreboard team or null if NameTag feature is disabled
* @return name of player's team
*/
String getTeamName();
/**
* Returns {@code true} if this is a bedrock player, {@code false} if not
*
* @return {@code true} if player is a bedrock player, {@code false} if not
*/
boolean isBedrockPlayer();
/**
* Returns user-friendly explanation of team name
* @return explanation behind team name
*/
String getTeamNameNote();
/**
* Returns player's primary permission group. If group has been changed using
* {@link #setTemporaryGroup(String)}, returns that value. Otherwise, returns group
* detected by standard group assign logic
*
* @return player's primary permission group
*/
String getGroup();
/**
* Returns true if player is disguised using LibsDisguises
* @return true if player is disguised, false if not
*/
boolean isDisguised();
/**
* Temporarily overrides player's group and applies all changes coming from new group.
* This includes all properties and sorting, if used.
*
* @param group
* New group to use
* @see #hasTemporaryGroup()
* @see #resetTemporaryGroup()
*/
void setTemporaryGroup(String group);
/**
* Returns true if player has invisibility potion, false if not. For bukkit, API is used, for BungeeCord
* bukkit bridge is used
* @return true if player has invisibility potion, false if not
*/
boolean hasInvisibilityPotion();
/**
* Returns temporary group applied to the player using {@link #setTemporaryGroup(String)}.
* If no group was set, returns {@code null}
*
* @return Temporary group assigned to player or {@code null} if not set
* @see #setTemporaryGroup(String)
* @see #resetTemporaryGroup()
*/
boolean hasTemporaryGroup();
/**
* Currently only PV hook on BungeeCord for global PlayerList function. Looking to expand this
* in the future
* @return true if vanished with PV on BungeeCord
*/
boolean isVanished();
/**
* Resets temporary group assigned using {@link #setTemporaryGroup(String)}.
* If no temporary group is set, doesn't do anything.
*
* @see #setTemporaryGroup(String)
* @see #hasTemporaryGroup()
*/
void resetTemporaryGroup();
/**
* Returns true if player is online according to server
* @return true if online, false if not
*/
boolean isOnline();
/**
* Returns player's game profile name as seen by other players. By default,
* this returns player's name. If using a nickname / disguise plugin that changes
* game profile name for other players and change was successfully detected, returns
* that value.
*
* @return player's game profile name as seen by other players
*/
String getNickname();
/**
* Returns player's armor stand manager if unlimited GameMode mode is enabled, null if disabled
* @return player's armor stand manager
*/
ArmorStandManager getArmorStandManager();
/**
* Returns player's chat message signing key. Returned object is a direct
* object from the server, which is different per platform.
*
* @return Player's direct chat message signing key
*/
Object getProfilePublicKey();
/**
* Sets player's armor stands manager to new instance
* @param armorStandManager - new instance
*/
void setArmorStandManager(ArmorStandManager armorStandManager);
/**
* Returns GameMode of the player (0 for survival, 1 creative, 2 adventure, 3 spectator)
* @return GameMode of the player
*/
int getGamemode();
boolean isBedrockPlayer();
void setTemporaryGroup(String group);
boolean hasTemporaryGroup();
void resetTemporaryGroup();
/**
* Return player's chat session ID
* @return player's chat session ID
*/
UUID getChatSessionId();
}
@@ -1,29 +1,168 @@
package me.neznamy.tab.api;
/**
* An interface allowing to modify tablist display names
* of players.
* <p>
* Instance can be obtained using {@link TabAPI#getTablistFormatManager()}.
* This requires the feature to be enabled in config, otherwise the method will
* return {@code null}.
*/
public interface TablistFormatManager {
void setPrefix(TabPlayer player, String prefix);
void setName(TabPlayer player, String customName);
void setSuffix(TabPlayer player, String suffix);
void resetPrefix(TabPlayer player);
void resetName(TabPlayer player);
void resetSuffix(TabPlayer player);
String getCustomPrefix(TabPlayer player);
String getCustomName(TabPlayer player);
String getCustomSuffix(TabPlayer player);
String getOriginalPrefix(TabPlayer player);
String getOriginalName(TabPlayer player);
String getOriginalSuffix(TabPlayer player);
/**
* Changes player's prefix to provided value. Supports placeholders,
* as well as any supported RGB formats.
*
* @param player
* player to change prefix of
* @param prefix
* new prefix value
* @see #resetPrefix(TabPlayer)
* @see #getCustomPrefix(TabPlayer)
* @see #getOriginalPrefix(TabPlayer)
*/
void setPrefix(TabPlayer player, String prefix);
/**
* Changes player's name to provided value. Supports placeholders,
* as well as any supported RGB formats.
*
* @param player
* player to change prefix of
* @param customName
* new customName value
* @see #resetName(TabPlayer)
* @see #getCustomName(TabPlayer)
* @see #getOriginalName(TabPlayer)
*/
void setName(TabPlayer player, String customName);
/**
* Changes player's suffix to provided value. Supports placeholders,
* as well as any supported RGB formats.
*
* @param player
* player to change prefix of
* @param suffix
* new suffix value
* @see #resetSuffix(TabPlayer)
* @see #getCustomSuffix(TabPlayer)
* @see #getOriginalSuffix(TabPlayer)
*/
void setSuffix(TabPlayer player, String suffix);
/**
* Resets player's prefix previously assigned using {@link #setPrefix(TabPlayer, String)}
* method. If nothing was set, this method will not do anything.
*
* @param player
* player to reset prefix of
* @see #setPrefix(TabPlayer, String)
* @see #getCustomPrefix(TabPlayer)
* @see #getOriginalPrefix(TabPlayer)
*/
void resetPrefix(TabPlayer player);
/**
* Resets player's custom name previously assigned using {@link #setName(TabPlayer, String)}
* method. If nothing was set, this method will not do anything.
*
* @param player
* player to reset name of
* @see #setName(TabPlayer, String)
* @see #getCustomName(TabPlayer)
* @see #getOriginalName(TabPlayer)
*/
void resetName(TabPlayer player);
/**
* Resets player's suffix previously assigned using {@link #setSuffix(TabPlayer, String)}
* method. If nothing was set, this method will not do anything.
*
* @param player
* player to reset suffix of
* @see #setSuffix(TabPlayer, String)
* @see #getCustomSuffix(TabPlayer)
* @see #getOriginalSuffix(TabPlayer)
*/
void resetSuffix(TabPlayer player);
/**
* Returns prefix assigned using {@link #setPrefix(TabPlayer, String)} method.
* If the method was not used, {@code null} is returned.
*
* @param player
* player to get custom prefix of
* @return player's custom prefix or {@code null} if not defined
* @see #setPrefix(TabPlayer, String)
* @see #resetPrefix(TabPlayer)
* @see #getOriginalPrefix(TabPlayer)
*/
String getCustomPrefix(TabPlayer player);
/**
* Returns custom name assigned using {@link #setName(TabPlayer, String)} method.
* If the method was not used, {@code null} is returned.
*
* @param player
* player to get custom name of
* @return player's custom name or {@code null} if not defined
* @see #setName(TabPlayer, String)
* @see #resetName(TabPlayer)
* @see #getOriginalName(TabPlayer)
*/
String getCustomName(TabPlayer player);
/**
* Returns suffix assigned using {@link #setSuffix(TabPlayer, String)} method.
* If the method was not used, {@code null} is returned.
*
* @param player
* player to get custom suffix of
* @return player's custom suffix or {@code null} if not defined
* @see #setSuffix(TabPlayer, String)
* @see #resetSuffix(TabPlayer)
* @see #getOriginalSuffix(TabPlayer)
*/
String getCustomSuffix(TabPlayer player);
/**
* Returns player's original prefix applied using plugin's internal logic.
* This value is not affected by API calls in any way.
*
* @param player
* player to get original prefix of
* @return player's original prefix
* @see #setPrefix(TabPlayer, String)
* @see #resetPrefix(TabPlayer)
* @see #getCustomPrefix(TabPlayer)
*/
String getOriginalPrefix(TabPlayer player);
/**
* Returns player's original name applied using plugin's internal logic.
* This value is not affected by API calls in any way.
*
* @param player
* player to get original name of
* @return player's original name
* @see #setName(TabPlayer, String)
* @see #resetName(TabPlayer)
* @see #getCustomName(TabPlayer)
*/
String getOriginalName(TabPlayer player);
/**
* Returns player's original suffix applied using plugin's internal logic.
* This value is not affected by API calls in any way.
*
* @param player
* player to get original suffix of
* @return player's original suffix
* @see #setSuffix(TabPlayer, String)
* @see #resetSuffix(TabPlayer)
* @see #getCustomSuffix(TabPlayer)
*/
String getOriginalSuffix(TabPlayer player);
}
@@ -6,11 +6,11 @@ package me.neznamy.tab.api.bossbar;
*/
public enum BarColor {
PINK,
BLUE,
RED,
GREEN,
YELLOW,
PURPLE,
WHITE
PINK,
BLUE,
RED,
GREEN,
YELLOW,
PURPLE,
WHITE
}
@@ -7,30 +7,30 @@ package me.neznamy.tab.api.bossbar;
*/
public enum BarStyle {
PROGRESS("SOLID"),
NOTCHED_6("SEGMENTED_6"),
NOTCHED_10("SEGMENTED_10"),
NOTCHED_12("SEGMENTED_12"),
NOTCHED_20("SEGMENTED_20");
PROGRESS("SOLID"),
NOTCHED_6("SEGMENTED_6"),
NOTCHED_10("SEGMENTED_10"),
NOTCHED_12("SEGMENTED_12"),
NOTCHED_20("SEGMENTED_20");
/** Style's name used in Bukkit API */
private final String bukkitName;
/** Style's name used in Bukkit API */
private final String bukkitName;
/**
* Initializes enum constant with given bukkit name
*
* @param bukkitName
* Name of color to be returned in {@code getBukkitName()}
*/
BarStyle(String bukkitName){
this.bukkitName = bukkitName;
}
/**
* Initializes enum constant with given bukkit name
*
* @param bukkitName
* Name of color to be returned in {@code getBukkitName()}
*/
BarStyle(String bukkitName){
this.bukkitName = bukkitName;
}
/**
* Returns name of this style in Bukkit API
* @return name of this style in Bukkit API
*/
public String getBukkitName() {
return bukkitName;
}
/**
* Returns name of this style in Bukkit API
* @return name of this style in Bukkit API
*/
public String getBukkitName() {
return bukkitName;
}
}
@@ -19,167 +19,192 @@ import me.neznamy.tab.api.TabPlayer;
*/
public interface BossBar {
/**
* Returns name of this BossBar. If it was defined in config, returns
* the name specified in config. If it was made using the API, returns
* randomly generated ID given to this BossBar when creating.
* @return name of BossBar
*/
String getName();
/**
* Returns name of this BossBar. If it was defined in config, returns
* the name specified in config. If it was made using the API, returns
* randomly generated ID given to this BossBar when creating.
*
* @return name of BossBar
*/
String getName();
/**
* Returns randomly generated uuid of this BossBar used internally to match
* the BossBar in client with packets or with Bukkit API BossBar instances.
* @return uuid of this BossBar
*/
UUID getUniqueId();
/**
* Returns randomly generated uuid of this BossBar used internally to match
* the BossBar in client with packets or with Bukkit API BossBar instances.
*
* @return uuid of this BossBar
*/
UUID getUniqueId();
/**
* Changes BossBar title to specified string, supporting placeholders.
* <p>
* If title contains new placeholders not used before, they are registered using
* standard registration logic and refreshed periodically. No need to call
* this method to try to keep placeholder values up to date.
* <p>
* If specified title is equal to the current one, nothing happens.
*
* @param title
* New title to display in this BossBar
*/
void setTitle(String title);
/**
* Changes BossBar title to specified string, supporting placeholders.
* <p>
* If title contains new placeholders not used before, they are registered using
* standard registration logic and refreshed periodically. No need to call
* this method to try to keep placeholder values up to date.
* <p>
* If specified title is equal to the current one, nothing happens.
*
* @param title
* New title to display in this BossBar
*/
void setTitle(String title);
/**
* Changes BossBar progress to specified string, supporting placeholders.
* The string must be a string version of a float value, or a placeholder that
* outputs a float value with range 0-100.
* <p>
* If progress contains new placeholders not used before, they are registered using
* standard registration logic and refreshed periodically. No need to call
* this method to try to keep placeholder values up to date.
* <p>
* If specified progress is equal to the current one, nothing happens.
*
* @param progress
* New progress to use in this BossBar
*/
void setProgress(String progress);
/**
* Changes BossBar progress to specified string, supporting placeholders.
* The string must be a string version of a float value, or a placeholder that
* outputs a float value with range 0-100.
* <p>
* If progress contains new placeholders not used before, they are registered using
* standard registration logic and refreshed periodically. No need to call
* this method to try to keep placeholder values up to date.
* <p>
* If specified progress is equal to the current one, nothing happens.
*
* @param progress
* New progress to use in this BossBar
*/
void setProgress(String progress);
/**
* Changes BossBar progress to specified value ranging from 0 to 100.
* <p>
* If specified progress is equal to the current one, nothing happens.
* @param progress
* New progress to use in this BossBar
*/
void setProgress(float progress);
/**
* Changes BossBar progress to specified value ranging from 0 to 100.
* <p>
* If specified progress is equal to the current one, nothing happens.
*
* @param progress
* New progress to use in this BossBar
*/
void setProgress(float progress);
/**
* Changes BossBar color to specified string, supporting placeholders.
* The string must be a string version of one of the supported values,
* or a placeholder that outputs one of them.
* <p>
* If color contains new placeholders not used before, they are registered using
* standard registration logic and refreshed periodically. No need to call
* this method to try to keep placeholder values up to date.
* <p>
* If specified color is equal to the current one, nothing happens.
*
* @param color
* New color to use in this BossBar
*/
void setColor(String color);
/**
* Changes BossBar color to specified string, supporting placeholders.
* The string must be a string version of one of the supported values,
* or a placeholder that outputs one of them.
* <p>
* If color contains new placeholders not used before, they are registered using
* standard registration logic and refreshed periodically. No need to call
* this method to try to keep placeholder values up to date.
* <p>
* If specified color is equal to the current one, nothing happens.
*
* @param color
* New color to use in this BossBar
*/
void setColor(String color);
/**
* Changes BossBar color to specified enum constant.
* <p>
* If specified color is equal to the current one, nothing happens.
*
* @param color
* New color to use in this BossBar
*/
void setColor(BarColor color);
/**
* Changes BossBar color to specified enum constant.
* <p>
* If specified color is equal to the current one, nothing happens.
*
* @param color
* New color to use in this BossBar
*/
void setColor(BarColor color);
/**
* Changes BossBar style to specified string, supporting placeholders.
* The string must be a string version of one of the supported values,
* or a placeholder that outputs one of them.
* <p>
* If style contains new placeholders not used before, they are registered using
* standard registration logic and refreshed periodically. No need to call
* this method to try to keep placeholder values up to date.
* <p>
* If specified style is equal to the current one, nothing happens.
*
* @param style
* New style to use in this BossBar
*/
void setStyle(String style);
/**
* Changes BossBar style to specified string, supporting placeholders.
* The string must be a string version of one of the supported values,
* or a placeholder that outputs one of them.
* <p>
* If style contains new placeholders not used before, they are registered using
* standard registration logic and refreshed periodically. No need to call
* this method to try to keep placeholder values up to date.
* <p>
* If specified style is equal to the current one, nothing happens.
*
* @param style
* New style to use in this BossBar
*/
void setStyle(String style);
/**
* Changes BossBar style to specified enum constant.
* <p>
* If specified style is equal to the current one, nothing happens.
*
* @param style
* New style to use in this BossBar
*/
void setStyle(BarStyle style);
/**
* Changes BossBar style to specified enum constant.
* <p>
* If specified style is equal to the current one, nothing happens.
*
* @param style
* New style to use in this BossBar
*/
void setStyle(BarStyle style);
/**
* Returns current title of the BossBar in raw format. If it contains placeholders,
* their raw identifiers are used in the result.
* @return title of the BossBar
*/
String getTitle();
/**
* Returns current title of the BossBar in raw format. If it contains placeholders,
* their raw identifiers are used in the result.
*
* @return title of the BossBar
*/
String getTitle();
/**
* Returns progress of the BossBar as a string, which is either entered string
* containing placeholders or entered number converted to string
* @return entered progress as a string
*/
String getProgress();
/**
* Returns progress of the BossBar as a string, which is either entered string
* containing placeholders or entered number converted to string
*
* @return entered progress as a string
*/
String getProgress();
/**
* Returns color of the BossBar as a string, which is either entered string
* containing placeholders or entered enum value converted to string
* @return entered color as a string
*/
String getColor();
/**
* Returns color of the BossBar as a string, which is either entered string
* containing placeholders or entered enum value converted to string
*
* @return entered color as a string
*/
String getColor();
/**
* Returns style of the BossBar as a string, which is either entered string
* containing placeholders or entered enum value converted to string
* @return entered style as a string
*/
String getStyle();
/**
* Returns style of the BossBar as a string, which is either entered string
* containing placeholders or entered enum value converted to string
*
* @return entered style as a string
*/
String getStyle();
/**
* Registers this BossBar to specified player.
* <p>
* If the player already sees this BossBar, nothing happens.
*
* @param player
* Player to register this BossBar to
*/
void addPlayer(TabPlayer player);
/**
* Registers this BossBar to specified player.
* <p>
* If the player already sees this BossBar, nothing happens.
*
* @param player
* Player to register this BossBar to
*/
void addPlayer(TabPlayer player);
/**
* Unregisters this BossBar from specified player.
* <p>
* If the player does not see this BossBar, nothing happens.
*
* @param player
* Player to unregister this BossBar from
*/
void removePlayer(TabPlayer player);
/**
* Unregisters this BossBar from specified player.
* <p>
* If the player does not see this BossBar, nothing happens.
*
* @param player
* Player to unregister this BossBar from
*/
void removePlayer(TabPlayer player);
/**
* Returns list of players who can see this BossBar.
* <p>
* The returned list can only be used to read. Writing
* will not work properly. For adding/removing players see {@link #addPlayer(TabPlayer)}
* and {@link #removePlayer(TabPlayer)}.
* @return List of players seeing this BossBar
*/
List<TabPlayer> getPlayers();
/**
* Returns list of players who can see this BossBar.
* <p>
* The returned list can only be used to read. Writing
* will not work properly. For adding/removing players see {@link #addPlayer(TabPlayer)}
* and {@link #removePlayer(TabPlayer)}.
*
* @return List of players seeing this BossBar
*/
List<TabPlayer> getPlayers();
/**
* Returns {@code true} if specified player can see this BossBar, {@code false} if not
*
* @param player
* Player to check for
* @return {@code true} if contains, {@code false} if not
*/
boolean containsPlayer(TabPlayer player);
/**
* Returns {@code true} if this bossbar in configured as announcement-only bar,
* {@code false} if not.
*
* @return {@code true} if bar is announcement only, {@code false} if not
*/
boolean isAnnouncementBar();
}
@@ -4,164 +4,162 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabPlayer;
/**
* An interface allowing work with BossBars such as creating, sending
* and toggling.
* <p>
* Instance can be obtained using {@link me.neznamy.tab.api.TabAPI#getBossBarManager()}.
* Instance can be obtained using {@link TabAPI#getBossBarManager()}.
* This requires the BossBar feature to be enabled in config, otherwise the method will
* return null.
* return {@code null}.
*/
public interface BossBarManager {
/**
* Creates BossBar with specified parameters and registers it to BossBar manager,
* allowing to retrieve it later.
*
* @param title
* Title to display, supports placeholders
*
* @param progress
* Progress to use. Must be between 0 and 100
*
* @param color
* Color to use
*
* @param style
* Style to use
* @return The newly created BossBar
*/
BossBar createBossBar(String title, float progress, BarColor color, BarStyle style);
/**
* Creates BossBar with specified parameters and registers it to BossBar manager,
* allowing to retrieve it later.
*
* @param title
* Title to display, supports placeholders
* @param progress
* Progress to use. Must be between 0 and 100
* @param color
* Color to use
* @param style
* Style to use
* @return The newly created BossBar
*/
BossBar createBossBar(String title, float progress, BarColor color, BarStyle style);
/**
* Creates BossBar with specified parameters and registers it to BossBar manager,
* allowing to retrieve it later.
*
* @param title
* Title to display, supports placeholders
*
* @param progress
* Progress to use. Must be a string version of a number, or
* a placeholder that returns a number
*
* @param color
* Color to use. Must be a string version of one of enum constants, or
* a placeholder that returns one of them
*
* @param style
* Style to use. Must be a string version of one of enum constants, or
* a placeholder that returns one of them
* @return The newly created BossBar
*/
BossBar createBossBar(String title, String progress, String color, String style);
/**
* Creates BossBar with specified parameters and registers it to BossBar manager,
* allowing to retrieve it later.
*
* @param title
* Title to display, supports placeholders
* @param progress
* Progress to use. Must be a string version of a number, or
* a placeholder that returns a number
* @param color
* Color to use. Must be a string version of one of enum constants, or
* a placeholder that returns one of them
* @param style
* Style to use. Must be a string version of one of enum constants, or
* a placeholder that returns one of them
* @return The newly created BossBar
*/
BossBar createBossBar(String title, String progress, String color, String style);
/**
* Returns registered BossBar with given name. For BossBars from config it is the name
* they were created with, for API BossBars their randomly generated name, which can be
* retrieved using {@link me.neznamy.tab.api.bossbar.BossBar#getName()}.
* Returns null if no such BossBar exists.
*
* @param name
* Name of registered BossBar
* @return BossBar with specified name
*/
BossBar getBossBar(String name);
/**
* Returns registered BossBar with given name. For BossBars from config it is the name
* they were created with, for API BossBars their randomly generated name, which can be
* retrieved using {@link me.neznamy.tab.api.bossbar.BossBar#getName()}.
* Returns null if no such BossBar exists.
*
* @param name
* Name of registered BossBar
* @return BossBar with specified name
*/
BossBar getBossBar(String name);
/**
* Returns registered BossBar with specified UUID. This UUID is randomly generated when
* BossBar is created and used in packets / to match which the one from Bukkit API.
* Returns null if no such BossBar exists.
*
* @param id
* UUID to return BossBar by
* @return BossBar with specified uuid
*/
BossBar getBossBar(UUID id);
/**
* Returns registered BossBar with specified UUID. This UUID is randomly generated when
* BossBar is created and used in packets / to match which the one from Bukkit API.
* Returns null if no such BossBar exists.
*
* @param id
* UUID to return BossBar by
* @return BossBar with specified uuid
*/
BossBar getBossBar(UUID id);
/**
* Returns a map of all registered BossBars. This includes both BossBars from config
* and those registered via API.
* <p>
* Map key is BossBar's name available in {@link me.neznamy.tab.api.bossbar.BossBar#getName()},
* map value is the BossBar interface.
* @return all registered BossBars
*/
Map<String, BossBar> getRegisteredBossBars();
/**
* Returns a map of all registered BossBars. This includes both BossBars from config
* and those registered via API.
* <p>
* Map key is BossBar's name available in {@link me.neznamy.tab.api.bossbar.BossBar#getName()},
* map value is the BossBar interface.
*
* @return all registered BossBars
*/
Map<String, BossBar> getRegisteredBossBars();
/**
* Toggles BossBar for this player and sends toggle message if {@code sendToggleMessage} is true.
*
* @param player
* Player to toggle BossBar for
*
* @param sendToggleMessage
* {@code true} if toggle message should be sent, {@code false} if not
* @see #hasBossBarVisible(TabPlayer)
* @see #setBossBarVisible(TabPlayer, boolean, boolean)
*/
void toggleBossBar(TabPlayer player, boolean sendToggleMessage);
/**
* Toggles BossBar for this player and sends toggle message if {@code sendToggleMessage} is true.
*
* @param player
* Player to toggle BossBar for
* @param sendToggleMessage
* {@code true} if toggle message should be sent, {@code false} if not
* @see #hasBossBarVisible(TabPlayer)
* @see #setBossBarVisible(TabPlayer, boolean, boolean)
*/
void toggleBossBar(TabPlayer player, boolean sendToggleMessage);
/**
* Returns true if player can see BossBars, false if toggled via command or API.
*
* @param player
* Player to check toggle status of
* @return {@code true} if visible, {@code false} if disabled (toggled)
* @see #toggleBossBar(TabPlayer, boolean)
* @see #setBossBarVisible(TabPlayer, boolean, boolean)
*/
boolean hasBossBarVisible(TabPlayer player);
/**
* Returns true if player can see BossBars, false if toggled via command or API.
*
* @param player
* Player to check toggle status of
* @return {@code true} if visible, {@code false} if disabled (toggled)
* @see #toggleBossBar(TabPlayer, boolean)
* @see #setBossBarVisible(TabPlayer, boolean, boolean)
*/
boolean hasBossBarVisible(TabPlayer player);
/**
* Sets BossBar visibility of player to set value. If value did not change, nothing happens.
* If visibility changed, toggle message is sent if {@code sendToggleMessage} is {@code true}.
*
* @param player
* Player to set BossBar visibility of
* @param visible
* {@code true} if BossBar should be visible, {@code false} if not
* @param sendToggleMessage
* {@code true} if toggle message should be sent if value changed, {@code false} if not
* @see #toggleBossBar(TabPlayer, boolean)
* @see #hasBossBarVisible(TabPlayer)
*/
void setBossBarVisible(TabPlayer player, boolean visible, boolean sendToggleMessage);
/**
* Sets BossBar visibility of player to set value. If value did not change, nothing happens.
* If visibility changed, toggle message is sent if {@code sendToggleMessage} is {@code true}.
*
* @param player
* Player to set BossBar visibility of
* @param visible
* {@code true} if BossBar should be visible, {@code false} if not
* @param sendToggleMessage
* {@code true} if toggle message should be sent if value changed, {@code false} if not
* @see #toggleBossBar(TabPlayer, boolean)
* @see #hasBossBarVisible(TabPlayer)
*/
void setBossBarVisible(TabPlayer player, boolean visible, boolean sendToggleMessage);
/**
* Temporarily displays registered BossBar to player for specified amount of time in milliseconds.
* @param player
* Player to show BossBar to
* @param bossBar
* Name of registered BossBar to show
* @param duration
* In milliseconds for how long should BossBar be displayed
* @throws IllegalArgumentException
* if no BossBar with specified name exists
* @see #announceBossBar(String, int)
* @see #getAnnouncedBossBars()
*/
void sendBossBarTemporarily(TabPlayer player, String bossBar, int duration);
/**
* Temporarily displays registered BossBar to player for specified amount of time in milliseconds.
*
* @param player
* Player to show BossBar to
* @param bossBar
* Name of registered BossBar to show
* @param duration
* In milliseconds for how long should BossBar be displayed
* @throws IllegalArgumentException
* if no BossBar with specified name exists
* @see #announceBossBar(String, int)
* @see #getAnnouncedBossBars()
*/
void sendBossBarTemporarily(TabPlayer player, String bossBar, int duration);
/**
* Temporarily displays registered BossBar to all players for specified amount of time in milliseconds.
* @param bossBar
* Name of registered BossBar to show
* @param duration
* In milliseconds for how long should BossBar be displayed
* @throws IllegalArgumentException
* if no BossBar with specified name exists
* @see #sendBossBarTemporarily(TabPlayer, String, int)
* @see #getAnnouncedBossBars()
*/
void announceBossBar(String bossBar, int duration);
/**
* Temporarily displays registered BossBar to all players for specified amount of time in milliseconds.
*
* @param bossBar
* Name of registered BossBar to show
* @param duration
* In milliseconds for how long should BossBar be displayed
* @throws IllegalArgumentException
* if no BossBar with specified name exists
* @see #sendBossBarTemporarily(TabPlayer, String, int)
* @see #getAnnouncedBossBars()
*/
void announceBossBar(String bossBar, int duration);
/**
* Returns list of BossBars which are currently being announced.
* @return list of currently active BossBar announcements
* @see #sendBossBarTemporarily(TabPlayer, String, int)
* @see #announceBossBar(String, int)
*/
List<BossBar> getAnnouncedBossBars();
/**
* Returns list of BossBars which are currently being announced.
*
* @return list of currently active BossBar announcements
* @see #sendBossBarTemporarily(TabPlayer, String, int)
* @see #announceBossBar(String, int)
*/
List<BossBar> getAnnouncedBossBars();
}
@@ -7,53 +7,56 @@ import me.neznamy.tab.api.util.Preconditions;
*/
public class ChatClickable {
/** Click action */
private final EnumClickAction action;
/** Click action */
private final EnumClickAction action;
/** Click value */
private final String value;
/** Click value */
private final String value;
/**
* Constructs new instance with given action and value.
* @param action
* click event action
* @param value
* click event value
* @throws IllegalArgumentException
* if {@code action} is null or {@code value} is null
*/
public ChatClickable(EnumClickAction action, String value) {
Preconditions.checkNotNull(action, "hover action cannot be null");
Preconditions.checkNotNull(value, "hover value cannot be null");
this.action = action;
this.value = value;
}
/**
* Constructs new instance with given action and value.
*
* @param action
* click event action
* @param value
* click event value
* @throws IllegalArgumentException
* if {@code action} is null or {@code value} is null
*/
public ChatClickable(EnumClickAction action, String value) {
Preconditions.checkNotNull(action, "hover action");
Preconditions.checkNotNull(value, "hover value");
this.action = action;
this.value = value;
}
/**
* Returns click action defined in constructor
* @return click action
*/
public EnumClickAction getAction() {
return action;
}
/**
* Returns click action defined in constructor
*
* @return click action
*/
public EnumClickAction getAction() {
return action;
}
/**
* Returns click action defined in constructor
* @return click action
*/
public String getValue() {
return value;
}
/**
* Returns click action defined in constructor
*
* @return click action
*/
public String getValue() {
return value;
}
/**
* Enum for all possible click actions
*/
public enum EnumClickAction {
OPEN_URL,
RUN_COMMAND,
CHANGE_PAGE, //since 1.8
SUGGEST_COMMAND,
COPY_TO_CLIPBOARD //since 1.15
}
/**
* Enum for all possible click actions
*/
public enum EnumClickAction {
OPEN_URL,
RUN_COMMAND,
CHANGE_PAGE, //since 1.8
SUGGEST_COMMAND,
COPY_TO_CLIPBOARD //since 1.15
}
}
@@ -2,37 +2,36 @@ package me.neznamy.tab.api.chat;
import java.util.UUID;
import com.google.gson.JsonObject;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.util.Preconditions;
public class ChatComponentEntity extends IChatBaseComponent {
private final String type;
private final UUID id;
private final String name;
public ChatComponentEntity(String type, UUID id, String name) {
this.type = type;
this.id = id;
this.name = name;
}
private final String CONTENTS;
private final String VALUE_1_13;
private final String VALUE_1_12;
private final String VALUE_1_8;
@Override
public String toRawText() {
return toString();
}
@Override
public String toString() {
return String.format("{\"type\":\"%s\",\"id\":\"%s\",\"name\":{\"text\":\"%s\"}}", type, id, name);
}
public JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("type", type);
json.addProperty("id", id.toString());
JsonObject json2 = new JsonObject();
json2.addProperty("text", name);
json.add("name", json2);
return json;
}
public ChatComponentEntity(String type, UUID id, String name) {
Preconditions.checkNotNull(type, "type");
Preconditions.checkNotNull(id, "id");
this.CONTENTS = String.format("{\"type\":\"%s\",\"id\":\"%s\",\"name\":{\"text\":\"%s\"}}", type, id, name);
this.VALUE_1_13 = String.format("{type:\"%s\",id:\"%s\",name:\"{\\\"text\\\":\\\"%s\\\"}\"}", type, id, name);
this.VALUE_1_12 = String.format("{type:\"%s\",id:\"%s\",name:\"%s\"}", type, id, name);
this.VALUE_1_8 = String.format("{type:%s,id:%s,name:%s}", type, id, name);
}
@Override
public String toRawText() {
return toString();
}
@Override
public String toString() {
if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 16) return CONTENTS;
if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 13) return VALUE_1_13;
if (TabAPI.getInstance().getServerVersion().getMinorVersion() == 12) return VALUE_1_12;
if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 8) return VALUE_1_8;
throw new IllegalStateException("show_entity hover action is not supported on <1.8");
}
}
@@ -7,51 +7,64 @@ import me.neznamy.tab.api.util.Preconditions;
*/
public class ChatHoverable {
/** Hover action */
private final EnumHoverAction action;
/** Hover action */
private final EnumHoverAction action;
/** Hover value */
private final IChatBaseComponent value;
/** Hover value */
private final IChatBaseComponent value;
/**
* Constructs new instance with given action and value.
* @param action
* hover event action
* @param value
* hover event value
* @throws IllegalArgumentException
* if {@code action} is null or {@code value} is null
*/
public ChatHoverable(EnumHoverAction action, IChatBaseComponent value) {
Preconditions.checkNotNull(action, "hover action cannot be null");
Preconditions.checkNotNull(value, "hover value cannot be null");
this.action = action;
this.value = value;
}
/**
* Constructs new instance with given action and value.
*
* @param action
* hover event action
* @param value
* hover event value
* @throws IllegalArgumentException
* if {@code action} is null or {@code value} is null
*/
public ChatHoverable(EnumHoverAction action, IChatBaseComponent value) {
Preconditions.checkNotNull(action, "hover action");
Preconditions.checkNotNull(value, "hover value");
this.action = action;
this.value = value;
}
/**
* Returns hover action defined in constructor
* @return hover action
*/
public EnumHoverAction getAction() {
return action;
}
/**
* Returns hover action defined in constructor
*
* @return hover action
*/
public EnumHoverAction getAction() {
return action;
}
/**
* Returns hover value defined in constructor
* @return hover value
*/
public IChatBaseComponent getValue() {
return value;
}
/**
* Returns hover value defined in constructor
*
* @return hover value
*/
public IChatBaseComponent getValue() {
return value;
}
/**
* Enum for all possible hover actions
*/
public enum EnumHoverAction {
/**
* Enum for all possible hover actions
*/
public enum EnumHoverAction {
SHOW_TEXT,
SHOW_ITEM,
SHOW_ENTITY
}
SHOW_TEXT("contents"),
SHOW_ITEM("value"),
SHOW_ENTITY("contents");
private final String preferredKey;
EnumHoverAction(String preferredKey) {
this.preferredKey = preferredKey;
}
public String getPreferredKey() {
return preferredKey;
}
}
}
@@ -2,6 +2,7 @@ package me.neznamy.tab.api.chat;
import java.util.UUID;
import me.neznamy.tab.api.util.Preconditions;
import org.json.simple.JSONObject;
import me.neznamy.tab.api.ProtocolVersion;
@@ -11,279 +12,305 @@ import me.neznamy.tab.api.chat.ChatHoverable.EnumHoverAction;
public class ChatModifier {
private TextColor color;
private TextColor color;
private Boolean bold;
private Boolean italic;
private Boolean underlined;
private Boolean strikethrough;
private Boolean obfuscated;
private Boolean bold;
private Boolean italic;
private Boolean underlined;
private Boolean strikethrough;
private Boolean obfuscated;
private ChatClickable clickEvent;
private ChatHoverable hoverEvent;
private String font;
private ProtocolVersion targetVersion;
public ChatModifier() {
}
public ChatModifier(ChatModifier modifier) {
this.color = modifier.color == null ? null : new TextColor(modifier.color);
this.bold = modifier.bold;
this.italic = modifier.italic;
this.underlined = modifier.underlined;
this.strikethrough = modifier.strikethrough;
this.obfuscated = modifier.obfuscated;
this.clickEvent = modifier.clickEvent == null ? null : new ChatClickable(modifier.clickEvent.getAction(), modifier.clickEvent.getValue());
this.hoverEvent = modifier.hoverEvent == null ? null : new ChatHoverable(modifier.hoverEvent.getAction(), modifier.hoverEvent.getValue());
this.font = modifier.font;
this.targetVersion = modifier.targetVersion;
}
private ChatClickable clickEvent;
private ChatHoverable hoverEvent;
public TextColor getColor() {
return color;
}
public void setColor(TextColor color) {
this.color = color;
}
private String font;
public Boolean getBold() {
return bold;
}
private ProtocolVersion targetVersion;
public void setBold(Boolean bold) {
this.bold = bold;
}
public ChatModifier() {
}
public Boolean getItalic() {
return italic;
}
public ChatModifier(ChatModifier modifier) {
Preconditions.checkNotNull(modifier, "modifier");
this.color = modifier.color == null ? null : new TextColor(modifier.color);
this.bold = modifier.bold;
this.italic = modifier.italic;
this.underlined = modifier.underlined;
this.strikethrough = modifier.strikethrough;
this.obfuscated = modifier.obfuscated;
this.clickEvent = modifier.clickEvent == null ? null : new ChatClickable(modifier.clickEvent.getAction(), modifier.clickEvent.getValue());
this.hoverEvent = modifier.hoverEvent == null ? null : new ChatHoverable(modifier.hoverEvent.getAction(), modifier.hoverEvent.getValue());
this.font = modifier.font;
this.targetVersion = modifier.targetVersion;
}
public void setItalic(Boolean italic) {
this.italic = italic;
}
public TextColor getColor() {
return color;
}
public Boolean getUnderlined() {
return underlined;
}
public void setColor(TextColor color) {
this.color = color;
}
public void setUnderlined(Boolean underlined) {
this.underlined = underlined;
}
public Boolean getBold() {
return bold;
}
public Boolean getStrikethrough() {
return strikethrough;
}
public void setBold(Boolean bold) {
this.bold = bold;
}
public void setStrikethrough(Boolean strikethrough) {
this.strikethrough = strikethrough;
}
public Boolean getItalic() {
return italic;
}
public Boolean getObfuscated() {
return obfuscated;
}
public void setItalic(Boolean italic) {
this.italic = italic;
}
public void setObfuscated(Boolean obfuscated) {
this.obfuscated = obfuscated;
}
/**
* Returns true if bold is defined and set to true, false otherwise
* @return true if bold is defined and set to true, false otherwise
*/
public boolean isBold(){
return Boolean.TRUE.equals(bold);
}
public Boolean getUnderlined() {
return underlined;
}
/**
* Returns true if italic is defined and set to true, false otherwise
* @return true if italic is defined and set to true, false otherwise
*/
public boolean isItalic(){
return Boolean.TRUE.equals(italic);
}
public void setUnderlined(Boolean underlined) {
this.underlined = underlined;
}
/**
* Returns true if underlined is defined and set to true, false otherwise
* @return true if underlined is defined and set to true, false otherwise
*/
public boolean isUnderlined(){
return Boolean.TRUE.equals(underlined);
}
public Boolean getStrikethrough() {
return strikethrough;
}
/**
* Returns true if strikethrough is defined and set to true, false otherwise
* @return true if strikethrough is defined and set to true, false otherwise
*/
public boolean isStrikethrough(){
return Boolean.TRUE.equals(strikethrough);
}
public void setStrikethrough(Boolean strikethrough) {
this.strikethrough = strikethrough;
}
/**
* Returns true if obfuscation is defined and set to true, false otherwise
* @return true if obfuscation is defined and set to true, false otherwise
*/
public boolean isObfuscated(){
return Boolean.TRUE.equals(obfuscated);
}
public Boolean getObfuscated() {
return obfuscated;
}
public ChatClickable getClickEvent() {
return clickEvent;
}
public void setObfuscated(Boolean obfuscated) {
this.obfuscated = obfuscated;
}
public void setClickEvent(ChatClickable clickEvent) {
this.clickEvent = clickEvent;
}
/**
* Returns true if bold is defined and set to true, false otherwise
*
* @return true if bold is defined and set to true, false otherwise
*/
public boolean isBold(){
return Boolean.TRUE.equals(bold);
}
public ChatHoverable getHoverEvent() {
return hoverEvent;
}
/**
* Returns true if italic is defined and set to true, false otherwise
*
* @return true if italic is defined and set to true, false otherwise
*/
public boolean isItalic(){
return Boolean.TRUE.equals(italic);
}
public void setHoverEvent(ChatHoverable hoverEvent) {
this.hoverEvent = hoverEvent;
}
/**
* Sets click action to OPEN_URL and url to given value
* @param url - url to open
*/
public void onClickOpenUrl(String url) {
clickEvent = new ChatClickable(EnumClickAction.OPEN_URL, url);
}
/**
* Returns true if underlined is defined and set to true, false otherwise
*
* @return true if underlined is defined and set to true, false otherwise
*/
public boolean isUnderlined(){
return Boolean.TRUE.equals(underlined);
}
/**
* Sets click action to RUN_COMMAND and command to given value
* @param command - command to perform, might be without / to send a chat message
*/
public void onClickRunCommand(String command) {
clickEvent = new ChatClickable(EnumClickAction.RUN_COMMAND, command);
}
/**
* Returns true if strikethrough is defined and set to true, false otherwise
*
* @return true if strikethrough is defined and set to true, false otherwise
*/
public boolean isStrikethrough(){
return Boolean.TRUE.equals(strikethrough);
}
/**
* Sets click action to SUGGEST_COMMAND and command to given value
* @param command - command to suggest
*/
public void onClickSuggestCommand(String command) {
clickEvent = new ChatClickable(EnumClickAction.SUGGEST_COMMAND, command);
}
/**
* Returns true if obfuscation is defined and set to true, false otherwise
*
* @return true if obfuscation is defined and set to true, false otherwise
*/
public boolean isObfuscated(){
return Boolean.TRUE.equals(obfuscated);
}
/**
* Sets click action to CHANGE_PAGE and page id to given value
* @param newPage - id of new page
*/
public void onClickChangePage(int newPage) {
if (TabAPI.getInstance().getServerVersion().getMinorVersion() < 8) throw new UnsupportedOperationException("change_page click action is not supported on <1.8");
clickEvent = new ChatClickable(EnumClickAction.CHANGE_PAGE, String.valueOf(newPage));
}
public ChatClickable getClickEvent() {
return clickEvent;
}
/**
* Sets click action to COPY_TO_CLIPBOARD and text to provided value
* @param text - text to copy to clipboard on click
*/
public void onClickCopyToClipBoard(String text) {
clickEvent = new ChatClickable(EnumClickAction.COPY_TO_CLIPBOARD, text);
}
public void onClick(EnumClickAction action, String value) {
clickEvent = new ChatClickable(action, value);
}
public void setClickEvent(ChatClickable clickEvent) {
this.clickEvent = clickEvent;
}
/**
* Sets hover action to SHOW_TEXT and text to given value
* @param text - text to show
*/
public void onHoverShowText(IChatBaseComponent text) {
hoverEvent = new ChatHoverable(EnumHoverAction.SHOW_TEXT, text);
}
public ChatHoverable getHoverEvent() {
return hoverEvent;
}
/**
* Sets hover action to SHOW_ITEM and item to given value
* @param serializedItem - item to show
*/
public void onHoverShowItem(String serializedItem) {
hoverEvent = new ChatHoverable(EnumHoverAction.SHOW_ITEM, new IChatBaseComponent(serializedItem));
}
public void setHoverEvent(ChatHoverable hoverEvent) {
this.hoverEvent = hoverEvent;
}
/**
* Sets hover action to SHOW_ENTITY and entity data to given values
* @param type - entity type
* @param id - entity uuid
* @param name - entity custom name
*/
public void onHoverShowEntity(String type, UUID id, String name) {
if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 16) {
hoverEvent = new ChatHoverable(EnumHoverAction.SHOW_ENTITY, new ChatComponentEntity(type, id, name));
} else if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 13) {
hoverEvent = new ChatHoverable(EnumHoverAction.SHOW_ENTITY, new IChatBaseComponent(String.format("{type:\"%s\",id:\"%s\",name:\"{\\\"text\\\":\\\"%s\\\"}\"}", type, id, name)));
} else if (TabAPI.getInstance().getServerVersion().getMinorVersion() == 12) {
hoverEvent = new ChatHoverable(EnumHoverAction.SHOW_ENTITY, new IChatBaseComponent(String.format("{type:\"%s\",id:\"%s\",name:\"%s\"}", type, id, name)));
} else if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 8) {
hoverEvent = new ChatHoverable(EnumHoverAction.SHOW_ENTITY, new IChatBaseComponent(String.format("{type:%s,id:%s,name:%s}", type, id, name)));
} else {
throw new IllegalStateException("show_entity hover action is not supported on <1.8");
}
}
public void onHover(EnumHoverAction action, IChatBaseComponent value) {
hoverEvent = new ChatHoverable(action, value);
}
/**
* Sets click action to OPEN_URL and url to given value
*
* @param url
* url to open
*/
public void onClickOpenUrl(String url) {
Preconditions.checkNotNull(url, "url");
clickEvent = new ChatClickable(EnumClickAction.OPEN_URL, url);
}
public String getFont() {
return font;
}
/**
* Sets click action to RUN_COMMAND and command to given value
*
* @param command
* command to perform, might be without / to send a chat message
*/
public void onClickRunCommand(String command) {
Preconditions.checkNotNull(command, "command");
clickEvent = new ChatClickable(EnumClickAction.RUN_COMMAND, command);
}
public void setFont(String font) {
this.font = font;
}
@SuppressWarnings("unchecked")
public JSONObject serialize() {
JSONObject json = new JSONObject();
if (color != null) json.put("color", targetVersion.getMinorVersion() >= 16 ? color.toString() : color.getLegacyColor().toString().toLowerCase());
if (bold != null) json.put("bold", bold);
if (italic != null) json.put("italic", italic);
if (underlined != null) json.put("underlined", underlined);
if (strikethrough != null) json.put("strikethrough", strikethrough);
if (obfuscated != null) json.put("obfuscated", obfuscated);
if (clickEvent != null) {
JSONObject click = new JSONObject();
click.put("action", clickEvent.getAction().toString().toLowerCase());
click.put("value", clickEvent.getValue());
json.put("clickEvent", click);
}
if (hoverEvent != null) {
JSONObject hover = new JSONObject();
hover.put("action", hoverEvent.getAction().toString().toLowerCase());
if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 16) {
hover.put("contents", hoverEvent.getValue());
} else {
hover.put("value", TabAPI.getInstance().getServerVersion().getMinorVersion() >= 9 ?
hoverEvent.getValue() : hoverEvent.getValue().toRawText());
}
json.put("hoverEvent", hover);
}
if (font != null) json.put("font", font);
return json;
}
public String getMagicCodes() {
StringBuilder builder = new StringBuilder();
if (isBold()) builder.append(EnumChatFormat.BOLD.getFormat());
if (isItalic()) builder.append(EnumChatFormat.ITALIC.getFormat());
if (isUnderlined()) builder.append(EnumChatFormat.UNDERLINE.getFormat());
if (isStrikethrough()) builder.append(EnumChatFormat.STRIKETHROUGH.getFormat());
if (isObfuscated()) builder.append(EnumChatFormat.OBFUSCATED.getFormat());
return builder.toString();
}
public void setTargetVersion(ProtocolVersion targetVersion) {
this.targetVersion = targetVersion;
}
public ProtocolVersion getTargetVersion() {
return targetVersion;
}
/**
* Sets click action to SUGGEST_COMMAND and command to given value
*
* @param command
* command to suggest
*/
public void onClickSuggestCommand(String command) {
Preconditions.checkNotNull(command, "command");
clickEvent = new ChatClickable(EnumClickAction.SUGGEST_COMMAND, command);
}
/**
* Sets click action to CHANGE_PAGE and page id to given value
*
* @param newPage
* id of new page
*/
public void onClickChangePage(int newPage) {
if (TabAPI.getInstance().getServerVersion().getMinorVersion() < 8) throw new UnsupportedOperationException("change_page click action is not supported on <1.8");
clickEvent = new ChatClickable(EnumClickAction.CHANGE_PAGE, String.valueOf(newPage));
}
/**
* Sets click action to COPY_TO_CLIPBOARD and text to provided value
*
* @param text
* text to copy to clipboard on click
*/
public void onClickCopyToClipBoard(String text) {
Preconditions.checkNotNull(text, "text");
clickEvent = new ChatClickable(EnumClickAction.COPY_TO_CLIPBOARD, text);
}
public void onClick(EnumClickAction action, String value) {
Preconditions.checkNotNull(action, "action");
Preconditions.checkNotNull(value, "value");
clickEvent = new ChatClickable(action, value);
}
/**
* Sets hover action to SHOW_TEXT and text to given value
*
* @param text
* text to show
*/
public void onHoverShowText(IChatBaseComponent text) {
Preconditions.checkNotNull(text, "text");
hoverEvent = new ChatHoverable(EnumHoverAction.SHOW_TEXT, text);
}
/**
* Sets hover action to SHOW_ITEM and item to given value
*
* @param serializedItem
* item to show
*/
public void onHoverShowItem(String serializedItem) {
Preconditions.checkNotNull(serializedItem, "item");
hoverEvent = new ChatHoverable(EnumHoverAction.SHOW_ITEM, new IChatBaseComponent(serializedItem));
}
/**
* Sets hover action to SHOW_ENTITY and entity data to given values
*
* @param type
* entity type
* @param id
* entity uuid
* @param name
* entity custom name
*/
public void onHoverShowEntity(String type, UUID id, String name) {
Preconditions.checkNotNull(type, "type");
Preconditions.checkNotNull(id, "id");
hoverEvent = new ChatHoverable(EnumHoverAction.SHOW_ENTITY, new ChatComponentEntity(type, id, name));
}
public void onHover(EnumHoverAction action, IChatBaseComponent value) {
Preconditions.checkNotNull(action, "action");
Preconditions.checkNotNull(value, "value");
hoverEvent = new ChatHoverable(action, value);
}
public String getFont() {
return font;
}
public void setFont(String font) {
this.font = font;
}
@SuppressWarnings("unchecked")
public JSONObject serialize() {
JSONObject json = new JSONObject();
if (color != null) json.put("color", targetVersion.getMinorVersion() >= 16 ? color.toString() : color.getLegacyColor().toString().toLowerCase());
if (bold != null) json.put("bold", bold);
if (italic != null) json.put("italic", italic);
if (underlined != null) json.put("underlined", underlined);
if (strikethrough != null) json.put("strikethrough", strikethrough);
if (obfuscated != null) json.put("obfuscated", obfuscated);
if (clickEvent != null) {
JSONObject click = new JSONObject();
click.put("action", clickEvent.getAction().toString().toLowerCase());
click.put("value", clickEvent.getValue());
json.put("clickEvent", click);
}
if (hoverEvent != null) {
JSONObject hover = new JSONObject();
hover.put("action", hoverEvent.getAction().toString().toLowerCase());
if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 16) {
hover.put(hoverEvent.getAction().getPreferredKey(), hoverEvent.getValue());
} else {
hover.put("value", TabAPI.getInstance().getServerVersion().getMinorVersion() >= 9 ?
hoverEvent.getValue() : hoverEvent.getValue().toRawText());
}
json.put("hoverEvent", hover);
}
if (font != null) json.put("font", font);
return json;
}
public String getMagicCodes() {
StringBuilder builder = new StringBuilder();
if (isBold()) builder.append(EnumChatFormat.BOLD.getFormat());
if (isItalic()) builder.append(EnumChatFormat.ITALIC.getFormat());
if (isUnderlined()) builder.append(EnumChatFormat.UNDERLINE.getFormat());
if (isStrikethrough()) builder.append(EnumChatFormat.STRIKETHROUGH.getFormat());
if (isObfuscated()) builder.append(EnumChatFormat.OBFUSCATED.getFormat());
return builder.toString();
}
public void setTargetVersion(ProtocolVersion targetVersion) {
this.targetVersion = targetVersion;
}
public ProtocolVersion getTargetVersion() {
return targetVersion;
}
}
@@ -0,0 +1,171 @@
package me.neznamy.tab.api.chat;
import me.neznamy.tab.api.ProtocolVersion;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.util.Preconditions;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.util.List;
/**
* A class representing a chat component that was serialized before,
* but deserialize was requested in PacketPlayOutPlayerInfo packet.
* This is used to read and modify the packet and then rewrite it.
* However, display name components must be deserialized to properly
* forward them in the new packet, but its value is not actually
* used anywhere.
* <p>
* To avoid this heavy and completely unnecessary operation, the original
* string is saved and returned during serialization. If any read/write
* operation is performed, the string is fully deserialized and this object
* will act like a normal chat component.
*/
@SuppressWarnings("unchecked")
public class DeserializedChatComponent extends IChatBaseComponent {
/** The original serialized component string */
private final String json;
/**
* Flag tracking whether this component was fully deserialized
* in case of a read operation to avoid repeating the deserialization
* process on every request.
*/
private boolean deserialized;
/**
* Flag tracking whether this component was modified, which means
* a new string has to be created instead of returning the input one.
*/
private boolean modified;
/**
* Constructs new instance with given parameter
*
* @param json
* serialized component as string
*/
public DeserializedChatComponent(String json) {
Preconditions.checkNotNull(json, "json");
this.json = json;
}
@Override
public String toString() {
if (modified) return super.toString();
return json;
}
@Override
public String toString(ProtocolVersion clientVersion) {
if (modified) return super.toString(clientVersion);
return json;
}
@Override
public List<IChatBaseComponent> getExtra(){
if (!deserialized) deserialize();
return super.getExtra();
}
@Override
public String getText() {
if (!deserialized) deserialize();
return super.getText();
}
@Override
public ChatModifier getModifier() {
if (!deserialized) deserialize();
return super.getModifier();
}
@Override
public IChatBaseComponent setExtra(List<IChatBaseComponent> components){
if (!deserialized) deserialize();
modified = true;
return super.setExtra(components);
}
@Override
public void addExtra(IChatBaseComponent child) {
if (!deserialized) deserialize();
modified = true;
super.addExtra(child);
}
@Override
public void setModifier(ChatModifier modifier) {
if (!deserialized) deserialize();
modified = true;
super.setModifier(modifier);
}
/**
* Performs a full deserialization process on this component.
*/
void deserialize() {
deserialized = true;
if (json.startsWith("\"") && json.endsWith("\"") && json.length() > 1) {
//simple component with only text used, minecraft serializer outputs the text in quotes instead of full json
setText(json.substring(1, json.length()-1));
return;
}
JSONObject jsonObject;
try {
jsonObject = (JSONObject) new JSONParser().parse(json);
} catch (ParseException e) {
TabAPI.getInstance().logError("Failed to deserialize json component " + json, e);
return;
}
/* if (jsonObject.containsKey("type")) {
return new ChatComponentEntity((String) jsonObject.get("type"), UUID.fromString((String) jsonObject.get("id")), IChatBaseComponent.deserialize(jsonObject.get("name").toString()).toFlatText());
}*/
setText((String) jsonObject.get("text"));
getModifier().setBold(getBoolean(jsonObject, "bold"));
getModifier().setItalic(getBoolean(jsonObject, "italic"));
getModifier().setUnderlined(getBoolean(jsonObject, "underlined"));
getModifier().setStrikethrough(getBoolean(jsonObject, "strikethrough"));
getModifier().setObfuscated(getBoolean(jsonObject, "obfuscated"));
getModifier().setColor(TextColor.fromString(((String) jsonObject.get("color"))));
if (jsonObject.containsKey("clickEvent")) {
JSONObject clickEvent = (JSONObject) jsonObject.get("clickEvent");
String action = (String) clickEvent.get("action");
String value = clickEvent.get("value").toString();
getModifier().onClick(ChatClickable.EnumClickAction.valueOf(action.toUpperCase()), value);
}
if (jsonObject.containsKey("hoverEvent")) {
JSONObject hoverEvent = (JSONObject) jsonObject.get("hoverEvent");
String action = (String) hoverEvent.get("action");
String value = (String) hoverEvent.get("value");
getModifier().onHover(ChatHoverable.EnumHoverAction.valueOf(action.toUpperCase()), deserialize(value));
}
if (jsonObject.containsKey("extra")) {
List<Object> list = (List<Object>) jsonObject.get("extra");
for (Object extra : list) {
String string = extra.toString();
//reverting .toString() removing "" for simple text
if (!string.startsWith("{")) string = "\"" + string + "\"";
addExtra(IChatBaseComponent.deserialize(string));
}
}
}
/**
* Returns boolean value of requested key from map
*
* @param jsonObject
* map to get value from
* @param key
* name of key
* @return value from json object or null if not present
*/
private static Boolean getBoolean(JSONObject jsonObject, String key) {
Preconditions.checkNotNull(jsonObject, "json object");
Preconditions.checkNotNull(key, "key");
String value = String.valueOf(jsonObject.getOrDefault(key, null));
return "null".equals(value) ? null : Boolean.parseBoolean(value);
}
}
@@ -7,239 +7,254 @@ import me.neznamy.tab.api.chat.rgb.RGBUtils;
*/
public enum EnumChatFormat {
BLACK('0', "000000"),
DARK_BLUE('1', "0000AA"),
DARK_GREEN('2', "00AA00"),
DARK_AQUA('3', "00AAAA"),
DARK_RED('4', "AA0000"),
DARK_PURPLE('5', "AA00AA"),
GOLD('6', "FFAA00"),
GRAY('7', "AAAAAA"),
DARK_GRAY('8', "555555"),
BLUE('9', "5555FF"),
GREEN('a', "55FF55"),
AQUA('b', "55FFFF"),
RED('c', "FF5555"),
LIGHT_PURPLE('d', "FF55FF"),
YELLOW('e', "FFFF55"),
WHITE('f', "FFFFFF"),
OBFUSCATED('k'),
BOLD('l'),
STRIKETHROUGH('m'),
UNDERLINE('n'),
ITALIC('o'),
RESET('r');
BLACK('0', "000000"),
DARK_BLUE('1', "0000AA"),
DARK_GREEN('2', "00AA00"),
DARK_AQUA('3', "00AAAA"),
DARK_RED('4', "AA0000"),
DARK_PURPLE('5', "AA00AA"),
GOLD('6', "FFAA00"),
GRAY('7', "AAAAAA"),
DARK_GRAY('8', "555555"),
BLUE('9', "5555FF"),
GREEN('a', "55FF55"),
AQUA('b', "55FFFF"),
RED('c', "FF5555"),
LIGHT_PURPLE('d', "FF55FF"),
YELLOW('e', "FFFF55"),
WHITE('f', "FFFFFF"),
OBFUSCATED('k'),
BOLD('l'),
STRIKETHROUGH('m'),
UNDERLINE('n'),
ITALIC('o'),
RESET('r');
/** Creating a constant to avoid memory allocations on each request */
private static final EnumChatFormat[] VALUES = values();
/** Creating a constant to avoid memory allocations on each request */
public static final EnumChatFormat[] VALUES = values();
/** The symbol minecraft uses to colorize text */
public static final char COLOR_CHAR = 0x00a7;
/** The symbol minecraft uses to colorize text */
public static final char COLOR_CHAR = 0x00a7;
/** The color symbol in form of a string */
public static final String COLOR_STRING = String.valueOf(COLOR_CHAR);
/** The color symbol in form of a string */
public static final String COLOR_STRING = String.valueOf(COLOR_CHAR);
/** Character representing the color or magic code */
private final char character;
/** Red value of this constant, 0 for magic codes */
private final short red;
/** Character representing the color or magic code */
private final char character;
/** Green value of this constant, 0 for magic codes */
private final short green;
/** Red value of this constant, 0 for magic codes */
private final short red;
/** Blue value of this constant, 0 for magic codes */
private final short blue;
/** Color as 6-digit hex code, null for magic codes */
private final String hexCode;
/** Color symbol followed by constant's character */
private final String chatFormat;
/** Green value of this constant, 0 for magic codes */
private final short green;
/**
* Constructs new color instance with given character and hex code
* @param character
* character which the color goes by
* @param hexCode
* 6-digit hex code of the color
*/
EnumChatFormat(char character, String hexCode) {
this.character = character;
this.chatFormat = String.valueOf(COLOR_CHAR) + character;
this.hexCode = hexCode;
int hexColor = Integer.parseInt(hexCode, 16);
red = (short) ((hexColor >> 16) & 0xFF);
green = (short) ((hexColor >> 8) & 0xFF);
blue = (short) (hexColor & 0xFF);
}
/**
* Constructs new magic code instance with given character
* @param character
* character representing the magic code
*/
EnumChatFormat(char character) {
this.character = character;
this.chatFormat = String.valueOf(COLOR_CHAR) + character;
red = 0;
green = 0;
blue = 0;
hexCode = null;
}
/** Blue value of this constant, 0 for magic codes */
private final short blue;
/**
* Returns red value of this color code
* @return red value
*/
public short getRed() {
return red;
}
/**
* Returns green value of this color code
* @return green value
*/
public short getGreen() {
return green;
}
/**
* Returns blue value of this color code
* @return blue value
*/
public short getBlue() {
return blue;
}
/**
* Returns enum value based on provided character or null if character is not valid
* @param c
* color code character (0-9, a-f, k-o, r)
* @return instance from the character or null if character is not valid
*/
public static EnumChatFormat getByChar(char c) {
for (EnumChatFormat format : VALUES) {
if (format.character == c) return format;
}
return null;
}
/**
* Returns enum value of last colors used in given string.
* If it's null, empty or does not contain color codes, WHITE is returned.
* @param string
* string to check last colors of
* @return last used color code in given string or WHITE if nothing is found
*/
public static EnumChatFormat lastColorsOf(String string) {
if (string == null || string.length() == 0) return EnumChatFormat.WHITE;
String legacyText = RGBUtils.getInstance().convertRGBtoLegacy(string);
String last = getLastColors(legacyText);
if (last.length() > 0) {
char c = last.toCharArray()[1];
for (EnumChatFormat e : VALUES) {
if (e.character == c) return e;
}
}
return EnumChatFormat.WHITE;
}
/**
* Returns color char followed by color's character
* @return color char followed by color's character
*/
public String getFormat() {
return chatFormat;
}
/**
* Returns character representing this color
* @return character representing this color
*/
public char getCharacter() {
return character;
}
/**
* Returns hex code of this format, null if this is a magic code
* @return hex code of this format
*/
public String getHexCode() {
return hexCode;
}
/**
* Returns enum value with exact red, green and blue values or null if no match was found
* @param red
* exact red value
* @param green
* exact green value
* @param blue
* exact blue value
* @return enum value or null if no such combination exists
*/
public static EnumChatFormat fromRGBExact(int red, int green, int blue){
for (EnumChatFormat format : VALUES) {
if (format.red == red && format.green == green && format.blue == blue) return format;
}
return null;
}
/** Color as 6-digit hex code, null for magic codes */
private final String hexCode;
/**
* Color translation method taken from bukkit, which converts '&' symbol into
* the actual color character if followed by a valid color character.
* @param textToTranslate
* text to replace color symbol in
* @return colorized string from provided text
*/
public static String color(String textToTranslate){
if (textToTranslate == null) return null;
if (!textToTranslate.contains("&")) return textToTranslate;
char[] b = textToTranslate.toCharArray();
for (int i = 0; i < b.length - 1; i++) {
if ((b[i] == '&') && ("0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx".indexOf(b[(i + 1)]) > -1)){
b[i] = COLOR_CHAR;
b[(i + 1)] = Character.toLowerCase(b[(i + 1)]);
}
}
return new String(b);
}
/** Color symbol followed by constant's character */
private final String chatFormat;
/**
* Turns back the color symbol into '&' symbol in provided text.
* @param text
* text to revert colors in
* @return reverted text
*/
public static String decolor(String text) {
if (text == null) return null;
if (!text.contains(COLOR_STRING)) return text;
return text.replace(COLOR_CHAR, '&');
}
/**
* Constructs new color instance with given character and hex code
*
* @param character
* character which the color goes by
* @param hexCode
* 6-digit hex code of the color
*/
EnumChatFormat(char character, String hexCode) {
this.character = character;
this.chatFormat = String.valueOf(COLOR_CHAR) + character;
this.hexCode = hexCode;
int hexColor = Integer.parseInt(hexCode, 16);
red = (short) ((hexColor >> 16) & 0xFF);
green = (short) ((hexColor >> 8) & 0xFF);
blue = (short) (hexColor & 0xFF);
}
/**
* Code taken from bukkit, which returns last color codes used in provided text.
* @param input
* text to get last colors from
* @return last colors used in provided text or empty string if nothing was found
*/
public static String getLastColors(String input) {
StringBuilder result = new StringBuilder();
int length = input.length();
for (int index = length - 1; index > -1; index--){
char section = input.charAt(index);
if ((section == COLOR_CHAR || section == '&') && (index < length - 1)){
char c = input.charAt(index + 1);
if ("0123456789AaBbCcDdEeFfKkLlMmNnOoRr".contains(String.valueOf(c))) {
result.insert(0, COLOR_CHAR);
result.insert(1, c);
if ("0123456789AaBbCcDdEeFfRr".contains(String.valueOf(c))) {
break;
}
}
}
}
return result.toString();
}
/**
* Constructs new magic code instance with given character
*
* @param character
* character representing the magic code
*/
EnumChatFormat(char character) {
this.character = character;
this.chatFormat = String.valueOf(COLOR_CHAR) + character;
red = 0;
green = 0;
blue = 0;
hexCode = null;
}
/**
* Returns red value of this color code
*
* @return red value
*/
public short getRed() {
return red;
}
/**
* Returns green value of this color code
*
* @return green value
*/
public short getGreen() {
return green;
}
/**
* Returns blue value of this color code
*
* @return blue value
*/
public short getBlue() {
return blue;
}
/**
* Returns enum value based on provided character or null if character is not valid
*
* @param c
* color code character (0-9, a-f, k-o, r)
* @return instance from the character or null if character is not valid
*/
public static EnumChatFormat getByChar(char c) {
for (EnumChatFormat format : VALUES) {
if (format.character == c) return format;
}
return null;
}
/**
* Returns enum value of last colors used in given string.
* If it's null, empty or does not contain color codes, WHITE is returned.
*
* @param string
* string to check last colors of
* @return last used color code in given string or WHITE if nothing is found
*/
public static EnumChatFormat lastColorsOf(String string) {
if (string == null || string.length() == 0) return EnumChatFormat.WHITE;
String legacyText = RGBUtils.getInstance().convertRGBtoLegacy(string);
String last = getLastColors(legacyText);
if (last.length() > 0) {
char c = last.toCharArray()[1];
for (EnumChatFormat e : VALUES) {
if (e.character == c) return e;
}
}
return EnumChatFormat.WHITE;
}
/**
* Returns color char followed by color's character
*
* @return color char followed by color's character
*/
public String getFormat() {
return chatFormat;
}
/**
* Returns character representing this color
*
* @return character representing this color
*/
public char getCharacter() {
return character;
}
/**
* Returns hex code of this format, null if this is a magic code
*
* @return hex code of this format
*/
public String getHexCode() {
return hexCode;
}
/**
* Returns enum value with exact red, green and blue values or null if no match was found
*
* @param red
* exact red value
* @param green
* exact green value
* @param blue
* exact blue value
* @return enum value or null if no such combination exists
*/
public static EnumChatFormat fromRGBExact(int red, int green, int blue){
for (EnumChatFormat format : VALUES) {
if (format.red == red && format.green == green && format.blue == blue) return format;
}
return null;
}
/**
* Color translation method taken from bukkit, which converts '&amp;' symbol into
* the actual color character if followed by a valid color character.
*
* @param textToTranslate
* text to replace color symbol in
* @return colorized string from provided text
*/
public static String color(String textToTranslate){
if (textToTranslate == null) return null;
if (!textToTranslate.contains("&")) return textToTranslate;
char[] b = textToTranslate.toCharArray();
for (int i = 0; i < b.length - 1; i++) {
if ((b[i] == '&') && ("0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx".indexOf(b[(i + 1)]) > -1)){
b[i] = COLOR_CHAR;
b[(i + 1)] = Character.toLowerCase(b[(i + 1)]);
}
}
return new String(b);
}
/**
* Turns back the color symbol into '&amp;' symbol in provided text.
*
* @param text
* text to revert colors in
* @return reverted text
*/
public static String decolor(String text) {
if (text == null) return null;
if (!text.contains(COLOR_STRING)) return text;
return text.replace(COLOR_CHAR, '&');
}
/**
* Code taken from bukkit, which returns last color codes used in provided text.
*
* @param input
* text to get last colors from
* @return last colors used in provided text or empty string if nothing was found
*/
public static String getLastColors(String input) {
if (input == null) return "";
StringBuilder result = new StringBuilder();
int length = input.length();
for (int index = length - 1; index > -1; index--){
char section = input.charAt(index);
if ((section == COLOR_CHAR || section == '&') && (index < length - 1)){
char c = input.charAt(index + 1);
if ("0123456789AaBbCcDdEeFfKkLlMmNnOoRr".contains(String.valueOf(c))) {
result.insert(0, COLOR_CHAR);
result.insert(1, c);
if ("0123456789AaBbCcDdEeFfRr".contains(String.valueOf(c))) {
break;
}
}
}
}
return result.toString();
}
}
@@ -1,17 +1,12 @@
package me.neznamy.tab.api.chat;
import java.util.*;
import me.neznamy.tab.api.util.Preconditions;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import me.neznamy.tab.api.ProtocolVersion;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.chat.ChatClickable.EnumClickAction;
import me.neznamy.tab.api.chat.ChatHoverable.EnumHoverAction;
import me.neznamy.tab.api.chat.rgb.RGBUtils;
import org.json.simple.parser.ParseException;
import me.neznamy.tab.api.util.Preconditions;
import org.json.simple.JSONObject;
import java.util.*;
/**
* A class representing the n.m.s.IChatBaseComponent class to make work with it much easier
@@ -19,458 +14,425 @@ import org.json.simple.parser.ParseException;
@SuppressWarnings("unchecked")
public class IChatBaseComponent {
/**
* Component cache maps to avoid large memory allocations as well as
* higher CPU usage when using animations which send the same text on repeat.
*/
private static final Map<String, IChatBaseComponent> componentCache = new HashMap<>();
private static final Map<IChatBaseComponent, String> serializeCacheModern = new HashMap<>();
private static final Map<IChatBaseComponent, String> serializeCacheLegacy = new HashMap<>();
/**
* Component cache maps to avoid large memory allocations as well as
* higher CPU usage when using animations which send the same text on repeat.
*/
private static final Map<String, IChatBaseComponent> componentCache = new HashMap<>();
private static final Map<IChatBaseComponent, String> serializeCacheModern = new HashMap<>();
private static final Map<IChatBaseComponent, String> serializeCacheLegacy = new HashMap<>();
/** Text of the component */
private String text;
/** Text of the component */
private String text;
/** Chat modifier containing color, magic codes, hover and click event */
private ChatModifier modifier = new ChatModifier();
/** Chat modifier containing color, magic codes, hover and click event */
private ChatModifier modifier = new ChatModifier();
/** Extra components used in "extra" field */
private List<IChatBaseComponent> extra;
/** Extra components used in "extra" field */
private List<IChatBaseComponent> extra;
/**
* Constructs a new empty component
*/
public IChatBaseComponent() {
}
/**
* Constructs a new component which is a clone of provided component
* @param component
* component to clone
*/
public IChatBaseComponent(IChatBaseComponent component) {
this.text = component.text;
this.modifier = new ChatModifier(component.modifier);
for (IChatBaseComponent child : component.getExtra()) {
addExtra(new IChatBaseComponent(child));
}
}
/**
* Constructs a new empty component
*/
public IChatBaseComponent() {
}
/**
* Constructs a new component which is a clone of provided component
*
* @param component
* component to clone
*/
public IChatBaseComponent(IChatBaseComponent component) {
Preconditions.checkNotNull(component, "component");
this.text = component.text;
this.modifier = new ChatModifier(component.modifier);
for (IChatBaseComponent child : component.getExtra()) {
addExtra(new IChatBaseComponent(child));
}
}
/**
* Constructs new instance with given text
* @param text
* text to display
*/
public IChatBaseComponent(String text) {
this.text = text;
}
/**
* Constructs new instance with given text
*
* @param text
* text to display
*/
public IChatBaseComponent(String text) {
this.text = text;
}
/**
* Returns list of extra components. If no extra components are defined, returns empty list.
* @return list of extra components
*/
public List<IChatBaseComponent> getExtra(){
if (extra == null) return Collections.emptyList();
return extra;
}
/**
* Returns list of extra components. If no extra components are defined, returns empty list.
*
* @return list of extra components
*/
public List<IChatBaseComponent> getExtra(){
if (extra == null) return Collections.emptyList();
return extra;
}
/**
* Sets full list of extra components to given list. Does not allow empty list.
* @param components
* components to use as extra
* @return self
* @throws IllegalArgumentException
* if {@code components} is an empty list
*/
public IChatBaseComponent setExtra(List<IChatBaseComponent> components){
if (components.isEmpty()) throw new IllegalArgumentException("Unexpected empty array of components"); //exception taken from minecraft
this.extra = components;
return this;
}
/**
* Sets full list of extra components to given list. Does not allow empty list.
*
* @param components
* components to use as extra
* @return self
* @throws IllegalArgumentException
* if {@code components} is an empty list
*/
public IChatBaseComponent setExtra(List<IChatBaseComponent> components){
if (components.isEmpty()) throw new IllegalArgumentException("Unexpected empty array of components"); //exception taken from minecraft
this.extra = components;
return this;
}
/**
* Appends provided component as extra component
* @param child
* component to append
*/
public void addExtra(IChatBaseComponent child) {
if (extra == null) extra = new ArrayList<>();
extra.add(child);
}
/**
* Appends provided component as extra component
*
* @param child
* component to append
*/
public void addExtra(IChatBaseComponent child) {
Preconditions.checkNotNull(child, "extra");
if (extra == null) extra = new ArrayList<>();
extra.add(child);
}
/**
* Returns text of this component
* @return text of this component
*/
public String getText() {
return text;
}
/**
* Returns text of this component
*
* @return text of this component
*/
public String getText() {
return text;
}
/**
* Returns chat modifier of this component
* @return chat modifier of this component
*/
public ChatModifier getModifier() {
return modifier;
}
/**
* Returns chat modifier of this component
*
* @return chat modifier of this component
*/
public ChatModifier getModifier() {
return modifier;
}
/**
* Sets modifier to provided value
* @param modifier
* modifier to set value to
* @throws IllegalArgumentException
* if {@code modifier} is null
*/
public void setModifier(ChatModifier modifier) {
Preconditions.checkNotNull(modifier, "modifier");
this.modifier = modifier;
}
/**
* Sets modifier to provided value
*
* @param modifier
* modifier to set value to
* @throws IllegalArgumentException
* if {@code modifier} is null
*/
public void setModifier(ChatModifier modifier) {
Preconditions.checkNotNull(modifier, "modifier");
this.modifier = modifier;
}
/**
* Changes text of this component
* @param text
* text to show
*/
public void setText(String text) {
this.text = text;
}
/**
* Changes text of this component
*
* @param text
* text to show
*/
public void setText(String text) {
this.text = text;
}
/**
* Deserializes string and returns created component. If provided string is null, returns null.
* @param json
* serialized string
* @return Deserialized component or null if input is null
*/
public static IChatBaseComponent deserialize(String json) {
if (json == null) return null;
if (json.startsWith("\"") && json.endsWith("\"") && json.length() > 1) {
//simple component with only text used, minecraft serializer outputs the text in quotes instead of full json
return new IChatBaseComponent(json.substring(1, json.length()-1));
}
JSONObject jsonObject;
try {
jsonObject = (JSONObject) new JSONParser().parse(json);
} catch (ParseException e) {
TabAPI.getInstance().logError("Failed to deserialize json component " + json, e);
return null;
}
IChatBaseComponent component;
if (jsonObject.containsKey("type")) {
return new ChatComponentEntity((String) jsonObject.get("type"), UUID.fromString((String) jsonObject.get("id")), IChatBaseComponent.deserialize(jsonObject.get("name").toString()).toFlatText());
}
component = new IChatBaseComponent();
component.setText((String) jsonObject.get("text"));
component.modifier.setBold(getBoolean(jsonObject, "bold"));
component.modifier.setItalic(getBoolean(jsonObject, "italic"));
component.modifier.setUnderlined(getBoolean(jsonObject, "underlined"));
component.modifier.setStrikethrough(getBoolean(jsonObject, "strikethrough"));
component.modifier.setObfuscated(getBoolean(jsonObject, "obfuscated"));
component.modifier.setColor(TextColor.fromString(((String) jsonObject.get("color"))));
if (jsonObject.containsKey("clickEvent")) {
JSONObject clickEvent = (JSONObject) jsonObject.get("clickEvent");
String action = (String) clickEvent.get("action");
String value = clickEvent.get("value").toString();
component.modifier.onClick(EnumClickAction.valueOf(action.toUpperCase()), value);
}
if (jsonObject.containsKey("hoverEvent")) {
JSONObject hoverEvent = (JSONObject) jsonObject.get("hoverEvent");
String action = (String) hoverEvent.get("action");
String value = (String) hoverEvent.get("value");
component.modifier.onHover(EnumHoverAction.valueOf(action.toUpperCase()), deserialize(value));
}
if (jsonObject.containsKey("extra")) {
List<Object> list = (List<Object>) jsonObject.get("extra");
for (Object extra : list) {
String string = extra.toString();
//reverting .toString() removing "" for simple text
if (!string.startsWith("{")) string = "\"" + string + "\"";
component.addExtra(deserialize(string));
}
}
return component;
}
/**
* Deserializes string and returns created component. If provided string is null, returns null.
*
* @param json
* serialized string
* @return Deserialized component or null if input is null
*/
public static IChatBaseComponent deserialize(String json) {
if (json == null) return null;
return new DeserializedChatComponent(json);
}
/**
* Returns boolean value of requested key from map
* @param jsonObject
* map to get value from
* @param key
* name of key
* @return value from json object or null if not present
*/
private static Boolean getBoolean(JSONObject jsonObject, String key) {
String value = String.valueOf(jsonObject.getOrDefault(key, null));
return "null".equals(value) ? null : Boolean.parseBoolean(value);
}
/**
* Converts the component to a string representing the serialized component.
* This method is only used internally by json library since it's missing
* protocol version field used by the method.
*
* @return serialized component in string form
* @see #toString(ProtocolVersion)
*/
@Override
public String toString() {
JSONObject json = new JSONObject();
if (text != null) json.put("text", text);
if (modifier.getTargetVersion() == null) modifier.setTargetVersion(TabAPI.getInstance().getServerVersion()); //packet.toString() was called as a part of a debug message
json.putAll(modifier.serialize());
if (extra != null) json.put("extra", extra);
return json.toString();
}
/**
* Converts the component to a string representing the serialized component.
* This method is only used internally by json library since it's missing
* protocol version field used by the method.
* @return serialized component in string form
* @see #toString(ProtocolVersion)
*/
@Override
public String toString() {
JSONObject json = new JSONObject();
if (text != null) json.put("text", text);
if (modifier.getTargetVersion() == null) modifier.setTargetVersion(TabAPI.getInstance().getServerVersion()); //packet.toString() was called as a part of a debug message
json.putAll(modifier.serialize());
if (extra != null) json.put("extra", extra);
return json.toString();
}
/**
* Serializes this component with colors based on client version.
* If client version is &lt;1.16, HEX colors will be converted to legacy colors.
*
* @param clientVersion
* client version to adapt component for
* @return serialized string
*/
public String toString(ProtocolVersion clientVersion) {
return toString(clientVersion, false);
}
/**
* Serializes this component with colors based on client version.
* If client version is <1.16, HEX colors will be converted to legacy colors.
* @param clientVersion
* client version to adapt component for
* @return serialized string
*/
public String toString(ProtocolVersion clientVersion) {
return toString(clientVersion, false);
}
/**
* Serializes this component with colors based on client version.
* If client version is &lt;1.16, HEX colors will be converted to legacy colors.
*
* @param clientVersion
* client version to adapt component for
* @param sendTranslatableIfEmpty
* whether empty translatable should be sent if text is empty or not
* @return serialized string
*/
public String toString(ProtocolVersion clientVersion, boolean sendTranslatableIfEmpty) {
if (extra == null) {
if (text == null) return null;
if (text.length() == 0) {
if (sendTranslatableIfEmpty) {
return "{\"translate\":\"\"}";
} else {
return "{\"text\":\"\"}";
}
}
}
modifier.setTargetVersion(clientVersion);
for (IChatBaseComponent child : getExtra()) {
child.modifier.setTargetVersion(clientVersion);
}
String string;
if (clientVersion.getMinorVersion() >= 16) {
if (serializeCacheModern.containsKey(this)) return serializeCacheModern.get(this);
string = toString();
if (serializeCacheModern.size() > 10000) serializeCacheModern.clear();
serializeCacheModern.put(this, string);
} else {
if (serializeCacheLegacy.containsKey(this)) return serializeCacheLegacy.get(this);
string = toString();
if (serializeCacheLegacy.size() > 10000) serializeCacheLegacy.clear();
serializeCacheLegacy.put(this, string);
}
return string;
}
/**
* Serializes this component with colors based on client version.
* If client version is <1.16, HEX colors will be converted to legacy colors.
* @param clientVersion
* client version to adapt component for
* @param sendTranslatableIfEmpty
* whether empty translatable should be sent if text is empty or not
* @return serialized string
*/
public String toString(ProtocolVersion clientVersion, boolean sendTranslatableIfEmpty) {
if (extra == null) {
if (text == null) return null;
if (text.length() == 0) {
if (sendTranslatableIfEmpty) {
return "{\"translate\":\"\"}";
} else {
return "{\"text\":\"\"}";
}
}
}
modifier.setTargetVersion(clientVersion);
for (IChatBaseComponent child : getExtra()) {
child.modifier.setTargetVersion(clientVersion);
}
String string;
if (clientVersion.getMinorVersion() >= 16) {
if (serializeCacheModern.containsKey(this)) return serializeCacheModern.get(this);
string = toString();
if (serializeCacheModern.size() > 10000) serializeCacheModern.clear();
serializeCacheModern.put(this, string);
} else {
if (serializeCacheLegacy.containsKey(this)) return serializeCacheLegacy.get(this);
string = toString();
if (serializeCacheLegacy.size() > 10000) serializeCacheLegacy.clear();
serializeCacheLegacy.put(this, string);
}
return string;
}
/**
* Returns organized component from colored text
*
* @param originalText
* text to convert
* @return organized component from colored text
*/
public static IChatBaseComponent fromColoredText(String originalText){
Preconditions.checkNotNull(originalText, "text");
String text = RGBUtils.getInstance().applyFormats(EnumChatFormat.color(originalText));
List<IChatBaseComponent> components = new ArrayList<>();
StringBuilder builder = new StringBuilder();
IChatBaseComponent component = new IChatBaseComponent();
for (int i = 0; i < text.length(); i++){
char c = text.charAt(i);
if (c == EnumChatFormat.COLOR_CHAR){
i++;
if (i >= text.length()) {
break;
}
c = text.charAt(i);
if ((c >= 'A') && (c <= 'Z')) {
c = (char)(c + ' ');
}
EnumChatFormat format = EnumChatFormat.getByChar(c);
if (format != null){
if (builder.length() > 0) {
component.setText(builder.toString());
components.add(component);
component = new IChatBaseComponent(component);
component.text = null;
builder = new StringBuilder();
}
switch (format){
case BOLD:
component.modifier.setBold(true);
break;
case ITALIC:
component.modifier.setItalic(true);
break;
case UNDERLINE:
component.modifier.setUnderlined(true);
break;
case STRIKETHROUGH:
component.modifier.setStrikethrough(true);
break;
case OBFUSCATED:
component.modifier.setObfuscated(true);
break;
case RESET:
component = new IChatBaseComponent();
component.modifier.setColor(new TextColor(EnumChatFormat.WHITE));
break;
default:
component = new IChatBaseComponent();
component.modifier.setColor(new TextColor(format));
break;
}
}
} else if (c == '#' && text.length() > i+6){
String hex = text.substring(i+1, i+7);
if (RGBUtils.getInstance().isHexCode(hex)) {
TextColor color;
if (containsLegacyCode(text, i)) {
color = new TextColor(hex, EnumChatFormat.getByChar(text.charAt(i+8)));
i += 8;
} else {
color = new TextColor(hex);
i += 6;
}
if (builder.length() > 0){
component.setText(builder.toString());
components.add(component);
builder = new StringBuilder();
}
component = new IChatBaseComponent();
component.modifier.setColor(color);
} else {
builder.append(c);
}
} else {
builder.append(c);
}
}
component.setText(builder.toString());
components.add(component);
return new IChatBaseComponent("").setExtra(components);
}
/**
* Returns organized component from colored text
* @param originalText
* text to convert
* @return organized component from colored text
*/
public static IChatBaseComponent fromColoredText(String originalText){
String text = RGBUtils.getInstance().applyFormats(EnumChatFormat.color(originalText), false);
List<IChatBaseComponent> components = new ArrayList<>();
StringBuilder builder = new StringBuilder();
IChatBaseComponent component = new IChatBaseComponent();
for (int i = 0; i < text.length(); i++){
char c = text.charAt(i);
if (c == EnumChatFormat.COLOR_CHAR){
i++;
if (i >= text.length()) {
break;
}
c = text.charAt(i);
if ((c >= 'A') && (c <= 'Z')) {
c = (char)(c + ' ');
}
EnumChatFormat format = EnumChatFormat.getByChar(c);
if (format != null){
if (builder.length() > 0) {
component.setText(builder.toString());
components.add(component);
component = new IChatBaseComponent(component);
component.text = null;
builder = new StringBuilder();
}
switch (format){
case BOLD:
component.modifier.setBold(true);
break;
case ITALIC:
component.modifier.setItalic(true);
break;
case UNDERLINE:
component.modifier.setUnderlined(true);
break;
case STRIKETHROUGH:
component.modifier.setStrikethrough(true);
break;
case OBFUSCATED:
component.modifier.setObfuscated(true);
break;
case RESET:
component = new IChatBaseComponent();
component.modifier.setColor(new TextColor(EnumChatFormat.WHITE));
break;
default:
component = new IChatBaseComponent();
component.modifier.setColor(new TextColor(format));
break;
}
}
} else if (c == '#' && text.length() > i+6){
String hex = text.substring(i+1, i+7);
if (RGBUtils.getInstance().isHexCode(hex)) {
TextColor color;
if (containsLegacyCode(text, i)) {
color = new TextColor(hex, EnumChatFormat.getByChar(text.charAt(i+8)));
i += 8;
} else {
color = new TextColor(hex);
i += 6;
}
if (builder.length() > 0){
component.setText(builder.toString());
components.add(component);
builder = new StringBuilder();
}
component = new IChatBaseComponent();
component.modifier.setColor(color);
} else {
builder.append(c);
}
} else {
builder.append(c);
}
}
component.setText(builder.toString());
components.add(component);
return new IChatBaseComponent("").setExtra(components);
}
/**
* Returns true if text contains legacy color request at defined RGB index start
*
* @param text
* text to check
* @param i
* current index start
* @return true if legacy color is defined, false if not
*/
private static boolean containsLegacyCode(String text, int i) {
if (text.length() - i < 9 || text.charAt(i+7) != '|') return false;
return EnumChatFormat.getByChar(text.charAt(i+8)) != null;
}
/**
* Returns true if text contains legacy color request at defined RGB index start
* @param text
* text to check
* @param i
* current index start
* @return true if legacy color is defined, false if not
*/
private static boolean containsLegacyCode(String text, int i) {
if (text.length() - i < 9 || text.charAt(i+7) != '|') return false;
return EnumChatFormat.getByChar(text.charAt(i+8)) != null;
}
/**
* Converts this component into a simple text with legacy colors (the closest match if color is set to RGB)
*
* @return The simple text format
*/
public String toLegacyText() {
StringBuilder builder = new StringBuilder();
append(builder, "");
return builder.toString();
}
/**
* Converts this component into a simple text with legacy colors (the closest match if color is set to RGB)
* @return The simple text format
*/
public String toLegacyText() {
StringBuilder builder = new StringBuilder();
append(builder, "");
return builder.toString();
}
/**
* Appends text to string builder, might also add color and magic codes if they are different
* from previous component in chain.
*
* @param builder
* builder to append text to
* @param previousFormatting
* colors and magic codes in previous component
* @return new formatting, might be identical to previous one
*/
private String append(StringBuilder builder, String previousFormatting) {
String formatting = previousFormatting;
if (text != null) {
formatting = getFormatting();
if (!formatting.equals(previousFormatting)) {
builder.append(formatting);
}
builder.append(text);
/**
* Appends text to string builder, might also add color and magic codes if they are different
* from previous component in chain.
* @param builder
* builder to append text to
* @param previousFormatting
* colors and magic codes in previous component
* @return wew formatting, might be identical to previous one
*/
private String append(StringBuilder builder, String previousFormatting) {
String formatting = previousFormatting;
if (text != null) {
formatting = getFormatting();
if (!formatting.equals(previousFormatting)) {
builder.append(formatting);
}
builder.append(text);
}
for (IChatBaseComponent component : getExtra()) {
formatting = component.append(builder, formatting);
}
return formatting;
}
}
for (IChatBaseComponent component : getExtra()) {
formatting = component.append(builder, formatting);
}
return formatting;
}
/**
* Returns colors and magic codes of this component
*
* @return used colors and magic codes
*/
private String getFormatting() {
StringBuilder builder = new StringBuilder();
if (modifier.getColor() != null) {
if (modifier.getColor().getLegacyColor() == EnumChatFormat.WHITE) {
//preventing unwanted &r -> &f conversion and stopping the <1.13 client bug fix from working
builder.append(EnumChatFormat.RESET.getFormat());
} else {
builder.append(modifier.getColor().getLegacyColor().getFormat());
}
}
builder.append(modifier.getMagicCodes());
return builder.toString();
}
/**
* Returns colors and magic codes of this component
* @return used colors and magic codes
*/
private String getFormatting() {
StringBuilder builder = new StringBuilder();
if (modifier.getColor() != null) {
if (modifier.getColor().getLegacyColor() == EnumChatFormat.WHITE) {
//preventing unwanted &r -> &f conversion and stopping the <1.13 client bug fix from working
builder.append(EnumChatFormat.RESET.getFormat());
} else {
builder.append(modifier.getColor().getLegacyColor().getFormat());
}
}
builder.append(modifier.getMagicCodes());
return builder.toString();
}
/**
* Returns raw text without colors, only works correctly when component is organized
*
* @return raw text in this component and all child components
*/
public String toRawText() {
StringBuilder builder = new StringBuilder();
if (text != null) builder.append(text);
for (IChatBaseComponent child : getExtra()) {
if (child.text != null) builder.append(child.text);
}
return builder.toString();
}
/**
* Returns raw text without colors, only works correctly when component is organized
* @return raw text in this component and all child components
*/
public String toRawText() {
StringBuilder builder = new StringBuilder();
if (text != null) builder.append(text);
for (IChatBaseComponent child : getExtra()) {
if (child.text != null) builder.append(child.text);
}
return builder.toString();
}
/**
* Converts the component into flat text with used colors (including rgb) and magic codes
*
* @return converted text
*/
public String toFlatText() {
StringBuilder builder = new StringBuilder();
if (modifier.getColor() != null) builder.append("#").append(modifier.getColor().getHexCode());
builder.append(modifier.getMagicCodes());
if (text != null) builder.append(text);
for (IChatBaseComponent child : getExtra()) {
builder.append(child.toFlatText());
}
return builder.toString();
}
/**
* Converts the component into flat text with used colors (including rgb) and magic codes
* @return converted text
*/
public String toFlatText() {
StringBuilder builder = new StringBuilder();
if (modifier.getColor() != null) builder.append("#").append(modifier.getColor().getHexCode());
builder.append(modifier.getMagicCodes());
if (text != null) builder.append(text);
for (IChatBaseComponent child : getExtra()) {
builder.append(child.toFlatText());
}
return builder.toString();
}
/**
* Returns the most optimized component based on text. Returns null if text is null,
* organized component if RGB colors are used or simple component with only text field
* containing the whole text when no RGB colors are used
* @param text
* text to create component from
* @return The most performance-optimized component based on text
*/
public static IChatBaseComponent optimizedComponent(String text){
if (text == null) return null;
if (componentCache.containsKey(text)) return componentCache.get(text);
IChatBaseComponent component;
if (text.contains("#") || text.contains("&x") || text.contains(EnumChatFormat.COLOR_CHAR + "x")){
//contains RGB colors
component = IChatBaseComponent.fromColoredText(text);
} else {
//no RGB
component = new IChatBaseComponent(text);
}
if (componentCache.size() > 10000) componentCache.clear();
componentCache.put(text, component);
return component;
}
/**
* Returns the most optimized component based on text. Returns null if text is null,
* organized component if RGB colors are used or simple component with only text field
* containing the whole text when no RGB colors are used
*
* @param text
* text to create component from
* @return The most performance-optimized component based on text
*/
public static IChatBaseComponent optimizedComponent(String text){
if (text == null) return null;
if (componentCache.containsKey(text)) return componentCache.get(text);
IChatBaseComponent component;
if (text.contains("#") || text.contains("&x") || text.contains(EnumChatFormat.COLOR_CHAR + "x") || text.contains("<")){
//contains RGB colors
component = IChatBaseComponent.fromColoredText(text);
} else {
//no RGB
component = new IChatBaseComponent(text);
}
if (componentCache.size() > 10000) componentCache.clear();
componentCache.put(text, component);
return component;
}
}
@@ -7,203 +7,216 @@ import me.neznamy.tab.api.util.Preconditions;
*/
public class TextColor {
/**
* RGB values as a single number of 3 8-bit numbers (0-255).
* It is only initialized if colors are actually used to avoid
* unnecessary memory allocations with string operations.
*/
private int rgb = -1;
/** Closest legacy color to this color object. */
private EnumChatFormat legacyColor;
/** HEX code of this color as a 6-digit hexadecimal string without "#" prefix. */
private String hexCode;
/**
* Boolean value whether the legacy color was forced with constructor or should be
* automatically assigned as closest color.
* This value is used in gradients when converting text for legacy players. */
private boolean legacyColorForced;
/**
* RGB values as a single number of 3 8-bit numbers (0-255).
* It is only initialized if colors are actually used to avoid
* unnecessary memory allocations with string operations.
*/
private int rgb = -1;
/**
* Constructs new instance as a clone of the provided color.
* @param color
* color to create a clone of
* @throws IllegalArgumentException
* if color is {@code null}
*/
public TextColor(TextColor color) {
Preconditions.checkNotNull(color, "color cannot be null");
rgb = color.rgb;
legacyColor = color.legacyColor;
hexCode = color.hexCode;
legacyColorForced = color.legacyColorForced;
}
/**
* Constructs new instance from provided 6-digit hex code string
* @param hexCode
* a 6-digit combination of hex numbers as a string
* @throws IllegalArgumentException
* if hexCode is {@code null}
*/
public TextColor(String hexCode) {
Preconditions.checkNotNull(hexCode, "hex code cannot be null");
this.hexCode = hexCode;
}
/**
* Constructs new instance from provided 6-digit hex code and forced legacy color
* @param hexCode
* 6-digit combination of hex numbers as a string
* @param legacyColor
* color to use for legacy clients instead of using the closest legacy color
* @throws IllegalArgumentException
* if {@code hexCode} is {@code null} or {@code legacyColor} is {@code null}
*/
public TextColor(String hexCode, EnumChatFormat legacyColor) {
Preconditions.checkNotNull(hexCode, "hex code cannot be null");
Preconditions.checkNotNull(legacyColor, "legacy color cannot be null");
this.hexCode = hexCode;
this.legacyColorForced = true;
this.legacyColor = legacyColor;
}
/**
* Constructs new instance from provided legacy color
* @param legacyColor
* legacy color to construct the instance from
* @throws IllegalArgumentException
* if {@code legacyColor} is {@code null}
*/
public TextColor(EnumChatFormat legacyColor) {
Preconditions.checkNotNull(legacyColor, "legacy color cannot be null");
this.rgb = (legacyColor.getRed() << 16) + (legacyColor.getGreen() << 8) + legacyColor.getBlue();
this.hexCode = legacyColor.getHexCode();
}
/**
* Constructs new instance with red, green and blue values
* @param red
* red value
* @param green
* green value
* @param blue
* blue value
* @throws IllegalArgumentException
* if {@code red}, {@code green} or {@code blue} is out of range ({@code 0-255})
*/
public TextColor(int red, int green, int blue) {
Preconditions.checkRange(red, 0, 255, "red color");
Preconditions.checkRange(green, 0, 255, "green color");
Preconditions.checkRange(blue, 0, 255, "blue color");
this.rgb = (red << 16) + (green << 8) + blue;
}
/**
* Loads the closest legacy color based currently provided values
*/
private EnumChatFormat loadClosestColor() {
double minMaxDist = 9999;
double maxDist;
EnumChatFormat closestColor = EnumChatFormat.WHITE;
for (EnumChatFormat color : EnumChatFormat.values()) {
int rDiff = Math.abs(color.getRed() - getRed());
int gDiff = Math.abs(color.getGreen() - getGreen());
int bDiff = Math.abs(color.getBlue() - getBlue());
maxDist = rDiff;
if (gDiff > maxDist) maxDist = gDiff;
if (bDiff > maxDist) maxDist = bDiff;
if (maxDist < minMaxDist) {
minMaxDist = maxDist;
closestColor = color;
}
}
return closestColor;
}
/**
* Returns {@code red} value
* @return red value
*/
public int getRed() {
if (rgb == -1) rgb = Integer.parseInt(hexCode, 16);
return (rgb >> 16) & 0xFF;
}
/** Closest legacy color to this color object. */
private EnumChatFormat legacyColor;
/**
* Returns {@code green} value
* @return green value
*/
public int getGreen() {
if (rgb == -1) rgb = Integer.parseInt(hexCode, 16);
return (rgb >> 8) & 0xFF;
}
/** HEX code of this color as a 6-digit hexadecimal string without "#" prefix. */
private String hexCode;
/**
* Returns {@code blue} value
* @return blue value
*/
public int getBlue() {
if (rgb == -1) rgb = Integer.parseInt(hexCode, 16);
return rgb & 0xFF;
}
/**
* Boolean value whether the legacy color was forced with constructor or should be
* automatically assigned as closest color.
* This value is used in gradients when converting text for legacy players. */
private boolean legacyColorForced;
/**
* Returns the closest legacy color of this color object.
* If the color was defined in constructor, it's returned.
* Otherwise, the closest color is calculated the then returned.
* @return closest legacy color
*/
public EnumChatFormat getLegacyColor() {
if (legacyColor == null) legacyColor = loadClosestColor();
return legacyColor;
}
/**
* Returns the rgb combination as a 6-digit hex code string
* @return the rgb combination as a 6-digit hex code string
*/
public String getHexCode() {
if (hexCode == null) hexCode = String.format("%06X", rgb);
return hexCode;
}
/**
* Converts the color into a valid color value used in color field in chat component.
* That is either 6-digit hex code prefixed with '#', or lowercase legacy color.
* @return the color serialized for use in chat component
*/
public String toString() {
EnumChatFormat legacyEquivalent = EnumChatFormat.fromRGBExact(getRed(), getGreen(), getBlue());
if (legacyEquivalent != null) {
//not sending old colors as RGB to 1.16 clients if not needed as <1.16 servers will fail to apply color
return legacyEquivalent.toString().toLowerCase();
}
return "#" + getHexCode();
}
/**
* Returns true if legacy color was forced with a constructor, false if not
* @return true if forced, false if not
*/
public boolean isLegacyColorForced() {
return legacyColorForced;
}
/**
* Constructs new instance as a clone of the provided color.
*
* @param color
* color to create a clone of
* @throws IllegalArgumentException
* if color is {@code null}
*/
public TextColor(TextColor color) {
Preconditions.checkNotNull(color, "color");
rgb = color.rgb;
legacyColor = color.legacyColor;
hexCode = color.hexCode;
legacyColorForced = color.legacyColorForced;
}
/**
* Reads the string as it appears in chat component and turns it into the color object.
* If the entered string is null, returns null.
* If it's prefixed with '#', it's considered as a hex code.
* Otherwise, it is considered being a lowercase legacy color.
* @param string
* string from color field in chat component
* @return An instance from specified string or null if string is null
*/
public static TextColor fromString(String string) {
if (string == null) return null;
if (string.startsWith("#")) return new TextColor(string.substring(1));
return new TextColor(EnumChatFormat.valueOf(string.toUpperCase()));
}
/**
* Constructs new instance from provided 6-digit hex code string
*
* @param hexCode
* a 6-digit combination of hex numbers as a string
* @throws IllegalArgumentException
* if hexCode is {@code null}
*/
public TextColor(String hexCode) {
Preconditions.checkNotNull(hexCode, "hex code");
this.hexCode = hexCode;
}
/**
* Constructs new instance from provided 6-digit hex code and forced legacy color
*
* @param hexCode
* 6-digit combination of hex numbers as a string
* @param legacyColor
* color to use for legacy clients instead of using the closest legacy color
* @throws IllegalArgumentException
* if {@code hexCode} is {@code null} or {@code legacyColor} is {@code null}
*/
public TextColor(String hexCode, EnumChatFormat legacyColor) {
Preconditions.checkNotNull(hexCode, "hex code");
Preconditions.checkNotNull(legacyColor, "legacy color");
this.hexCode = hexCode;
this.legacyColorForced = true;
this.legacyColor = legacyColor;
}
/**
* Constructs new instance from provided legacy color
*
* @param legacyColor
* legacy color to construct the instance from
* @throws IllegalArgumentException
* if {@code legacyColor} is {@code null}
*/
public TextColor(EnumChatFormat legacyColor) {
Preconditions.checkNotNull(legacyColor, "legacy color");
this.rgb = (legacyColor.getRed() << 16) + (legacyColor.getGreen() << 8) + legacyColor.getBlue();
this.hexCode = legacyColor.getHexCode();
}
/**
* Constructs new instance with red, green and blue values
*
* @param red
* red value
* @param green
* green value
* @param blue
* blue value
* @throws IllegalArgumentException
* if {@code red}, {@code green} or {@code blue} is out of range ({@code 0-255})
*/
public TextColor(int red, int green, int blue) {
Preconditions.checkRange(red, 0, 255, "red");
Preconditions.checkRange(green, 0, 255, "green");
Preconditions.checkRange(blue, 0, 255, "blue");
this.rgb = (red << 16) + (green << 8) + blue;
}
/**
* Loads the closest legacy color based currently provided values
*/
private EnumChatFormat loadClosestColor() {
double minMaxDist = 9999;
double maxDist;
EnumChatFormat closestColor = EnumChatFormat.WHITE;
for (EnumChatFormat color : EnumChatFormat.VALUES) {
int rDiff = Math.abs(color.getRed() - getRed());
int gDiff = Math.abs(color.getGreen() - getGreen());
int bDiff = Math.abs(color.getBlue() - getBlue());
maxDist = rDiff;
if (gDiff > maxDist) maxDist = gDiff;
if (bDiff > maxDist) maxDist = bDiff;
if (maxDist < minMaxDist) {
minMaxDist = maxDist;
closestColor = color;
}
}
return closestColor;
}
/**
* Returns {@code red} value
*
* @return red value
*/
public int getRed() {
if (rgb == -1) rgb = Integer.parseInt(hexCode, 16);
return (rgb >> 16) & 0xFF;
}
/**
* Returns {@code green} value
*
* @return green value
*/
public int getGreen() {
if (rgb == -1) rgb = Integer.parseInt(hexCode, 16);
return (rgb >> 8) & 0xFF;
}
/**
* Returns {@code blue} value
*
* @return blue value
*/
public int getBlue() {
if (rgb == -1) rgb = Integer.parseInt(hexCode, 16);
return rgb & 0xFF;
}
/**
* Returns the closest legacy color of this color object.
* If the color was defined in constructor, it's returned.
* Otherwise, the closest color is calculated the then returned.
*
* @return closest legacy color
*/
public EnumChatFormat getLegacyColor() {
if (legacyColor == null) legacyColor = loadClosestColor();
return legacyColor;
}
/**
* Returns the rgb combination as a 6-digit hex code string
*
* @return the rgb combination as a 6-digit hex code string
*/
public String getHexCode() {
if (hexCode == null) hexCode = String.format("%06X", rgb);
return hexCode;
}
/**
* Converts the color into a valid color value used in color field in chat component.
* That is either 6-digit hex code prefixed with '#', or lowercase legacy color.
*
* @return the color serialized for use in chat component
*/
public String toString() {
EnumChatFormat legacyEquivalent = EnumChatFormat.fromRGBExact(getRed(), getGreen(), getBlue());
if (legacyEquivalent != null) {
//not sending old colors as RGB to 1.16 clients if not needed as <1.16 servers will fail to apply color
return legacyEquivalent.toString().toLowerCase();
}
return "#" + getHexCode();
}
/**
* Returns true if legacy color was forced with a constructor, false if not
*
* @return true if forced, false if not
*/
public boolean isLegacyColorForced() {
return legacyColorForced;
}
/**
* Reads the string as it appears in chat component and turns it into the color object.
* If the entered string is null, returns null.
* If it's prefixed with '#', it's considered as a hex code.
* Otherwise, it is considered being a lowercase legacy color.
*
* @param string
* string from color field in chat component
* @return An instance from specified string or null if string is null
*/
public static TextColor fromString(String string) {
if (string == null) return null;
if (string.startsWith("#")) return new TextColor(string.substring(1));
return new TextColor(EnumChatFormat.valueOf(string.toUpperCase()));
}
}
@@ -0,0 +1,75 @@
package me.neznamy.tab.api.chat;
import me.neznamy.tab.api.ProtocolVersion;
import java.util.List;
/**
* A class extending IChatBaseComponent that holds the original value
* to avoid deserializing it, which is an unnecessary operation that
* slows down the performance, as well as makes code more complicated
* and open to problems.
*/
public class WrappedChatComponent extends IChatBaseComponent {
/** Original NMS component */
private final Object originalComponent;
/**
* Constructs new instance with given parameter
*
* @param originalComponent
* Original NMS component
*/
public WrappedChatComponent(Object originalComponent) {
this.originalComponent = originalComponent;
}
/**
* Returns the original NMS component
* @return the original NMS component
*/
public Object get() {
return originalComponent;
}
@Override
public String toString() {
throw new UnsupportedOperationException("Not supported for " + getClass().getSimpleName());
}
@Override
public String toString(ProtocolVersion clientVersion) {
throw new UnsupportedOperationException("Not supported for " + getClass().getSimpleName());
}
@Override
public List<IChatBaseComponent> getExtra(){
throw new UnsupportedOperationException("Not supported for " + getClass().getSimpleName());
}
@Override
public String getText() {
throw new UnsupportedOperationException("Not supported for " + getClass().getSimpleName());
}
@Override
public ChatModifier getModifier() {
throw new UnsupportedOperationException("Not supported for " + getClass().getSimpleName());
}
@Override
public IChatBaseComponent setExtra(List<IChatBaseComponent> components){
throw new UnsupportedOperationException("Not supported for " + getClass().getSimpleName());
}
@Override
public void addExtra(IChatBaseComponent child) {
throw new UnsupportedOperationException("Not supported for " + getClass().getSimpleName());
}
@Override
public void setModifier(ChatModifier modifier) {
throw new UnsupportedOperationException("Not supported for " + getClass().getSimpleName());
}
}
@@ -1,183 +1,209 @@
package me.neznamy.tab.api.chat.rgb;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import me.neznamy.tab.api.chat.EnumChatFormat;
import me.neznamy.tab.api.chat.TextColor;
import me.neznamy.tab.api.chat.rgb.format.BukkitFormat;
import me.neznamy.tab.api.chat.rgb.format.CMIFormat;
import me.neznamy.tab.api.chat.rgb.format.HtmlFormat;
import me.neznamy.tab.api.chat.rgb.format.RGBFormatter;
import me.neznamy.tab.api.chat.rgb.format.UnnamedFormat1;
import me.neznamy.tab.api.chat.rgb.format.*;
import me.neznamy.tab.api.chat.rgb.gradient.CMIGradient;
import me.neznamy.tab.api.chat.rgb.gradient.CommonGradient;
import me.neznamy.tab.api.chat.rgb.gradient.GradientPattern;
import me.neznamy.tab.api.chat.rgb.gradient.KyoriGradient;
import me.neznamy.tab.api.util.Preconditions;
/**
* A helper class to reformat all RGB formats into the default #RRGGBB and apply gradients
*/
public class RGBUtils {
/** Instance of the class */
private static final RGBUtils instance = new RGBUtils();
/** Registered RGB formatters */
private final RGBFormatter[] formats;
/** Registered gradient patterns */
private final GradientPattern[] gradients;
/** TAB's RGB pattern, used to convert text to bukkit format for boss bar */
private final Pattern tabPattern = Pattern.compile("#[0-9a-fA-F]{6}");
/** Instance of the class */
private static final RGBUtils instance = new RGBUtils();
/** RGB pattern for legacy codes */
private final Pattern tabPatternLegacy = Pattern.compile("#[0-9a-fA-F]{6}\\|.");
/** Registered RGB formatters */
private final RGBFormatter[] formats;
/**
* Constructs new instance and loads all RGB patterns and gradients
*/
public RGBUtils() {
formats = new RGBFormatter[] {
new BukkitFormat(),
new CMIFormat(),
new UnnamedFormat1(),
new HtmlFormat()
};
gradients = new GradientPattern[] {
//{#RRGGBB>}text{#RRGGBB<}
new CMIGradient(),
//<#RRGGBB>Text</#RRGGBB>
new CommonGradient(Pattern.compile("<#[0-9a-fA-F]{6}>[^<]*</#[0-9a-fA-F]{6}>"),
Pattern.compile("<#[0-9a-fA-F]{6}\\|.>[^<]*</#[0-9a-fA-F]{6}>"),
"<#", 9, 2, 9, 7),
//<$#RRGGBB>Text<$#RRGGBB>
new CommonGradient(Pattern.compile("<\\$#[0-9a-fA-F]{6}>[^<]*<\\$#[0-9a-fA-F]{6}>"),
Pattern.compile("<\\$#[0-9a-fA-F]{6}\\|.>[^<]*<\\$#[0-9a-fA-F]{6}>"),
"<$", 10, 3, 10, 7),
new KyoriGradient()
};
}
/**
* Returns instance of this class
* @return instance
*/
public static RGBUtils getInstance() {
return instance;
}
/**
* Applies all RGB formats and gradients to text and returns it.
* This method is called on every property initialization to convert formats
* once instead of on every refresh, if possible. Because of that, parameter
* {@code ignorePlaceholders} exists to not break raw placeholder identifiers
* @param text
* original text
* @param ignorePlaceholders
* whether placeholders should be ignored or not
* @return text where everything is converted to #RRGGBB
*/
public String applyFormats(String text, boolean ignorePlaceholders) {
String replaced = text;
for (GradientPattern pattern : gradients) {
replaced = pattern.applyPattern(replaced, ignorePlaceholders);
}
for (RGBFormatter formatter : formats) {
replaced = formatter.reformat(replaced);
}
return replaced;
}
/** Registered gradient patterns */
private final GradientPattern[] gradients;
/**
* Converts TAB's RGB format (#RRGGBB) into bukkit one (&x&r&r&g&g&b&b)
* for modern clients (1.16+), for legacy clients it will use the closest color.
* @param text
* text to convert
* @param rgbClient
* whether client accepts RGB or not
* @return converted text
*/
public String convertToBukkitFormat(String text, boolean rgbClient) {
if (text == null) return null;
if (!text.contains("#")) return text; //no rgb codes
if (rgbClient) {
//converting random formats to TAB one
String replaced = applyFormats(text, false);
for (Pattern p : new Pattern[]{tabPatternLegacy, tabPattern}) {
Matcher m = p.matcher(replaced);
while (m.find()) {
String hexCode = m.group();
String fixed = "&x&" + hexCode.charAt(1) + "&" + hexCode.charAt(2) + "&" + hexCode.charAt(3) + "&" + hexCode.charAt(4) + "&" + hexCode.charAt(5) + "&" + hexCode.charAt(6);
replaced = replaced.replace(hexCode, EnumChatFormat.color(fixed));
}
}
return replaced;
} else {
return convertRGBtoLegacy(text);
}
}
/** TAB's RGB pattern, used to convert text to bukkit format for boss bar */
private final Pattern tabPattern = Pattern.compile("#[0-9a-fA-F]{6}");
/**
* Converts all hex codes in given string to legacy codes
* @param text
* text to convert
* @return translated text
*/
public String convertRGBtoLegacy(String text) {
if (text == null) return null;
if (!text.contains("#")) return EnumChatFormat.color(text);
String applied = applyFormats(text, false);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < applied.length(); i++){
char c = applied.charAt(i);
if (c == '#' && applied.length() > i+6) {
String hexCode = applied.substring(i+1, i+7);
if (isHexCode(hexCode)) {
if (containsLegacyCode(applied, i)) {
sb.append(new TextColor(hexCode, EnumChatFormat.getByChar(applied.charAt(i+8))).getLegacyColor().getFormat());
i += 8;
} else {
sb.append(new TextColor(hexCode).getLegacyColor().getFormat());
i += 6;
}
} else {
sb.append(c);
}
} else {
sb.append(c);
}
}
return sb.toString();
}
/** RGB pattern for legacy codes */
private final Pattern tabPatternLegacy = Pattern.compile("#[0-9a-fA-F]{6}\\|.");
/**
* Returns true if entered string is a valid 6-digit combination of
* hexadecimal numbers, false if not
* @param string
* string to check
* @return {@code true} if valid, {@code false} if not
*/
public boolean isHexCode(String string) {
if (string.length() != 6) return false;
for (int i=0; i<6; i++) {
char c = string.charAt(i);
if (c < 48 || (c > 57 && c < 65) || (c > 70 && c < 97) || c > 102) return false;
}
return true;
}
/**
* Returns true if text contains legacy color request at defined RGB index start
* @param text
* text to check
* @param i
* current index start
* @return {@code true} if legacy color is defined and valid, {@code false} otherwise
*/
private static boolean containsLegacyCode(String text, int i) {
if (text.length() - i < 9 || text.charAt(i+7) != '|') return false;
return EnumChatFormat.getByChar(text.charAt(i+8)) != null;
}
/**
* Constructs new instance and loads all RGB patterns and gradients
*/
public RGBUtils() {
List<RGBFormatter> list = new ArrayList<>();
list.add(new BukkitFormat());
list.add(new CMIFormat());
list.add(new UnnamedFormat1());
list.add(new HtmlFormat());
list.add(new KyoriFormat());
try {
Class.forName("net.kyori.adventure.text.minimessage.MiniMessage");
list.add(new MiniMessageFormat());
} catch (ClassNotFoundException ignored) {}
formats = list.toArray(new RGBFormatter[0]);
gradients = new GradientPattern[] {
//{#RRGGBB>}text{#RRGGBB<}
new CMIGradient(),
//<#RRGGBB>Text</#RRGGBB>
new CommonGradient(Pattern.compile("<#[0-9a-fA-F]{6}>[^<]*</#[0-9a-fA-F]{6}>"),
Pattern.compile("<#[0-9a-fA-F]{6}\\|.>[^<]*</#[0-9a-fA-F]{6}>"),
"<#", 9, 2, 9, 7),
//<$#RRGGBB>Text<$#RRGGBB>
new CommonGradient(Pattern.compile("<\\$#[0-9a-fA-F]{6}>[^<]*<\\$#[0-9a-fA-F]{6}>"),
Pattern.compile("<\\$#[0-9a-fA-F]{6}\\|.>[^<]*<\\$#[0-9a-fA-F]{6}>"),
"<$", 10, 3, 10, 7),
new KyoriGradient()
};
}
/**
* Returns instance of this class
*
* @return instance
*/
public static RGBUtils getInstance() {
return instance;
}
/**
* Applies all RGB formats and gradients to text and returns it.
*
* @param text
* original text
* @return text where everything is converted to #RRGGBB
*/
public String applyFormats(String text) {
Preconditions.checkNotNull(text, "text");
String replaced = text;
for (GradientPattern pattern : gradients) {
replaced = pattern.applyPattern(replaced, false);
}
for (RGBFormatter formatter : formats) {
replaced = formatter.reformat(replaced);
}
return replaced;
}
/**
* Applies all gradient formats to text and returns it. This only affects
* usage where no placeholder is used inside.
*
* @param text
* original text
* @return text where all gradients with static text are converted to #RRGGBB
*/
public String applyCleanGradients(String text) {
Preconditions.checkNotNull(text, "text");
String replaced = text;
for (GradientPattern pattern : gradients) {
replaced = pattern.applyPattern(replaced, true);
}
return replaced;
}
/**
* Converts TAB's RGB format (#RRGGBB) into bukkit one
* (&amp;x&amp;r&amp;r&amp;g&amp;g&amp;b&amp;b) for modern
* clients (1.16+), for legacy clients it will use the closest color.
*
* @param text
* text to convert
* @param rgbClient
* whether client accepts RGB or not
* @return converted text
*/
public String convertToBukkitFormat(String text, boolean rgbClient) {
if (text == null) return null;
if (!text.contains("#")) return text; //no rgb codes
if (rgbClient) {
//converting random formats to TAB one
String replaced = applyFormats(text);
for (Pattern p : new Pattern[]{tabPatternLegacy, tabPattern}) {
Matcher m = p.matcher(replaced);
while (m.find()) {
String hexCode = m.group();
String fixed = "&x&" + hexCode.charAt(1) + "&" + hexCode.charAt(2) + "&" + hexCode.charAt(3) + "&" + hexCode.charAt(4) + "&" + hexCode.charAt(5) + "&" + hexCode.charAt(6);
replaced = replaced.replace(hexCode, EnumChatFormat.color(fixed));
}
}
return replaced;
} else {
return convertRGBtoLegacy(text);
}
}
/**
* Converts all hex codes in given string to legacy codes
*
* @param text
* text to convert
* @return translated text
*/
public String convertRGBtoLegacy(String text) {
if (text == null) return null;
if (!text.contains("#")) return EnumChatFormat.color(text);
String applied = applyFormats(text);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < applied.length(); i++){
char c = applied.charAt(i);
if (c == '#' && applied.length() > i+6) {
String hexCode = applied.substring(i+1, i+7);
if (isHexCode(hexCode)) {
if (containsLegacyCode(applied, i)) {
sb.append(new TextColor(hexCode, EnumChatFormat.getByChar(applied.charAt(i+8))).getLegacyColor().getFormat());
i += 8;
} else {
sb.append(new TextColor(hexCode).getLegacyColor().getFormat());
i += 6;
}
} else {
sb.append(c);
}
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* Returns true if entered string is a valid 6-digit combination of
* hexadecimal numbers, false if not
*
* @param string
* string to check
* @return {@code true} if valid, {@code false} if not
*/
public boolean isHexCode(String string) {
Preconditions.checkNotNull(string, "string");
if (string.length() != 6) return false;
for (int i=0; i<6; i++) {
char c = string.charAt(i);
if (c < 48 || (c > 57 && c < 65) || (c > 70 && c < 97) || c > 102) return false;
}
return true;
}
/**
* Returns true if text contains legacy color request at defined RGB index start
*
* @param text
* text to check
* @param i
* current index start
* @return {@code true} if legacy color is defined and valid, {@code false} otherwise
*/
private static boolean containsLegacyCode(String text, int i) {
if (text.length() - i < 9 || text.charAt(i+7) != '|') return false;
return EnumChatFormat.getByChar(text.charAt(i+8)) != null;
}
}
@@ -6,22 +6,22 @@ import java.util.regex.Pattern;
import me.neznamy.tab.api.chat.EnumChatFormat;
/**
* Formatter for &x&R&R&G&G&B&B
* Formatter for &amp;x&amp;R&amp;R&amp;G&amp;G&amp;B&amp;B
*/
public class BukkitFormat implements RGBFormatter {
private final Pattern pattern = Pattern.compile("[" + EnumChatFormat.COLOR_CHAR + "&]x[" + EnumChatFormat.COLOR_CHAR + "&\\p{XDigit}]{12}");
@Override
public String reformat(String text) {
if (!text.contains("&") && !text.contains(EnumChatFormat.COLOR_CHAR + "x")) return text;
String replaced = text;
Matcher m = pattern.matcher(replaced);
while (m.find()) {
String hexCode = m.group();
String fixed = new String(new char[] {'#', hexCode.charAt(3), hexCode.charAt(5), hexCode.charAt(7), hexCode.charAt(9), hexCode.charAt(11), hexCode.charAt(13)});
replaced = replaced.replace(hexCode, fixed);
}
return replaced;
}
private final Pattern pattern = Pattern.compile("[" + EnumChatFormat.COLOR_CHAR + "&]x[" + EnumChatFormat.COLOR_CHAR + "&\\p{XDigit}]{12}");
@Override
public String reformat(String text) {
if (!text.contains("&x") && !text.contains(EnumChatFormat.COLOR_CHAR + "x")) return text;
String replaced = text;
Matcher m = pattern.matcher(replaced);
while (m.find()) {
String hexCode = m.group();
String fixed = new String(new char[] {'#', hexCode.charAt(3), hexCode.charAt(5), hexCode.charAt(7), hexCode.charAt(9), hexCode.charAt(11), hexCode.charAt(13)});
replaced = replaced.replace(hexCode, fixed);
}
return replaced;
}
}
@@ -8,18 +8,18 @@ import java.util.regex.Pattern;
*/
public class CMIFormat implements RGBFormatter {
private final Pattern pattern = Pattern.compile("\\{#[0-9a-fA-F]{6}}");
@Override
public String reformat(String text) {
if (!text.contains("{#")) return text;
String replaced = text;
Matcher m = pattern.matcher(replaced);
while (m.find()) {
String hexCode = m.group();
String fixed = hexCode.substring(2, 8);
replaced = replaced.replace(hexCode, "#" + fixed);
}
return replaced;
}
private final Pattern pattern = Pattern.compile("\\{#[0-9a-fA-F]{6}}");
@Override
public String reformat(String text) {
if (!text.contains("{#")) return text;
String replaced = text;
Matcher m = pattern.matcher(replaced);
while (m.find()) {
String hexCode = m.group();
String fixed = hexCode.substring(2, 8);
replaced = replaced.replace(hexCode, "#" + fixed);
}
return replaced;
}
}
@@ -4,22 +4,22 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Formatter for #<RRGGBB>
* Formatter for #&lt;RRGGBB&gt;
*/
public class HtmlFormat implements RGBFormatter {
private final Pattern pattern = Pattern.compile("#<[0-9a-fA-F]{6}>");
@Override
public String reformat(String text) {
if (!text.contains("#<")) return text;
Matcher m = pattern.matcher(text);
String replaced = text;
while (m.find()) {
String hexCode = m.group();
String fixed = hexCode.substring(2, 8);
replaced = replaced.replace(hexCode, "#" + fixed);
}
return replaced;
}
private final Pattern pattern = Pattern.compile("#<[0-9a-fA-F]{6}>");
@Override
public String reformat(String text) {
if (!text.contains("#<")) return text;
Matcher m = pattern.matcher(text);
String replaced = text;
while (m.find()) {
String hexCode = m.group();
String fixed = hexCode.substring(2, 8);
replaced = replaced.replace(hexCode, "#" + fixed);
}
return replaced;
}
}
@@ -0,0 +1,25 @@
package me.neznamy.tab.api.chat.rgb.format;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Formatter for &lt;#RRGGBB&gt;
*/
public class KyoriFormat implements RGBFormatter {
private final Pattern pattern = Pattern.compile("<#[0-9a-fA-F]{6}>");
@Override
public String reformat(String text) {
if (!text.contains("<#")) return text;
String replaced = text;
Matcher m = pattern.matcher(replaced);
while (m.find()) {
String hexCode = m.group();
String fixed = hexCode.substring(2, 8);
replaced = replaced.replace(hexCode, "#" + fixed);
}
return replaced;
}
}
@@ -0,0 +1,22 @@
package me.neznamy.tab.api.chat.rgb.format;
import me.neznamy.tab.api.chat.EnumChatFormat;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
/**
* Formatter for &lt;#RRGGBB&gt;
*/
public class MiniMessageFormat implements RGBFormatter {
@Override
public String reformat(String text) {
if (!text.contains("<")) return text;
String format = text.replace(EnumChatFormat.RESET.getFormat(), ""); //remove &r from header/footer newline inserts
try {
return LegacyComponentSerializer.legacySection().serialize(MiniMessage.miniMessage().deserialize(format));
} catch (Throwable ignored) {
return text;
}
}
}
@@ -5,10 +5,12 @@ package me.neznamy.tab.api.chat.rgb.format;
*/
public interface RGBFormatter {
/**
* Reformats RGB codes in provided text into #RRGGBB format
* @param text - text to format
* @return reformatted text
*/
String reformat(String text);
/**
* Reformats RGB codes in provided text into #RRGGBB format
*
* @param text
* text to format
* @return reformatted text
*/
String reformat(String text);
}
@@ -1,12 +1,12 @@
package me.neznamy.tab.api.chat.rgb.format;
/**
* Formatter for &#RRGGBB
* Formatter for &amp;#RRGGBB
*/
public class UnnamedFormat1 implements RGBFormatter {
@Override
public String reformat(String text) {
return text.contains("&#") ? text.replace("&#", "#") : text;
}
@Override
public String reformat(String text) {
return text.contains("&#") ? text.replace("&#", "#") : text;
}
}
@@ -5,26 +5,26 @@ import java.util.regex.Pattern;
public class CMIGradient extends CommonGradient {
//pattern for {#RRGGBB<>}
private final Pattern shortcutPattern = Pattern.compile("\\{#[0-9a-fA-F]{6}<>}");
public CMIGradient() {
super(Pattern.compile("\\{#[0-9a-fA-F]{6}>}[^{]*\\{#[0-9a-fA-F]{6}<}"),
Pattern.compile("\\{#[0-9a-fA-F]{6}\\|.>}[^{]*\\{#[0-9a-fA-F]{6}<}"),
"{#", 9, 2, 10, 8);
}
@Override
public String applyPattern(String text, boolean ignorePlaceholders) {
String replaced = text;
if (replaced.contains("<>}")) {
Matcher m = shortcutPattern.matcher(replaced);
while (m.find()) {
String format = m.group();
String code = format.substring(2, 8);
replaced = replaced.replace(format, "{#" + code + "<}{#" + code + ">}");
}
}
return super.applyPattern(replaced, ignorePlaceholders);
}
//pattern for {#RRGGBB<>}
private final Pattern shortcutPattern = Pattern.compile("\\{#[0-9a-fA-F]{6}<>}");
public CMIGradient() {
super(Pattern.compile("\\{#[0-9a-fA-F]{6}>}[^{]*\\{#[0-9a-fA-F]{6}<}"),
Pattern.compile("\\{#[0-9a-fA-F]{6}\\|.>}[^{]*\\{#[0-9a-fA-F]{6}<}"),
"{#", 9, 2, 10, 8);
}
@Override
public String applyPattern(String text, boolean ignorePlaceholders) {
String replaced = text;
if (replaced.contains("<>}")) {
Matcher m = shortcutPattern.matcher(replaced);
while (m.find()) {
String format = m.group();
String code = format.substring(2, 8);
replaced = replaced.replace(format, "{#" + code + "<}{#" + code + ">}");
}
}
return super.applyPattern(replaced, ignorePlaceholders);
}
}
@@ -8,50 +8,50 @@ import me.neznamy.tab.api.chat.TextColor;
public class CommonGradient implements GradientPattern {
private final Pattern pattern;
private final Pattern legacyPattern;
private final String containCheck;
private final int legacyCharPosition;
private final int startColorStart;
private final int messageStart;
private final int endColorStartSub;
public CommonGradient(Pattern pattern, Pattern legacyPattern, String containCheck, int legacyCharPosition,
int startColorStart, int messageStart, int endColorStartSub) {
this.pattern = pattern;
this.legacyPattern = legacyPattern;
this.containCheck = containCheck;
this.legacyCharPosition = legacyCharPosition;
this.startColorStart = startColorStart;
this.messageStart = messageStart;
this.endColorStartSub = endColorStartSub;
}
@Override
public String applyPattern(String text, boolean ignorePlaceholders) {
if (!text.contains(containCheck)) return text;
String replaced = text;
Matcher m = legacyPattern.matcher(replaced);
while (m.find()) {
String format = m.group();
EnumChatFormat legacyColor = EnumChatFormat.getByChar(format.charAt(legacyCharPosition));
if ((ignorePlaceholders && format.contains("%")) || legacyColor == null) continue;
TextColor start = new TextColor(format.substring(startColorStart, startColorStart+6), legacyColor);
String message = format.substring(messageStart+2, format.length()-10);
TextColor end = new TextColor(format.substring(format.length()-endColorStartSub, format.length()-endColorStartSub+6));
String applied = asGradient(start, message, end);
replaced = replaced.replace(format, applied);
}
m = pattern.matcher(replaced);
while (m.find()) {
String format = m.group();
if (ignorePlaceholders && format.contains("%")) continue;
TextColor start = new TextColor(format.substring(startColorStart, startColorStart+6));
String message = format.substring(messageStart, format.length()-10);
TextColor end = new TextColor(format.substring(format.length()-endColorStartSub, format.length()-endColorStartSub+6));
String applied = asGradient(start, message, end);
replaced = replaced.replace(format, applied);
}
return replaced;
}
private final Pattern pattern;
private final Pattern legacyPattern;
private final String containCheck;
private final int legacyCharPosition;
private final int startColorStart;
private final int messageStart;
private final int endColorStartSub;
public CommonGradient(Pattern pattern, Pattern legacyPattern, String containCheck, int legacyCharPosition,
int startColorStart, int messageStart, int endColorStartSub) {
this.pattern = pattern;
this.legacyPattern = legacyPattern;
this.containCheck = containCheck;
this.legacyCharPosition = legacyCharPosition;
this.startColorStart = startColorStart;
this.messageStart = messageStart;
this.endColorStartSub = endColorStartSub;
}
@Override
public String applyPattern(String text, boolean ignorePlaceholders) {
if (!text.contains(containCheck)) return text;
String replaced = text;
Matcher m = legacyPattern.matcher(replaced);
while (m.find()) {
String format = m.group();
EnumChatFormat legacyColor = EnumChatFormat.getByChar(format.charAt(legacyCharPosition));
if ((ignorePlaceholders && format.contains("%")) || legacyColor == null) continue;
TextColor start = new TextColor(format.substring(startColorStart, startColorStart+6), legacyColor);
String message = format.substring(messageStart+2, format.length()-10);
TextColor end = new TextColor(format.substring(format.length()-endColorStartSub, format.length()-endColorStartSub+6));
String applied = asGradient(start, message, end);
replaced = replaced.replace(format, applied);
}
m = pattern.matcher(replaced);
while (m.find()) {
String format = m.group();
if (ignorePlaceholders && format.contains("%")) continue;
TextColor start = new TextColor(format.substring(startColorStart, startColorStart+6));
String message = format.substring(messageStart, format.length()-10);
TextColor end = new TextColor(format.substring(format.length()-endColorStartSub, format.length()-endColorStartSub+6));
String applied = asGradient(start, message, end);
replaced = replaced.replace(format, applied);
}
return replaced;
}
}
@@ -8,44 +8,50 @@ import me.neznamy.tab.api.chat.TextColor;
*/
public interface GradientPattern {
/**
* Applies gradients in provided text and returns text using only #RRGGBB
* @param text - text to be reformatted
* @return reformatted text
*/
String applyPattern(String text, boolean ignorePlaceholders);
/**
* Returns gradient text based on start color, text and end color
* @param start - start color
* @param text - text to be reformatted
* @param end - end color
* @return reformatted text
*/
default String asGradient(TextColor start, String text, TextColor end) {
//lazy support for magic codes in gradients
String magicCodes = EnumChatFormat.getLastColors(text);
String deColorized = text.substring(magicCodes.length());
StringBuilder sb = new StringBuilder();
int length = deColorized.length();
if (length == 1) {
sb.append("#");
sb.append(new TextColor(start.getRed(), start.getGreen(), start.getBlue()).getHexCode());
if (start.isLegacyColorForced()) sb.append("|").append(start.getLegacyColor().getCharacter());
sb.append(magicCodes);
sb.append(deColorized);
return sb.toString();
}
for (int i=0; i<length; i++) {
int red = (int) (start.getRed() + (float)(end.getRed() - start.getRed())/(length-1)*i);
int green = (int) (start.getGreen() + (float)(end.getGreen() - start.getGreen())/(length-1)*i);
int blue = (int) (start.getBlue() + (float)(end.getBlue() - start.getBlue())/(length-1)*i);
sb.append("#");
sb.append(new TextColor(red, green, blue).getHexCode());
if (start.isLegacyColorForced()) sb.append("|").append(start.getLegacyColor().getCharacter());
sb.append(magicCodes);
sb.append(deColorized.charAt(i));
}
return sb.toString();
}
/**
* Applies gradients in provided text and returns text using only #RRGGBB
*
* @param text
* text to be reformatted
* @return reformatted text
*/
String applyPattern(String text, boolean ignorePlaceholders);
/**
* Returns gradient text based on start color, text and end color
*
* @param start
* start color
* @param text
* text to be reformatted
* @param end
* end color
* @return reformatted text
*/
default String asGradient(TextColor start, String text, TextColor end) {
//lazy support for magic codes in gradients
String magicCodes = EnumChatFormat.getLastColors(text);
String deColorized = text.substring(magicCodes.length());
StringBuilder sb = new StringBuilder();
int length = deColorized.length();
if (length == 1) {
sb.append("#");
sb.append(new TextColor(start.getRed(), start.getGreen(), start.getBlue()).getHexCode());
if (start.isLegacyColorForced()) sb.append("|").append(start.getLegacyColor().getCharacter());
sb.append(magicCodes);
sb.append(deColorized);
return sb.toString();
}
for (int i=0; i<length; i++) {
int red = (int) (start.getRed() + (float)(end.getRed() - start.getRed())/(length-1)*i);
int green = (int) (start.getGreen() + (float)(end.getGreen() - start.getGreen())/(length-1)*i);
int blue = (int) (start.getBlue() + (float)(end.getBlue() - start.getBlue())/(length-1)*i);
sb.append("#");
sb.append(new TextColor(red, green, blue).getHexCode());
if (start.isLegacyColorForced()) sb.append("|").append(start.getLegacyColor().getCharacter());
sb.append(magicCodes);
sb.append(deColorized.charAt(i));
}
return sb.toString();
}
}
@@ -7,39 +7,40 @@ import me.neznamy.tab.api.chat.EnumChatFormat;
import me.neznamy.tab.api.chat.TextColor;
/**
* Gradient applier for <gradient:#RRGGBB:#RRGGBB>Text</gradient> and <gradient:#RRGGBB|L:#RRGGBB>Text</gradient>
* Gradient applier for &lt;gradient:#RRGGBB:#RRGGBB&gt;Text&lt;/gradient&gt;
* and &lt;gradient:#RRGGBB|L:#RRGGBB&gt;Text&lt;/gradient&gt;
*/
public class KyoriGradient implements GradientPattern {
private final Pattern pattern = Pattern.compile("<gradient:#[0-9a-fA-F]{6}:#[0-9a-fA-F]{6}>[^<]*</gradient>");
private final Pattern pattern = Pattern.compile("<gradient:#[0-9a-fA-F]{6}:#[0-9a-fA-F]{6}>[^<]*</gradient>");
private final Pattern patternLegacy = Pattern.compile("<gradient:#[0-9a-fA-F]{6}\\|.:#[0-9a-fA-F]{6}>[^<]*</gradient>");
@Override
public String applyPattern(String text, boolean ignorePlaceholders) {
if (!text.contains("<grad")) return text;
String replaced = text;
Matcher m = patternLegacy.matcher(replaced);
while (m.find()) {
String format = m.group();
EnumChatFormat legacyColor = EnumChatFormat.getByChar(format.charAt(18));
if ((ignorePlaceholders && format.contains("%")) || legacyColor == null) continue;
TextColor start = new TextColor(format.substring(11, 17), legacyColor);
String message = format.substring(28, format.length()-11);
TextColor end = new TextColor(format.substring(21, 27));
String applied = asGradient(start, message, end);
replaced = replaced.replace(format, applied);
}
m = pattern.matcher(replaced);
while (m.find()) {
String format = m.group();
if (ignorePlaceholders && format.contains("%")) continue;
TextColor start = new TextColor(format.substring(11, 17));
String message = format.substring(26, format.length()-11);
TextColor end = new TextColor(format.substring(19, 25));
String applied = asGradient(start, message, end);
replaced = replaced.replace(format, applied);
}
return replaced;
}
private final Pattern patternLegacy = Pattern.compile("<gradient:#[0-9a-fA-F]{6}\\|.:#[0-9a-fA-F]{6}>[^<]*</gradient>");
@Override
public String applyPattern(String text, boolean ignorePlaceholders) {
if (!text.contains("<grad")) return text;
String replaced = text;
Matcher m = patternLegacy.matcher(replaced);
while (m.find()) {
String format = m.group();
EnumChatFormat legacyColor = EnumChatFormat.getByChar(format.charAt(18));
if ((ignorePlaceholders && format.contains("%")) || legacyColor == null) continue;
TextColor start = new TextColor(format.substring(11, 17), legacyColor);
String message = format.substring(28, format.length()-11);
TextColor end = new TextColor(format.substring(21, 27));
String applied = asGradient(start, message, end);
replaced = replaced.replace(format, applied);
}
m = pattern.matcher(replaced);
while (m.find()) {
String format = m.group();
if (ignorePlaceholders && format.contains("%")) continue;
TextColor start = new TextColor(format.substring(11, 17));
String message = format.substring(26, format.length()-11);
TextColor end = new TextColor(format.substring(19, 25));
String applied = asGradient(start, message, end);
replaced = replaced.replace(format, applied);
}
return replaced;
}
}
@@ -17,6 +17,7 @@ import java.util.Map;
import java.util.Map.Entry;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.util.Preconditions;
/**
* Abstract class for configuration file
@@ -24,404 +25,406 @@ import me.neznamy.tab.api.TabAPI;
@SuppressWarnings("unchecked")
public abstract class ConfigurationFile {
/** Comments on top of the file */
protected List<String> header;
/** Comments on top of the file */
protected List<String> header;
/** Configuration file content */
protected Map<String, Object> values;
/** Configuration file content */
protected Map<String, Object> values;
/** File to use */
protected final File file;
/** File to use */
protected final File file;
/**
* Constructs new instance and attempts to load specified configuration file.
* If file does not exist, default file is copied from {@code source}.
*
* @param source
* Source to copy file from if it does not exist
* @param destination
* File destination to use
* @throws IllegalArgumentException
* if {@code destination} is null
* @throws IllegalStateException
* if file does not exist and source is null
* @throws IOException
* if I/O operation with the file unexpectedly fails
*/
protected ConfigurationFile(InputStream source, File destination) throws IOException {
if (destination == null) throw new IllegalArgumentException("Destination cannot be null");
this.file = destination;
if (file.getParentFile() != null) Files.createDirectories(file.getParentFile().toPath());
if (!file.exists() && source == null) throw new IllegalStateException("File does not exist and source is null");
if (file.createNewFile()) {
// Files.copy(source, file.toPath());
//avoiding MalformedInputException thrown on fabric due to file not having UTF8 encoding by default
//also automatically using the encoding so users don't have to worry about it anymore
try (BufferedReader reader = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))){
String line;
while ((line = reader.readLine()) != null) {
writer.write(line + "\n");
}
}
}
detectHeader();
}
/**
* Constructs new instance and attempts to load specified configuration file.
* If file does not exist, default file is copied from {@code source}.
*
* @param source
* Source to copy file from if it does not exist
* @param destination
* File destination to use
* @throws IllegalArgumentException
* if {@code destination} is null
* @throws IllegalStateException
* if file does not exist and source is null
* @throws IOException
* if I/O operation with the file unexpectedly fails
*/
protected ConfigurationFile(InputStream source, File destination) throws IOException {
Preconditions.checkNotNull(destination, "destination");
this.file = destination;
if (file.getParentFile() != null && !file.getParentFile().exists()) Files.createDirectories(file.getParentFile().toPath());
if (!file.exists() && source == null) throw new IllegalStateException("File does not exist and source is null");
if (file.createNewFile()) {
// Files.copy(source, file.toPath());
//avoiding MalformedInputException thrown on fabric due to file not having UTF8 encoding by default
//also automatically using the encoding so users don't have to worry about it anymore
try (BufferedReader reader = new BufferedReader(new InputStreamReader(source, StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))){
String line;
while ((line = reader.readLine()) != null) {
writer.write(line + "\n");
}
}
}
detectHeader();
}
/**
* Saves values from map to the file
*/
public abstract void save();
/**
* Saves values from map to the file
*/
public abstract void save();
/**
* Returns simple name of the file
* @return simple name of the file
*/
public String getName() {
return file.getName();
}
/**
* Returns simple name of the file
*
* @return simple name of the file
*/
public String getName() {
return file.getName();
}
/**
* Returns the root value map
* @return the root value map
*/
public Map<String, Object> getValues(){
return values;
}
/**
* Returns the root value map
*
* @return the root value map
*/
public Map<String, Object> getValues(){
return values;
}
public void setValues(Map<String, Object> values) {
this.values = values;
}
public void setValues(Map<String, Object> values) {
this.values = values;
}
/**
* Gets config option with specified path. If the option is not present and
* {@code defaultValue} is not {@code null}, value is inserted, {@link #save()}
* called and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file
*/
public Object getObject(String path, Object defaultValue) {
Object value = values;
for (String section : path.contains(".") ? path.split("\\.") : new String[] {path}) {
if (!(value instanceof Map)) {
if (defaultValue != null) set(path, defaultValue);
return defaultValue;
}
value = getIgnoreCase((Map<Object, Object>) value, section);
}
if (value == null && defaultValue != null) {
TabAPI.getInstance().debug("Inserting missing config option \"" + path + "\" with value \"" + defaultValue + "\" into " + file.getName());
set(path, defaultValue);
return defaultValue;
}
return value;
}
/**
* Gets config option with specified path. If the option is not present and
* {@code defaultValue} is not {@code null}, value is inserted, {@link #save()}
* called and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file
*/
public Object getObject(String path, Object defaultValue) {
Object value = values;
for (String section : path.contains(".") ? path.split("\\.") : new String[] {path}) {
if (!(value instanceof Map)) {
if (defaultValue != null) set(path, defaultValue);
return defaultValue;
}
value = getIgnoreCase((Map<Object, Object>) value, section);
}
if (value == null && defaultValue != null) {
TabAPI.getInstance().debug("Inserting missing config option \"" + path + "\" with value \"" + defaultValue + "\" into " + file.getName());
set(path, defaultValue);
return defaultValue;
}
return value;
}
/**
* Returns config option with specified path. If option is not present,
* {@code null} is returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from configuration file or null if not present
*/
public Object getObject(String path) {
return getObject(path, null);
}
public Object getObject(String[] path) {
Object value = values;
for (String section : path) {
if (!(value instanceof Map)) {
return null;
}
value = getIgnoreCase((Map<Object, Object>) value, section);
}
return value;
}
/**
* Returns config option with specified path. If option is not present,
* {@code null} is returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from configuration file or null if not present
*/
public Object getObject(String path) {
return getObject(path, null);
}
/**
* Returns value from map without case sensitivity of the key. Returns
* {@code null} if no such key is found.
*
* @param map
* map to get value from
* @param key
* case-insensitive key name
* @return map value from case-insensitive key
*/
private Object getIgnoreCase(Map<Object, Object> map, String key) {
for (Entry<Object, Object> entry : map.entrySet()) {
if (entry.getKey().toString().equalsIgnoreCase(key)) return entry.getValue();
}
return map.get(key);
}
public Object getObject(String[] path) {
Object value = values;
for (String section : path) {
if (!(value instanceof Map)) {
return null;
}
value = getIgnoreCase((Map<Object, Object>) value, section);
}
return value;
}
/**
* Returns config option with specified path as {@code String}. Returns {@code null} if
* option is not present.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from file or null if not present
*/
public String getString(String path) {
return getString(path, null);
}
/**
* Returns value from map without case sensitivity of the key. Returns
* {@code null} if no such key is found.
*
* @param map
* map to get value from
* @param key
* case-insensitive key name
* @return map value from case-insensitive key
*/
private Object getIgnoreCase(Map<Object, Object> map, String key) {
for (Entry<Object, Object> entry : map.entrySet()) {
if (entry.getKey().toString().equalsIgnoreCase(key)) return entry.getValue();
}
return map.get(key);
}
/**
* Returns config option with specified path as {@code String}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code String}
*/
public String getString(String path, String defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
return String.valueOf(value);
}
/**
* Returns config option with specified path as {@code String}. Returns {@code null} if
* option is not present.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from file or null if not present
*/
public String getString(String path) {
return getString(path, null);
}
/**
* Returns config option with specified path as {@code List<String>}. Returns {@code null} if
* option is not present.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from file or null if not present
*/
public List<String> getStringList(String path) {
return getStringList(path, null);
}
/**
* Returns config option with specified path as {@code String}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code String}
*/
public String getString(String path, String defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
return String.valueOf(value);
}
/**
* Returns config option with specified path as {@code List<String>}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path of the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code List<String>}
*/
public List<String> getStringList(String path, List<String> defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
if (!(value instanceof List)) {
return new ArrayList<>();
}
List<String> fixedList = new ArrayList<>();
for (Object key : (List<Object>)value) {
fixedList.add(key.toString());
}
return fixedList;
}
/**
* Returns config option with specified path as {@code List<String>}. Returns {@code null} if
* option is not present.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from file or null if not present
*/
public List<String> getStringList(String path) {
return getStringList(path, null);
}
/**
* Returns config option with specified path as {@code Integer}. Returns {@code null} if
* option is not present.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from file or null if not present
*/
public Integer getInt(String path) {
return getInt(path, null);
}
/**
* Returns config option with specified path as {@code List<String>}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path of the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code List<String>}
*/
public List<String> getStringList(String path, List<String> defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
if (!(value instanceof List)) {
return new ArrayList<>();
}
List<String> fixedList = new ArrayList<>();
for (Object key : (List<Object>)value) {
fixedList.add(String.valueOf(key));
}
return fixedList;
}
/**
* Returns config option with specified path as {@code Integer}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code Integer}
*/
public Integer getInt(String path, Integer defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
try{
return Integer.parseInt(value.toString());
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Returns config option with specified path as {@code Integer}. Returns {@code null} if
* option is not present.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from file or null if not present
*/
public Integer getInt(String path) {
return getInt(path, null);
}
/**
* Returns config option with specified path as {@code Boolean}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code Boolean}
*/
public boolean getBoolean(String path, boolean defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
return Boolean.parseBoolean(value.toString());
}
/**
* Returns config option with specified path as {@code Integer}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code Integer}
*/
public Integer getInt(String path, Integer defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
try{
return Integer.parseInt(value.toString());
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Returns config option with specified path as {@code Double}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code Double}
*/
public Double getDouble(String path, double defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
try {
return Double.parseDouble(value.toString());
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Returns config option with specified path as {@code Boolean}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code Boolean}
*/
public boolean getBoolean(String path, boolean defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
return Boolean.parseBoolean(value.toString());
}
/**
* Returns config option with specified path as {@code Map<K, V>}. If the option
* is not present or value is not a {@code Map}, new empty {@code Map} is returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from configuration file as {@code Map<K, V>}
*/
public <K, V> Map<K, V> getConfigurationSection(String path) {
if (path == null || path.length() == 0) return (Map<K, V>) values;
Object value = getObject(path, null);
if (value instanceof Map) {
return (Map<K, V>) value;
} else {
return new LinkedHashMap<>();
}
}
/**
* Returns config option with specified path as {@code Double}. If the option is not present
* and {@code defaultValue} is not {@code null}, value is inserted, {@link #save()} called
* and {@code defaultValue} returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param defaultValue
* Value to be inserted and returned if option is not present
* @return value from configuration file as {@code Double}
*/
public Double getDouble(String path, double defaultValue) {
Object value = getObject(path, defaultValue);
if (value == null) return defaultValue;
try {
return Double.parseDouble(value.toString());
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Returns {@code true} if the file has option with specified path, {@code false}
* if not.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return {@code true} if present, {@code false} if not
*/
public boolean hasConfigOption(String path) {
return getObject(path) != null;
}
/**
* Returns config option with specified path as {@code Map<K, V>}. If the option
* is not present or value is not a {@code Map}, new empty {@code Map} is returned.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return value from configuration file as {@code Map<K, V>}
*/
public <K, V> Map<K, V> getConfigurationSection(String path) {
if (path == null || path.length() == 0) return (Map<K, V>) values;
Object value = getObject(path, null);
if (value instanceof Map) {
return (Map<K, V>) value;
} else {
return new LinkedHashMap<>();
}
}
/**
* Sets value to the specified path and saves the file to disk by calling {@link #save()}.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param value
* Value to save
*/
public void set(String path, Object value) {
set(values, path, value);
save();
}
/**
* Returns {@code true} if the file has option with specified path, {@code false}
* if not.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @return {@code true} if present, {@code false} if not
*/
public boolean hasConfigOption(String path) {
return getObject(path) != null;
}
/**
* Sets value to the specified map with specified path and value. This is an internal method
* that correctly creates maps to separate sections using "{@code .}".
*
* @param map
* Map to insert value to
* @param path
* Path to the option with sections separated with "{@code .}"
* @param value
* Value to save
* @return the first argument to allow chaining
*/
private Map<String, Object> set(Map<String, Object> map, String path, Object value) {
if (path.contains(".")) {
String keyWord = getRealKey(map, path.split("\\.")[0]);
Object subMap = map.get(keyWord);
if (!(subMap instanceof Map)) {
subMap = new LinkedHashMap<>();
}
map.put(keyWord, set((Map<String, Object>) subMap, path.substring(keyWord.length()+1), value));
} else {
if (value == null) {
map.remove(getRealKey(map, path));
} else {
map.put(path, value);
}
}
return map;
}
/**
* Sets value to the specified path and saves the file to disk by calling {@link #save()}.
*
* @param path
* Path to the option with sections separated with "{@code .}"
* @param value
* Value to save
*/
public void set(String path, Object value) {
set(values, path, value);
save();
}
/**
* Returns the real key name without case sensitivity from map. If not found, inserted key
* is returned.
*
* @param map
* Map to check keys of
* @param key
* Key to find
* @return The real key name
*/
private String getRealKey(Map<?, ?> map, String key) {
for (Object mapKey : map.keySet()) {
if (mapKey.toString().equalsIgnoreCase(key)) return mapKey.toString();
}
return key;
}
/**
* Sets value to the specified map with specified path and value. This is an internal method
* that correctly creates maps to separate sections using "{@code .}".
*
* @param map
* Map to insert value to
* @param path
* Path to the option with sections separated with "{@code .}"
* @param value
* Value to save
* @return the first argument to allow chaining
*/
private Map<String, Object> set(Map<String, Object> map, String path, Object value) {
if (path.contains(".")) {
String keyWord = getRealKey(map, path.split("\\.")[0]);
Object subMap = map.get(keyWord);
if (!(subMap instanceof Map)) {
subMap = new LinkedHashMap<>();
}
map.put(keyWord, set((Map<String, Object>) subMap, path.substring(keyWord.length()+1), value));
} else {
if (value == null) {
map.remove(getRealKey(map, path));
} else {
map.put(path, value);
}
}
return map;
}
/**
* Detects header of a file (first lines of file starting with #).
*
* @throws IOException
* if I/O operation fails
*/
private void detectHeader() throws IOException {
header = new ArrayList<>();
for (String line : Files.readAllLines(file.toPath())) {
if (line.startsWith("#")) {
header.add(line);
} else {
break;
}
}
}
/**
* Returns the real key name without case sensitivity from map. If not found, inserted key
* is returned.
*
* @param map
* Map to check keys of
* @param key
* Key to find
* @return The real key name
*/
private String getRealKey(Map<?, ?> map, String key) {
for (Object mapKey : map.keySet()) {
if (mapKey.toString().equalsIgnoreCase(key)) return mapKey.toString();
}
return key;
}
/**
* Inserts header back into file. This is required after calling {@link #save()}, because
* it destroys the header.
*
* @throws IOException
* if I/O operation fails
*/
public void fixHeader() throws IOException {
if (header == null) return;
List<String> content = new ArrayList<>(header);
content.addAll(Files.readAllLines(file.toPath()));
Files.delete(file.toPath());
if (file.createNewFile()) {
Files.write(file.toPath(), content);
}
}
/**
* Detects header of a file (first lines of file starting with #).
*
* @throws IOException
* if I/O operation fails
*/
private void detectHeader() throws IOException {
header = new ArrayList<>();
for (String line : Files.readAllLines(file.toPath())) {
if (line.startsWith("#")) {
header.add(line);
} else {
break;
}
}
}
public File getFile() {
return file;
}
/**
* Inserts header back into file. This is required after calling {@link #save()}, because
* it destroys the header.
*
* @throws IOException
* if I/O operation fails
*/
public void fixHeader() throws IOException {
if (header == null) return;
List<String> content = new ArrayList<>(header);
content.addAll(Files.readAllLines(file.toPath()));
Files.delete(file.toPath());
if (file.createNewFile()) {
Files.write(file.toPath(), content);
}
}
public File getFile() {
return file;
}
}
@@ -23,65 +23,66 @@ import me.neznamy.yamlassist.YamlAssist;
*/
public class YamlConfigurationFile extends ConfigurationFile {
/** SnakeYAML instance */
private static final Yaml yaml;
static {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
yaml = new Yaml(options);
}
/** SnakeYAML instance */
private static final Yaml yaml;
/**
* Constructs new instance and attempts to load specified configuration file.
* If file does not exist, default file is copied from {@code source}.
*
* @param source
* Source to copy file from if it does not exist
* @param destination
* File destination to use
* @throws IllegalArgumentException
* if {@code destination} is null
* @throws IllegalStateException
* if file does not exist and source is null
* @throws YAMLException
* if file has invalid YAML syntax
* @throws IOException
* if I/O operation with the file unexpectedly fails
*/
public YamlConfigurationFile(InputStream source, File destination) throws YAMLException, IOException {
super(source, destination);
FileInputStream input = null;
try {
input = new FileInputStream(file);
values = yaml.load(input);
if (values == null) values = new LinkedHashMap<>();
input.close();
} catch (YAMLException e) {
if (input != null) input.close();
TabAPI tab = TabAPI.getInstance();
tab.sendConsoleMessage("&c[TAB] File " + destination + " has broken syntax.", true);
tab.sendConsoleMessage("&6[TAB] Error message from yaml parser: " + e.getMessage(), true);
List<String> suggestions = YamlAssist.getSuggestions(file);
if (!suggestions.isEmpty()) {
tab.sendConsoleMessage("&d[TAB] Suggestions to fix yaml syntax:", true);
for (String suggestion : suggestions) {
tab.sendConsoleMessage("&d[TAB] - " + suggestion, true);
}
}
throw e;
}
}
static {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
yaml = new Yaml(options);
}
@Override
public void save() {
try {
Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);
yaml.dump(values, writer);
writer.close();
fixHeader();
} catch (IOException e) {
TabAPI.getInstance().sendConsoleMessage("&c[TAB] Failed to save yaml file " + file.getPath() + " with content " + values.toString(), true);
}
}
/**
* Constructs new instance and attempts to load specified configuration file.
* If file does not exist, default file is copied from {@code source}.
*
* @param source
* Source to copy file from if it does not exist
* @param destination
* File destination to use
* @throws IllegalArgumentException
* if {@code destination} is null
* @throws IllegalStateException
* if file does not exist and source is null
* @throws YAMLException
* if file has invalid YAML syntax
* @throws IOException
* if I/O operation with the file unexpectedly fails
*/
public YamlConfigurationFile(InputStream source, File destination) throws YAMLException, IOException {
super(source, destination);
FileInputStream input = null;
try {
input = new FileInputStream(file);
values = yaml.load(input);
if (values == null) values = new LinkedHashMap<>();
input.close();
} catch (YAMLException e) {
if (input != null) input.close();
TabAPI tab = TabAPI.getInstance();
tab.setBrokenFile(destination.getName());
tab.sendConsoleMessage("&cFile " + destination + " has broken syntax.", true);
tab.sendConsoleMessage("&6Error message from yaml parser: " + e.getMessage(), true);
List<String> suggestions = YamlAssist.getSuggestions(file);
if (!suggestions.isEmpty()) {
tab.sendConsoleMessage("&dSuggestions to fix yaml syntax:", true);
for (String suggestion : suggestions) {
tab.sendConsoleMessage("&d- " + suggestion, true);
}
}
throw e;
}
}
@Override
public void save() {
try {
Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);
yaml.dump(values, writer);
writer.close();
fixHeader();
} catch (IOException e) {
TabAPI.getInstance().sendConsoleMessage("&cFailed to save yaml file " + file.getPath() + " with content " + values.toString(), true);
}
}
}
@@ -11,14 +11,15 @@ public interface PlayerLoadEvent extends TabEvent {
/**
* Gets the player that was loaded.
*
* @return the player that was loaded
* @return the player that was loaded
*/
TabPlayer getPlayer();
/**
* Returns true if player was loaded because they joined,
* false if the player was loaded because tab reload command was executed
* @return true if player joined, false if plugin was reloaded
*
* @return true if player joined, false if plugin was reloaded
*/
boolean isJoin();
}
@@ -2,15 +2,13 @@ package me.neznamy.tab.api.placeholder;
public interface Placeholder {
int getRefresh();
int getRefresh();
String getIdentifier();
String getIdentifier();
boolean isTriggerMode();
void enableTriggerMode(Runnable onActivation, Runnable onDisable);
void enableTriggerMode();
void unload();
void enableTriggerMode(Runnable onActivation, Runnable onDisable);
void unload();
boolean isUsed();
}
@@ -10,37 +10,44 @@ import me.neznamy.tab.api.TabPlayer;
public interface PlaceholderManager {
/**
* Registers a server placeholder (placeholder with same output for all players)
*/
ServerPlaceholder registerServerPlaceholder(String identifier, int refresh, Supplier<Object> supplier);
/**
* Registers a player placeholder (placeholder with player-specific output)
*/
PlayerPlaceholder registerPlayerPlaceholder(String identifier, int refresh, Function<TabPlayer, Object> function);
/**
* Registers a server placeholder (placeholder with same output for all players)
*/
ServerPlaceholder registerServerPlaceholder(String identifier, int refresh, Supplier<Object> supplier);
/**
* Registers a relational placeholder (different output for each player pair)
*/
RelationalPlaceholder registerRelationalPlaceholder(String identifier, int refresh, BiFunction<TabPlayer, TabPlayer, Object> function);
/**
* Detects placeholders in text using %% pattern and returns list of all detected identifiers
* @param text - text to detect placeholders in
* @return list of detected identifiers
*/
List<String> detectPlaceholders(String text);
/**
* Registers a player placeholder (placeholder with player-specific output)
*/
PlayerPlaceholder registerPlayerPlaceholder(String identifier, int refresh, Function<TabPlayer, Object> function);
/**
* Adds placeholder to list of used placeholders and assigns this feature as using it,
* which will then receive refresh() if values changes
* @param identifier - placeholder identifier
* @param feature - feature using the placeholder
*/
void addUsedPlaceholder(String identifier, TabFeature feature);
String findReplacement(String placeholder, String output);
/**
* Registers a relational placeholder (different output for each player pair)
*/
RelationalPlaceholder registerRelationalPlaceholder(String identifier, int refresh, BiFunction<TabPlayer, TabPlayer, Object> function);
List<String> getUsedPlaceholders();
/**
* Detects placeholders in text using %% pattern and returns list of all detected identifiers
*
* @param text
* text to detect placeholders in
* @return list of detected identifiers
*/
List<String> detectPlaceholders(String text);
/**
* Adds placeholder to list of used placeholders and assigns this feature as using it,
* which will then receive refresh() if values changes
*
* @param identifier
* placeholder identifier
* @param feature
* feature using the placeholder
*/
void addUsedPlaceholder(String identifier, TabFeature feature);
String findReplacement(String placeholder, String output);
List<String> getUsedPlaceholders();
Placeholder getPlaceholder(String identifier);
}
@@ -4,7 +4,25 @@ import me.neznamy.tab.api.TabPlayer;
public interface PlayerPlaceholder extends Placeholder {
void updateValue(TabPlayer player, Object value);
Object request(TabPlayer p);
void updateValue(TabPlayer player, Object value);
/**
* Calls the placeholder request function and returns the output.
* If the placeholder threw an exception, it is logged in {@code placeholder-errors.log}
* file and "ERROR" is returned.
*
* @param p
* player to get placeholder value for
* @return value placeholder returned or "ERROR" if it threw an error
*/
Object request(TabPlayer p);
/**
* Returns last known value for given player.
*
* @param player
* player to get last value for
* @return last known value for entered player
*/
String getLastValue(TabPlayer player);
}
@@ -4,7 +4,30 @@ import me.neznamy.tab.api.TabPlayer;
public interface RelationalPlaceholder extends Placeholder {
void updateValue(TabPlayer viewer, TabPlayer target, Object value);
Object request(TabPlayer viewer, TabPlayer target);
void updateValue(TabPlayer viewer, TabPlayer target, Object value);
/**
* Calls the placeholder request function and returns the output.
* If the placeholder threw an exception, it is logged in {@code placeholder-errors.log}
* file and "ERROR" is returned.
*
* @param viewer
* player looking at output of the placeholder
* @param target
* player the placeholder is displayed on
* @return value placeholder returned or "ERROR" if it threw an error
*/
Object request(TabPlayer viewer, TabPlayer target);
/**
* Returns last known value for given players. First player is viewer,
* second player is target.
*
* @param viewer
* viewer of the placeholder
* @param target
* target who is the text displayed on
* @return last known value for entered player duo
*/
String getLastValue(TabPlayer viewer, TabPlayer target);
}
@@ -2,7 +2,21 @@ package me.neznamy.tab.api.placeholder;
public interface ServerPlaceholder extends Placeholder {
void updateValue(Object value);
Object request();
void updateValue(Object value);
/**
* Calls the placeholder request function and returns the output.
* If the placeholder threw an exception, it is logged in {@code placeholder-errors.log}
* file and "ERROR" is returned.
*
* @return value placeholder returned or "ERROR" if it threw an error
*/
Object request();
/**
* Returns last known value of the placeholder.
*
* @return last known value
*/
String getLastValue();
}
@@ -8,233 +8,262 @@ import me.neznamy.tab.api.chat.EnumChatFormat;
import me.neznamy.tab.api.chat.IChatBaseComponent;
import me.neznamy.tab.api.chat.rgb.RGBUtils;
import me.neznamy.tab.api.util.BiFunctionWithException;
import me.neznamy.tab.api.util.Preconditions;
/**
* An interface representing a platform-specific packet builder
* A class for packet building, methods are overridden in
* platform-specific builders which extend this class to build actual packets.
* Packets which don't have a method override are forwarded into
* sendPacket method, where they are handled using platform's API instead.
*/
public abstract class PacketBuilder {
public class PacketBuilder {
/** Function map turning custom packet class into platform-specific packets */
protected final Map<Class<? extends TabPacket>, BiFunctionWithException<TabPacket, ProtocolVersion, Object>> buildMap = new HashMap<>();
/** Function map turning custom packet class into platform-specific packets */
protected final Map<Class<? extends TabPacket>, BiFunctionWithException<TabPacket, ProtocolVersion, Object>> buildMap = new HashMap<>();
/**
* Constructs new instance and fills {@link #buildMap} with abstract build methods
* of packets present on all platforms.
*/
protected PacketBuilder() {
buildMap.put(PacketPlayOutBoss.class, (packet, version) -> build((PacketPlayOutBoss)packet, version));
buildMap.put(PacketPlayOutChat.class, (packet, version) -> build((PacketPlayOutChat)packet, version));
buildMap.put(PacketPlayOutPlayerInfo.class, (packet, version) -> build((PacketPlayOutPlayerInfo)packet, version));
buildMap.put(PacketPlayOutPlayerListHeaderFooter.class, (packet, version) -> build((PacketPlayOutPlayerListHeaderFooter)packet, version));
buildMap.put(PacketPlayOutScoreboardDisplayObjective.class, (packet, version) -> build((PacketPlayOutScoreboardDisplayObjective)packet, version));
buildMap.put(PacketPlayOutScoreboardObjective.class, (packet, version) -> build((PacketPlayOutScoreboardObjective)packet, version));
buildMap.put(PacketPlayOutScoreboardScore.class, (packet, version) -> build((PacketPlayOutScoreboardScore)packet, version));
buildMap.put(PacketPlayOutScoreboardTeam.class, (packet, version) -> build((PacketPlayOutScoreboardTeam)packet, version));
}
/**
* Constructs new instance and fills {@link #buildMap} with abstract build methods
* of packets present on all platforms.
*/
public PacketBuilder() {
buildMap.put(PacketPlayOutBoss.class, (packet, version) -> build((PacketPlayOutBoss)packet, version));
buildMap.put(PacketPlayOutChat.class, (packet, version) -> build((PacketPlayOutChat)packet, version));
buildMap.put(PacketPlayOutPlayerInfo.class, (packet, version) -> build((PacketPlayOutPlayerInfo)packet, version));
buildMap.put(PacketPlayOutPlayerListHeaderFooter.class, (packet, version) -> build((PacketPlayOutPlayerListHeaderFooter)packet, version));
buildMap.put(PacketPlayOutScoreboardDisplayObjective.class, (packet, version) -> build((PacketPlayOutScoreboardDisplayObjective)packet, version));
buildMap.put(PacketPlayOutScoreboardObjective.class, (packet, version) -> build((PacketPlayOutScoreboardObjective)packet, version));
buildMap.put(PacketPlayOutScoreboardScore.class, (packet, version) -> build((PacketPlayOutScoreboardScore)packet, version));
buildMap.put(PacketPlayOutScoreboardTeam.class, (packet, version) -> build((PacketPlayOutScoreboardTeam)packet, version));
}
/**
* Converts custom packet into platform-specific packet by calling a function from
* {@link #buildMap}.
*
* @param packet
* Packet to build
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if reflection fails
*/
public Object build(TabPacket packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return buildMap.get(packet.getClass()).apply(packet, clientVersion);
}
/**
* Converts custom packet into platform-specific packet by calling a function from
* {@link #buildMap}.
*
* @param packet
* Packet to build
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if reflection fails
*/
public Object build(TabPacket packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
Preconditions.checkNotNull(packet, "packet");
Preconditions.checkNotNull(clientVersion, "clientVersion");
return buildMap.get(packet.getClass()).apply(packet, clientVersion);
}
/**
* Constructs platform-specific PacketPlayOutBoss class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract Object build(PacketPlayOutBoss packet, ProtocolVersion clientVersion) throws ReflectiveOperationException;
/**
* Constructs platform-specific PacketPlayOutBoss class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public Object build(PacketPlayOutBoss packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return packet;
}
/**
* Constructs platform-specific PacketPlayOutChat class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract Object build(PacketPlayOutChat packet, ProtocolVersion clientVersion) throws ReflectiveOperationException;
/**
* Constructs platform-specific PacketPlayOutChat class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public Object build(PacketPlayOutChat packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return packet;
}
/**
* Constructs platform-specific PacketPlayOutPlayerInfo class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract Object build(PacketPlayOutPlayerInfo packet, ProtocolVersion clientVersion) throws ReflectiveOperationException;
/**
* Constructs platform-specific PacketPlayOutPlayerInfo class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public Object build(PacketPlayOutPlayerInfo packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return packet;
}
/**
* Constructs platform-specific PacketPlayOutPlayerListHeaderFooter class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract Object build(PacketPlayOutPlayerListHeaderFooter packet, ProtocolVersion clientVersion) throws ReflectiveOperationException;
/**
* Constructs platform-specific PacketPlayOutPlayerListHeaderFooter class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public Object build(PacketPlayOutPlayerListHeaderFooter packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return packet;
}
/**
* Constructs platform-specific PacketPlayOutScoreboardDisplayObjective class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract Object build(PacketPlayOutScoreboardDisplayObjective packet, ProtocolVersion clientVersion) throws ReflectiveOperationException;
/**
* Constructs platform-specific PacketPlayOutScoreboardDisplayObjective class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public Object build(PacketPlayOutScoreboardDisplayObjective packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return packet;
}
/**
* Constructs platform-specific PacketPlayOutScoreboardObjective class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract Object build(PacketPlayOutScoreboardObjective packet, ProtocolVersion clientVersion) throws ReflectiveOperationException;
/**
* Constructs platform-specific PacketPlayOutScoreboardObjective class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public Object build(PacketPlayOutScoreboardObjective packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return packet;
}
/**
* Constructs platform-specific PacketPlayOutScoreboardScore class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract Object build(PacketPlayOutScoreboardScore packet, ProtocolVersion clientVersion) throws ReflectiveOperationException;
/**
* Constructs platform-specific PacketPlayOutScoreboardScore class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public Object build(PacketPlayOutScoreboardScore packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return packet;
}
/**
* Constructs platform-specific PacketPlayOutScoreboardTeam class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract Object build(PacketPlayOutScoreboardTeam packet, ProtocolVersion clientVersion) throws ReflectiveOperationException;
/**
* Constructs platform-specific PacketPlayOutScoreboardTeam class based on custom packet class
*
* @param packet
* Custom packet to be built
* @param clientVersion
* Protocol version of player to build the packet for
* @return Platform-specific packet
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public Object build(PacketPlayOutScoreboardTeam packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return packet;
}
/**
* Cuts given string to specified character length (or length-1 if last character is a color character)
* and translates RGB to legacy colors. If string is not that long, the original string is returned.
* RGB codes are converted into legacy, since cutting is only needed for <1.13.
* If {@code string} is {@code null}, empty string is returned.
* @param string
* String to cut
* @param length
* Length to cut to
* @return string cut to {@code length} characters
*/
public String cutTo(String string, int length) {
if (string == null) return "";
String legacyText = string;
if (string.contains("#")) {
//converting RGB to legacy colors
legacyText = RGBUtils.getInstance().convertRGBtoLegacy(string);
}
if (legacyText.length() <= length) return legacyText;
if (legacyText.charAt(length-1) == EnumChatFormat.COLOR_CHAR) {
return legacyText.substring(0, length-1); //cutting one extra character to prevent prefix ending with "&"
} else {
return legacyText.substring(0, length);
}
}
/**
* Cuts given string to specified character length (or length-1 if last character is a color character)
* and translates RGB to legacy colors. If string is not that long, the original string is returned.
* RGB codes are converted into legacy, since cutting is only needed for &lt;1.13.
* If {@code string} is {@code null}, empty string is returned.
*
* @param string
* String to cut
* @param length
* Length to cut to
* @return string cut to {@code length} characters
*/
public String cutTo(String string, int length) {
if (string == null) return "";
String legacyText = string;
if (string.contains("#")) {
//converting RGB to legacy colors
legacyText = RGBUtils.getInstance().convertRGBtoLegacy(string);
}
if (legacyText.length() <= length) return legacyText;
if (legacyText.charAt(length-1) == EnumChatFormat.COLOR_CHAR) {
return legacyText.substring(0, length-1); //cutting one extra character to prevent prefix ending with "&"
} else {
return legacyText.substring(0, length);
}
}
/**
* If {@code clientVersion} is >= 1.13, creates a component from given text and returns
* it as a serialized component, which BungeeCord uses.
* <p>
* If {@code clientVersion} is < 1.12, the text is cut to {@code length} characters if
* needed and returned.
*
* @param text
* Text to convert
* @param clientVersion
* Version of player to convert text for
* @return serialized component for 1.13+ clients, cut string for 1.12-
*/
public String jsonOrCut(String text, ProtocolVersion clientVersion, int length) {
if (text == null) return null;
if (clientVersion.getMinorVersion() >= 13) {
return IChatBaseComponent.optimizedComponent(text).toString(clientVersion);
} else {
return cutTo(text, length);
}
}
/**
* If {@code clientVersion} is &gt;= 1.13, creates a component from given text and returns
* it as a serialized component, which BungeeCord uses.
* <p>
* If {@code clientVersion} is &lt; 1.12, the text is cut to {@code length} characters if
* needed and returned.
*
* @param text
* Text to convert
* @param clientVersion
* Version of player to convert text for
* @return serialized component for 1.13+ clients, cut string for 1.12-
*/
public String jsonOrCut(String text, ProtocolVersion clientVersion, int length) {
if (text == null) return null;
if (clientVersion.getMinorVersion() >= 13) {
return IChatBaseComponent.optimizedComponent(text).toString(clientVersion);
} else {
return cutTo(text, length);
}
}
/**
* Converts platform-specific instance of player info packet into
* {@link PacketPlayOutPlayerInfo} object.
*
* @param packet
* platform-specific info packet
* @param clientVersion
* Version of client receiving the packet
* @return The packet converted into {@link PacketPlayOutPlayerInfo}
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract PacketPlayOutPlayerInfo readPlayerInfo(Object packet, ProtocolVersion clientVersion) throws ReflectiveOperationException;
/**
* Converts platform-specific instance of player info packet into
* {@link PacketPlayOutPlayerInfo} object.
*
* @param packet
* platform-specific info packet
* @param clientVersion
* Version of client receiving the packet
* @return The packet converted into {@link PacketPlayOutPlayerInfo}
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public PacketPlayOutPlayerInfo readPlayerInfo(Object packet, ProtocolVersion clientVersion) throws ReflectiveOperationException {
return null;
}
/**
* Converts platform-specific instance of objective packet into
* {@link PacketPlayOutScoreboardObjective} object.
*
* @param packet
* platform-specific objective packet
* @return The packet converted into {@link PacketPlayOutScoreboardObjective}
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract PacketPlayOutScoreboardObjective readObjective(Object packet) throws ReflectiveOperationException;
/**
* Converts platform-specific instance of objective packet into
* {@link PacketPlayOutScoreboardObjective} object.
*
* @param packet
* platform-specific objective packet
* @return The packet converted into {@link PacketPlayOutScoreboardObjective}
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public PacketPlayOutScoreboardObjective readObjective(Object packet) throws ReflectiveOperationException {
return null;
}
/**
* Converts platform-specific instance of display objective packet into
* {@link PacketPlayOutScoreboardDisplayObjective} object.
*
* @param packet
* platform-specific display objective packet
* @return The packet converted into {@link PacketPlayOutScoreboardDisplayObjective}
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public abstract PacketPlayOutScoreboardDisplayObjective readDisplayObjective(Object packet) throws ReflectiveOperationException;
/**
* Converts platform-specific instance of display objective packet into
* {@link PacketPlayOutScoreboardDisplayObjective} object.
*
* @param packet
* platform-specific display objective packet
* @return The packet converted into {@link PacketPlayOutScoreboardDisplayObjective}
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
public PacketPlayOutScoreboardDisplayObjective readDisplayObjective(Object packet) throws ReflectiveOperationException {
return null;
}
}
@@ -4,277 +4,299 @@ import java.util.UUID;
import me.neznamy.tab.api.bossbar.BarColor;
import me.neznamy.tab.api.bossbar.BarStyle;
import me.neznamy.tab.api.util.Preconditions;
/**
* A class representing platform specific packet class
*/
public class PacketPlayOutBoss implements TabPacket {
/** UUID of the BossBar */
private final UUID id;
/** UUID of the BossBar */
private final UUID id;
/** Action of this packet */
private final Action operation;
/** Action of this packet */
private final Action action;
/** BossBar title */
private String name;
/** BossBar title */
private String name;
/** BossBar progress (0-1)*/
private float pct;
/** BossBar progress (0-1)*/
private float pct;
/** BossBar color */
private BarColor color;
/** BossBar color */
private BarColor color;
/** BossBar style */
private BarStyle overlay;
/** BossBar style */
private BarStyle overlay;
/** Darken screen flag */
private boolean darkenScreen;
/** Darken screen flag */
private boolean darkenScreen;
/** Play music flag */
private boolean playMusic;
/** Play music flag */
private boolean playMusic;
/** Create fog flag */
private boolean createWorldFog;
/** Create fog flag */
private boolean createWorldFog;
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.ADD action
*
* @param id
* BossBar uuid
* @param name
* BossBar title
* @param pct
* BossBar progress
* @param color
* BossBar color
* @param overlay
* BossBar style
*/
public PacketPlayOutBoss(UUID id, String name, float pct, BarColor color, BarStyle overlay) {
this.operation = Action.ADD;
this.id = id;
this.name = name;
this.pct = pct;
this.color = color;
this.overlay = overlay;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.ADD action
*
* @param id
* BossBar uuid
* @param name
* BossBar title
* @param pct
* BossBar progress
* @param color
* BossBar color
* @param overlay
* BossBar style
*/
public PacketPlayOutBoss(UUID id, String name, float pct, BarColor color, BarStyle overlay) {
Preconditions.checkNotNull(id, "uuid");
Preconditions.checkNotNull(name, "title");
Preconditions.checkNotNull(color, "color");
Preconditions.checkNotNull(overlay, "style");
this.action = Action.ADD;
this.id = id;
this.name = name;
this.pct = pct;
this.color = color;
this.overlay = overlay;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.REMOVE action
*
* @param id
* BossBar uuid
*/
public PacketPlayOutBoss(UUID id) {
this.operation = Action.REMOVE;
this.id = id;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.REMOVE action
*
* @param id
* BossBar uuid
*/
public PacketPlayOutBoss(UUID id) {
Preconditions.checkNotNull(id, "uuid");
this.action = Action.REMOVE;
this.id = id;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.UPDATE_PCT action
*
* @param id
* BossBar uuid
* @param pct
* BossBar progress
*/
public PacketPlayOutBoss(UUID id, float pct) {
this.operation = Action.UPDATE_PCT;
this.id = id;
this.pct = pct;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.UPDATE_PCT action
*
* @param id
* BossBar uuid
* @param pct
* BossBar progress
*/
public PacketPlayOutBoss(UUID id, float pct) {
Preconditions.checkNotNull(id, "uuid");
this.action = Action.UPDATE_PCT;
this.id = id;
this.pct = pct;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.UPDATE_NAME action
*
* @param id
* BossBar uuid
* @param name
* BossBar title
*/
public PacketPlayOutBoss(UUID id, String name) {
this.operation = Action.UPDATE_NAME;
this.id = id;
this.name = name;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.UPDATE_NAME action
*
* @param id
* BossBar uuid
* @param name
* BossBar title
*/
public PacketPlayOutBoss(UUID id, String name) {
Preconditions.checkNotNull(id, "uuid");
Preconditions.checkNotNull(name, "title");
this.action = Action.UPDATE_NAME;
this.id = id;
this.name = name;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.UPDATE_STYLE action
*
* @param id
* BossBar uuid
* @param color
* BossBar color
* @param overlay
* BossBar style
*/
public PacketPlayOutBoss(UUID id, BarColor color, BarStyle overlay) {
this.operation = Action.UPDATE_STYLE;
this.id = id;
this.color = color;
this.overlay = overlay;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.UPDATE_STYLE action
*
* @param id
* BossBar uuid
* @param color
* BossBar color
* @param overlay
* BossBar style
*/
public PacketPlayOutBoss(UUID id, BarColor color, BarStyle overlay) {
Preconditions.checkNotNull(id, "uuid");
Preconditions.checkNotNull(color, "color");
Preconditions.checkNotNull(overlay, "style");
this.action = Action.UPDATE_STYLE;
this.id = id;
this.color = color;
this.overlay = overlay;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.UPDATE_PROPERTIES action
*
* @param darkenScreen
* Darken screen flag
* @param playMusic
* Play music flag
* @param createWorldFog
* Create fog flag
*/
public PacketPlayOutBoss(UUID id, boolean darkenScreen, boolean playMusic, boolean createWorldFog) {
this.operation = Action.UPDATE_PROPERTIES;
this.id = id;
this.darkenScreen = darkenScreen;
this.playMusic = playMusic;
this.createWorldFog = createWorldFog;
}
/**
* Constructs new instance with given parameters and
* {@link me.neznamy.tab.api.protocol.PacketPlayOutBoss.Action}.UPDATE_PROPERTIES action
*
* @param darkenScreen
* Darken screen flag
* @param playMusic
* Play music flag
* @param createWorldFog
* Create fog flag
*/
public PacketPlayOutBoss(UUID id, boolean darkenScreen, boolean playMusic, boolean createWorldFog) {
Preconditions.checkNotNull(id, "uuid");
this.action = Action.UPDATE_PROPERTIES;
this.id = id;
this.darkenScreen = darkenScreen;
this.playMusic = playMusic;
this.createWorldFog = createWorldFog;
}
/**
* Returns bitmask based on {@link #darkenScreen}, {@link #playMusic} and {@link #darkenScreen} values.
* <p>
* {@link #darkenScreen} adds {@code 1}, {@link #playMusic} {@code 2} and {@link #darkenScreen} {@code 4}
* to the final value.
*
* @return the bitmask
*/
public byte getFlags(){
byte value = 0;
if (darkenScreen) value += 1;
if (playMusic) value += 2;
if (createWorldFog) value += 4;
return value;
}
/**
* Returns bitmask based on {@link #darkenScreen}, {@link #playMusic} and {@link #darkenScreen} values.
* <p>
* {@link #darkenScreen} adds {@code 1}, {@link #playMusic} {@code 2} and {@link #darkenScreen} {@code 4}
* to the final value.
*
* @return the bitmask
*/
public byte getFlags(){
byte value = 0;
if (darkenScreen) value += 1;
if (playMusic) value += 2;
if (createWorldFog) value += 4;
return value;
}
@Override
public String toString() {
return String.format("PacketPlayOutBoss{id=%s,operation=%s,name=%s,pct=%s,color=%s,overlay=%s,darkenScreen=%s,playMusic=%s,createWorldFog=%s}",
id, operation, name, pct, color, overlay, darkenScreen, playMusic, createWorldFog);
}
@Override
public String toString() {
return String.format("PacketPlayOutBoss{id=%s,operation=%s,name=%s,pct=%s,color=%s,overlay=%s,darkenScreen=%s,playMusic=%s,createWorldFog=%s}",
id, action, name, pct, color, overlay, darkenScreen, playMusic, createWorldFog);
}
/**
* Returns {@link #color}
* @return color
*/
public BarColor getColor() {
return color;
}
/**
* Returns {@link #color}
*
* @return color
*/
public BarColor getColor() {
return color;
}
/**
* Returns {@link #overlay}
* @return style
*/
public BarStyle getOverlay() {
return overlay;
}
/**
* Returns {@link #overlay}
*
* @return style
*/
public BarStyle getOverlay() {
return overlay;
}
/**
* Returns {@link #name}
* @return name
*/
public String getName() {
return name;
}
/**
* Returns {@link #name}
*
* @return name
*/
public String getName() {
return name;
}
/**
* Returns {@link #id}
* @return id
*/
public UUID getId() {
return id;
}
/**
* Returns {@link #id}
*
* @return id
*/
public UUID getId() {
return id;
}
/**
* Returns {@link #pct}
* @return progress
*/
public float getPct() {
return pct;
}
/**
* Returns {@link #pct}
*
* @return progress
*/
public float getPct() {
return pct;
}
/**
* Returns {@link #operation}
* @return packet action
*/
public Action getOperation() {
return operation;
}
/**
* Returns {@link #action}
*
* @return packet action
*/
public Action getAction() {
return action;
}
/**
* Returns {@link #darkenScreen}
* @return darkenScreen
*/
public boolean isDarkenScreen() {
return darkenScreen;
}
/**
* Returns {@link #darkenScreen}
*
* @return darkenScreen
*/
public boolean isDarkenScreen() {
return darkenScreen;
}
/**
* Returns {@link #createWorldFog}
* @return createWorldFog
*/
public boolean isCreateWorldFog() {
return createWorldFog;
}
/**
* Returns {@link #createWorldFog}
*
* @return createWorldFog
*/
public boolean isCreateWorldFog() {
return createWorldFog;
}
/**
* Returns {@link #playMusic}
* @return playMusic
*/
public boolean isPlayMusic() {
return playMusic;
}
/**
* Returns {@link #playMusic}
*
* @return playMusic
*/
public boolean isPlayMusic() {
return playMusic;
}
/**
* Sets {@link #darkenScreen} to specified value
*
* @param darkenScreen
* Darken screen flag
*/
public void setDarkenScreen(boolean darkenScreen) {
this.darkenScreen = darkenScreen;
}
/**
* Sets {@link #darkenScreen} to specified value
*
* @param darkenScreen
* Darken screen flag
*/
public void setDarkenScreen(boolean darkenScreen) {
this.darkenScreen = darkenScreen;
}
/**
* Sets {@link #createWorldFog} to specified value
*
* @param createWorldFog
* Create fog flag
*/
public void setCreateWorldFog(boolean createWorldFog) {
this.createWorldFog = createWorldFog;
}
/**
* Sets {@link #createWorldFog} to specified value
*
* @param createWorldFog
* Create fog flag
*/
public void setCreateWorldFog(boolean createWorldFog) {
this.createWorldFog = createWorldFog;
}
/**
* Sets {@link #playMusic} to specified value
*
* @param playMusic
* Play music flag
*/
public void setPlayMusic(boolean playMusic) {
this.playMusic = playMusic;
}
/**
* Sets {@link #playMusic} to specified value
*
* @param playMusic
* Play music flag
*/
public void setPlayMusic(boolean playMusic) {
this.playMusic = playMusic;
}
/**
* An enum representing all valid boss packet actions.
* Calling ordinal() will return action's network ID.
*/
public enum Action {
/**
* An enum representing all valid boss packet actions.
* Calling ordinal() will return action's network ID.
*/
public enum Action {
ADD,
REMOVE,
UPDATE_PCT,
UPDATE_NAME,
UPDATE_STYLE,
UPDATE_PROPERTIES
}
ADD,
REMOVE,
UPDATE_PCT,
UPDATE_NAME,
UPDATE_STYLE,
UPDATE_PROPERTIES
}
}
@@ -1,60 +1,65 @@
package me.neznamy.tab.api.protocol;
import me.neznamy.tab.api.chat.IChatBaseComponent;
import me.neznamy.tab.api.util.Preconditions;
/**
* A class representing platform specific packet class
*/
public class PacketPlayOutChat implements TabPacket {
/** Message to be sent */
private final IChatBaseComponent message;
/** Message to be sent */
private final IChatBaseComponent message;
/** Message position */
private final ChatMessageType type;
/** Message position */
private final ChatMessageType type;
/**
* Constructs new instance with given parameters
*
* @param message
* Chat message to be sent
* @param type
* Message position
*/
public PacketPlayOutChat(IChatBaseComponent message, ChatMessageType type) {
this.message = message;
this.type = type;
}
/**
* Constructs new instance with given parameters
*
* @param message
* Chat message to be sent
* @param type
* Message position
*/
public PacketPlayOutChat(IChatBaseComponent message, ChatMessageType type) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkNotNull(type, "message type");
this.message = message;
this.type = type;
}
@Override
public String toString() {
return String.format("PacketPlayOutChat{message=%s,type=%s}", message, type);
}
@Override
public String toString() {
return String.format("PacketPlayOutChat{message=%s,type=%s}", message, type);
}
/**
* Returns {@link #message}
* @return message
*/
public IChatBaseComponent getMessage() {
return message;
}
/**
* Returns {@link #message}
*
* @return message
*/
public IChatBaseComponent getMessage() {
return message;
}
/**
* Returns {@link #type}
* @return type
*/
public ChatMessageType getType() {
return type;
}
/**
* Returns {@link #type}
*
* @return type
*/
public ChatMessageType getType() {
return type;
}
/**
* An enum representing positions of a chat message
* Calling ordinal() will return type's network ID.
*/
public enum ChatMessageType {
/**
* An enum representing positions of a chat message
* Calling ordinal() will return type's network ID.
*/
public enum ChatMessageType {
CHAT,
SYSTEM,
GAME_INFO
}
CHAT,
SYSTEM,
GAME_INFO
}
}
@@ -1,305 +1,440 @@
package me.neznamy.tab.api.protocol;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.UUID;
import me.neznamy.tab.api.chat.IChatBaseComponent;
import me.neznamy.tab.api.util.Preconditions;
/**
* A class representing platform specific packet class
*/
public class PacketPlayOutPlayerInfo implements TabPacket {
/** Packet action */
private final EnumPlayerInfoAction action;
/** Packet actions */
private final EnumSet<EnumPlayerInfoAction> actions;
/** List of affected entries */
private final List<PlayerInfoData> entries;
/** List of affected entries */
private final List<PlayerInfoData> entries;
/**
* Constructs new instance with given parameters.
*
* @param action
* Packet action
* @param entries
* Affected entries
*/
public PacketPlayOutPlayerInfo(EnumPlayerInfoAction action, PlayerInfoData... entries) {
this.action = action;
this.entries = Arrays.asList(entries);
}
/**
* Constructs new instance with given parameters.
*
* @param action
* Packet action
* @param entries
* Affected entries
*/
public PacketPlayOutPlayerInfo(EnumPlayerInfoAction action, PlayerInfoData... entries) {
this(action, Arrays.asList(entries));
}
/**
* Constructs new instance with given parameters.
*
* @param action
* Packet action
* @param entries
* Affected entries
*/
public PacketPlayOutPlayerInfo(EnumPlayerInfoAction action, List<PlayerInfoData> entries) {
this.action = action;
this.entries = entries;
}
/**
* Constructs new instance with given parameters.
*
* @param action
* Packet action
* @param entries
* Affected entries
*/
public PacketPlayOutPlayerInfo(EnumPlayerInfoAction action, List<PlayerInfoData> entries) {
Preconditions.checkNotNull(action, "action");
Preconditions.checkNotNull(entries, "entries");
if (action == EnumPlayerInfoAction.ADD_PLAYER) {
actions = EnumSet.of(EnumPlayerInfoAction.ADD_PLAYER,
EnumPlayerInfoAction.INITIALIZE_CHAT,
EnumPlayerInfoAction.UPDATE_GAME_MODE,
EnumPlayerInfoAction.UPDATE_LISTED,
EnumPlayerInfoAction.UPDATE_LATENCY,
EnumPlayerInfoAction.UPDATE_DISPLAY_NAME);
} else {
this.actions = EnumSet.of(action);
}
this.entries = entries;
}
@Override
public String toString() {
return String.format("PacketPlayOutPlayerInfo{action=%s,entries=%s}", action, entries);
}
/**
* Constructs new instance with given parameters.
*
* @param actions
* Packet actions
* @param entries
* Affected entries
*/
public PacketPlayOutPlayerInfo(EnumSet<EnumPlayerInfoAction> actions, PlayerInfoData... entries) {
Preconditions.checkNotNull(actions, "action");
Preconditions.checkNotNull(entries, "entries");
this.actions = actions;
this.entries = Arrays.asList(entries);
}
/**
* Returns {@link #action}
* @return packet action
*/
public EnumPlayerInfoAction getAction() {
return action;
}
/**
* Constructs new instance with given parameters.
*
* @param actions
* Packet actions
* @param entries
* Affected entries
*/
public PacketPlayOutPlayerInfo(EnumSet<EnumPlayerInfoAction> actions, List<PlayerInfoData> entries) {
Preconditions.checkNotNull(actions, "action");
Preconditions.checkNotNull(entries, "entries");
this.actions = actions;
this.entries = entries;
}
/**
* Returns {@link #entries}
* @return affected entries
*/
public List<PlayerInfoData> getEntries() {
return entries;
}
@Override
public String toString() {
return String.format("PacketPlayOutPlayerInfo{actions=%s,entries=%s}", actions, entries);
}
/**
* A subclass representing player list entry
*/
public static class PlayerInfoData {
/**
* Returns {@link #actions}
*
* @return packet action
*/
public EnumSet<EnumPlayerInfoAction> getActions() {
return actions;
}
/** Latency */
private int latency;
/**
* Returns {@link #entries}
*
* @return affected entries
*/
public List<PlayerInfoData> getEntries() {
return entries;
}
/** GameMode */
private EnumGamemode gameMode = EnumGamemode.SURVIVAL; //ProtocolLib causes NPE even when action does not use GameMode
/**
* A subclass representing player list entry
*/
public static class PlayerInfoData {
/**
* Display name displayed in TabList. Using {@code null} results in no display name
* and scoreboard team prefix/suffix being visible in TabList instead.
*/
private IChatBaseComponent displayName;
/** Latency */
private int latency;
/** Real name of affected player */
private String name;
/** I have no idea what this is */
private boolean listed;
/** Player UUID */
private UUID uniqueId;
/** GameMode */
private EnumGamemode gameMode = EnumGamemode.SURVIVAL; //ProtocolLib causes NPE even when action does not use GameMode
/** platform-specific skin object */
private Object skin;
/**
* Display name displayed in TabList. Using {@code null} results in no display name
* and scoreboard team prefix/suffix being visible in TabList instead.
*/
private IChatBaseComponent displayName;
/**
* Constructs new instance with given parameters. Suitable for
* {@link EnumPlayerInfoAction}.ADD_PLAYER action
*
* @param name
* Player's name
* @param uniqueId
* Player's uuid
* @param skin
* Player's platform-specific skin object
* @param latency
* Player's ping
* @param gameMode
* Player's GameMode
* @param displayName
* Player's display name
*/
public PlayerInfoData(String name, UUID uniqueId, Object skin, int latency, EnumGamemode gameMode, IChatBaseComponent displayName) {
this.name = name;
this.uniqueId = uniqueId;
this.skin = skin;
this.latency = latency;
this.gameMode = gameMode;
this.displayName = displayName;
}
/** Real name of affected player */
private String name;
/**
* Constructs new instance with given parameters. Suitable for
* {@link EnumPlayerInfoAction}.UPDATE_GAME_MODE action
*
* @param uniqueId
* Player's uuid
* @param gameMode
* Player's GameMode
*/
public PlayerInfoData(UUID uniqueId, EnumGamemode gameMode) {
this.uniqueId = uniqueId;
this.gameMode = gameMode;
}
/** Player UUID */
private UUID uniqueId;
/**
* Constructs new instance with given parameters. Suitable for
* {@link EnumPlayerInfoAction}.UPDATE_LATENCY action
*
* @param uniqueId
* Player's uuid
* @param latency
* Player's ping
*/
public PlayerInfoData(UUID uniqueId, int latency) {
this.uniqueId = uniqueId;
this.latency = latency;
}
/** Player's skin, null for empty skin */
private Skin skin;
/**
* Constructs new instance with given parameters. Suitable for
* {@link EnumPlayerInfoAction}.UPDATE_DISPLAY_NAME action
*
* @param uniqueId
* Player's uuid
* @param displayName
* Player's display name
*/
public PlayerInfoData(UUID uniqueId, IChatBaseComponent displayName) {
this.uniqueId = uniqueId;
this.displayName = displayName;
}
/** Chat session ID */
private UUID chatSessionId;
/**
* Constructs new instance with given parameter. Suitable for
* {@link EnumPlayerInfoAction}.REMOVE_PLAYER action
*
* @param uniqueId
* Player's uuid
*/
public PlayerInfoData(UUID uniqueId) {
this.uniqueId = uniqueId;
}
/** Player's chat signing key */
private Object profilePublicKey;
@Override
public String toString() {
return String.format("PlayerInfoData{latency=%s,gameMode=%s,displayName=%s,name=%s,uniqueId=%s,skin=%s}",
latency, gameMode, displayName, name, uniqueId, skin);
}
/**
* Constructs new instance with given parameters. Suitable for
* {@link EnumPlayerInfoAction#ADD_PLAYER} action
*
* @param name
* Player's name
* @param uniqueId
* Player's uuid
* @param skin
* Player's platform-specific skin object
* @param latency
* Player's ping
* @param gameMode
* Player's GameMode
* @param displayName
* Player's display name
* @param profilePublicKey
* Player's chat signing key
*/
public PlayerInfoData(String name, UUID uniqueId, Skin skin, boolean listed, int latency, EnumGamemode gameMode, IChatBaseComponent displayName, UUID chatSessionId, Object profilePublicKey) {
Preconditions.checkNotNull(uniqueId, "uuid");
this.name = name;
this.uniqueId = uniqueId;
this.skin = skin;
this.listed = listed;
this.latency = latency;
this.gameMode = gameMode;
this.displayName = displayName;
this.chatSessionId = chatSessionId;
this.profilePublicKey = profilePublicKey;
}
/**
* Returns {@link #latency}
* @return latency
*/
public int getLatency() {
return latency;
}
/**
* Constructs new instance with given parameters. Suitable for
* {@link EnumPlayerInfoAction#UPDATE_GAME_MODE} action
*
* @param uniqueId
* Player's uuid
* @param gameMode
* Player's GameMode
*/
public PlayerInfoData(UUID uniqueId, EnumGamemode gameMode) {
Preconditions.checkNotNull(uniqueId, "uuid");
this.uniqueId = uniqueId;
this.gameMode = gameMode;
}
/**
* Sets {@link #latency} to specified value
* @param latency
* Latency to use
*/
public void setLatency(int latency) {
this.latency = latency;
}
/**
* Constructs new instance with given parameters. Suitable for
* {@link EnumPlayerInfoAction#UPDATE_LATENCY} action
*
* @param uniqueId
* Player's uuid
* @param latency
* Player's ping
*/
public PlayerInfoData(UUID uniqueId, int latency) {
Preconditions.checkNotNull(uniqueId, "uuid");
this.uniqueId = uniqueId;
this.latency = latency;
}
/**
* Returns {@link #displayName}
* @return displayName
*/
public IChatBaseComponent getDisplayName() {
return displayName;
}
/**
* Constructs new instance with given parameters. Suitable for
* {@link EnumPlayerInfoAction#UPDATE_DISPLAY_NAME} action
*
* @param uniqueId
* Player's uuid
* @param displayName
* Player's display name
*/
public PlayerInfoData(UUID uniqueId, IChatBaseComponent displayName) {
Preconditions.checkNotNull(uniqueId, "uuid");
this.uniqueId = uniqueId;
this.displayName = displayName;
}
/**
* Sets {@link #displayName} to specified value
* @param displayName
* Display name to use
*/
public void setDisplayName(IChatBaseComponent displayName) {
this.displayName = displayName;
}
/**
* Constructs new instance with given parameter. Suitable for
* {@link EnumPlayerInfoAction#REMOVE_PLAYER} action
*
* @param uniqueId
* Player's uuid
*/
public PlayerInfoData(UUID uniqueId) {
this.uniqueId = uniqueId;
}
/**
* Returns {@link #uniqueId}
* @return uniqueId
*/
public UUID getUniqueId() {
return uniqueId;
}
@Override
public String toString() {
return String.format("PlayerInfoData{latency=%s,gameMode=%s,displayName=%s,name=%s,uniqueId=%s,skin=%s,profilePublicKey=%s}",
latency, gameMode, displayName, name, uniqueId, skin, profilePublicKey);
}
/**
* Sets {@link #uniqueId} to specified value
* @param uniqueId
* UUID to use
*/
public void setUniqueId(UUID uniqueId) {
this.uniqueId = uniqueId;
}
/**
* Returns {@link #latency}
*
* @return latency
*/
public int getLatency() {
return latency;
}
/**
* Returns {@link #name}
* @return name
*/
public String getName() {
return name;
}
/**
* Sets {@link #latency} to specified value
*
* @param latency
* Latency to use
*/
public void setLatency(int latency) {
this.latency = latency;
}
/**
* Sets {@link #name} to specified value
* @param name
* name to use
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns {@link #listed}
*
* @return listed
*/
public boolean isListed() {
return listed;
}
/**
* Returns {@link #gameMode}
* @return gameMode
*/
public EnumGamemode getGameMode() {
return gameMode;
}
/**
* Sets {@link #listed} to specified value
*
* @param listed
* Listed flag
*/
public void setListed(boolean listed) {
this.listed = listed;
}
/**
* Sets {@link #gameMode} to specified value
* @param gameMode
* GameMode to use
*/
public void setGameMode(EnumGamemode gameMode) {
this.gameMode = gameMode;
}
/**
* Returns {@link #displayName}
*
* @return displayName
*/
public IChatBaseComponent getDisplayName() {
return displayName;
}
/**
* Returns {@link #skin}
* @return skin
*/
public Object getSkin() {
return skin;
}
/**
* Sets {@link #displayName} to specified value
*
* @param displayName
* Display name to use
*/
public void setDisplayName(IChatBaseComponent displayName) {
this.displayName = displayName;
}
/**
* Sets {@link #skin} to specified value
* @param skin
* Skin to use
*/
public void setSkin(Object skin) {
this.skin = skin;
}
}
/**
* Returns {@link #uniqueId}
*
* @return uniqueId
*/
public UUID getUniqueId() {
return uniqueId;
}
/**
* En enum representing packet action
* Calling ordinal() will return action's network ID.
*/
public enum EnumPlayerInfoAction {
/**
* Sets {@link #uniqueId} to specified value
*
* @param uniqueId
* UUID to use
*/
public void setUniqueId(UUID uniqueId) {
this.uniqueId = uniqueId;
}
ADD_PLAYER,
UPDATE_GAME_MODE,
UPDATE_LATENCY,
UPDATE_DISPLAY_NAME,
REMOVE_PLAYER
}
/**
* Returns {@link #name}
*
* @return name
*/
public String getName() {
return name;
}
/**
* An enum representing GameMode
*/
public enum EnumGamemode {
/**
* Sets {@link #name} to specified value
*
* @param name
* name to use
*/
public void setName(String name) {
this.name = name;
}
NOT_SET,
SURVIVAL,
CREATIVE,
ADVENTURE,
SPECTATOR
}
/**
* Returns {@link #gameMode}
*
* @return gameMode
*/
public EnumGamemode getGameMode() {
return gameMode;
}
/**
* Sets {@link #gameMode} to specified value
*
* @param gameMode
* GameMode to use
*/
public void setGameMode(EnumGamemode gameMode) {
this.gameMode = gameMode;
}
/**
* Returns {@link #skin}
*
* @return skin
*/
public Skin getSkin() {
return skin;
}
/**
* Sets {@link #skin} to specified value
*
* @param skin
* Skin to use
*/
public void setSkin(Skin skin) {
this.skin = skin;
}
/**
* Returns {@link #chatSessionId}
*
* @return chatSessionId
*/
public UUID getChatSessionId() {
return chatSessionId;
}
/**
* Sets {@link #chatSessionId} to specified value
*
* @param chatSessionId
* chatSessionId to use
*/
public void setChatSessionId(UUID chatSessionId) {
this.chatSessionId = chatSessionId;
}
/**
* Returns {@link #profilePublicKey}
*
* @return profilePublicKey
*/
public Object getProfilePublicKey() {
return profilePublicKey;
}
/**
* Sets {@link #profilePublicKey} to specified value
*
* @param profilePublicKey
* profilePublicKey to use
*/
public void setProfilePublicKey(Object profilePublicKey) {
this.profilePublicKey = profilePublicKey;
}
}
/**
* En enum representing packet action
*/
public enum EnumPlayerInfoAction {
ADD_PLAYER,
INITIALIZE_CHAT,
UPDATE_GAME_MODE,
UPDATE_LISTED,
UPDATE_LATENCY,
UPDATE_DISPLAY_NAME,
REMOVE_PLAYER
}
/**
* An enum representing GameMode
*/
public enum EnumGamemode {
NOT_SET,
SURVIVAL,
CREATIVE,
ADVENTURE,
SPECTATOR;
/** Value array to iterate over to avoid array creations on each call */
public static final EnumGamemode[] VALUES = values();
}
}
@@ -1,63 +1,70 @@
package me.neznamy.tab.api.protocol;
import me.neznamy.tab.api.chat.IChatBaseComponent;
import me.neznamy.tab.api.util.Preconditions;
/**
* A class representing platform specific packet class
*/
public class PacketPlayOutPlayerListHeaderFooter implements TabPacket {
/** TabList header */
private final IChatBaseComponent header;
/** TabList header */
private final IChatBaseComponent header;
/** TabList footer */
private final IChatBaseComponent footer;
/** TabList footer */
private final IChatBaseComponent footer;
/**
* Constructs new instance with given parameters. They are converted to {@link IChatBaseComponent}
* using {@link IChatBaseComponent#optimizedComponent(String)} method.
*
* @param header
* TabList header
* @param footer
* TabList footer
*/
public PacketPlayOutPlayerListHeaderFooter(String header, String footer) {
this.header = IChatBaseComponent.optimizedComponent(header);
this.footer = IChatBaseComponent.optimizedComponent(footer);
}
/**
* Constructs new instance with given parameters. They are converted to {@link IChatBaseComponent}
* using {@link IChatBaseComponent#optimizedComponent(String)} method.
*
* @param header
* TabList header
* @param footer
* TabList footer
*/
public PacketPlayOutPlayerListHeaderFooter(String header, String footer) {
Preconditions.checkNotNull(header, "header");
Preconditions.checkNotNull(footer, "footer");
this.header = IChatBaseComponent.optimizedComponent(header);
this.footer = IChatBaseComponent.optimizedComponent(footer);
}
/**
* Constructs new instance with given parameters.
*
* @param header
* TabList header
* @param footer
* TabList footer
*/
public PacketPlayOutPlayerListHeaderFooter(IChatBaseComponent header, IChatBaseComponent footer) {
this.header = header;
this.footer = footer;
}
/**
* Constructs new instance with given parameters.
*
* @param header
* TabList header
* @param footer
* TabList footer
*/
public PacketPlayOutPlayerListHeaderFooter(IChatBaseComponent header, IChatBaseComponent footer) {
Preconditions.checkNotNull(header, "header");
Preconditions.checkNotNull(footer, "footer");
this.header = header;
this.footer = footer;
}
@Override
public String toString() {
return String.format("PacketPlayOutPlayerListHeaderFooter{header=%s,footer=%s}", header, footer);
}
@Override
public String toString() {
return String.format("PacketPlayOutPlayerListHeaderFooter{header=%s,footer=%s}", header, footer);
}
/**
* Returns {@link #header}
* @return header
*/
public IChatBaseComponent getHeader() {
return header;
}
/**
* Returns {@link #header}
*
* @return header
*/
public IChatBaseComponent getHeader() {
return header;
}
/**
* Returns {@link #footer}
* @return footer
*/
public IChatBaseComponent getFooter() {
return footer;
}
/**
* Returns {@link #footer}
*
* @return footer
*/
public IChatBaseComponent getFooter() {
return footer;
}
}
@@ -1,55 +1,60 @@
package me.neznamy.tab.api.protocol;
import me.neznamy.tab.api.util.Preconditions;
/**
* A class representing platform specific packet class
*/
public class PacketPlayOutScoreboardDisplayObjective implements TabPacket {
/**
* Display slot.
* 0 = PlayerList,
* 1 = SideBar,
* 2 = BelowName.
*/
private final int slot;
/** Up to 16 characters long objective name */
private final String objectiveName;
/**
* Display slot.
* 0 = PlayerList,
* 1 = SideBar,
* 2 = BelowName.
*/
private final int slot;
/**
* Constructs new instance with given parameters
* @param slot
* Display slot
* @param objectiveName
* Internal name of the objective
* @throws IllegalArgumentException
* if {@code objectiveName} is null or longer than 16 characters
*/
public PacketPlayOutScoreboardDisplayObjective(int slot, String objectiveName) {
if (objectiveName == null) throw new IllegalArgumentException("objectiveName cannot be null");
if (objectiveName.length() > 16) throw new IllegalArgumentException("objectiveName cannot be longer than 16 character (is " + objectiveName.length() + ")");
this.slot = slot;
this.objectiveName = objectiveName;
}
/** Up to 16 characters long objective name */
private final String objectiveName;
@Override
public String toString() {
return String.format("PacketPlayOutScoreboardDisplayObjective{slot=%s,objectiveName=%s}", slot, objectiveName);
}
/**
* Constructs new instance with given parameters
*
* @param slot
* Display slot
* @param objectiveName
* Internal name of the objective
* @throws IllegalArgumentException
* if {@code objectiveName} is null or longer than 16 characters
*/
public PacketPlayOutScoreboardDisplayObjective(int slot, String objectiveName) {
Preconditions.checkNotNull(objectiveName, "objective name");
Preconditions.checkMaxLength(objectiveName, 16, "objective name");
this.slot = slot;
this.objectiveName = objectiveName;
}
/**
* Returns {@link #slot}
* @return slot
*/
public int getSlot() {
return slot;
}
@Override
public String toString() {
return String.format("PacketPlayOutScoreboardDisplayObjective{slot=%s,objectiveName=%s}", slot, objectiveName);
}
/**
* Returns {@link #objectiveName}
* @return objectiveName
*/
public String getObjectiveName() {
return objectiveName;
}
/**
* Returns {@link #slot}
*
* @return slot
*/
public int getSlot() {
return slot;
}
/**
* Returns {@link #objectiveName}
*
* @return objectiveName
*/
public String getObjectiveName() {
return objectiveName;
}
}
@@ -1,109 +1,119 @@
package me.neznamy.tab.api.protocol;
import me.neznamy.tab.api.util.Preconditions;
/**
* A class representing platform specific packet class
*/
public class PacketPlayOutScoreboardObjective implements TabPacket {
/** Up to 16 characters long objective name */
private final String objectiveName;
/** Up to 16 characters long objective name */
private final String objectiveName;
/** Display name */
private final String displayName;
/** Display name */
private final String displayName;
/** Display type, only takes effect in PlayerList */
private final EnumScoreboardHealthDisplay renderType;
/** Display type, only takes effect in PlayerList */
private final EnumScoreboardHealthDisplay renderType;
/**
* Packet action.
* 0 = register,
* 1 = unregister,
* 2 = update title
*/
private final int method;
/**
* Packet action.
* 0 = register,
* 1 = unregister,
* 2 = update title
*/
private final int action;
/**
* Constructs new instance with given parameters.
*
* @param method
* Packet action (0 = add, 1 = remove, 2 = update title)
* @param objectiveName
* objective name, up to 16 characters long
* @param displayName - title
* @param renderType - display type
* @throws IllegalArgumentException
* if {@code objectiveName} is null or longer than 16 characters
*/
public PacketPlayOutScoreboardObjective(int method, String objectiveName, String displayName, EnumScoreboardHealthDisplay renderType) {
if (objectiveName == null) throw new IllegalArgumentException("objectiveName cannot be null");
if (objectiveName.length() > 16) throw new IllegalArgumentException("objectiveName cannot be longer than 16 character (is " + objectiveName.length() + ")");
this.objectiveName = objectiveName;
this.displayName = displayName;
this.renderType = renderType;
this.method = method;
}
/**
* Constructs new instance with given parameters.
*
* @param action
* Packet action (0 = add, 1 = remove, 2 = update title)
* @param objectiveName
* objective name, up to 16 characters long
* @param displayName
* title
* @param renderType
* display type
* @throws IllegalArgumentException
* if {@code objectiveName} is null or longer than 16 characters
*/
public PacketPlayOutScoreboardObjective(int action, String objectiveName, String displayName, EnumScoreboardHealthDisplay renderType) {
Preconditions.checkRange(action, 0, 2, "action");
Preconditions.checkNotNull(objectiveName, "objective name");
Preconditions.checkMaxLength(objectiveName, 16, "objective name");
Preconditions.checkNotNull(renderType, "render type");
this.objectiveName = objectiveName;
this.displayName = displayName;
this.renderType = renderType;
this.action = action;
}
/**
* Constructs new packet with given objective name and 1 (unregister) action.
*
* @param objectiveName
* objective name, up to 16 characters long
* @throws IllegalArgumentException
* if {@code objectiveName} is null or longer than 16 characters
*/
public PacketPlayOutScoreboardObjective(String objectiveName) {
if (objectiveName == null) throw new IllegalArgumentException("objectiveName cannot be null");
if (objectiveName.length() > 16) throw new IllegalArgumentException("objectiveName cannot be longer than 16 character (is " + objectiveName.length() + ")");
this.objectiveName = objectiveName;
this.displayName = ""; //avoiding NPE on <1.7
this.method = 1;
this.renderType = null;
}
/**
* Constructs new packet with given objective name and 1 (unregister) action.
*
* @param objectiveName
* objective name, up to 16 characters long
* @throws IllegalArgumentException
* if {@code objectiveName} is null or longer than 16 characters
*/
public PacketPlayOutScoreboardObjective(String objectiveName) {
Preconditions.checkNotNull(objectiveName, "objective name");
Preconditions.checkMaxLength(objectiveName, 16, "objective name");
this.objectiveName = objectiveName;
this.displayName = ""; //avoiding NPE on <1.7
this.action = 1;
this.renderType = null;
}
@Override
public String toString() {
return String.format("PacketPlayOutScoreboardObjective{objectiveName=%s,displayName=%s,renderType=%s,method=%s}",
objectiveName, displayName, renderType, method);
}
@Override
public String toString() {
return String.format("PacketPlayOutScoreboardObjective{objectiveName=%s,displayName=%s,renderType=%s,method=%s}",
objectiveName, displayName, renderType, action);
}
/**
* Returns {@link #displayName}
* @return displayName
*/
public String getDisplayName() {
return displayName;
}
/**
* Returns {@link #displayName}
*
* @return displayName
*/
public String getDisplayName() {
return displayName;
}
/**
* Returns {@link #objectiveName}
* @return objectiveName
*/
public String getObjectiveName() {
return objectiveName;
}
/**
* Returns {@link #objectiveName}
*
* @return objectiveName
*/
public String getObjectiveName() {
return objectiveName;
}
/**
* Returns {@link #renderType}
* @return renderType
*/
public EnumScoreboardHealthDisplay getRenderType() {
return renderType;
}
/**
* Returns {@link #renderType}
*
* @return renderType
*/
public EnumScoreboardHealthDisplay getRenderType() {
return renderType;
}
/**
* Returns {@link #method}
* @return packet action
*/
public int getMethod() {
return method;
}
/**
* Returns {@link #action}
*
* @return packet action
*/
public int getAction() {
return action;
}
/**
* An enum representing available display types.
* They only take effect in PlayerList position.
*/
public enum EnumScoreboardHealthDisplay {
/**
* An enum representing available display types.
* They only take effect in PlayerList position.
*/
public enum EnumScoreboardHealthDisplay {
INTEGER, HEARTS
}
INTEGER, HEARTS
}
}
@@ -1,89 +1,97 @@
package me.neznamy.tab.api.protocol;
import me.neznamy.tab.api.util.Preconditions;
/**
* A class representing platform specific packet class
*/
public class PacketPlayOutScoreboardScore implements TabPacket {
/** Packet action */
private final Action action;
/** Packet action */
private final Action action;
/** Objective name */
private final String objectiveName;
/** Objective name */
private final String objectiveName;
/** Affected player */
private final String player;
/** Affected player */
private final String player;
/** Player's score */
private final int score;
/** Player's score */
private final int score;
/**
* Constructs new instance with given parameters
*
* @param action
* Packet action
* @param objectiveName
* Objective name
* @param player
* Affected player
* @param score
* Player's score
* @throws IllegalArgumentException
* if {@code objectiveName} is null or longer than 16 characters
*/
public PacketPlayOutScoreboardScore(Action action, String objectiveName, String player, int score) {
if (objectiveName == null) throw new IllegalArgumentException("objectiveName cannot be null");
if (objectiveName.length() > 16) throw new IllegalArgumentException("objectiveName cannot be longer than 16 character (is " + objectiveName.length() + ")");
this.action = action;
this.objectiveName = objectiveName;
this.player = player;
this.score = score;
}
/**
* Constructs new instance with given parameters
*
* @param action
* Packet action
* @param objectiveName
* Objective name
* @param player
* Affected player
* @param score
* Player's score
* @throws IllegalArgumentException
* if {@code objectiveName} is null or longer than 16 characters
*/
public PacketPlayOutScoreboardScore(Action action, String objectiveName, String player, int score) {
Preconditions.checkNotNull(action, "action");
Preconditions.checkNotNull(objectiveName, "objective name");
Preconditions.checkMaxLength(objectiveName, 16, "objective name");
Preconditions.checkNotNull(player, "player");
this.action = action;
this.objectiveName = objectiveName;
this.player = player;
this.score = score;
}
@Override
public String toString() {
return String.format("PacketPlayOutScoreboardScore{action=%s,objectiveName=%s,player=%s,score=%s}",
action, objectiveName, player, score);
}
@Override
public String toString() {
return String.format("PacketPlayOutScoreboardScore{action=%s,objectiveName=%s,player=%s,score=%s}",
action, objectiveName, player, score);
}
/**
* Returns {@link #action}
* @return packet action
*/
public Action getAction() {
return action;
}
/**
* Returns {@link #action}
*
* @return packet action
*/
public Action getAction() {
return action;
}
/**
* Returns {@link #objectiveName}
* @return objective name
*/
public String getObjectiveName() {
return objectiveName;
}
/**
* Returns {@link #objectiveName}
*
* @return objective name
*/
public String getObjectiveName() {
return objectiveName;
}
/**
* Returns {@link #player}
* @return player
*/
public String getPlayer() {
return player;
}
/**
* Returns {@link #player}
*
* @return player
*/
public String getPlayer() {
return player;
}
/**
* Returns {@link #score}
* @return score
*/
public int getScore() {
return score;
}
/**
* Returns {@link #score}
*
* @return score
*/
public int getScore() {
return score;
}
/**
* An enum representing action types
*/
public enum Action {
/**
* An enum representing action types
*/
public enum Action {
CHANGE,
REMOVE
}
CHANGE,
REMOVE
}
}
@@ -4,261 +4,282 @@ import java.util.Collection;
import java.util.Collections;
import me.neznamy.tab.api.chat.EnumChatFormat;
import me.neznamy.tab.api.util.Preconditions;
/**
* A class representing platform specific packet class
*/
public class PacketPlayOutScoreboardTeam implements TabPacket {
/**
* Up to 16 characters long team name.
* It is used to sort players alphabetically in TabList by team names.
* Team-less players appear on top, sorted by usernames.
* Players with teams are below them, players of the same team are
* sorted alphabetically by usernames.
*/
private final String name;
/**
* Up to 16 characters long team name.
* It is used to sort players alphabetically in TabList by team names.
* Team-less players appear on top, sorted by usernames.
* Players with teams are below them, players of the same team are
* sorted alphabetically by usernames.
*/
private final String name;
/** Team prefix, limited to 16 characters, on <1.13 */
private String playerPrefix;
/** Team prefix, limited to 16 characters, on &lt;1.13 */
private String playerPrefix;
/** Team suffix, limited to 16 characters, on <1.13 */
private String playerSuffix;
/** Team suffix, limited to 16 characters, on &lt;1.13 */
private String playerSuffix;
/**
* NameTag visibility rule since 1.8. Possible options are:
* {@code always}, {@code hideForOtherTeams}, {@code hideForOwnTeam}, {@code never}
*/
private String nameTagVisibility;
/**
* NameTag visibility rule since 1.8. Possible options are:
* {@code always}, {@code hideForOtherTeams}, {@code hideForOwnTeam}, {@code never}
*/
private String nameTagVisibility;
/**
* Collision rule added in 1.9. Possible options are:
* {@code always}, {@code pushOtherTeams}, {@code pushOwnTeam}, {@code never}
*/
private String collisionRule;
/**
* Collision rule added in 1.9. Possible options are:
* {@code always}, {@code pushOtherTeams}, {@code pushOwnTeam}, {@code never}
*/
private String collisionRule;
/**
* Team color field added in 1.13. It determines name color,
* prefix and suffix color start.
*/
private EnumChatFormat color;
/**
* Team color field added in 1.13. It determines name color,
* prefix and suffix color start.
*/
private EnumChatFormat color;
/**
* Affected entities. For players, it is their name, for other entities their UUID
* Player name length is limited to 16 characters for <1.8, 40 characters since 1.8.
* Entity UUID option was added in 1.8.
*/
private Collection<String> players = Collections.emptyList();
/**
* Affected entities. For players, it is their name, for other entities their UUID
* Player name length is limited to 16 characters for &lt;1.8, 40 characters since 1.8.
* Entity UUID option was added in 1.8.
*/
private Collection<String> players = Collections.emptyList();
/**
* Packet action.
* 0 = create team,
* 1 = remove team,
* 2 = update team info,
* 3 = add entries,
* 4 = remove entries
*/
private final int method;
/**
* Packet action.
* 0 = create team,
* 1 = remove team,
* 2 = update team info,
* 3 = add entries,
* 4 = remove entries
*/
private final int action;
/**
* Bit mask.
* 0x01: Allow friendly fire,
* 0x02: Can see invisible players on the same team.
*/
private int options;
/**
* Bit mask.
* 0x01: Allow friendly fire,
* 0x02: Can see invisible players on the same team.
*/
private int options;
/**
* Constructs new instance with given parameters. Private constructor used
* internally to validate team name and set action.
*
* @param method
* Packet action (0 = create team, 1 = remove team, 2 = update team info,
* 3 = add entries, 4 = remove entries)
* @param name
* Team name, up to 16 characters long
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
private PacketPlayOutScoreboardTeam(int method, String name) {
if (name == null || name.length() == 0) throw new IllegalArgumentException("Team name cannot be null/empty");
if (name.length() > 16) throw new IllegalArgumentException("name cannot be longer than 16 character (is " + name.length() + ")");
this.method = method;
this.name = name;
}
/**
* Constructs new instance with given parameters. Private constructor used
* internally to validate team name and set action.
*
* @param action
* Packet action (0 = create team, 1 = remove team, 2 = update team info,
* 3 = add entries, 4 = remove entries)
* @param name
* Team name, up to 16 characters long
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
private PacketPlayOutScoreboardTeam(int action, String name) {
Preconditions.checkNotNull(name, "team name");
Preconditions.checkMaxLength(name, 16, "team name");
this.action = action;
this.name = name;
}
/**
* Constructs new instance with given parameters and 0 (CREATE) action
*
* @param team
* Team name, up to 16 characters long
* @param prefix
* Prefix of players in team
* @param suffix
* Suffix of players in team
* @param visibility
* NameTag visibility rule
* @param collision
* Collision rule
* @param players
* Affected entities
* @param options
* bitmask of team options
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
public PacketPlayOutScoreboardTeam(String team, String prefix, String suffix, String visibility, String collision, Collection<String> players, int options) {
this(0, team);
this.playerPrefix = prefix;
this.playerSuffix = suffix;
this.nameTagVisibility = visibility;
this.collisionRule = collision;
this.players = players;
this.options = options;
}
/**
* Constructs new instance with given parameters and 0 (CREATE) action
*
* @param team
* Team name, up to 16 characters long
* @param prefix
* Prefix of players in team
* @param suffix
* Suffix of players in team
* @param visibility
* NameTag visibility rule
* @param collision
* Collision rule
* @param players
* Affected entities
* @param options
* bitmask of team options
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
public PacketPlayOutScoreboardTeam(String team, String prefix, String suffix, String visibility, String collision, Collection<String> players, int options) {
this(0, team);
Preconditions.checkNotNull(prefix, "prefix");
Preconditions.checkNotNull(suffix, "suffix");
Preconditions.checkNotNull(visibility, "visibility");
Preconditions.checkNotNull(collision, "collision");
Preconditions.checkNotNull(players, "players");
this.playerPrefix = prefix;
this.playerSuffix = suffix;
this.nameTagVisibility = visibility;
this.collisionRule = collision;
this.players = players;
this.options = options;
}
/**
* Constructs new instance with given parameters and 1 (REMOVE) action
*
* @param team
* Team name, up to 16 characters long
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
public PacketPlayOutScoreboardTeam(String team) {
this(1, team);
}
/**
* Constructs new instance with given parameters and 1 (REMOVE) action
*
* @param team
* Team name, up to 16 characters long
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
public PacketPlayOutScoreboardTeam(String team) {
this(1, team);
}
/**
* Constructs new instance with given parameters and 2 (UPDATE_TEAM_INFO) action
*
* @param team
* Team name, up to 16 characters long
* @param prefix
* Prefix of players in team
* @param suffix
* Suffix of players in team
* @param visibility
* NameTag visibility rule
* @param collision
* Collision rule
* @param options
* bitmask of team options
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
public PacketPlayOutScoreboardTeam(String team, String prefix, String suffix, String visibility, String collision, int options) {
this(2, team);
this.playerPrefix = prefix;
this.playerSuffix = suffix;
this.nameTagVisibility = visibility;
this.collisionRule = collision;
this.options = options;
}
/**
* Constructs new instance with given parameters and 2 (UPDATE_TEAM_INFO) action
*
* @param team
* Team name, up to 16 characters long
* @param prefix
* Prefix of players in team
* @param suffix
* Suffix of players in team
* @param visibility
* NameTag visibility rule
* @param collision
* Collision rule
* @param options
* bitmask of team options
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
public PacketPlayOutScoreboardTeam(String team, String prefix, String suffix, String visibility, String collision, int options) {
this(2, team);
Preconditions.checkNotNull(prefix, "prefix");
Preconditions.checkNotNull(suffix, "suffix");
Preconditions.checkNotNull(visibility, "visibility");
Preconditions.checkNotNull(collision, "collision");
Preconditions.checkNotNull(players, "players");
this.playerPrefix = prefix;
this.playerSuffix = suffix;
this.nameTagVisibility = visibility;
this.collisionRule = collision;
this.options = options;
}
/**
* Constructs new instance with given parameters and 3 (ADD_PLAYERS) if {@code add} is {@code true},
* 4 (REMOVE_PLAYERS) if {@code add} is {@code false} action.
*
* @param team
* Team name, up to 16 characters long
* @param players
* Affected entities
* @param add
* {@code true} if players should be added, {@code false} if removed from team
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
public PacketPlayOutScoreboardTeam(String team, Collection<String> players, boolean add) {
this(add ? 3 : 4, team);
this.players = players;
}
/**
* Constructs new instance with given parameters and 3 (ADD_PLAYERS) if {@code add} is {@code true},
* 4 (REMOVE_PLAYERS) if {@code add} is {@code false} action.
*
* @param team
* Team name, up to 16 characters long
* @param players
* Affected entities
* @param add
* {@code true} if players should be added, {@code false} if removed from team
* @throws IllegalArgumentException
* if {@code name} is null, empty or longer than 16 characters
*/
public PacketPlayOutScoreboardTeam(String team, Collection<String> players, boolean add) {
this(add ? 3 : 4, team);
Preconditions.checkNotNull(players, "players");
this.players = players;
}
@Override
public String toString() {
return String.format("PacketPlayOutScoreboardTeam{name=%s,playerPrefix=%s,playerSuffix=%s,nameTagVisibility=%s,"
+ "collisionRule=%s,color=%s,players=%s,method=%s,options=%s}",
name, playerPrefix, playerSuffix, nameTagVisibility, collisionRule, color, players, method, options);
}
@Override
public String toString() {
return String.format("PacketPlayOutScoreboardTeam{name=%s,playerPrefix=%s,playerSuffix=%s,nameTagVisibility=%s,"
+ "collisionRule=%s,color=%s,players=%s,method=%s,options=%s}",
name, playerPrefix, playerSuffix, nameTagVisibility, collisionRule, color, players, action, options);
}
/**
* Returns {@link #name}
* @return team name
*/
public String getName() {
return name;
}
/**
* Returns {@link #name}
*
* @return team name
*/
public String getName() {
return name;
}
/**
* Returns {@link #playerPrefix}
* @return playerPrefix
*/
public String getPlayerPrefix() {
return playerPrefix;
}
/**
* Returns {@link #playerPrefix}
*
* @return playerPrefix
*/
public String getPlayerPrefix() {
return playerPrefix;
}
/**
* Returns {@link #playerSuffix}
* @return playerSuffix
*/
public String getPlayerSuffix() {
return playerSuffix;
}
/**
* Returns {@link #playerSuffix}
*
* @return playerSuffix
*/
public String getPlayerSuffix() {
return playerSuffix;
}
/**
* Returns {@link #nameTagVisibility}
* @return nameTagVisibility
*/
public String getNameTagVisibility() {
return nameTagVisibility;
}
/**
* Returns {@link #collisionRule}
* @return collisionRule
*/
public String getCollisionRule() {
return collisionRule;
}
/**
* Returns {@link #nameTagVisibility}
*
* @return nameTagVisibility
*/
public String getNameTagVisibility() {
return nameTagVisibility;
}
/**
* Returns {@link #collisionRule}
*
* @return collisionRule
*/
public String getCollisionRule() {
return collisionRule;
}
/**
* Sets {@link #color} to specified color
*
* @param color
* color to use
*/
public void setColor(EnumChatFormat color) {
this.color = color;
}
/**
* Sets {@link #color} to specified color
*
* @param color
* color to use
*/
public void setColor(EnumChatFormat color) {
this.color = color;
}
/**
* Returns {@link #color}
* @return color
*/
public EnumChatFormat getColor() {
return color;
}
/**
* Returns {@link #color}
*
* @return color
*/
public EnumChatFormat getColor() {
return color;
}
/**
* Returns {@link #players}
* @return players
*/
public Collection<String> getPlayers() {
return players;
}
/**
* Returns {@link #players}
*
* @return players
*/
public Collection<String> getPlayers() {
return players;
}
/**
* Returns {@link #options}
* @return team options
*/
public int getOptions() {
return options;
}
/**
* Returns {@link #method}
* @return packet action
*/
public int getMethod() {
return method;
}
/**
* Returns {@link #options}
*
* @return team options
*/
public int getOptions() {
return options;
}
/**
* Returns {@link #action}
*
* @return packet action
*/
public int getAction() {
return action;
}
}
@@ -0,0 +1,59 @@
package me.neznamy.tab.api.protocol;
import me.neznamy.tab.api.util.Preconditions;
/**
* Class representing a minecraft skin as a value - signature pair.
*/
public class Skin {
/** Skin value */
private final String value;
/** Skin signature, can be null */
private final String signature;
/**
* Constructs new instance with given parameters
*
* @param value
* skin value
* @param signature
* skin signature
* @throws IllegalArgumentException
* if {@code value} is {@code null}
*/
public Skin(String value, String signature) {
Preconditions.checkNotNull(value, "value");
this.value = value;
this.signature = signature;
}
/**
* Returns skin value
*
* @return skin value
*/
public String getValue() {
return value;
}
/**
* Returns skin signature
*
* @return skin signature
*/
public String getSignature() {
return signature;
}
/**
* Returns user-friendly representation of this object
*
* @return user-friendly representation of this object
*/
@Override
public String toString() {
return String.format("Skin{value=%s,signature=%s}", value, signature);
}
}
@@ -8,9 +8,10 @@ package me.neznamy.tab.api.protocol;
*/
public interface TabPacket {
/**
* Returns user-friendly string representation of the packet
* @return user-friendly string representation of the packet
*/
String toString();
/**
* Returns user-friendly string representation of the packet
*
* @return user-friendly string representation of the packet
*/
String toString();
}
@@ -7,23 +7,23 @@ package me.neznamy.tab.api.scoreboard;
*/
public interface Line {
/**
* Returns configured raw text of this line. Placeholders
* remain in raw format.
*
* @return raw text of this line
*/
String getText();
/**
* Changes text to new value. Supports placeholders, which are
* automatically registered if needed and refreshed periodically
* based on configuration. No need to call this method to try to
* keep placeholders up to date. If value is identical to previous
* one, nothing happens.
*
* @param text
* Raw text to display in the line
*/
void setText(String text);
/**
* Returns configured raw text of this line. Placeholders
* remain in raw format.
*
* @return raw text of this line
*/
String getText();
/**
* Changes text to new value. Supports placeholders, which are
* automatically registered if needed and refreshed periodically
* based on configuration. No need to call this method to try to
* keep placeholders up to date. If value is identical to previous
* one, nothing happens.
*
* @param text
* Raw text to display in the line
*/
void setText(String text);
}
@@ -10,78 +10,78 @@ import java.util.List;
*/
public interface Scoreboard {
/**
* Returns internal name of this scoreboard. Name is defined in registration,
* scoreboards from config use name they were defined with. This value is
* used internally and in /tab announce scoreboard command.
*
* @return custom name of this scoreboard
*/
String getName();
/**
* Returns internal name of this scoreboard. Name is defined in registration,
* scoreboards from config use name they were defined with. This value is
* used internally and in /tab announce scoreboard command.
*
* @return custom name of this scoreboard
*/
String getName();
/**
* Returns raw title of this scoreboard. Placeholders stay in their
* raw format.
*
* @return scoreboard title
* @see #setTitle(String)
*/
String getTitle();
/**
* Returns raw title of this scoreboard. Placeholders stay in their
* raw format.
*
* @return scoreboard title
* @see #setTitle(String)
*/
String getTitle();
/**
* Sets title to specified value. Placeholders are refreshed automatically
* with refresh intervals defined in config. No need to call this method
* to try to keep placeholders up to date. Supports RGB codes using any of
* the supported formats.
* <p>
* Length is limited to 32 characters on <1.13. If the limit is exceeded,
* text will be cut to 32 characters.
* <p>
* Calling this method with same title as before will not do anything.
*
* @param title
* New title to use with placeholder support
* @see #getTitle()
*/
void setTitle(String title);
/**
* Sets title to specified value. Placeholders are refreshed automatically
* with refresh intervals defined in config. No need to call this method
* to try to keep placeholders up to date. Supports RGB codes using any of
* the supported formats.
* <p>
* Length is limited to 32 characters on &lt;1.13. If the limit is exceeded,
* text will be cut to 32 characters.
* <p>
* Calling this method with same title as before will not do anything.
*
* @param title
* New title to use with placeholder support
* @see #getTitle()
*/
void setTitle(String title);
/**
* Returns list of lines in this scoreboard in the order they appear
* in game (first line is on top). This list should only be used for reading,
* for adding/removing lines see {@link #addLine(String)} and {@link #removeLine(int)}.
*
* @return list of lines in this scoreboard
* @see #addLine(String)
* @see #removeLine(int)
*/
List<Line> getLines();
/**
* Returns list of lines in this scoreboard in the order they appear
* in game (first line is on top). This list should only be used for reading,
* for adding/removing lines see {@link #addLine(String)} and {@link #removeLine(int)}.
*
* @return list of lines in this scoreboard
* @see #addLine(String)
* @see #removeLine(int)
*/
List<Line> getLines();
/**
* Adds line with specified text on the bottom of scoreboard. Supports
* placeholders, which will automatically be refreshed.
*
* @param text
* Text to display
* @see #getLines()
* @see #removeLine(int)
*/
void addLine(String text);
/**
* Adds line with specified text on the bottom of scoreboard. Supports
* placeholders, which will automatically be refreshed.
*
* @param text
* Text to display
* @see #getLines()
* @see #removeLine(int)
*/
void addLine(String text);
/**
* Removes line with specified index. Index starts at {@code 0} and ends at
* {@link #getLines()}.size()-1.
*
* @param index
* Index of line to remove, starting at 0
* @throws IndexOutOfBoundsException
* if the index is out of range (index < 0 || index >= {@link #getLines()}.size())
* @see #getLines()
* @see #addLine(String)
*/
void removeLine(int index);
/**
* Removes line with specified index. Index starts at {@code 0} and ends at
* {@link #getLines()}.size()-1.
*
* @param index
* Index of line to remove, starting at 0
* @throws IndexOutOfBoundsException
* if the index is out of range (index &lt; 0 || index &gt;= {@link #getLines()}.size())
* @see #getLines()
* @see #addLine(String)
*/
void removeLine(int index);
/**
* Unregisters this scoreboard from all players who can see it.
*/
void unregister();
/**
* Unregisters this scoreboard from all players who can see it.
*/
void unregister();
}
@@ -15,151 +15,152 @@ import me.neznamy.tab.api.TabPlayer;
*/
public interface ScoreboardManager {
/**
* Creates new scoreboard, registers and returns it. {@code name}
* is internal name of the scoreboard, which it will be registered
* with and further work where scoreboard name is used allowed.
* <p>
* Scoreboard {@code title} is limited to 32 characters on <1.13. If needed,
* it will be cut to 32 character for those players (after replacing
* placeholders). RGB is supported using any of the supported formats.
* Placeholders are supported.
* <p>
* {@code lines} will display in the same order as contained in the list.
* Client can only see up to 15 lines at a time, however you can define
* more, if some of them only consist of placeholder that might be empty.
*
* @param name
* Internal name of the scoreboard
* @param title
* The scoreboard title
* @param lines
* Lines of text in scoreboard (supports placeholders)
* @return The new scoreboard with given parameters
* @throws NullPointerException
* if {@code name} or {@code lines} is {@code null}
*/
Scoreboard createScoreboard(String name, String title, List<String> lines);
/**
* Returns map of registered scoreboards via config and API.
* Map key is name of scoreboard, which is defined either in config
* or using {@link #createScoreboard(String, String, List)}.
* Map value is reference to the actual scoreboard.
*
* @return map of registered scoreboards
*/
Map<String, Scoreboard> getRegisteredScoreboards();
/**
* Displays scoreboard to defined player. This will disable all display
* condition checks until the scoreboard is unregistered using
* {@link #resetScoreboard(TabPlayer)}. If the player already sees
* this scoreboard, nothing will happen.
*
* @param player
* Player to send scoreboard to
* @param scoreboard
* Scoreboard to display
* @throws NullPointerException
* if {@code player} or {@code scoreboard} is {@code null}
* @see #hasCustomScoreboard(TabPlayer)
* @see #resetScoreboard(TabPlayer)
*/
void showScoreboard(TabPlayer player, Scoreboard scoreboard);
/**
* Returns {@code true} if player has custom scoreboard set using
* {@link #showScoreboard(TabPlayer, Scoreboard)}, {@code false} if not.
*
* @param player
* Player to check
* @return {@code true} if player has custom scoreboard set using API,
* {@code false} if not
* @see #showScoreboard(TabPlayer, Scoreboard)
* @see #resetScoreboard(TabPlayer)
*/
boolean hasCustomScoreboard(TabPlayer player);
/**
* Hides custom scoreboard sent using {@link #showScoreboard(TabPlayer, Scoreboard)}
* and re-enables internal display logic with conditions. If player does not
* have any forced scoreboard, nothing happens.
*
* @param player
* Player to hide custom scoreboard from
* @see #showScoreboard(TabPlayer, Scoreboard)
* @see #hasCustomScoreboard(TabPlayer)
*/
void resetScoreboard(TabPlayer player);
/**
* Returns {@code true} if player has scoreboard enabled, {@code false}
* if disabled (toggled) using either toggle command,
* {@link #toggleScoreboard(TabPlayer, boolean)} or
* {@link #setScoreboardVisible(TabPlayer, boolean, boolean)}.
*
* @param player
* Player to get visibility status of
* @return {@code true} if visible, {@code false} if disabled
* @see #setScoreboardVisible(TabPlayer, boolean, boolean)
* @see #toggleScoreboard(TabPlayer, boolean)
*/
boolean hasScoreboardVisible(TabPlayer player);
/**
* Sets scoreboard visibility of player to defined value. If visibility status
* is same as player had before, nothing happens. If this call changes
* visibility, scoreboard is toggled. If {@code sendToggleMessage} is {@code true},
* toggle message defined in configuration is sent.
*
* @param player
* Player to set visibility for
* @param visible
* New visibility status
* @param sendToggleMessage
* whether to send toggle message if status changed or not
* @see #toggleScoreboard(TabPlayer, boolean)
* @see #hasScoreboardVisible(TabPlayer)
*/
void setScoreboardVisible(TabPlayer player, boolean visible, boolean sendToggleMessage);
/**
* Toggles scoreboard for specified player. If player had scoreboard visible,
* hides it. If hidden, shows it. If {@code sendToggleMessage} is {@code true},
* toggle message defined in configuration is sent.
*
* @param player
* Player to toggle scoreboard for
* @param sendToggleMessage
* Whether to send toggle message or not
* @see #hasScoreboardVisible(TabPlayer)
* @see #setScoreboardVisible(TabPlayer, boolean, boolean)
*/
void toggleScoreboard(TabPlayer player, boolean sendToggleMessage);
/**
* Temporarily displays scoreboard to all players for specified amount of
* time (in milliseconds). Scoreboard name is either defined in config, or
* through API in {@link #createScoreboard(String, String, List)}.
*
* @param scoreboard
* Scoreboard from config or registered via API
* @param duration
* Duration in milliseconds
* @throws IllegalArgumentException
* if no scoreboard was found with such name or {@code duration}
* is < 0.
*/
void announceScoreboard(String scoreboard, int duration);
/**
* Creates new scoreboard, registers and returns it. {@code name}
* is internal name of the scoreboard, which it will be registered
* with and further work where scoreboard name is used allowed.
* <p>
* Scoreboard {@code title} is limited to 32 characters on &lt;1.13. If needed,
* it will be cut to 32 character for those players (after replacing
* placeholders). RGB is supported using any of the supported formats.
* Placeholders are supported.
* <p>
* {@code lines} will display in the same order as contained in the list.
* Client can only see up to 15 lines at a time, however you can define
* more, if some of them only consist of placeholder that might be empty.
*
* @param name
* Internal name of the scoreboard
* @param title
* The scoreboard title
* @param lines
* Lines of text in scoreboard (supports placeholders)
* @return The new scoreboard with given parameters
* @throws NullPointerException
* if {@code name} or {@code lines} is {@code null}
*/
Scoreboard createScoreboard(String name, String title, List<String> lines);
/**
* Returns map of registered scoreboards via config and API.
* Map key is name of scoreboard, which is defined either in config
* or using {@link #createScoreboard(String, String, List)}.
* Map value is reference to the actual scoreboard.
*
* @return map of registered scoreboards
*/
Map<String, Scoreboard> getRegisteredScoreboards();
/**
* Displays scoreboard to defined player. This will disable all display
* condition checks until the scoreboard is unregistered using
* {@link #resetScoreboard(TabPlayer)}. If the player already sees
* this scoreboard, nothing will happen.
*
* @param player
* Player to send scoreboard to
* @param scoreboard
* Scoreboard to display
* @throws NullPointerException
* if {@code player} or {@code scoreboard} is {@code null}
* @see #hasCustomScoreboard(TabPlayer)
* @see #resetScoreboard(TabPlayer)
*/
void showScoreboard(TabPlayer player, Scoreboard scoreboard);
/**
* Returns {@code true} if player has custom scoreboard set using
* {@link #showScoreboard(TabPlayer, Scoreboard)}, {@code false} if not.
*
* @param player
* Player to check
* @return {@code true} if player has custom scoreboard set using API,
* {@code false} if not
* @see #showScoreboard(TabPlayer, Scoreboard)
* @see #resetScoreboard(TabPlayer)
*/
boolean hasCustomScoreboard(TabPlayer player);
/**
* Hides custom scoreboard sent using {@link #showScoreboard(TabPlayer, Scoreboard)}
* and re-enables internal display logic with conditions. If player does not
* have any forced scoreboard, nothing happens.
*
* @param player
* Player to hide custom scoreboard from
* @see #showScoreboard(TabPlayer, Scoreboard)
* @see #hasCustomScoreboard(TabPlayer)
*/
void resetScoreboard(TabPlayer player);
/**
* Returns {@code true} if player has scoreboard enabled, {@code false}
* if disabled (toggled) using either toggle command,
* {@link #toggleScoreboard(TabPlayer, boolean)} or
* {@link #setScoreboardVisible(TabPlayer, boolean, boolean)}.
*
* @param player
* Player to get visibility status of
* @return {@code true} if visible, {@code false} if disabled
* @see #setScoreboardVisible(TabPlayer, boolean, boolean)
* @see #toggleScoreboard(TabPlayer, boolean)
*/
boolean hasScoreboardVisible(TabPlayer player);
/**
* Sets scoreboard visibility of player to defined value. If visibility status
* is same as player had before, nothing happens. If this call changes
* visibility, scoreboard is toggled. If {@code sendToggleMessage} is {@code true},
* toggle message defined in configuration is sent.
*
* @param player
* Player to set visibility for
* @param visible
* New visibility status
* @param sendToggleMessage
* whether to send toggle message if status changed or not
* @see #toggleScoreboard(TabPlayer, boolean)
* @see #hasScoreboardVisible(TabPlayer)
*/
void setScoreboardVisible(TabPlayer player, boolean visible, boolean sendToggleMessage);
/**
* Toggles scoreboard for specified player. If player had scoreboard visible,
* hides it. If hidden, shows it. If {@code sendToggleMessage} is {@code true},
* toggle message defined in configuration is sent.
*
* @param player
* Player to toggle scoreboard for
* @param sendToggleMessage
* Whether to send toggle message or not
* @see #hasScoreboardVisible(TabPlayer)
* @see #setScoreboardVisible(TabPlayer, boolean, boolean)
*/
void toggleScoreboard(TabPlayer player, boolean sendToggleMessage);
/**
* Temporarily displays scoreboard to all players for specified amount of
* time (in milliseconds). Scoreboard name is either defined in config, or
* through API in {@link #createScoreboard(String, String, List)}.
*
* @param scoreboard
* Scoreboard from config or registered via API
* @param duration
* Duration in milliseconds
* @throws IllegalArgumentException
* if no scoreboard was found with such name or {@code duration}
* is &lt; 0.
*/
void announceScoreboard(String scoreboard, int duration);
/**
* Returns player's currently displayed scoreboard. This can be either with
* configuration, overridden with commands or the API. Will return {@code null}
* if player does not see any scoreboard due to not meeting any display condition.
* @param player
* player to get active scoreboard of
* @return player's active scoreboard or {@code null} if player has no scoreboard
*/
Scoreboard getActiveScoreboard(TabPlayer player);
/**
* Returns player's currently displayed scoreboard. This can be either with
* configuration, overridden with commands or the API. Will return {@code null}
* if player does not see any scoreboard due to not meeting any display condition.
*
* @param player
* player to get active scoreboard of
* @return player's active scoreboard or {@code null} if player has no scoreboard
*/
Scoreboard getActiveScoreboard(TabPlayer player);
}
@@ -1,12 +0,0 @@
package me.neznamy.tab.api.task;
public interface RepeatingTask {
int getInterval();
void setInterval(int paramInt);
void cancel();
void setRunnable(Runnable paramRunnable);
}
@@ -4,53 +4,93 @@ import java.util.concurrent.Future;
import me.neznamy.tab.api.TabFeature;
/**
* Manager for scheduling and executing tasks in TAB's
* main thread to avoid concurrency issues.
*/
public interface ThreadManager {
/**
* Starts a task in new thread and measures how long it took to process
* @param errorDescription - description to use if this task throws an error
* @param feature - feature to add cpu usage to
* @param type - usage type to add cpu usage to
* @param task - the task
*/
Future<Void> runMeasuredTask(String errorDescription, TabFeature feature, String type, Runnable task);
/**
* Starts a task in new thread and measures how long it took to process
*
* @param feature
* feature to add cpu usage to
* @param type
* usage type to add cpu usage to
* @param task
* the task
*/
Future<Void> runMeasuredTask(TabFeature feature, String type, Runnable task);
/**
* Starts a task in new thread and measures how long it took to process
* @param errorDescription - description to use if this task throws an error
* @param feature - feature to add cpu usage to
* @param type - usage type to add cpu usage to
* @param task - the task
*/
Future<Void> runMeasuredTask(String errorDescription, String feature, String type, Runnable task);
/**
* Runs task in a new thread
* @param errorDescription - description to use if this task throws an error
* @param task - the task
*/
Future<Void> runTask(String errorDescription, Runnable task);
/**
* Starts a new task with defined repeat interval that measures cpu usage
* @param intervalMilliseconds - task interval
* @param errorDescription - description to use if this task throws an error
* @param feature - feature to add cpu usage to
* @param type - usage type to add cpu usage to
* @param task - the task
*/
RepeatingTask startRepeatingMeasuredTask(int intervalMilliseconds, String errorDescription, TabFeature feature, String type, Runnable task);
/**
* Runs task with a delay and measures how long it took to process
* @param delayMilliseconds - how long to run the task after
* @param errorDescription - description to use if this task throws an error
* @param feature - feature to add cpu usage to
* @param type - usage type to add cpu usage to
* @param task - the task
* @return future allowing to cancel the task
*/
Future<?> runTaskLater(int delayMilliseconds, String errorDescription, TabFeature feature, String type, Runnable task);
/**
* Starts a task in new thread and measures how long it took to process
*
* @param feature
* feature to add cpu usage to
* @param type
* usage type to add cpu usage to
* @param task
* the task
*/
Future<Void> runMeasuredTask(String feature, String type, Runnable task);
Future<?> runTaskLater(int delayMilliseconds, String errorDescription, Runnable task);
/**
* Runs task in a new thread
*
* @param task
* the task
*/
Future<Void> runTask(Runnable task);
/**
* Starts a new task with defined repeat interval that measures cpu usage
*
* @param intervalMilliseconds
* task interval
* @param feature
* feature to add cpu usage to
* @param type
* usage type to add cpu usage to
* @param task
* the task
* @return Future representing the submitted task
*/
Future<?> startRepeatingMeasuredTask(int intervalMilliseconds, TabFeature feature, String type, Runnable task);
/**
* Starts a new task with defined repeat interval
*
* @param intervalMilliseconds
* task interval
* @param task
* the task
* @return Future representing the submitted task
*/
Future<?> startRepeatingTask(int intervalMilliseconds, Runnable task);
/**
* Runs task with a delay and measures how long it took to process
*
* @param delayMilliseconds
* how long to run the task after
* @param feature
* feature to add cpu usage to
* @param type
* usage type to add cpu usage to
* @param task
* the task
* @return Future representing the submitted task
*/
Future<?> runTaskLater(int delayMilliseconds, TabFeature feature, String type, Runnable task);
/**
* Runs task with a delay
*
* @param delayMilliseconds
* how long to run the task after
* @param task
* the task
* @return Future representing the submitted task
*/
Future<?> runTaskLater(int delayMilliseconds, Runnable task);
}
@@ -4,110 +4,179 @@ import me.neznamy.tab.api.TabPlayer;
public interface TeamManager {
/**
* Makes player's NameTag globally invisible
* @see #showNametag(TabPlayer)
* @see #hasHiddenNametag(TabPlayer)
*/
void hideNametag(TabPlayer player);
/**
* Makes player's NameTag globally invisible
*
* @param player
* player to hide nametag of
* @see #showNametag(TabPlayer)
* @see #hasHiddenNametag(TabPlayer)
*/
void hideNametag(TabPlayer player);
/**
* Hides player's NameTag for specified player until it's shown again
* @param viewer - player to hide NameTag for
*/
void hideNametag(TabPlayer player, TabPlayer viewer);
/**
* Hides player's NameTag for specified player until it's shown again
*
* @param player
* player to hide nametag of
* @param viewer
* player to hide NameTag for
*/
void hideNametag(TabPlayer player, TabPlayer viewer);
/**
* Makes player's NameTag visible again
* @see #hideNametag(TabPlayer)
* @see #hasHiddenNametag(TabPlayer)
*/
void showNametag(TabPlayer player);
/**
* Makes player's NameTag visible again
*
* @param player
* player to show nametag of
* @see #hideNametag(TabPlayer)
* @see #hasHiddenNametag(TabPlayer)
*/
void showNametag(TabPlayer player);
/**
* Shows player's NameTag for specified viewer if it was hidden before
* @param viewer - player to show NameTag back for
*/
void showNametag(TabPlayer player, TabPlayer viewer);
/**
* Shows player's NameTag for specified viewer if it was hidden before
*
* @param player
* player to show nametag of
* @param viewer
* player to show NameTag back for
*/
void showNametag(TabPlayer player, TabPlayer viewer);
/**
* Return whether player has hidden NameTag or not
* @return Whether player has hidden NameTag or not
* @see #hideNametag(TabPlayer)
* @see #showNametag(TabPlayer)
*/
boolean hasHiddenNametag(TabPlayer player);
/**
* Return whether player has hidden NameTag or not
*
* @param player
* player to check nametag visibility status of
* @return Whether player has hidden NameTag or not
* @see #hideNametag(TabPlayer)
* @see #showNametag(TabPlayer)
*/
boolean hasHiddenNametag(TabPlayer player);
/**
* Returns true if NameTag is hidden for specified viewer, false if not
* @param viewer - player to check visibility status for
* @return true if hidden, false if not
*/
boolean hasHiddenNametag(TabPlayer player, TabPlayer viewer);
/**
* Unregisters player's team and no longer handles it, as well as disables anti-override for teams.
* This can be resumed using resumeTeamHandling(). If team handling was paused already, nothing happens.
*/
void pauseTeamHandling(TabPlayer player);
/**
* Returns true if NameTag is hidden for specified viewer, false if not
*
* @param player
* player to check visibility status of
* @param viewer
* player to check visibility status for
* @return true if hidden, false if not
*/
boolean hasHiddenNametag(TabPlayer player, TabPlayer viewer);
/**
* Resumes team handling if it was before paused using pauseTeamHandling(), if not, nothing happens
*/
void resumeTeamHandling(TabPlayer player);
/**
* Unregisters player's team and no longer handles it, as well as disables anti-override for teams.
* This can be resumed using resumeTeamHandling(). If team handling was paused already, nothing happens.
*
* @param player
* player to pause team handling of
*/
void pauseTeamHandling(TabPlayer player);
/**
* Returns true if team handling is paused for this player using pauseTeamHandling(), false if not, or
* it was resumed already using resumeTeamHandling
* @return true if paused, false if not
*/
boolean hasTeamHandlingPaused(TabPlayer player);
/**
* Forces new team name for the player until this method is called again with null argument and
* performs all actions to change player's team name
* @param teamName - forced team name
*/
void forceTeamName(TabPlayer player, String teamName);
/**
* Resumes team handling if it was before paused using pauseTeamHandling(), if not, nothing happens
*
* @param player
* player to resume team handling of
*/
void resumeTeamHandling(TabPlayer player);
/**
* Returns forced team name of player or null if not forced
* @return forced team name of player or null if not forced
*/
String getForcedTeamName(TabPlayer player);
/**
* Returns true if team handling is paused for this player using pauseTeamHandling(), false if not, or
* it was resumed already using resumeTeamHandling
*
* @param player
* player to check handling status of
* @return true if paused, false if not
*/
boolean hasTeamHandlingPaused(TabPlayer player);
/**
* Forces collision rule for the player. Setting it to null will remove forced value
* @param collision - forced collision rule
*/
void setCollisionRule(TabPlayer player, Boolean collision);
/**
* Forces new team name for the player until this method is called again with null argument and
* performs all actions to change player's team name
*
* @param player
* player to set team name of
* @param teamName
* forced team name
*/
void forceTeamName(TabPlayer player, String teamName);
/**
* Returns forced collision rule or null if collision is not forced using setCollisionRule
* @return forced value or null if not forced
*/
Boolean getCollisionRule(TabPlayer player);
/**
* Sends update team's properties packet of player's team to everyone
* @param player - player to update team data of
*/
void updateTeamData(TabPlayer player);
void setPrefix(TabPlayer player, String prefix);
void setSuffix(TabPlayer player, String suffix);
void resetPrefix(TabPlayer player);
void resetSuffix(TabPlayer player);
String getCustomPrefix(TabPlayer player);
String getCustomSuffix(TabPlayer player);
String getOriginalPrefix(TabPlayer player);
String getOriginalSuffix(TabPlayer player);
/**
* Returns forced team name of player or null if not forced
*
* @param player
* player to check forced team name of
* @return forced team name of player or null if not forced
*/
String getForcedTeamName(TabPlayer player);
/**
* Forces collision rule for the player. Setting it to null will remove forced value
*
* @param player
* player to set collision rule of
* @param collision
* forced collision rule
*/
void setCollisionRule(TabPlayer player, Boolean collision);
/**
* Returns forced collision rule or null if collision is not forced using setCollisionRule
*
* @param player
* player to get forced collision of
* @return forced value or null if not forced
*/
Boolean getCollisionRule(TabPlayer player);
/**
* Sends update team's properties packet of player's team to everyone
*
* @param player
* player to update team data of
*/
void updateTeamData(TabPlayer player);
void setPrefix(TabPlayer player, String prefix);
void setSuffix(TabPlayer player, String suffix);
void resetPrefix(TabPlayer player);
void resetSuffix(TabPlayer player);
String getCustomPrefix(TabPlayer player);
String getCustomSuffix(TabPlayer player);
String getOriginalPrefix(TabPlayer player);
String getOriginalSuffix(TabPlayer player);
/**
* Toggles nametag visibility view on all players for specified player.
* On first call, nametags of all players will become invisible for specified
* player. On second call, they will become visible again.
*
* @param player
* player to toggle nametag visibility view for
* @param sendToggleMessage
* {@code true} if configured toggle message should be sent, {@code false} if not
*/
void toggleNameTagVisibilityView(TabPlayer player, boolean sendToggleMessage);
/**
* Returns {@code true} if player has hidden nametags by either calling
* {@link #toggleNameTagVisibilityView(TabPlayer, boolean)} or using a command,
* {@code false} if not.
*
* @param player
* player to check
* @return {@code true} if hidden, {@code false} if not
*/
boolean hasHiddenNameTagVisibilityView(TabPlayer player);
}
@@ -6,27 +6,27 @@ import me.neznamy.tab.api.TabPlayer;
public interface UnlimitedNametagManager extends TeamManager {
void disableArmorStands(TabPlayer player);
void enableArmorStands(TabPlayer player);
boolean hasDisabledArmorStands(TabPlayer player);
void disableArmorStands(TabPlayer player);
void enableArmorStands(TabPlayer player);
boolean hasDisabledArmorStands(TabPlayer player);
void setName(TabPlayer player, String customName);
void setLine(TabPlayer player, String line, String value);
void setName(TabPlayer player, String customName);
void setLine(TabPlayer player, String line, String value);
void resetName(TabPlayer player);
void resetLine(TabPlayer player, String line);
void resetName(TabPlayer player);
void resetLine(TabPlayer player, String line);
String getCustomName(TabPlayer player);
String getCustomLineValue(TabPlayer player, String line);
String getCustomName(TabPlayer player);
String getCustomLineValue(TabPlayer player, String line);
String getOriginalName(TabPlayer player);
String getOriginalLineValue(TabPlayer player, String line);
List<String> getDefinedLines();
String getOriginalName(TabPlayer player);
String getOriginalLineValue(TabPlayer player, String line);
List<String> getDefinedLines();
}
@@ -2,17 +2,28 @@ package me.neznamy.tab.api.util;
/**
* An interface for BiFunction which can throw an exception.
* Used in {@link me.neznamy.tab.api.protocol.PacketBuilder} to
* Used in {@link me.neznamy.tab.api.protocol.PacketBuilder} to
* call build method for packet classes, which may throw an
* exception due to reflection.
* @param <A>
* First function argument
* @param <B>
* Second function argument
* @param <C>
* Function return type
* @param <A>
* First function argument
* @param <B>
* Second function argument
* @param <C>
* Function return type
*/
public interface BiFunctionWithException<A, B, C> {
C apply(A a, B b) throws ReflectiveOperationException;
/**
* Runs the function and returns the result
*
* @param a
* First argument
* @param b
* Second argument
* @return Output from function
* @throws ReflectiveOperationException
* If reflective operation fails
*/
C apply(A a, B b) throws ReflectiveOperationException;
}
@@ -1,13 +1,78 @@
package me.neznamy.tab.api.util;
import me.neznamy.tab.api.TabPlayer;
/**
* Class with utility methods used in the plugin
*/
public class Preconditions {
public static void checkNotNull(Object obj, String exceptionMessage){
if (obj == null) throw new IllegalArgumentException(exceptionMessage);
/**
* Private constructor preventing the class from being instantiated
*/
private Preconditions() {}
/**
* Checks object and throws {@code IllegalArgumentException} if it is {@code null}.
*
* @param obj
* Object to check
* @param name
* Name of the variable to use in exception message
* @throws IllegalArgumentException
* if {@code obj} is {@code null}
*/
public static void checkNotNull(Object obj, String name) {
if (obj == null) throw new IllegalArgumentException(name + " cannot be null");
}
public static void checkRange(Number number, Number min, Number max, String variable){
/**
* Checks length of String and throws {@code IllegalArgumentException} if it is
* longer than limit.
*
* @param string
* String to check
* @param maxLength
* Maximum length of the string
* @param name
* Name of the variable to use in exception message
* @throws IllegalArgumentException
* if {@code string} is {@code null} or longer than {@code maxLength} characters
*/
public static void checkMaxLength(String string, int maxLength, String name) {
checkNotNull(string, name);
if (string.length() > maxLength) throw new IllegalArgumentException(name + " is longer than " + maxLength + " characters (" + string.length() + ")");
}
/**
* Checks number range and throws {@code IllegalArgumentException} if number
* is out of range.
*
* @param number
* Number to check
* @param min
* Minimum allowed value
* @param max
* Maximum allowed value
* @param variable
* Variable name to use in exception message
* @throws IllegalArgumentException
* if {@code number} is out of range ({@code number < min || number > max})
*/
public static void checkRange(Number number, Number min, Number max, String variable) {
if (number.doubleValue() < min.doubleValue() || number.doubleValue() > max.doubleValue())
throw new IllegalArgumentException("Number index out of range (" + min + " - " + max + ") for " + variable);
throw new IllegalArgumentException(variable + " index out of range (" + min + " - " + max + "), was " + number);
}
/**
* Checks if player is loaded and throws {@code IllegalStateException} if not.
*
* @param player
* Player to check
* @throws IllegalStateException
* If player is not loaded
*/
public static void checkLoaded(TabPlayer player) {
if (!player.isLoaded()) throw new IllegalStateException("Player is not loaded yet. Try again later.");
}
}
+9 -4
View File
@@ -2,11 +2,16 @@ plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
mavenCentral()
dependencies {
implementation(libs.plugin.indra)
implementation(libs.plugin.shadow)
}
dependencies {
implementation("gradle.plugin.com.github.johnrengelman", "shadow", "7.1.0")
compileOnly(files(libs::class.java.protectionDomain.codeSource.location))
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
+16
View File
@@ -0,0 +1,16 @@
rootProject.name = "build-logic"
dependencyResolutionManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
// You're a great IDE IntelliJ, but sometimes you really annoy me. >:(
// repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
versionCatalogs {
register("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
@@ -1,7 +1,16 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
`java-library`
id("net.kyori.indra")
}
indra {
javaVersions {
target(8)
}
github("NEZNAMY", "TAB") {
ci(true)
}
}
tasks {
@@ -13,18 +22,9 @@ tasks {
))
}
javadoc {
// This saves a decent bit of processing power on slow machines
enabled = false
options.encoding = Charsets.UTF_8.name()
(options as StandardJavadocDocletOptions).addStringOption("Xdoclint:none", "-quiet")
}
compileJava {
options.encoding = Charsets.UTF_8.name()
options.release.set(8)
options.compilerArgs.addAll(listOf("-nowarn", "-Xlint:-unchecked", "-Xlint:-deprecation"))
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
@@ -1,31 +1,12 @@
import gradle.kotlin.dsl.accessors._4cbb0020bd75d48acc42cabded5fdbb7.publishing
import org.gradle.api.artifacts.repositories.PasswordCredentials
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.repositories
plugins {
id("tab.base-conventions")
`maven-publish`
id("net.kyori.indra.publishing")
}
java {
withSourcesJar()
withJavadocJar()
}
publishing {
repositories {
maven {
name = "krypton"
url = uri("https://repo.kryptonmc.org/releases")
credentials(PasswordCredentials::class)
}
}
publications.create<MavenPublication>("mavenJava") {
groupId = rootProject.group as String
artifactId = "tab-${project.name}"
version = rootProject.version as String
from(components["java"])
indra {
publishReleasesTo("krypton", "https://repo.kryptonmc.org/releases")
publishSnapshotsTo("krypton", "https://repo.kryptonmc.org/snapshots")
configurePublications {
artifactId = "tab-$artifactId"
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ allprojects {
val platforms = setOf(
projects.bukkit,
projects.bungeecord,
projects.krypton,
// projects.krypton,
projects.velocity
).map { it.dependencyProject }
+3 -2
View File
@@ -6,6 +6,7 @@ dependencies {
compileOnly(libs.vault)
compileOnly(libs.via)
compileOnly(libs.authlib)
compileOnly(libs.libsDisguises)
compileOnly(libs.essentials)
compileOnly(libs.essentials) {
exclude("org.spigotmc", "spigot-api")
}
}
@@ -1,5 +1,6 @@
package me.neznamy.tab.platforms.bukkit;
import me.neznamy.tab.api.TabAPI;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@@ -8,56 +9,72 @@ import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import me.neznamy.tab.shared.TAB;
/**
* The core for bukkit forwarding events into all enabled features
*/
public class BukkitEventListener implements Listener {
private final Main main;
public BukkitEventListener(Main main) {
this.main = main;
}
/**
* Listener to PlayerQuitEvent to remove player data and forward the event to features
* @param e quit event
*/
@EventHandler(priority = EventPriority.HIGHEST)
public void onQuit(PlayerQuitEvent e){
if (TAB.getInstance().isDisabled()) return;
TAB.getInstance().getCPUManager().runTask("processing PlayerQuitEvent", () -> TAB.getInstance().getFeatureManager().onQuit(TAB.getInstance().getPlayer(e.getPlayer().getUniqueId())));
}
/**
* Listener to PlayerJoinEvent to create player data and forward the event to features
* @param e join event
*/
@EventHandler(priority = EventPriority.LOWEST)
public void onJoin(PlayerJoinEvent e) {
if (TAB.getInstance().isDisabled()) return;
TAB.getInstance().getCPUManager().runTask("processing PlayerJoinEvent", () -> TAB.getInstance().getFeatureManager().onJoin(new BukkitTabPlayer(e.getPlayer(), main.getProtocolVersion(e.getPlayer()))));
}
/** Platform instance */
private final BukkitPlatform platform;
/**
* Listener to PlayerChangedWorldEvent to forward the event to features
* @param e world changed event
*/
@EventHandler(priority = EventPriority.LOWEST)
public void onWorldChange(PlayerChangedWorldEvent e){
if (TAB.getInstance().isDisabled()) return;
TAB.getInstance().getCPUManager().runTask("processing PlayerChangedWorldEvent", () -> TAB.getInstance().getFeatureManager().onWorldChange(e.getPlayer().getUniqueId(), e.getPlayer().getWorld().getName()));
}
/**
* Constructs new instance with given parameter
*
* @param platform
* Platform instance
*/
public BukkitEventListener(BukkitPlatform platform) {
this.platform = platform;
}
/**
* Listener to PlayerQuitEvent to remove player data and forward the event to features
*
* @param e
* quit event
*/
@EventHandler(priority = EventPriority.HIGHEST)
public void onQuit(PlayerQuitEvent e){
if (TabAPI.getInstance().isPluginDisabled()) return;
TabAPI.getInstance().getThreadManager().runTask(() -> TabAPI.getInstance().getFeatureManager().onQuit(TabAPI.getInstance().getPlayer(e.getPlayer().getUniqueId())));
}
/**
* Listener to PlayerJoinEvent to create player data and forward the event to features
*
* @param e
* join event
*/
@EventHandler(priority = EventPriority.LOWEST)
public void onJoin(PlayerJoinEvent e) {
if (TabAPI.getInstance().isPluginDisabled()) return;
TabAPI.getInstance().getThreadManager().runTask(() ->
TabAPI.getInstance().getFeatureManager().onJoin(new BukkitTabPlayer(e.getPlayer(), platform.getProtocolVersion(e.getPlayer()))));
}
/**
* Listener to PlayerChangedWorldEvent to forward the event to features
* @param e command preprocess event
*/
@EventHandler
public void onCommand(PlayerCommandPreprocessEvent e) {
if (TAB.getInstance().isDisabled()) return;
if (TAB.getInstance().getFeatureManager().onCommand(TAB.getInstance().getPlayer(e.getPlayer().getUniqueId()), e.getMessage())) e.setCancelled(true);
}
/**
* Listener to PlayerChangedWorldEvent to forward the event to features
*
* @param e
* world changed event
*/
@EventHandler(priority = EventPriority.LOWEST)
public void onWorldChange(PlayerChangedWorldEvent e){
if (TabAPI.getInstance().isPluginDisabled()) return;
TabAPI.getInstance().getThreadManager().runTask(() ->
TabAPI.getInstance().getFeatureManager().onWorldChange(e.getPlayer().getUniqueId(), e.getPlayer().getWorld().getName()));
}
/**
* Listener to PlayerChangedWorldEvent to forward the event to features
*
* @param e
* command preprocess event
*/
@EventHandler
public void onCommand(PlayerCommandPreprocessEvent e) {
if (TabAPI.getInstance().isPluginDisabled()) return;
if (TabAPI.getInstance().getFeatureManager().onCommand(
TabAPI.getInstance().getPlayer(e.getPlayer().getUniqueId()), e.getMessage())) e.setCancelled(true);
}
}
File diff suppressed because it is too large. Load diff
@@ -2,6 +2,7 @@ package me.neznamy.tab.platforms.bukkit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Function;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
@@ -9,120 +10,140 @@ import io.netty.channel.ChannelPromise;
import me.neznamy.tab.api.TabFeature;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.NMSStorage;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.features.NickCompatibility;
import me.neznamy.tab.shared.features.PipelineInjector;
import me.neznamy.tab.shared.features.sorting.Sorting;
import org.jetbrains.annotations.NotNull;
/**
* Pipeline injection for bukkit
*/
public class BukkitPipelineInjector extends PipelineInjector {
//nms storage
private final NMSStorage nms = NMSStorage.getInstance();
/** NMS data storage */
private final NMSStorage nms = NMSStorage.getInstance();
/**
* Constructs new instance
*/
public BukkitPipelineInjector(){
super("packet_handler");
channelFunction = BukkitChannelDuplexHandler::new;
}
/**
* Constructs new instance
*/
public BukkitPipelineInjector(){
super("packet_handler");
}
/**
* Custom channel duplex handler override
*/
public class BukkitChannelDuplexHandler extends ChannelDuplexHandler {
//injected player
private final TabPlayer player;
/**
* Constructs new instance with given player
* @param player - player to inject
*/
public BukkitChannelDuplexHandler(TabPlayer player) {
this.player = player;
}
@Override
public Function<TabPlayer, ChannelDuplexHandler> getChannelFunction() {
return BukkitChannelDuplexHandler::new;
}
@Override
public void channelRead(@NotNull ChannelHandlerContext context, @NotNull Object packet) {
try {
if (TAB.getInstance().getFeatureManager().onPacketReceive(player, packet)) return;
super.channelRead(context, packet);
} catch (Exception e){
TAB.getInstance().getErrorManager().printError("An error occurred when reading packets", e);
}
}
/**
* Custom channel duplex handler override
*/
public class BukkitChannelDuplexHandler extends ChannelDuplexHandler {
@Override
public void write(ChannelHandlerContext context, Object packet, ChannelPromise channelPromise) {
try {
if (nms.PacketPlayOutPlayerInfo.isInstance(packet)) {
super.write(context, TAB.getInstance().getFeatureManager().onPacketPlayOutPlayerInfo(player, packet), channelPromise);
return;
}
if (antiOverrideTeams && nms.PacketPlayOutScoreboardTeam != null && nms.PacketPlayOutScoreboardTeam.isInstance(packet)) {
modifyPlayers(packet);
super.write(context, packet, channelPromise);
return;
}
if (nms.PacketPlayOutScoreboardDisplayObjective.isInstance(packet)){
TAB.getInstance().getFeatureManager().onDisplayObjective(player, packet);
}
if (nms.PacketPlayOutScoreboardObjective.isInstance(packet)) {
TAB.getInstance().getFeatureManager().onObjective(player, packet);
}
TAB.getInstance().getFeatureManager().onPacketSend(player, packet);
} catch (Exception e){
TAB.getInstance().getErrorManager().printError("An error occurred when reading packets", e);
}
try {
super.write(context, packet, channelPromise);
} catch (Exception e) {
TAB.getInstance().getErrorManager().printError("Failed to forward packet " + packet.getClass().getSimpleName() + " to " + player.getName(), e);
}
}
/**
* Removes all real players from team if packet does not come from TAB and reports this to override log
* @param packetPlayOutScoreboardTeam - team packet
* @throws ReflectiveOperationException
* nmsGameMode
*/
@SuppressWarnings("unchecked")
private void modifyPlayers(Object packetPlayOutScoreboardTeam) throws ReflectiveOperationException {
long time = System.nanoTime();
Collection<String> players = (Collection<String>) nms.PacketPlayOutScoreboardTeam_PLAYERS.get(packetPlayOutScoreboardTeam);
String teamName = (String) nms.PacketPlayOutScoreboardTeam_NAME.get(packetPlayOutScoreboardTeam);
if (players == null) return;
//creating a new list to prevent NoSuchFieldException in minecraft packet encoder when a player is removed
Collection<String> newList = new ArrayList<>();
for (String entry : players) {
TabPlayer p = getPlayer(entry);
if (p == null) {
newList.add(entry);
continue;
}
if (!((TabFeature)TAB.getInstance().getTeamManager()).isDisabledPlayer(p) &&
!TAB.getInstance().getTeamManager().hasTeamHandlingPaused(p) && !teamName.equals(p.getTeamName())) {
logTeamOverride(teamName, p.getName(), p.getTeamName());
} else {
newList.add(entry);
}
}
nms.setField(packetPlayOutScoreboardTeam, nms.PacketPlayOutScoreboardTeam_PLAYERS, newList);
TAB.getInstance().getCPUManager().addTime("NameTags", TabConstants.CpuUsageCategory.ANTI_OVERRIDE, System.nanoTime()-time);
}
private TabPlayer getPlayer(String name) {
for (TabPlayer p : TAB.getInstance().getOnlinePlayers()) {
if (getName(p).equals(name)) return p;
}
return null;
}
/** Injected player */
private final TabPlayer player;
private String getName(TabPlayer p) {
return ((NickCompatibility) TAB.getInstance().getFeatureManager().getFeature(TabConstants.Feature.NICK_COMPATIBILITY)).getNickname(p);
}
}
/**
* Constructs new instance with given player
*
* @param player
* player to inject
*/
public BukkitChannelDuplexHandler(TabPlayer player) {
this.player = player;
}
@Override
public void channelRead(@NotNull ChannelHandlerContext context, @NotNull Object packet) {
try {
if (TAB.getInstance().getFeatureManager().onPacketReceive(player, packet)) return;
super.channelRead(context, packet);
} catch (Exception e){
TAB.getInstance().getErrorManager().printError("An error occurred when reading packets", e);
}
}
@Override
public void write(ChannelHandlerContext context, Object packet, ChannelPromise channelPromise) {
try {
if (nms.PacketPlayOutPlayerInfo.isInstance(packet) ||
(nms.ClientboundPlayerInfoRemovePacket != null && nms.ClientboundPlayerInfoRemovePacket.isInstance(packet))) {
super.write(context, TAB.getInstance().getFeatureManager().onPacketPlayOutPlayerInfo(player, packet), channelPromise);
return;
}
if (antiOverrideTeams && nms.PacketPlayOutScoreboardTeam != null && nms.PacketPlayOutScoreboardTeam.isInstance(packet)) {
long time = System.nanoTime();
modifyPlayers(packet);
TAB.getInstance().getCPUManager().addTime("NameTags", TabConstants.CpuUsageCategory.ANTI_OVERRIDE, System.nanoTime()-time);
super.write(context, packet, channelPromise);
return;
}
if (nms.PacketPlayOutScoreboardDisplayObjective.isInstance(packet)){
TAB.getInstance().getFeatureManager().onDisplayObjective(player, packet);
}
if (nms.PacketPlayOutScoreboardObjective.isInstance(packet)) {
TAB.getInstance().getFeatureManager().onObjective(player, packet);
}
TAB.getInstance().getFeatureManager().onPacketSend(player, packet);
} catch (Exception e){
TAB.getInstance().getErrorManager().printError("An error occurred when reading packets", e);
}
try {
super.write(context, packet, channelPromise);
} catch (Exception e) {
TAB.getInstance().getErrorManager().printError("Failed to forward packet " + packet.getClass().getSimpleName() + " to " + player.getName(), e);
}
}
/**
* Removes all real players from team if packet does not come from TAB and reports this to override log
*
* @param packetPlayOutScoreboardTeam
* team packet
* @throws ReflectiveOperationException
* nmsGameMode
*/
@SuppressWarnings("unchecked")
private void modifyPlayers(Object packetPlayOutScoreboardTeam) throws ReflectiveOperationException {
int action = nms.PacketPlayOutScoreboardTeam_ACTION.getInt(packetPlayOutScoreboardTeam);
if (action == 1 || action == 2 || action == 4) return;
Collection<String> players = (Collection<String>) nms.PacketPlayOutScoreboardTeam_PLAYERS.get(packetPlayOutScoreboardTeam);
String teamName = (String) nms.PacketPlayOutScoreboardTeam_NAME.get(packetPlayOutScoreboardTeam);
if (players == null) return;
//creating a new list to prevent NoSuchFieldException in minecraft packet encoder when a player is removed
Collection<String> newList = new ArrayList<>();
for (String entry : players) {
TabPlayer p = getPlayer(entry);
if (p == null) {
newList.add(entry);
continue;
}
Sorting sorting = (Sorting) TAB.getInstance().getFeatureManager().getFeature(TabConstants.Feature.SORTING);
if (!((TabFeature)TAB.getInstance().getTeamManager()).isDisabledPlayer(p) &&
!TAB.getInstance().getTeamManager().hasTeamHandlingPaused(p) && !teamName.equals(sorting.getShortTeamName(p))) {
logTeamOverride(teamName, p.getName(), sorting.getShortTeamName(p));
} else {
newList.add(entry);
}
}
nms.setField(packetPlayOutScoreboardTeam, nms.PacketPlayOutScoreboardTeam_PLAYERS, newList);
}
/**
* Returns player with matching game profile name. This is different from
* real name when a nick plugin changing names of players is used. If no
* player was found, returns {@code null}.
*
* @param name
* Game profile name
* @return Player with matching game profile name
*/
private TabPlayer getPlayer(String name) {
for (TabPlayer p : TAB.getInstance().getOnlinePlayers()) {
if (p.getNickname().equals(name)) return p;
}
return null;
}
}
}
@@ -1,209 +1,147 @@
package me.neznamy.tab.platforms.bukkit;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Locale;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.placeholder.PlayerPlaceholder;
import me.neznamy.tab.shared.TAB;
import net.ess3.api.events.NickChangeEvent;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import com.earth2me.essentials.Essentials;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.placeholder.PlaceholderManager;
import me.neznamy.tab.api.placeholder.PlayerPlaceholder;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.placeholders.PlaceholderRegistry;
import me.neznamy.tab.shared.placeholders.UniversalPlaceholderRegistry;
import net.milkbowl.vault.chat.Chat;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Bukkit registry to register bukkit-only placeholders
* Bukkit registry to register bukkit-only and universal placeholders
*/
public class BukkitPlaceholderRegistry implements PlaceholderRegistry {
public class BukkitPlaceholderRegistry extends UniversalPlaceholderRegistry {
//formatter for 2 decimal places
public final DecimalFormat decimal2 = ((DecimalFormat)NumberFormat.getNumberInstance(Locale.US));
/** Number formatter for 2 decimal places */
private final NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.US);
private final Plugin plugin;
/** JavaPlugin reference for registering events */
private final JavaPlugin plugin;
private Object chat;
private final Plugin essentials = Bukkit.getPluginManager().getPlugin("Essentials");
private Object server;
private Field recentTps;
private boolean paperTps;
private boolean paperMspt;
private boolean purpur;
private Method playerIsAfk;
/** Vault Chat hook */
private Chat chat;
private Listener healthListener = null;
/** NMS server to get TPS from on spigot */
private Object server;
/**
* Constructs new instance with given parameter
* @param plugin - plugin instance
*/
public BukkitPlaceholderRegistry(Plugin plugin) {
this.plugin = plugin;
decimal2.applyPattern("#.##");
try {
if (Bukkit.getPluginManager().isPluginEnabled("Vault")) {
RegisteredServiceProvider<?> rspChat = Bukkit.getServicesManager().getRegistration(Class.forName("net.milkbowl.vault.chat.Chat"));
if (rspChat != null) chat = rspChat.getProvider();
}
} catch (ClassNotFoundException e) {
//modded server without vault
}
try {
server = Bukkit.getServer().getClass().getMethod("getServer").invoke(Bukkit.getServer());
recentTps = server.getClass().getField("recentTps");
} catch (ReflectiveOperationException e) {
//not spigot
}
try {
Bukkit.class.getMethod("getTPS");
paperTps = true;
} catch (NoSuchMethodException e) {
//not paper
}
try {
Bukkit.class.getMethod("getAverageTickTime");
paperMspt = true;
} catch (NoSuchMethodException e) {
//not paper
}
try {
playerIsAfk = Player.class.getMethod("isAfk");
purpur = true;
} catch (NoSuchMethodException e) {
//not purpur
}
}
/** TPS field*/
private Field recentTps;
@SuppressWarnings("deprecation")
@Override
public void registerPlaceholders(PlaceholderManager manager) {
NumberFormat roundDown = NumberFormat.getNumberInstance(Locale.ENGLISH);
roundDown.setRoundingMode(RoundingMode.DOWN);
roundDown.setMaximumFractionDigits(2);
manager.registerPlayerPlaceholder("%displayname%", 500, p -> ((Player) p.getPlayer()).getDisplayName());
if (paperTps) {
manager.registerServerPlaceholder("%tps%", 1000, () -> formatTPS(Bukkit.getTPS()[0]));
} else if (recentTps != null) {
manager.registerServerPlaceholder("%tps%", 1000, () -> {
try {
return formatTPS(((double[]) recentTps.get(server))[0]);
} catch (IllegalAccessException e) {
return "-1";
}
});
} else {
manager.registerServerPlaceholder("%tps%", -1, () -> "-1").enableTriggerMode();
}
if (paperMspt) {
manager.registerServerPlaceholder("%mspt%", 1000, () -> roundDown.format(Bukkit.getAverageTickTime()));
}
manager.registerPlayerPlaceholder("%afk%", 500, p -> {
if (essentials != null && ((Essentials)essentials).getUser(p.getUniqueId()).isAfk()) return true;
if (!purpur || playerIsAfk == null) return false;
try {
return playerIsAfk.invoke(p.getPlayer());
} catch (final IllegalAccessException | InvocationTargetException exception) {
TAB.getInstance().getErrorManager().printError("Failed to invoke isAfk!", exception);
}
return false;
});
manager.registerPlayerPlaceholder("%essentialsnick%", 1000, p -> {
String nickname = null;
if (essentials != null)
nickname = ((Essentials)essentials).getUser(p.getUniqueId()).getNickname();
return nickname == null || nickname.length() == 0 ? p.getName() : nickname;
});
if (chat != null) {
manager.registerPlayerPlaceholder("%vault-prefix%", 500, p -> ((Chat) chat).getPlayerPrefix((Player) p.getPlayer()));
manager.registerPlayerPlaceholder("%vault-suffix%", 500, p -> ((Chat) chat).getPlayerSuffix((Player) p.getPlayer()));
} else {
manager.registerServerPlaceholder("%vault-prefix%", -1, () -> "").enableTriggerMode();
manager.registerServerPlaceholder("%vault-suffix%", -1, () -> "").enableTriggerMode();
}
registerOnlinePlaceholders(manager);
registerHealthPlaceholder(manager);
}
/** Detection for presence of Paper's TPS getter */
private Method paperTps;
private String formatTPS(double tps) {
return decimal2.format(Math.min(20, tps));
}
/** Detection for presence of Paper's MSPT getter */
private Method paperMspt;
private void registerOnlinePlaceholders(PlaceholderManager manager) {
manager.registerPlayerPlaceholder("%online%", 1000, p -> {
int count = 0;
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()){
if (((Player) p.getPlayer()).canSee((Player) all.getPlayer())) count++;
}
return count;
});
manager.registerPlayerPlaceholder("%staffonline%", 2000, p -> {
int count = 0;
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()){
if (all.hasPermission(TabConstants.Permission.STAFF) && ((Player) p.getPlayer()).canSee((Player) all.getPlayer())) count++;
}
return count;
});
manager.registerPlayerPlaceholder("%nonstaffonline%", 2000, p -> {
int count = 0;
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()){
if (!all.hasPermission(TabConstants.Permission.STAFF) && ((Player) p.getPlayer()).canSee((Player) all.getPlayer())) count++;
}
return count;
});
}
/** Detection for presence of Purpur's AFK getter */
private Method purpurIsAfk;
@SuppressWarnings("deprecation")
private void registerHealthPlaceholder(PlaceholderManager manager) {
if (TAB.getInstance().getServerVersion().getMinorVersion() >= 6) {
PlayerPlaceholder health = manager.registerPlayerPlaceholder("%health%", -1, p -> (int) Math.ceil(((Player) p.getPlayer()).getHealth()));
health.enableTriggerMode(() -> {
healthListener = new Listener() {
/**
* Constructs new instance with given parameter and loads hooks
*
* @param plugin
* reference to the plugin's main class
*/
public BukkitPlaceholderRegistry(JavaPlugin plugin) {
this.plugin = plugin;
numberFormat.setMaximumFractionDigits(2);
if (Bukkit.getPluginManager().isPluginEnabled(TabConstants.Plugin.VAULT)) {
RegisteredServiceProvider<Chat> rspChat = Bukkit.getServicesManager().getRegistration(Chat.class);
if (rspChat != null) chat = rspChat.getProvider();
}
try {
server = Bukkit.getServer().getClass().getMethod("getServer").invoke(Bukkit.getServer());
recentTps = server.getClass().getField("recentTps");
} catch (ReflectiveOperationException e) {
//not spigot
}
try { paperTps = Bukkit.class.getMethod("getTPS"); } catch (NoSuchMethodException ignored) {}
try { paperMspt = Bukkit.class.getMethod("getAverageTickTime"); } catch (NoSuchMethodException ignored) {}
try { purpurIsAfk = Player.class.getMethod("isAfk"); } catch (NoSuchMethodException ignored) {}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onDamage(EntityDamageEvent e) {
if (e.getEntity() instanceof Player) {
Player p = (Player) e.getEntity();
TabPlayer tabp = TAB.getInstance().getPlayer(e.getEntity().getUniqueId());
if (tabp == null) return;
health.updateValue(tabp, (int) Math.ceil(Math.max(p.getHealth() - e.getFinalDamage(), 0)));
}
}
@SuppressWarnings("deprecation")
@Override
public void registerPlaceholders(PlaceholderManager manager) {
manager.registerPlayerPlaceholder(TabConstants.Placeholder.DISPLAY_NAME, 500, p -> ((Player) p.getPlayer()).getDisplayName());
if (paperTps != null) {
manager.registerServerPlaceholder(TabConstants.Placeholder.TPS, 1000, () -> formatTPS(Bukkit.getTPS()[0]));
} else if (recentTps != null) {
manager.registerServerPlaceholder(TabConstants.Placeholder.TPS, 1000, () -> {
try {
return formatTPS(((double[]) recentTps.get(server))[0]);
} catch (IllegalAccessException e) {
return -1;
}
});
} else {
manager.registerServerPlaceholder(TabConstants.Placeholder.TPS, -1, () -> -1);
}
if (paperMspt != null) {
manager.registerServerPlaceholder(TabConstants.Placeholder.MSPT, 1000, () -> numberFormat.format(Bukkit.getAverageTickTime()));
}
Plugin essentials = Bukkit.getPluginManager().getPlugin(TabConstants.Plugin.ESSENTIALS);
manager.registerPlayerPlaceholder(TabConstants.Placeholder.AFK, 500, p -> {
if (essentials != null && ((Essentials)essentials).getUser(p.getUniqueId()).isAfk()) return true;
return purpurIsAfk != null && ((Player)p.getPlayer()).isAfk();
});
if (essentials != null) {
PlayerPlaceholder nick = manager.registerPlayerPlaceholder(TabConstants.Placeholder.ESSENTIALS_NICK, -1, p -> {
String nickname = ((Essentials)essentials).getUser(p.getUniqueId()).getNickname();
return nickname == null ? p.getName() : nickname;
});
Listener nickListener = new Listener() {
@EventHandler
public void onNickChange(NickChangeEvent e) {
String name = e.getValue() == null ? e.getController().getName() : e.getValue();
TabPlayer player = TAB.getInstance().getPlayer(e.getController().getUUID());
if (player == null) return;
nick.updateValue(player, name);
}
};
nick.enableTriggerMode(() -> Bukkit.getPluginManager().registerEvents(nickListener, plugin),
() -> HandlerList.unregisterAll(nickListener));
} else {
manager.registerPlayerPlaceholder(TabConstants.Placeholder.ESSENTIALS_NICK, -1, TabPlayer::getName);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onRegain(EntityRegainHealthEvent e) {
if (e.getEntity() instanceof Player) {
Player p = (Player) e.getEntity();
TabPlayer tabp = TAB.getInstance().getPlayer(e.getEntity().getUniqueId());
if (tabp == null) return;
health.updateValue(tabp, (int) Math.ceil(Math.min(p.getHealth() + e.getAmount(), p.getMaxHealth())));
}
}
if (chat != null) {
manager.registerPlayerPlaceholder(TabConstants.Placeholder.VAULT_PREFIX, 1000, p -> chat.getPlayerPrefix((Player) p.getPlayer()));
manager.registerPlayerPlaceholder(TabConstants.Placeholder.VAULT_SUFFIX, 1000, p -> chat.getPlayerSuffix((Player) p.getPlayer()));
} else {
manager.registerServerPlaceholder(TabConstants.Placeholder.VAULT_PREFIX, -1, () -> "");
manager.registerServerPlaceholder(TabConstants.Placeholder.VAULT_SUFFIX, -1, () -> "");
}
manager.registerPlayerPlaceholder(TabConstants.Placeholder.HEALTH, 100, p -> (int) Math.ceil(((Player) p.getPlayer()).getHealth()));
super.registerPlaceholders(manager);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onRespawn(PlayerRespawnEvent e) {
health.updateValue(TAB.getInstance().getPlayer(e.getPlayer().getUniqueId()), (int) e.getPlayer().getMaxHealth());
}
};
Bukkit.getPluginManager().registerEvents(healthListener, plugin);
}, () -> HandlerList.unregisterAll(healthListener));
}
}
/**
* Formats TPS using number formatter with 2 decimal places.
*
* @param tps
* TPS to format
* @return Formatted TPS as a String
*/
private String formatTPS(double tps) {
return numberFormat.format(Math.min(20, tps));
}
}
@@ -1,274 +1,285 @@
package me.neznamy.tab.platforms.bukkit;
import java.io.File;
import java.util.Collection;
import java.util.List;
import me.neznamy.tab.shared.TabConstants;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import com.earth2me.essentials.Essentials;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
import com.viaversion.viaversion.api.Via;
import me.clip.placeholderapi.PlaceholderAPI;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.chat.EnumChatFormat;
import me.neznamy.tab.api.protocol.PacketBuilder;
import me.neznamy.tab.platforms.bukkit.event.TabLoadEvent;
import me.neznamy.tab.platforms.bukkit.event.TabPlayerLoadEvent;
import me.neznamy.tab.platforms.bukkit.features.BukkitTabExpansion;
import me.neznamy.tab.platforms.bukkit.features.PerWorldPlayerList;
import me.neznamy.tab.platforms.bukkit.features.PetFix;
import me.neznamy.tab.platforms.bukkit.features.TabExpansion;
import me.neznamy.tab.platforms.bukkit.features.WitherBossBar;
import me.neznamy.tab.platforms.bukkit.features.unlimitedtags.NameTagX;
import me.neznamy.tab.platforms.bukkit.features.unlimitedtags.BukkitNameTagX;
import me.neznamy.tab.platforms.bukkit.permission.Vault;
import me.neznamy.tab.shared.Platform;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.features.PlaceholderManagerImpl;
import me.neznamy.tab.shared.features.bossbar.BossBarManagerImpl;
import me.neznamy.tab.shared.features.nametags.NameTag;
import me.neznamy.tab.shared.features.sorting.Sorting;
import me.neznamy.tab.shared.permission.LuckPerms;
import me.neznamy.tab.shared.permission.None;
import me.neznamy.tab.shared.permission.PermissionPlugin;
import me.neznamy.tab.shared.permission.UltraPermissions;
import me.neznamy.tab.shared.placeholders.PlayerPlaceholderImpl;
import me.neznamy.tab.shared.placeholders.UniversalPlaceholderRegistry;
import net.milkbowl.vault.permission.Permission;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Collection;
/**
* Bukkit's implementation of Platform interface
* Implementation of Platform interface for Bukkit platform
*/
public class BukkitPlatform implements Platform {
public class BukkitPlatform extends Platform {
//plugin instance
private final Main plugin;
/** Plugin instance for registering tasks and events */
private final JavaPlugin plugin;
private final BukkitPacketBuilder packetBuilder = new BukkitPacketBuilder();
/** Variables checking presence of other plugins to hook into */
private final boolean placeholderAPI = Bukkit.getPluginManager().isPluginEnabled(TabConstants.Plugin.PLACEHOLDER_API);
private boolean libsDisguises = Bukkit.getPluginManager().isPluginEnabled(TabConstants.Plugin.LIBS_DISGUISES);
private Plugin viaVersion;
private final boolean protocolSupport = Bukkit.getPluginManager().isPluginEnabled(TabConstants.Plugin.PROTOCOL_SUPPORT);
//booleans to check plugin presence
private boolean placeholderAPI;
private boolean libsDisguises;
private Plugin essentials;
/**
* Constructs new instance with given plugin parameter
*
* @param plugin
* plugin instance
*/
public BukkitPlatform(JavaPlugin plugin) {
super(new BukkitPacketBuilder());
this.plugin = plugin;
}
/**
* Constructs new instance with given parameters
* @param plugin - plugin instance
*/
public BukkitPlatform(Main plugin) {
this.plugin = plugin;
}
@Override
public PermissionPlugin detectPermissionPlugin() {
if (Bukkit.getPluginManager().isPluginEnabled(TabConstants.Plugin.LUCKPERMS)) {
return new LuckPerms(getPluginVersion(TabConstants.Plugin.LUCKPERMS));
} else if (Bukkit.getPluginManager().isPluginEnabled(TabConstants.Plugin.VAULT)) {
RegisteredServiceProvider<Permission> provider = Bukkit.getServicesManager().getRegistration(Permission.class);
if (provider == null) return new None();
return new Vault(provider.getProvider(), getPluginVersion(TabConstants.Plugin.VAULT));
} else {
return new None();
}
}
@Override
public PermissionPlugin detectPermissionPlugin() {
if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) {
return new LuckPerms(getPluginVersion("LuckPerms"));
} else if (Bukkit.getPluginManager().isPluginEnabled("UltraPermissions")) {
return new UltraPermissions(getPluginVersion("UltraPermissions"));
} else if (Bukkit.getPluginManager().isPluginEnabled("Vault")) {
RegisteredServiceProvider<Permission> provider = Bukkit.getServicesManager().getRegistration(Permission.class);
if (provider == null) return new None();
return new Vault(provider.getProvider(), getPluginVersion("Vault"));
} else {
return new None();
}
}
@Override
public void loadFeatures() {
if (Bukkit.getPluginManager().isPluginEnabled(TabConstants.Plugin.VIAVERSION)) {
try {
Class.forName("com.viaversion.viaversion.api.Via");
viaVersion = Bukkit.getPluginManager().getPlugin(TabConstants.Plugin.VIAVERSION);
} catch (ClassNotFoundException e) {
TAB.getInstance().sendConsoleMessage("&cAn outdated version of ViaVersion (" + getPluginVersion(TabConstants.Plugin.VIAVERSION) + ") was detected.", true);
TAB.getInstance().sendConsoleMessage("&cTAB only supports ViaVersion 4.0.0 and above. Disabling ViaVersion hook.", true);
TAB.getInstance().sendConsoleMessage("&cThis might cause problems, such as limitations still being present for latest MC clients as well as RGB not working.", true);
}
}
TAB tab = TAB.getInstance();
if (tab.getConfiguration().isPipelineInjection())
tab.getFeatureManager().registerFeature(TabConstants.Feature.PIPELINE_INJECTION, new BukkitPipelineInjector());
new BukkitPlaceholderRegistry(plugin).registerPlaceholders(tab.getPlaceholderManager());
if (tab.getConfiguration().getConfig().getBoolean("scoreboard-teams.enabled", true)) {
tab.getFeatureManager().registerFeature(TabConstants.Feature.SORTING, new Sorting());
if (tab.getConfiguration().getConfig().getBoolean("scoreboard-teams.unlimited-nametag-mode.enabled", false) && tab.getServerVersion().getMinorVersion() >= 8) {
tab.getFeatureManager().registerFeature(TabConstants.Feature.UNLIMITED_NAME_TAGS, new BukkitNameTagX(plugin));
} else {
tab.getFeatureManager().registerFeature(TabConstants.Feature.NAME_TAGS, new NameTag());
}
}
tab.loadUniversalFeatures();
if (tab.getConfiguration().getConfig().getBoolean("bossbar.enabled", false)) {
if (tab.getServerVersion().getMinorVersion() < 9) {
tab.getFeatureManager().registerFeature(TabConstants.Feature.BOSS_BAR, new WitherBossBar(plugin));
} else {
tab.getFeatureManager().registerFeature(TabConstants.Feature.BOSS_BAR, new BossBarManagerImpl());
}
}
if (tab.getServerVersion().getMinorVersion() >= 9 && tab.getConfiguration().getConfig().getBoolean("fix-pet-names.enabled", false))
tab.getFeatureManager().registerFeature(TabConstants.Feature.PET_FIX, new PetFix());
if (tab.getConfiguration().getConfig().getBoolean("per-world-playerlist.enabled", false))
tab.getFeatureManager().registerFeature(TabConstants.Feature.PER_WORLD_PLAYER_LIST, new PerWorldPlayerList(plugin));
if (placeholderAPI && tab.getConfiguration().getConfig().getBoolean("placeholders.register-tab-expansion", true)) {
BukkitTabExpansion expansion = new BukkitTabExpansion();
expansion.register();
TAB.getInstance().getPlaceholderManager().setTabExpansion(expansion);
}
for (Player p : getOnlinePlayers()) {
tab.addPlayer(new BukkitTabPlayer(p, getProtocolVersion(p)));
}
}
@Override
public void loadFeatures() {
if (Bukkit.getPluginManager().isPluginEnabled("ViaVersion")) {
try {
Class.forName("com.viaversion.viaversion.api.Via");
} catch (ClassNotFoundException e) {
TAB.getInstance().sendConsoleMessage("&c[TAB] An outdated version of ViaVersion (" + getPluginVersion("ViaVersion") + ") was detected.", true);
TAB.getInstance().sendConsoleMessage("&c[TAB] TAB only supports ViaVersion 4.0.0 and above. Disabling ViaVersion hook.", true);
TAB.getInstance().sendConsoleMessage("&c[TAB] This might cause problems, such as limitations still being present for latest MC clients as well as RGB not working.", true);
}
}
if (Bukkit.getPluginManager().isPluginEnabled("Tablisknu")) {
TAB.getInstance().sendConsoleMessage("&c[TAB] Detected plugin \"Tablisknu\", which causes TAB to not work properly. Consider removing the plugin.", true);
}
placeholderAPI = Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI");
libsDisguises = Bukkit.getPluginManager().isPluginEnabled("LibsDisguises");
essentials = Bukkit.getPluginManager().getPlugin("Essentials");
TAB tab = TAB.getInstance();
if (tab.getConfiguration().isPipelineInjection())
tab.getFeatureManager().registerFeature(TabConstants.Feature.PIPELINE_INJECTION, new BukkitPipelineInjector());
new BukkitPlaceholderRegistry(plugin).registerPlaceholders(tab.getPlaceholderManager());
new UniversalPlaceholderRegistry().registerPlaceholders(tab.getPlaceholderManager());
loadNameTagFeature(tab);
tab.loadUniversalFeatures();
if (tab.getConfiguration().getConfig().getBoolean("bossbar.enabled", false)) {
if (tab.getServerVersion().getMinorVersion() < 9) {
tab.getFeatureManager().registerFeature(TabConstants.Feature.BOSS_BAR, new WitherBossBar(plugin));
} else {
tab.getFeatureManager().registerFeature(TabConstants.Feature.BOSS_BAR, new BossBarManagerImpl());
}
}
if (tab.getServerVersion().getMinorVersion() >= 9 && tab.getConfiguration().getConfig().getBoolean("fix-pet-names.enabled", false))
tab.getFeatureManager().registerFeature(TabConstants.Feature.PET_FIX, new PetFix());
if (tab.getConfiguration().getConfig().getBoolean("per-world-playerlist.enabled", false))
tab.getFeatureManager().registerFeature(TabConstants.Feature.PER_WORLD_PLAYER_LIST, new PerWorldPlayerList(plugin));
if (placeholderAPI) {
new TabExpansion(plugin);
}
for (Player p : getOnlinePlayers()) {
tab.addPlayer(new BukkitTabPlayer(p, plugin.getProtocolVersion(p)));
}
}
private String getPluginVersion(String plugin) {
Plugin pl = Bukkit.getPluginManager().getPlugin(plugin);
return pl == null ? "" : pl.getDescription().getVersion();
}
@Override
public String getPluginVersion(String plugin) {
Plugin pl = Bukkit.getPluginManager().getPlugin(plugin);
return pl == null ? null : pl.getDescription().getVersion();
}
/**
* Loads NameTag feature from config
* @param tab - tab instance
*/
private void loadNameTagFeature(TAB tab) {
if (tab.getConfiguration().getConfig().getBoolean("scoreboard-teams.enabled", true)) {
if (tab.getConfiguration().getConfig().getBoolean("scoreboard-teams.unlimited-nametag-mode.enabled", false) && tab.getServerVersion().getMinorVersion() >= 8) {
tab.getFeatureManager().registerFeature(TabConstants.Feature.UNLIMITED_NAME_TAGS, new NameTagX(plugin));
} else {
tab.getFeatureManager().registerFeature(TabConstants.Feature.NAME_TAGS, new NameTag());
}
}
}
/**
* Returns online players from Bukkit API
*
* @return online players from Bukkit API
*/
@SuppressWarnings("unchecked")
private Player[] getOnlinePlayers() {
try {
Object players = Bukkit.class.getMethod("getOnlinePlayers").invoke(null);
if (players instanceof Player[]) {
//1.7-
return (Player[]) players;
} else {
//1.8+
return ((Collection<Player>)players).toArray(new Player[0]);
}
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("Failed to get online players", e);
return new Player[0];
}
}
/**
* Returns list of online players from Bukkit API
* @return list of online players from Bukkit API
*/
@SuppressWarnings("unchecked")
private Player[] getOnlinePlayers() {
try {
Object players = Bukkit.class.getMethod("getOnlinePlayers").invoke(null);
if (players instanceof Player[]) {
//1.7.x
return (Player[]) players;
} else {
//1.8+
return ((Collection<Player>)players).toArray(new Player[0]);
}
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("Failed to get online players", e);
return new Player[0];
}
}
@Override
public void registerUnknownPlaceholder(String identifier) {
PlaceholderManagerImpl pl = TAB.getInstance().getPlaceholderManager();
if (identifier.startsWith("%rel_")) {
//relational placeholder
TAB.getInstance().getPlaceholderManager().registerRelationalPlaceholder(identifier, pl.getRelationalRefresh(identifier), (viewer, target) ->
placeholderAPI ? PlaceholderAPI.setRelationalPlaceholders((Player) viewer.getPlayer(), (Player) target.getPlayer(), identifier) : identifier);
} else {
//normal placeholder
if (identifier.startsWith("%sync:")) {
int refresh = pl.getServerPlaceholderRefreshIntervals().getOrDefault(identifier,
pl.getPlayerPlaceholderRefreshIntervals().getOrDefault(identifier, pl.getDefaultRefresh()));
String syncedPlaceholder = "%" + identifier.substring(6, identifier.length()-1) + "%";
pl.registerPlaceholder(new PlayerPlaceholderImpl(identifier, refresh, null) {
@Override
public Object request(TabPlayer p) {
Bukkit.getScheduler().runTask(plugin, () -> {
long time = System.nanoTime();
updateValue(p, placeholderAPI ? PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), syncedPlaceholder) : identifier);
TAB.getInstance().getCPUManager().addPlaceholderTime(getIdentifier(), System.nanoTime()-time);
});
return null;
}
});
return;
}
if (pl.getServerPlaceholderRefreshIntervals().containsKey(identifier)) {
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier, pl.getServerPlaceholderRefreshIntervals().get(identifier), () ->
placeholderAPI ? PlaceholderAPI.setPlaceholders(null, identifier) : identifier);
} else {
int refresh = pl.getPlayerPlaceholderRefreshIntervals().getOrDefault(identifier, pl.getDefaultRefresh());
TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier, refresh, p ->
placeholderAPI ? PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), identifier) : identifier);
}
}
}
@Override
public void sendConsoleMessage(String message, boolean translateColors) {
Bukkit.getConsoleSender().sendMessage(translateColors ? EnumChatFormat.color(message) : message);
}
/**
* Returns status of LibsDisguises plugin presence
*
* @return {@code true} if plugin is enabled, {@code false} if not
*/
public boolean isLibsDisguisesEnabled() {
return libsDisguises;
}
@Override
public void registerUnknownPlaceholder(String identifier) {
PlaceholderManagerImpl pl = TAB.getInstance().getPlaceholderManager();
if (identifier.startsWith("%rel_")) {
//relational placeholder
TAB.getInstance().getPlaceholderManager().registerRelationalPlaceholder(identifier, pl.getRelationalRefresh(identifier), (viewer, target) ->
placeholderAPI ? PlaceholderAPI.setRelationalPlaceholders((Player) viewer.getPlayer(), (Player) target.getPlayer(), identifier) : identifier);
} else {
//normal placeholder
if (identifier.startsWith("%sync:")) {
int refresh = pl.getServerPlaceholderRefreshIntervals().getOrDefault(identifier,
pl.getPlayerPlaceholderRefreshIntervals().getOrDefault(identifier, pl.getDefaultRefresh()));
pl.registerPlaceholder(new PlayerPlaceholderImpl(identifier, refresh, null) {
@Override
public Object request(TabPlayer p) {
Bukkit.getScheduler().runTask(plugin, () -> {
/**
* Sets LibsDisguises presence status to provided value. This is used
* to disable LibsDisguises hook in case the plugin is not correctly loaded
* for any reason to avoid error spam in the hook.
*
* @param enabled
* New status of LibsDisguises presence
*/
public void setLibsDisguisesEnabled(boolean enabled) {
libsDisguises = enabled;
}
long time = System.nanoTime();
String syncedPlaceholder = identifier.substring(6, identifier.length()-1);
String value = placeholderAPI ? PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), "%" + syncedPlaceholder + "%") : identifier;
getLastValues().put(p, value);
getForceUpdate().add(p);
TAB.getInstance().getCPUManager().addPlaceholderTime(getIdentifier(), System.nanoTime()-time);
});
String value = getLastValues().get(p);
return value == null ? identifier : value;
}
});
return;
}
if (pl.getServerPlaceholderRefreshIntervals().containsKey(identifier)) {
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier, pl.getServerPlaceholderRefreshIntervals().get(identifier), () ->
placeholderAPI ? PlaceholderAPI.setPlaceholders(null, identifier) : identifier);
} else {
int refresh = pl.getPlayerPlaceholderRefreshIntervals().getOrDefault(identifier, pl.getDefaultRefresh());
TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder(identifier, refresh, p ->
placeholderAPI ? PlaceholderAPI.setPlaceholders((Player) p.getPlayer(), identifier) : identifier);
}
}
}
/**
* Gets protocol version of requested player and returns it.
*
* @param player
* Player to get protocol version of
* @return protocol version of the player
*/
public int getProtocolVersion(Player player) {
if (protocolSupport){
int version = getProtocolVersionPS(player);
//some PS versions return -1 on unsupported server versions instead of throwing exception
if (version != -1 && version < TAB.getInstance().getServerVersion().getNetworkId()) return version;
}
if (viaVersion != null) {
return getProtocolVersionVia(player, 0);
}
return TAB.getInstance().getServerVersion().getNetworkId();
}
@Override
public String getServerVersion() {
return Bukkit.getBukkitVersion().split("-")[0] + " (" + Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3] + ")";
}
/**
* Returns protocol version of requested player using ProtocolSupport
*
* @param player
* Player to get protocol version of
* @return protocol version of the player using ProtocolSupport
*/
private int getProtocolVersionPS(Player player){
try {
Object protocolVersion = Class.forName("protocolsupport.api.ProtocolSupportAPI").getMethod("getProtocolVersion", Player.class).invoke(null, player);
int version = (int) protocolVersion.getClass().getMethod("getId").invoke(protocolVersion);
TAB.getInstance().debug("ProtocolSupport returned protocol version " + version + " for " + player.getName() + " (online=" + player.isOnline() + ")");
return version;
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError(String.format("Failed to get protocol version of %s using ProtocolSupport", player.getName()), e);
return TAB.getInstance().getServerVersion().getNetworkId();
}
}
@Override
public File getDataFolder() {
return plugin.getDataFolder();
}
/**
* Returns protocol version of requested player using ViaVersion
*
* @param player
* Player to get protocol version of
* @return protocol version of the player using ViaVersion
*/
private int getProtocolVersionVia(Player player, int retryLevel){
try {
if (retryLevel == 10) {
TAB.getInstance().debug("Failed to get protocol version of " + player.getName() + " after 10 retries");
return TAB.getInstance().getServerVersion().getNetworkId();
}
int version = Via.getAPI().getPlayerVersion(player.getUniqueId());
if (version == -1) {
if (!player.isOnline()) return TAB.getInstance().getServerVersion().getNetworkId();
Thread.sleep(5);
return getProtocolVersionVia(player, retryLevel + 1);
}
TAB.getInstance().debug("ViaVersion returned protocol version " + version + " for " + player.getName() + " (online=" + player.isOnline() + ")");
return version;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1;
} catch (Exception | LinkageError e) {
TAB.getInstance().getErrorManager().printError(String.format("Failed to get protocol version of %s using ViaVersion v%s", player.getName(), viaVersion.getDescription().getVersion()), e);
return TAB.getInstance().getServerVersion().getNetworkId();
}
}
@Override
public void callLoadEvent() {
Bukkit.getPluginManager().callEvent(new TabLoadEvent());
}
@Override
public void callLoadEvent(TabPlayer player) {
Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> Bukkit.getPluginManager().callEvent(new TabPlayerLoadEvent(player)));
}
@Override
public int getMaxPlayers() {
return Bukkit.getMaxPlayers();
}
public boolean isLibsDisguisesEnabled() {
return libsDisguises;
}
public void setLibsDisguisesEnabled(boolean enabled) {
libsDisguises = enabled;
}
public Essentials getEssentials() {
return (Essentials) essentials;
}
@Override
public PacketBuilder getPacketBuilder() {
return packetBuilder;
}
@SuppressWarnings("unchecked")
@Override
public Object getSkin(List<String> properties) {
PropertyMap map = new PropertyMap();
map.put("textures", new Property("textures", properties.get(0), properties.get(1)));
return map;
}
@Override
public boolean isProxy() {
return false;
}
@Override
public boolean isPluginEnabled(String plugin) {
return Bukkit.getPluginManager().isPluginEnabled(plugin);
}
@Override
public String getConfigName() {
return "bukkitconfig.yml";
}
/**
* Sends console message using ConsoleCommandSender, due to
* Paper not translating colors correctly in Logger messages
*
* @param message
* Message to send
* @param translateColors
* Whether color codes should be translated or not
*/
@Override
public void sendConsoleMessage(String message, boolean translateColors) {
Bukkit.getConsoleSender().sendMessage("[TAB] " + (translateColors ? EnumChatFormat.color(message) : message));
}
}
@@ -1,20 +1,21 @@
package me.neznamy.tab.platforms.bukkit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import com.mojang.authlib.properties.Property;
import me.neznamy.tab.api.protocol.PacketPlayOutChat;
import me.neznamy.tab.api.protocol.Skin;
import me.neznamy.tab.api.util.Preconditions;
import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarFlag;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.potion.PotionEffectType;
import com.earth2me.essentials.Essentials;
import com.mojang.authlib.GameProfile;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.api.legacy.bossbar.BossColor;
@@ -22,8 +23,6 @@ import com.viaversion.viaversion.api.legacy.bossbar.BossFlag;
import com.viaversion.viaversion.api.legacy.bossbar.BossStyle;
import io.netty.channel.Channel;
import me.libraryaddict.disguise.DisguiseAPI;
import me.neznamy.tab.api.ProtocolVersion;
import me.neznamy.tab.api.chat.rgb.RGBUtils;
import me.neznamy.tab.api.protocol.PacketPlayOutBoss;
import me.neznamy.tab.platforms.bukkit.nms.NMSStorage;
@@ -31,231 +30,303 @@ import me.neznamy.tab.shared.ITabPlayer;
import me.neznamy.tab.shared.TAB;
/**
* TabPlayer for Bukkit
* TabPlayer implementation for Bukkit platform
*/
public class BukkitTabPlayer extends ITabPlayer {
//nms handle
private Object handle;
/** Player's NMS handle (EntityPlayer), preloading for speed */
private Object handle;
//nms player connection
private Object playerConnection;
//player's visible boss bars
private final Map<UUID, BossBar> bossBars = new HashMap<>();
private final Map<UUID, com.viaversion.viaversion.api.legacy.bossbar.BossBar> viaBossBars = new HashMap<>();
/** Player's connection for sending packets, preloading for speed */
private Object playerConnection;
/** Bukkit BossBars the player can currently see */
private final Map<UUID, BossBar> bossBars = new HashMap<>();
/**
* Constructs new instance with given parameter
* @param p - bukkit player
*/
public BukkitTabPlayer(Player p, int protocolVersion){
super(p, p.getUniqueId(), p.getName(), "N/A", p.getWorld().getName());
try {
handle = NMSStorage.getInstance().getHandle.invoke(player);
playerConnection = NMSStorage.getInstance().PLAYER_CONNECTION.get(handle);
if (NMSStorage.getInstance().CHANNEL != null)
channel = (Channel) NMSStorage.getInstance().CHANNEL.get(NMSStorage.getInstance().NETWORK_MANAGER.get(playerConnection));
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("Failed to get playerConnection or channel of " + p.getName(), e);
}
version = ProtocolVersion.fromNetworkId(protocolVersion);
}
/** ViaVersion BossBars this 1.9+ player can see on 1.8 server */
private final Map<UUID, com.viaversion.viaversion.api.legacy.bossbar.BossBar> viaBossBars = new HashMap<>();
@Override
public boolean hasPermission(String permission) {
long time = System.nanoTime();
boolean value = getPlayer().hasPermission(permission);
TAB.getInstance().getCPUManager().addMethodTime("hasPermission", System.nanoTime()-time);
return value;
}
/**
* Constructs new instance with given bukkit player and protocol version
*
* @param p
* bukkit player
* @param protocolVersion
* Player's protocol network id
*/
public BukkitTabPlayer(Player p, int protocolVersion){
super(p, p.getUniqueId(), p.getName(), TAB.getInstance().getConfiguration().getServerName(), p.getWorld().getName(), protocolVersion, true);
try {
handle = NMSStorage.getInstance().getHandle.invoke(player);
playerConnection = NMSStorage.getInstance().PLAYER_CONNECTION.get(handle);
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("Failed to get playerConnection of " + p.getName(), e);
}
}
@Override
public int getPing() {
try {
int ping = NMSStorage.getInstance().PING.getInt(handle);
if (ping > 10000 || ping < 0) ping = -1;
return ping;
} catch (IllegalAccessException e) {
return -1;
}
}
@Override
public boolean hasPermission(String permission) {
Preconditions.checkNotNull(permission, "permission");
long time = System.nanoTime();
boolean value = getPlayer().hasPermission(permission);
TAB.getInstance().getCPUManager().addMethodTime("hasPermission", System.nanoTime()-time);
return value;
}
@Override
public void sendPacket(Object nmsPacket) {
if (nmsPacket == null || !getPlayer().isOnline()) return;
long time = System.nanoTime();
try {
if (nmsPacket instanceof PacketPlayOutBoss) {
if (TAB.getInstance().getServerVersion().getMinorVersion() >= 9) {
handle((PacketPlayOutBoss) nmsPacket);
} else {
handleVia((PacketPlayOutBoss) nmsPacket);
}
} else {
NMSStorage.getInstance().sendPacket.invoke(playerConnection, nmsPacket);
}
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("An error occurred when sending " + nmsPacket.getClass().getSimpleName(), e);
}
TAB.getInstance().getCPUManager().addMethodTime("sendPacket", System.nanoTime()-time);
}
private void handle(PacketPlayOutBoss packet) {
BossBar bar = bossBars.get(packet.getId());
if (packet.getOperation() == PacketPlayOutBoss.Action.ADD) {
if (bossBars.containsKey(packet.getId())) return;
bar = Bukkit.createBossBar(RGBUtils.getInstance().convertToBukkitFormat(packet.getName(), getVersion().getMinorVersion() >= 16 && TAB.getInstance().getServerVersion().getMinorVersion() >= 16),
BarColor.valueOf(packet.getColor().name()),
BarStyle.valueOf(packet.getOverlay().getBukkitName()));
if (packet.isCreateWorldFog()) bar.addFlag(BarFlag.CREATE_FOG);
if (packet.isDarkenScreen()) bar.addFlag(BarFlag.DARKEN_SKY);
if (packet.isPlayMusic()) bar.addFlag(BarFlag.PLAY_BOSS_MUSIC);
bar.setProgress(packet.getPct());
bossBars.put(packet.getId(), bar);
bar.addPlayer(getPlayer());
return;
}
if (bar == null) return; //no idea how
switch (packet.getOperation()) {
case REMOVE:
bar.removePlayer(getPlayer());
bossBars.remove(packet.getId());
break;
case UPDATE_PCT:
bar.setProgress(packet.getPct());
break;
case UPDATE_NAME:
bar.setTitle(RGBUtils.getInstance().convertToBukkitFormat(packet.getName(), getVersion().getMinorVersion() >= 16 && TAB.getInstance().getServerVersion().getMinorVersion() >= 16));
break;
case UPDATE_STYLE:
bar.setColor(BarColor.valueOf(packet.getColor().name()));
bar.setStyle(BarStyle.valueOf(packet.getOverlay().getBukkitName()));
break;
case UPDATE_PROPERTIES:
bar = bossBars.get(packet.getId());
processFlag(bar, packet.isCreateWorldFog(), BarFlag.CREATE_FOG);
processFlag(bar, packet.isDarkenScreen(), BarFlag.DARKEN_SKY);
processFlag(bar, packet.isPlayMusic(), BarFlag.PLAY_BOSS_MUSIC);
break;
default:
break;
}
}
private void handleVia(PacketPlayOutBoss packet) {
com.viaversion.viaversion.api.legacy.bossbar.BossBar bar;
switch (packet.getOperation()) {
case ADD:
if (viaBossBars.containsKey(packet.getId())) return;
bar = Via.getAPI().legacyAPI().createLegacyBossBar(RGBUtils.getInstance().convertToBukkitFormat(packet.getName(), getVersion().getMinorVersion() >= 16),
packet.getPct(),
BossColor.valueOf(packet.getColor().name()),
BossStyle.valueOf(packet.getOverlay().getBukkitName()));
//fog missing from via API
if (packet.isDarkenScreen()) bar.addFlag(BossFlag.DARKEN_SKY);
if (packet.isPlayMusic()) bar.addFlag(BossFlag.PLAY_BOSS_MUSIC);
viaBossBars.put(packet.getId(), bar);
bar.addPlayer(getPlayer().getUniqueId());
break;
case REMOVE:
viaBossBars.get(packet.getId()).removePlayer(getPlayer().getUniqueId());
viaBossBars.remove(packet.getId());
break;
case UPDATE_PCT:
viaBossBars.get(packet.getId()).setHealth(packet.getPct());
break;
case UPDATE_NAME:
viaBossBars.get(packet.getId()).setTitle(RGBUtils.getInstance().convertToBukkitFormat(packet.getName(), getVersion().getMinorVersion() >= 16));
break;
case UPDATE_STYLE:
viaBossBars.get(packet.getId()).setColor(BossColor.valueOf(packet.getColor().name()));
viaBossBars.get(packet.getId()).setStyle(BossStyle.valueOf(packet.getOverlay().getBukkitName()));
break;
case UPDATE_PROPERTIES:
bar = viaBossBars.get(packet.getId());
//fog missing from via API
processFlagVia(bar, packet.isDarkenScreen(), BossFlag.DARKEN_SKY);
processFlagVia(bar, packet.isPlayMusic(), BossFlag.PLAY_BOSS_MUSIC);
break;
default:
break;
}
}
private void processFlag(BossBar bar, boolean targetValue, BarFlag flag) {
if (targetValue) {
if (!bar.hasFlag(flag)) {
bar.addFlag(flag);
}
} else {
if (bar.hasFlag(flag)) {
bar.removeFlag(flag);
}
}
}
private void processFlagVia(com.viaversion.viaversion.api.legacy.bossbar.BossBar bar, boolean targetValue, BossFlag flag) {
if (targetValue) {
if (!bar.hasFlag(flag)) {
bar.addFlag(flag);
}
} else {
if (bar.hasFlag(flag)) {
bar.removeFlag(flag);
}
}
}
@Override
public int getPing() {
try {
int ping = NMSStorage.getInstance().PING.getInt(handle);
if (ping > 10000 || ping < 0) ping = -1;
return ping;
} catch (IllegalAccessException e) {
return -1;
}
}
@Override
public boolean hasInvisibilityPotion() {
return getPlayer().hasPotionEffect(PotionEffectType.INVISIBILITY);
}
@Override
public void sendPacket(Object nmsPacket) {
if (nmsPacket == null || !getPlayer().isOnline()) return;
long time = System.nanoTime();
try {
if (nmsPacket instanceof PacketPlayOutBoss) {
if (TAB.getInstance().getServerVersion().getMinorVersion() >= 9) {
handle((PacketPlayOutBoss) nmsPacket);
} else {
handleVia((PacketPlayOutBoss) nmsPacket);
}
} else if (nmsPacket instanceof PacketPlayOutChat) {
getPlayer().sendMessage(((PacketPlayOutChat) nmsPacket).getMessage().toLegacyText());
} else {
NMSStorage.getInstance().sendPacket.invoke(playerConnection, nmsPacket);
}
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("An error occurred when sending " + nmsPacket.getClass().getSimpleName(), e);
}
TAB.getInstance().getCPUManager().addMethodTime("sendPacket", System.nanoTime()-time);
}
@Override
public boolean isDisguised() {
try {
if (!((BukkitPlatform)TAB.getInstance().getPlatform()).isLibsDisguisesEnabled()) return false;
return DisguiseAPI.isDisguised(getPlayer());
} catch (LinkageError e) {
//java.lang.NoClassDefFoundError: Could not initialize class me.libraryaddict.disguise.DisguiseAPI
TAB.getInstance().getErrorManager().printError("Failed to check disguise status using LibsDisguises", e);
((BukkitPlatform)TAB.getInstance().getPlatform()).setLibsDisguisesEnabled(false);
return false;
}
}
/**
* Handles PacketPlayOutBoss packet send request using Bukkit API,
* since the API offers everything we need and makes us not need to
* deal with NMS code at all.
*
* @param packet
* packet request to handle using Bukkit API
*/
private void handle(PacketPlayOutBoss packet) {
BossBar bar = bossBars.get(packet.getId());
if (packet.getAction() == PacketPlayOutBoss.Action.ADD) {
if (bossBars.containsKey(packet.getId())) return;
bar = Bukkit.createBossBar(RGBUtils.getInstance().convertToBukkitFormat(packet.getName(), getVersion().getMinorVersion() >= 16 && TAB.getInstance().getServerVersion().getMinorVersion() >= 16),
BarColor.valueOf(packet.getColor().name()),
BarStyle.valueOf(packet.getOverlay().getBukkitName()));
if (packet.isCreateWorldFog()) bar.addFlag(BarFlag.CREATE_FOG);
if (packet.isDarkenScreen()) bar.addFlag(BarFlag.DARKEN_SKY);
if (packet.isPlayMusic()) bar.addFlag(BarFlag.PLAY_BOSS_MUSIC);
bar.setProgress(packet.getPct());
bossBars.put(packet.getId(), bar);
bar.addPlayer(getPlayer());
return;
}
if (bar == null) return; //no idea how
switch (packet.getAction()) {
case REMOVE:
bar.removePlayer(getPlayer());
bossBars.remove(packet.getId());
break;
case UPDATE_PCT:
bar.setProgress(packet.getPct());
break;
case UPDATE_NAME:
bar.setTitle(RGBUtils.getInstance().convertToBukkitFormat(packet.getName(), getVersion().getMinorVersion() >= 16 && TAB.getInstance().getServerVersion().getMinorVersion() >= 16));
break;
case UPDATE_STYLE:
bar.setColor(BarColor.valueOf(packet.getColor().name()));
bar.setStyle(BarStyle.valueOf(packet.getOverlay().getBukkitName()));
break;
case UPDATE_PROPERTIES:
bar = bossBars.get(packet.getId());
processFlag(bar, packet.isCreateWorldFog(), BarFlag.CREATE_FOG);
processFlag(bar, packet.isDarkenScreen(), BarFlag.DARKEN_SKY);
processFlag(bar, packet.isPlayMusic(), BarFlag.PLAY_BOSS_MUSIC);
break;
default:
break;
}
}
@Override
public Object getSkin() {
try {
return ((GameProfile)NMSStorage.getInstance().getProfile.invoke(handle)).getProperties();
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("Failed to get skin of " + getName(), e);
return null;
}
}
/**
* Handles PacketPlayOutBoss packet request for 1.9+ clients on
* 1.8 servers using ViaVersion API instead of using Wither.
*
* @param packet
* packet request to handle using ViaVersion API
*/
private void handleVia(PacketPlayOutBoss packet) {
com.viaversion.viaversion.api.legacy.bossbar.BossBar bar;
switch (packet.getAction()) {
case ADD:
if (viaBossBars.containsKey(packet.getId())) return;
bar = Via.getAPI().legacyAPI().createLegacyBossBar(RGBUtils.getInstance().convertToBukkitFormat(packet.getName(), getVersion().getMinorVersion() >= 16),
packet.getPct(),
BossColor.valueOf(packet.getColor().name()),
BossStyle.valueOf(packet.getOverlay().getBukkitName()));
//fog missing from via API
if (packet.isDarkenScreen()) bar.addFlag(BossFlag.DARKEN_SKY);
if (packet.isPlayMusic()) bar.addFlag(BossFlag.PLAY_BOSS_MUSIC);
viaBossBars.put(packet.getId(), bar);
bar.addPlayer(getPlayer().getUniqueId());
break;
case REMOVE:
viaBossBars.get(packet.getId()).removePlayer(getPlayer().getUniqueId());
viaBossBars.remove(packet.getId());
break;
case UPDATE_PCT:
viaBossBars.get(packet.getId()).setHealth(packet.getPct());
break;
case UPDATE_NAME:
viaBossBars.get(packet.getId()).setTitle(RGBUtils.getInstance().convertToBukkitFormat(packet.getName(), getVersion().getMinorVersion() >= 16));
break;
case UPDATE_STYLE:
viaBossBars.get(packet.getId()).setColor(BossColor.valueOf(packet.getColor().name()));
viaBossBars.get(packet.getId()).setStyle(BossStyle.valueOf(packet.getOverlay().getBukkitName()));
break;
case UPDATE_PROPERTIES:
bar = viaBossBars.get(packet.getId());
//fog missing from via API
processFlagVia(bar, packet.isDarkenScreen(), BossFlag.DARKEN_SKY);
processFlagVia(bar, packet.isPlayMusic(), BossFlag.PLAY_BOSS_MUSIC);
break;
default:
break;
}
}
@Override
public Player getPlayer() {
return (Player) player;
}
/**
* Sets BossBar flag to requested target value.
*
* @param bar
* BossBar to set flag of
* @param targetValue
* Target value of the flag
* @param flag
* Flag to set value of
*/
private void processFlag(BossBar bar, boolean targetValue, BarFlag flag) {
if (targetValue && !bar.hasFlag(flag)) bar.addFlag(flag);
if (!targetValue && bar.hasFlag(flag)) bar.removeFlag(flag);
}
@Override
public boolean isOnline() {
return getPlayer().isOnline();
}
/**
* Sets BossBar flag to requested target value.
*
* @param bar
* BossBar to set flag of
* @param targetValue
* Target value of the flag
* @param flag
* Flag to set value of
*/
private void processFlagVia(com.viaversion.viaversion.api.legacy.bossbar.BossBar bar, boolean targetValue, BossFlag flag) {
if (targetValue && !bar.hasFlag(flag)) bar.addFlag(flag);
if (!targetValue && bar.hasFlag(flag)) bar.removeFlag(flag);
}
@Override
public boolean isVanished() {
Essentials essentials = ((BukkitPlatform)TAB.getInstance().getPlatform()).getEssentials();
if (essentials != null && essentials.getUser(getUniqueId()).isVanished()) return true;
List<MetadataValue> metadata = getPlayer().getMetadata("vanished");
return !metadata.isEmpty() && metadata.get(0).asBoolean();
}
@Override
public boolean hasInvisibilityPotion() {
return getPlayer().hasPotionEffect(PotionEffectType.INVISIBILITY);
}
@SuppressWarnings("deprecation")
@Override
public int getGamemode() {
return getPlayer().getGameMode().getValue();
}
@Override
public boolean isDisguised() {
try {
if (!((BukkitPlatform)TAB.getInstance().getPlatform()).isLibsDisguisesEnabled()) return false;
return (boolean) Class.forName("me.libraryaddict.disguise.DisguiseAPI").getMethod("isDisguised", Entity.class).invoke(null, getPlayer());
} catch (LinkageError | ReflectiveOperationException e) {
//java.lang.NoClassDefFoundError: Could not initialize class me.libraryaddict.disguise.DisguiseAPI
TAB.getInstance().getErrorManager().printError("Failed to check disguise status using LibsDisguises", e);
((BukkitPlatform)TAB.getInstance().getPlatform()).setLibsDisguisesEnabled(false);
return false;
}
}
@Override
public Skin getSkin() {
try {
Collection<Property> col = ((GameProfile)NMSStorage.getInstance().getProfile.invoke(handle)).getProperties().get("textures");
if (col.isEmpty()) return null; //offline mode
Property property = col.iterator().next();
return new Skin(property.getValue(), property.getSignature());
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("Failed to get skin of " + getName(), e);
return null;
}
}
@Override
public Player getPlayer() {
return (Player) player;
}
@Override
public boolean isOnline() {
return getPlayer().isOnline();
}
@Override
public boolean isVanished() {
return getPlayer().getMetadata("vanished").stream().anyMatch(MetadataValue::asBoolean);
}
@SuppressWarnings("deprecation")
@Override
public int getGamemode() {
return getPlayer().getGameMode().getValue();
}
@Override
public Object getProfilePublicKey() {
if (NMSStorage.getInstance().getMinorVersion() < 19) return null;
try {
if (NMSStorage.getInstance().RemoteChatSession != null) {
//1.19.3+
Object session = NMSStorage.getInstance().EntityPlayer_RemoteChatSession.get(handle);
if (session == null) return null;
Object key = NMSStorage.getInstance().RemoteChatSession_getProfilePublicKey.invoke(session);
return NMSStorage.getInstance().ProfilePublicKey_getRecord.invoke(key);
} else {
Object key = NMSStorage.getInstance().EntityHuman_ProfilePublicKey.get(handle);
if (key == null) return null;
return NMSStorage.getInstance().ProfilePublicKey_getRecord.invoke(key);
}
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("Failed to get profile key of " + getName(), e);
return null;
}
}
@Override
public UUID getChatSessionId() {
if (NMSStorage.getInstance().RemoteChatSession != null) return null;
try {
Object session = NMSStorage.getInstance().EntityPlayer_RemoteChatSession.get(handle);
if (session == null) return null;
return (UUID) NMSStorage.getInstance().RemoteChatSession_getSessionId.invoke(session);
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError("Failed to get chat session of " + getName(), e);
return null;
}
}
@Override
public Channel getChannel() {
try {
if (NMSStorage.getInstance().CHANNEL != null)
return (Channel) NMSStorage.getInstance().CHANNEL.get(NMSStorage.getInstance().NETWORK_MANAGER.get(playerConnection));
} catch (IllegalAccessException e) {
TAB.getInstance().getErrorManager().printError("Failed to get channel of " + getName(), e);
}
return null;
}
}
@@ -3,187 +3,137 @@ package me.neznamy.tab.platforms.bukkit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.platforms.bukkit.nms.*;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.bukkit.Bukkit;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import com.viaversion.viaversion.api.Via;
import me.neznamy.tab.api.ProtocolVersion;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.chat.EnumChatFormat;
import me.neznamy.tab.platforms.bukkit.nms.NMSStorage;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.api.TabConstants;
import org.jetbrains.annotations.NotNull;
/**
* Main class for Bukkit platform
*/
public class Main extends JavaPlugin {
private boolean viaVersion;
private boolean protocolSupport;
@Override
public void onEnable(){
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&7[TAB] Server version: " + Bukkit.getBukkitVersion().split("-")[0] + " (" + Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3] + ")"));
if (!isVersionSupported()){
Bukkit.getPluginManager().disablePlugin(this);
return;
}
viaVersion = Bukkit.getPluginManager().isPluginEnabled("ViaVersion");
protocolSupport = Bukkit.getPluginManager().isPluginEnabled("ProtocolSupport");
TAB.setInstance(new TAB(new BukkitPlatform(this), ProtocolVersion.fromFriendlyName(Bukkit.getBukkitVersion().split("-")[0])));
if (TAB.getInstance().getServerVersion() == ProtocolVersion.UNKNOWN) {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&c[TAB] Unknown server version: " + Bukkit.getBukkitVersion() + "! Plugin may not work correctly."));
}
Bukkit.getPluginManager().registerEvents(new BukkitEventListener(this), this);
TAB.getInstance().load();
Metrics metrics = new Metrics(this, 5304);
metrics.addCustomChart(new SimplePie("unlimited_nametag_mode_enabled", () -> TAB.getInstance().getFeatureManager().isFeatureEnabled(TabConstants.Feature.UNLIMITED_NAME_TAGS) ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("placeholderapi", () -> Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI") ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("permission_system", () -> TAB.getInstance().getGroupManager().getPlugin().getName()));
metrics.addCustomChart(new SimplePie("server_version", () -> "1." + TAB.getInstance().getServerVersion().getMinorVersion() + ".x"));
PluginCommand cmd = Bukkit.getPluginCommand("tab");
if (cmd == null) return;
TABCommand command = new TABCommand();
cmd.setExecutor(command);
cmd.setTabCompleter(command);
}
@Override
public void onDisable() {
//null check due to L23 return making L30 not run
if (TAB.getInstance() != null) TAB.getInstance().unload();
}
/**
* Initializes all used NMS classes, constructors, fields and methods and returns true if everything went successfully and version is marked as compatible
* @return true if compatible, false if not
*/
private boolean isVersionSupported(){
List<String> supportedVersions = Arrays.asList(
"v1_5_R1", "v1_5_R2", "v1_5_R3", "v1_6_R1", "v1_6_R2", "v1_6_R3",
"v1_7_R1", "v1_7_R2", "v1_7_R3", "v1_7_R4", "v1_8_R1", "v1_8_R2", "v1_8_R3",
"v1_9_R1", "v1_9_R2", "v1_10_R1", "v1_11_R1", "v1_12_R1", "v1_13_R1", "v1_13_R2",
"v1_14_R1", "v1_15_R1", "v1_16_R1", "v1_16_R2", "v1_16_R3", "v1_17_R1", "v1_18_R1");
String serverPackage = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
try {
long time = System.currentTimeMillis();
NMSStorage.setInstance(new NMSStorage());
if (supportedVersions.contains(serverPackage)) {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&7[TAB] Loaded NMS hook in " + (System.currentTimeMillis()-time) + "ms"));
return true;
} else {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&c[TAB] No compatibility issue was found, but this plugin version does not claim to support your server version. This jar has only been tested on 1.5.x - 1.18. Disabling just to stay safe."));
}
} catch (Exception ex) {
if (supportedVersions.contains(serverPackage)) {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&c[TAB] Your server version is marked as compatible, but a compatibility issue was found. Please report the error below (include your server version & fork too)"));
Bukkit.getConsoleSender().sendMessage(ex.getClass().getName() + ": " + ex.getMessage());
for (StackTraceElement e : ex.getStackTrace()) {
Bukkit.getConsoleSender().sendMessage("\t" + e.toString());
}
} else {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&c[TAB] Your server version is completely unsupported. This plugin version only supports 1.5.x - 1.18. Disabling."));
}
}
return false;
}
/**
* Gets protocol version and returns it
* @return protocol version of this player
*/
public int getProtocolVersion(Player player) {
if (protocolSupport){
int version = getProtocolVersionPS(player);
//some PS versions return -1 on unsupported server versions instead of throwing exception
if (version != -1 && version < TAB.getInstance().getServerVersion().getNetworkId()) return version;
}
if (viaVersion) {
return getProtocolVersionVia(player, 0);
}
return TAB.getInstance().getServerVersion().getNetworkId();
}
@Override
public void onEnable(){
String version = Bukkit.getBukkitVersion().split("-")[0];
String serverPackage = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("[TAB] Server version: " + version + " (" + serverPackage + ")"));
if (!isVersionSupported()){
Bukkit.getPluginManager().disablePlugin(this);
return;
}
BukkitPlatform platform = new BukkitPlatform(this);
TAB.setInstance(new TAB(platform, ProtocolVersion.fromFriendlyName(version), version + " (" + serverPackage + ")", getDataFolder(), getLogger()));
if (TAB.getInstance().getServerVersion() == ProtocolVersion.UNKNOWN_SERVER_VERSION) {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&c[TAB] Unknown server version: " + Bukkit.getBukkitVersion() + "! Plugin may not work correctly."));
}
Bukkit.getPluginManager().registerEvents(new BukkitEventListener(platform), this);
TAB.getInstance().load();
Metrics metrics = new Metrics(this, 5304);
metrics.addCustomChart(new SimplePie(TabConstants.MetricsChart.UNLIMITED_NAME_TAG_MODE_ENABLED, () -> TAB.getInstance().getFeatureManager().isFeatureEnabled(TabConstants.Feature.UNLIMITED_NAME_TAGS) ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie(TabConstants.MetricsChart.PLACEHOLDER_API, () -> Bukkit.getPluginManager().isPluginEnabled(TabConstants.Plugin.PLACEHOLDER_API) ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie(TabConstants.MetricsChart.PERMISSION_SYSTEM, () -> TAB.getInstance().getGroupManager().getPlugin().getName()));
metrics.addCustomChart(new SimplePie(TabConstants.MetricsChart.SERVER_VERSION, () -> "1." + TAB.getInstance().getServerVersion().getMinorVersion() + ".x"));
PluginCommand cmd = Bukkit.getPluginCommand("tab");
if (cmd == null) return;
TABCommand command = new TABCommand();
cmd.setExecutor(command);
cmd.setTabCompleter(command);
}
/**
* Returns protocol version of this player using ProtocolSupport
* @return protocol version of this player using ProtocolSupport
*/
private int getProtocolVersionPS(Player player){
try {
Object protocolVersion = Class.forName("protocolsupport.api.ProtocolSupportAPI").getMethod("getProtocolVersion", Player.class).invoke(null, player);
int version = (int) protocolVersion.getClass().getMethod("getId").invoke(protocolVersion);
TAB.getInstance().debug("ProtocolSupport returned protocol version " + version + " for " + player.getName() + "(online=" + player.isOnline() + ")");
return version;
} catch (ReflectiveOperationException e) {
TAB.getInstance().getErrorManager().printError(String.format("Failed to get protocol version of %s using ProtocolSupport", player.getName()), e);
return TAB.getInstance().getServerVersion().getNetworkId();
}
}
@Override
public void onDisable() {
//null check due to compatibility check making instance not get set on unsupported versions
if (TAB.getInstance() != null) TAB.getInstance().unload();
}
/**
* Initializes all used NMS classes, constructors, fields and methods.
* Returns {@code true} if everything went successfully and version is marked as compatible,
* {@code false} if anything went wrong or version is not marked as compatible.
*
* @return {@code true} if server version is compatible, {@code false} if not
*/
private boolean isVersionSupported(){
List<String> supportedVersions = Arrays.asList(
"v1_5_R1", "v1_5_R2", "v1_5_R3", "v1_6_R1", "v1_6_R2", "v1_6_R3",
"v1_7_R1", "v1_7_R2", "v1_7_R3", "v1_7_R4", "v1_8_R1", "v1_8_R2", "v1_8_R3",
"v1_9_R1", "v1_9_R2", "v1_10_R1", "v1_11_R1", "v1_12_R1", "v1_13_R1", "v1_13_R2",
"v1_14_R1", "v1_15_R1", "v1_16_R1", "v1_16_R2", "v1_16_R3", "v1_17_R1", "v1_18_R1",
"v1_18_R2", "v1_19_R1", "v1_19_R2");
String serverPackage = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
try {
long time = System.currentTimeMillis();
int minorVersion = Integer.parseInt(serverPackage.split("_")[1]);
if (minorVersion >= 17) {
try {
NMSStorage.setInstance(new BukkitModernNMSStorage());
} catch (ClassNotFoundException e) {
e.printStackTrace();
NMSStorage.setInstance(new MojangModernNMSStorage());
}
} else {
NMSStorage.setInstance(new LegacyNMSStorage());
}
if (supportedVersions.contains(serverPackage)) {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("[TAB] Loaded NMS hook in " + (System.currentTimeMillis()-time) + "ms"));
return true;
} else {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&c[TAB] No compatibility issue was found, but this plugin version does not claim to support your server package (" + serverPackage + "). This jar has only been tested on 1.5.2 - 1.19.3. Disabling just to stay safe."));
}
} catch (Exception ex) {
if (supportedVersions.contains(serverPackage)) {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&c[TAB] Your server version is marked as compatible, but a compatibility issue was found. Please report the error below (include your server version & fork too)"));
getLogger().log(Level.SEVERE, "", ex);
} else {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.color("&c[TAB] Your server version is completely unsupported. This plugin version only supports 1.5.2 - 1.19.3. Disabling."));
}
}
return false;
}
/**
* Returns protocol version of this player using ViaVersion
* @return protocol version of this player using ViaVersion
*/
private int getProtocolVersionVia(Player player, int retryLevel){
try {
if (retryLevel == 10) {
TAB.getInstance().debug("Failed to get protocol version of " + player.getName() + " after 10 retries");
return TAB.getInstance().getServerVersion().getNetworkId();
}
int version = Via.getAPI().getPlayerVersion(player.getUniqueId());
if (version == -1) {
if (!player.isOnline()) return TAB.getInstance().getServerVersion().getNetworkId();
Thread.sleep(5);
return getProtocolVersionVia(player, retryLevel + 1);
}
TAB.getInstance().debug("ViaVersion returned protocol version " + version + " for " + player.getName() + "(online=" + player.isOnline() + ")");
return version;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1;
} catch (Exception | LinkageError e) {
Plugin via = Bukkit.getPluginManager().getPlugin("ViaVersion");
TAB.getInstance().getErrorManager().printError(String.format("Failed to get protocol version of %s using ViaVersion v%s", player.getName(), via == null ? "" : via.getDescription().getVersion()), e);
return TAB.getInstance().getServerVersion().getNetworkId();
}
}
public static class TABCommand implements CommandExecutor, TabCompleter {
/**
* Command handler for /tab command
*/
private static class TABCommand implements CommandExecutor, TabCompleter {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (TAB.getInstance().isDisabled()) {
for (String message : TAB.getInstance().getDisabledCommand().execute(args, sender.hasPermission(TabConstants.Permission.COMMAND_RELOAD), sender.hasPermission(TabConstants.Permission.COMMAND_ALL))) {
sender.sendMessage(EnumChatFormat.color(message));
}
} else {
TabPlayer p = null;
if (sender instanceof Player) {
p = TAB.getInstance().getPlayer(((Player)sender).getUniqueId());
if (p == null) return true; //player not loaded correctly
}
TAB.getInstance().getCommand().execute(p, args);
}
return false;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (TabAPI.getInstance().isPluginDisabled()) {
for (String message : TAB.getInstance().getDisabledCommand().execute(args, sender.hasPermission(TabConstants.Permission.COMMAND_RELOAD), sender.hasPermission(TabConstants.Permission.COMMAND_ALL))) {
sender.sendMessage(EnumChatFormat.color(message));
}
} else {
TabPlayer p = null;
if (sender instanceof Player) {
p = TAB.getInstance().getPlayer(((Player)sender).getUniqueId());
if (p == null) return true; //player not loaded correctly
}
TAB.getInstance().getCommand().execute(p, args);
}
return false;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
TabPlayer p = null;
if (sender instanceof Player) {
p = TAB.getInstance().getPlayer(((Player)sender).getUniqueId());
if (p == null) return new ArrayList<>(); //player not loaded correctly
}
return TAB.getInstance().getCommand().complete(p, args);
}
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
TabPlayer p = null;
if (sender instanceof Player) {
p = TAB.getInstance().getPlayer(((Player)sender).getUniqueId());
if (p == null) return new ArrayList<>(); //player not loaded correctly
}
return TAB.getInstance().getCommand().complete(p, args);
}
}
}
@@ -1,26 +0,0 @@
package me.neznamy.tab.platforms.bukkit.event;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* Bukkit event that is called each time TAB fully loads. This includes server startup, reload,
* /tab reload and /plugman reload tab
*
* @deprecated for removal, use {@link me.neznamy.tab.api.event.plugin.TabLoadEvent}
*/
@Deprecated
public class TabLoadEvent extends Event {
private static final HandlerList handlers = new HandlerList();
@Override
public @NotNull HandlerList getHandlers(){
return getHandlerList();
}
public static HandlerList getHandlerList() {
return handlers;
}
}
@@ -1,37 +0,0 @@
package me.neznamy.tab.platforms.bukkit.event;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import me.neznamy.tab.api.TabPlayer;
import org.jetbrains.annotations.NotNull;
/**
* Bukkit event that is called when player is successfully loaded after joining. This also includes plugin reloading.
*
* @deprecated for removal, use {@link me.neznamy.tab.api.event.player.PlayerLoadEvent}
*/
@Deprecated
public class TabPlayerLoadEvent extends Event {
private static final HandlerList handlers = new HandlerList();
private final TabPlayer player;
public TabPlayerLoadEvent(TabPlayer player) {
this.player = player;
}
@Override
public @NotNull HandlerList getHandlers(){
return getHandlerList();
}
public static HandlerList getHandlerList() {
return handlers;
}
public TabPlayer getPlayer() {
return player;
}
}
@@ -0,0 +1,73 @@
package me.neznamy.tab.platforms.bukkit.features;
import me.clip.placeholderapi.PlaceholderAPI;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.api.TabFeature;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.features.TabExpansion;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
/**
* TAB's expansion for PlaceholderAPI
*/
public class BukkitTabExpansion extends PlaceholderExpansion implements TabExpansion {
/** Map holding all values for all players for easy and high-performance access */
private final Map<TabPlayer, Map<String, String>> values = new WeakHashMap<>();
@Override
public boolean persist(){
return true;
}
@Override
public @NotNull String getAuthor(){
return "NEZNAMY";
}
@Override
public @NotNull String getIdentifier(){
return "tab";
}
@Override
public @NotNull String getVersion() {
return TabConstants.PLUGIN_VERSION;
}
@Override
public String onPlaceholderRequest(Player player, @NotNull String identifier){
if (identifier.startsWith("replace_")) {
String text = "%" + identifier.substring(8) + "%";
String textBefore;
do {
textBefore = text;
for (String placeholder : TAB.getInstance().getPlaceholderManager().detectPlaceholders(text)) {
text = text.replace(placeholder, TabAPI.getInstance().getPlaceholderManager().findReplacement(placeholder,
PlaceholderAPI.setPlaceholders(player, placeholder)));
}
} while (!textBefore.equals(text));
return text;
}
if (identifier.startsWith("placeholder_")) {
TabAPI.getInstance().getPlaceholderManager().addUsedPlaceholder("%" + identifier.substring(12) + "%", (TabFeature) TabAPI.getInstance().getPlaceholderManager());
}
if (player == null) return "<Player cannot be null>";
TabPlayer p = TabAPI.getInstance().getPlayer(player.getUniqueId());
if (p == null || !p.isLoaded()) return "<Player is not loaded>";
return values.get(p).get(identifier);
}
@Override
public void setValue(TabPlayer player, String key, String value) {
values.computeIfAbsent(player, p -> new HashMap<>()).put(key, value);
}
}
@@ -5,6 +5,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import me.neznamy.tab.api.TabAPI;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -15,7 +16,7 @@ import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;
import me.neznamy.tab.api.TabFeature;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.shared.TAB;
/**
@@ -23,76 +24,94 @@ import me.neznamy.tab.shared.TAB;
*/
@SuppressWarnings("deprecation")
public class PerWorldPlayerList extends TabFeature implements Listener {
//plugin instance
private final JavaPlugin plugin;
//config options
private final boolean allowBypass = TAB.getInstance().getConfiguration().getConfig().getBoolean("per-world-playerlist.allow-bypass-permission", false);
private final List<String> ignoredWorlds = TAB.getInstance().getConfiguration().getConfig().getStringList("per-world-playerlist.ignore-effect-in-worlds", Arrays.asList("ignoredworld", "build"));
private final Map<String, List<String>> sharedWorlds = TAB.getInstance().getConfiguration().getConfig().getConfigurationSection("per-world-playerlist.shared-playerlist-world-groups");
/**
* Constructs new instance with given parameters and loads config options
* @param plugin - plugin instance
*/
public PerWorldPlayerList(JavaPlugin plugin) {
super("Per world PlayerList", null);
this.plugin = plugin;
TAB.getInstance().debug(String.format("Loaded PerWorldPlayerList feature with parameters allowBypass=%s, ignoredWorlds=%s, sharedWorlds=%s", allowBypass, ignoredWorlds, sharedWorlds));
Bukkit.getPluginManager().registerEvents(this, plugin);
}
@Override
public void load(){
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getOnlinePlayers().forEach(this::checkPlayer));
}
@Override
public void unload(){
for (Player p : Bukkit.getOnlinePlayers()) {
for (Player pl : Bukkit.getOnlinePlayers()) {
p.showPlayer(pl);
}
}
HandlerList.unregisterAll(this);
}
@EventHandler
public void onJoin(PlayerJoinEvent e) {
long time = System.nanoTime();
checkPlayer(e.getPlayer());
TAB.getInstance().getCPUManager().addTime(getFeatureName(), TabConstants.CpuUsageCategory.PLAYER_JOIN, System.nanoTime()-time);
}
@EventHandler
public void onWorldChange(PlayerChangedWorldEvent e) {
long time = System.nanoTime();
checkPlayer(e.getPlayer());
TAB.getInstance().getCPUManager().addTime(getFeatureName(), TabConstants.CpuUsageCategory.WORLD_SWITCH, System.nanoTime()-time);
}
private void checkPlayer(Player p) {
for (Player all : Bukkit.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);
}
}
private boolean shouldSee(Player viewer, Player target) {
if (target == viewer) return true;
if ((allowBypass && viewer.hasPermission(TabConstants.Permission.PER_WORLD_PLAYERLIST_BYPASS)) || ignoredWorlds.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";
for (Entry<String, List<String>> group : sharedWorlds.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();
}
}
return viewerWorldGroup.equals(targetWorldGroup);
}
/** Reference to main class */
private final JavaPlugin plugin;
/** Config options */
private final boolean allowBypass = TabAPI.getInstance().getConfig().getBoolean("per-world-playerlist.allow-bypass-permission", false);
private final List<String> ignoredWorlds = TabAPI.getInstance().getConfig().getStringList("per-world-playerlist.ignore-effect-in-worlds", Arrays.asList("ignoredworld", "build"));
private final Map<String, List<String>> sharedWorlds = TabAPI.getInstance().getConfig().getConfigurationSection("per-world-playerlist.shared-playerlist-world-groups");
/**
* Constructs new instance with given parameters and loads config options
*
* @param plugin
* plugin instance
*/
public PerWorldPlayerList(JavaPlugin plugin) {
super("Per world PlayerList", null);
this.plugin = plugin;
Bukkit.getPluginManager().registerEvents(this, plugin);
TabAPI.getInstance().debug(String.format("Loaded PerWorldPlayerList feature with parameters allowBypass=%s, ignoredWorlds=%s, sharedWorlds=%s", allowBypass, ignoredWorlds, sharedWorlds));
}
@Override
public void load(){
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getOnlinePlayers().forEach(this::checkPlayer));
}
@Override
public void unload(){
for (Player p : Bukkit.getOnlinePlayers()) {
for (Player pl : Bukkit.getOnlinePlayers()) {
p.showPlayer(pl);
}
}
HandlerList.unregisterAll(this);
}
@EventHandler
public void onJoin(PlayerJoinEvent e) {
long time = System.nanoTime();
checkPlayer(e.getPlayer());
TAB.getInstance().getCPUManager().addTime(getFeatureName(), TabConstants.CpuUsageCategory.PLAYER_JOIN, System.nanoTime()-time);
}
@EventHandler
public void onWorldChange(PlayerChangedWorldEvent e) {
long time = System.nanoTime();
checkPlayer(e.getPlayer());
TAB.getInstance().getCPUManager().addTime(getFeatureName(), TabConstants.CpuUsageCategory.WORLD_SWITCH, System.nanoTime()-time);
}
/**
* Performs visibility check on the player. Shows players this player should see and does not,
* hides players the player should not see, shows the player to those who should see player and hides
* from those, who should not see.
*
* @param p
* Player to update
*/
private void checkPlayer(Player p) {
for (Player all : Bukkit.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);
}
}
/**
* Returns {@code true} if viewer should see target player, {@code false} if not.
* @param viewer
* Player viewing the TabList
* @param target
* Target displayed in the TabList
* @return {@code true} if viewer should see target, {@code false} if not.
*/
private boolean shouldSee(Player viewer, Player target) {
if (target == viewer) return true;
if ((allowBypass && viewer.hasPermission(TabConstants.Permission.PER_WORLD_PLAYERLIST_BYPASS)) || ignoredWorlds.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";
for (Entry<String, List<String>> group : sharedWorlds.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();
}
}
return viewerWorldGroup.equals(targetWorldGroup);
}
}
@@ -1,124 +1,158 @@
package me.neznamy.tab.platforms.bukkit.features;
import java.util.*;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabFeature;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.NMSStorage;
import me.neznamy.tab.platforms.bukkit.nms.datawatcher.DataWatcher;
import me.neznamy.tab.platforms.bukkit.nms.datawatcher.DataWatcherItem;
import me.neznamy.tab.shared.TAB;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Optional;
import java.util.WeakHashMap;
/**
* A feature to disable minecraft 1.9+ feature making tamed animals with custom names copy NameTag properties of their owner
* This is achieved by listening to entity spawn (<1.15) / entity metadata packets and removing owner field from the DataWatcher list
* Since 1.16 this results in client sending entity use packet twice, so we must cancel the 2nd one to prevent double toggle
* A feature to disable minecraft 1.9+ feature making tamed animals
* with custom names copy NameTag properties of their owner.
* This is achieved by listening to entity spawn (&lt;1.15) / entity metadata
* packets and removing owner field from the DataWatcher list.
* Since 1.16 this results in client sending entity use packet twice,
* so we must cancel the 2nd one to prevent double toggle.
*/
public class PetFix extends TabFeature {
//nms storage
private final NMSStorage nms = NMSStorage.getInstance();
/** NMS Storage reference for quick access */
private final NMSStorage nms = NMSStorage.getInstance();
//DataWatcher position of pet owner field
private final int petOwnerPosition = getPetOwnerPosition();
/** DataWatcher position of pet owner field */
private final int petOwnerPosition = getPetOwnerPosition();
//logger of last interacts to prevent feature not working on 1.16
private final WeakHashMap<TabPlayer, Long> lastInteractFix = new WeakHashMap<>();
/** Logger of last interacts to prevent feature not working on 1.16 */
private final WeakHashMap<TabPlayer, Long> lastInteractFix = new WeakHashMap<>();
/**
* Constructs new instance with given parameter
*/
public PetFix() {
super("Pet name fix", null);
TAB.getInstance().debug("Loaded PetFix feature");
}
/**
* Since 1.16, client sends interact packet twice for entities affected
* by removed owner field. Because of that, we need to cancel the duplicated
* packet to avoid double toggle and preventing the entity from getting
* its pose changed. The duplicated packet is usually sent instantly in the
* same millisecond, however, when installing ProtocolLib with MyPet, the delay
* is up to 3 ticks. When holding right-click on an entity, interact is sent every
* 200 milliseconds, which is the value we should not go above. Optimal value is
* therefore between 150 and 200 milliseconds.
*/
private static final int INTERACT_COOLDOWN = 160;
/**
* Returns position of pet owner field based on server version
* @return position of pet owner field based on server version
*/
private int getPetOwnerPosition() {
if (nms.getMinorVersion() >= 17) {
//1.17.x, 1.18.x
return 18;
} else if (nms.getMinorVersion() >= 15) {
//1.15.x, 1.16.x
return 17;
} else if (nms.getMinorVersion() >= 14) {
//1.14.x
return 16;
} else if (nms.getMinorVersion() >= 10) {
//1.10.x - 1.13.x
return 14;
} else {
//1.9.x
return 13;
}
}
/**
* Constructs new instance with given parameter
*/
public PetFix() {
super("Pet name fix", null);
TabAPI.getInstance().debug("Loaded PetFix feature");
}
/**
* Cancels a packet if previous one arrived with no delay to prevent double toggle on 1.16
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
@Override
public boolean onPacketReceive(TabPlayer sender, Object packet) throws ReflectiveOperationException {
if (nms.PacketPlayInUseEntity.isInstance(packet)) {
if (lastInteractFix.containsKey(sender) && (System.currentTimeMillis() - lastInteractFix.get(sender) < 5)) {
//last interact packet was sent right now, cancelling to prevent double-toggle due to this feature enabled
return true;
}
if (isInteract(nms.PacketPlayInUseEntity_ACTION.get(packet))) {
//this is the first packet, saving player so the next packet can be cancelled
lastInteractFix.put(sender, System.currentTimeMillis());
}
}
return false;
}
/**
* Returns position of pet owner field based on server version
*
* @return position of pet owner field based on server version
*/
private int getPetOwnerPosition() {
if (nms.getMinorVersion() >= 17) {
//1.17.x, 1.18.x, 1.19.x
return 18;
} else if (nms.getMinorVersion() >= 15) {
//1.15.x, 1.16.x
return 17;
} else if (nms.getMinorVersion() >= 14) {
//1.14.x
return 16;
} else if (nms.getMinorVersion() >= 10) {
//1.10.x - 1.13.x
return 14;
} else {
//1.9.x
return 13;
}
}
private boolean isInteract(Object action) {
if (nms.getMinorVersion() >= 17) {
return nms.PacketPlayInUseEntity$d.isInstance(action);
} else {
return action.toString().equals("INTERACT");
}
}
/**
* Cancels a packet if previous one arrived with no delay to prevent double toggle since 1.16
*
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
@Override
public boolean onPacketReceive(TabPlayer sender, Object packet) throws ReflectiveOperationException {
if (nms.PacketPlayInUseEntity.isInstance(packet) && isInteract(nms.PacketPlayInUseEntity_ACTION.get(packet))) {
if (System.currentTimeMillis() - lastInteractFix.getOrDefault(sender, 0L) < INTERACT_COOLDOWN) {
//last interact packet was sent right now, cancelling to prevent double-toggle due to this feature enabled
return true;
}
//this is the first packet, saving player so the next packet can be cancelled
lastInteractFix.put(sender, System.currentTimeMillis());
}
return false;
}
/**
* Removes pet owner field from DataWatcher
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
@SuppressWarnings("unchecked")
@Override
public void onPacketSend(TabPlayer receiver, Object packet) throws ReflectiveOperationException {
if (nms.PacketPlayOutEntityMetadata.isInstance(packet)) {
Object removedEntry = null;
List<Object> items = (List<Object>) nms.PacketPlayOutEntityMetadata_LIST.get(packet);
if (items == null) return;
try {
for (Object item : items) {
if (item == null) continue;
if (nms.DataWatcherObject_SLOT.getInt(nms.DataWatcherItem_TYPE.get(item)) == petOwnerPosition) {
Object value = nms.DataWatcherItem_VALUE.get(item);
if (value instanceof java.util.Optional || value instanceof com.google.common.base.Optional) {
removedEntry = item;
}
}
}
} catch (ConcurrentModificationException e) {
//no idea how can this list change in another thread since it's created for the packet but whatever, try again
onPacketSend(receiver, packet);
}
if (removedEntry != null) items.remove(removedEntry);
} else if (nms.PacketPlayOutSpawnEntityLiving.isInstance(packet) && nms.PacketPlayOutSpawnEntityLiving_DATAWATCHER != null) {
//<1.15
DataWatcher watcher = DataWatcher.fromNMS(nms.PacketPlayOutSpawnEntityLiving_DATAWATCHER.get(packet));
DataWatcherItem petOwner = watcher.getItem(petOwnerPosition);
if (petOwner != null && (petOwner.getValue() instanceof java.util.Optional || petOwner.getValue() instanceof com.google.common.base.Optional)) {
watcher.removeValue(petOwnerPosition);
nms.setField(packet, nms.PacketPlayOutSpawnEntityLiving_DATAWATCHER, watcher.toNMS());
}
}
}
/**
* Checks if the provided entity use action is INTERACT or not.
*
* @param action
* Action to check
* @return {@code true} if action is INTERACT, {@code false} if not.
*/
private boolean isInteract(Object action) {
if (nms.getMinorVersion() >= 17) {
return nms.PacketPlayInUseEntity$d.isInstance(action);
} else {
return action.toString().equals("INTERACT");
}
}
/**
* Removes pet owner field from DataWatcher
*
* @throws ReflectiveOperationException
* if thrown by reflective operation
*/
@SuppressWarnings("unchecked")
@Override
public void onPacketSend(TabPlayer receiver, Object packet) throws ReflectiveOperationException {
if (nms.PacketPlayOutEntityMetadata.isInstance(packet)) {
Object removedEntry = null;
List<Object> items = (List<Object>) nms.PacketPlayOutEntityMetadata_LIST.get(packet);
if (items == null) return;
try {
for (Object item : items) {
if (item == null) continue;
int slot;
Object value;
if (nms.is1_19_3Plus()) {
slot = nms.DataWatcher$DataValue_POSITION.getInt(item);
value = nms.DataWatcher$DataValue_VALUE.get(item);
} else {
slot = nms.DataWatcherObject_SLOT.getInt(nms.DataWatcherItem_TYPE.get(item));
value = nms.DataWatcherItem_VALUE.get(item);
}
if (slot == petOwnerPosition) {
if (value instanceof Optional || value instanceof com.google.common.base.Optional) {
removedEntry = item;
}
}
}
} catch (ConcurrentModificationException e) {
//no idea how can this list change in another thread since it's created for the packet but whatever, try again
onPacketSend(receiver, packet);
}
if (removedEntry != null) items.remove(removedEntry);
} else if (nms.PacketPlayOutSpawnEntityLiving.isInstance(packet) && nms.PacketPlayOutSpawnEntityLiving_DATAWATCHER != null) {
//<1.15
DataWatcher watcher = DataWatcher.fromNMS(nms.PacketPlayOutSpawnEntityLiving_DATAWATCHER.get(packet));
DataWatcherItem petOwner = watcher.getItem(petOwnerPosition);
if (petOwner != null && (petOwner.getValue() instanceof Optional || petOwner.getValue() instanceof com.google.common.base.Optional)) {
watcher.removeValue(petOwnerPosition);
nms.setField(packet, nms.PacketPlayOutSpawnEntityLiving_DATAWATCHER, watcher.toNMS());
}
}
}
}
@@ -1,137 +0,0 @@
package me.neznamy.tab.platforms.bukkit.features;
import me.neznamy.tab.shared.TabConstants;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import me.clip.placeholderapi.PlaceholderAPI;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import me.neznamy.tab.api.Property;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.bossbar.BossBarManager;
import me.neznamy.tab.api.scoreboard.Scoreboard;
import me.neznamy.tab.api.scoreboard.ScoreboardManager;
import me.neznamy.tab.shared.TAB;
import org.jetbrains.annotations.NotNull;
/**
* TAB's expansion for PlaceholderAPI
*/
public class TabExpansion extends PlaceholderExpansion {
//plugin to take plugin.yml data from
private final JavaPlugin plugin;
/**
* Constructs new instance of the class and registers it
* @param plugin - plugin to take plugin.yml data from
*/
public TabExpansion(JavaPlugin plugin) {
this.plugin = plugin;
register();
}
@Override
public boolean persist(){
return true;
}
@Override
public boolean canRegister(){
return true;
}
@Override
public @NotNull String getAuthor(){
return plugin.getDescription().getAuthors().toString();
}
@Override
public @NotNull String getIdentifier(){
return "tab";
}
@Override
public @NotNull String getVersion(){
return plugin.getDescription().getVersion();
}
@Override
public String onPlaceholderRequest(Player player, @NotNull String identifier){
if (player == null) return "";
TabPlayer p = TAB.getInstance().getPlayer(player.getUniqueId());
if ("scoreboard_visible".equals(identifier)) {
return translate(hasScoreboardVisible(p));
}
if ("scoreboard_name".equals(identifier)) {
return getActiveScoreboard(p);
}
if ("bossbar_visible".equals(identifier)) {
return translate(hasBossBarVisible(p));
}
if ("ntpreview".equals(identifier)) {
return translate(p.isPreviewingNametag());
}
if (identifier.startsWith("replace_")) {
return findReplacement("%" + identifier.substring(8) + "%", player);
}
if (identifier.startsWith("placeholder_")) {
return TAB.getInstance().getPlaceholderManager().getPlaceholder("%" + identifier.substring(12) + "%").getLastValue(p);
}
return getProperty(identifier, p);
}
private boolean hasBossBarVisible(TabPlayer p) {
BossBarManager boss = (BossBarManager) TAB.getInstance().getFeatureManager().getFeature(TabConstants.Feature.BOSS_BAR);
if (boss == null) return false;
return boss.hasBossBarVisible(p);
}
private boolean hasScoreboardVisible(TabPlayer p) {
ScoreboardManager scoreboard = (ScoreboardManager) TAB.getInstance().getFeatureManager().getFeature(TabConstants.Feature.SCOREBOARD);
if (scoreboard == null) return false;
return scoreboard.hasScoreboardVisible(p);
}
private String getActiveScoreboard(TabPlayer p) {
ScoreboardManager sb = TabAPI.getInstance().getScoreboardManager();
if (sb == null) return "";
Scoreboard active = sb.getActiveScoreboard(p);
if (active == null) return "";
return active.getName();
}
private String translate(boolean b) {
return b ? "Enabled" : "Disabled";
}
/**
* Find replacement for specified placeholder
* @param placeholder - placeholder to find replacement for
* @param player - player to set placeholder for
* @return replacement
*/
private String findReplacement(String placeholder, Player player) {
String output = PlaceholderAPI.setPlaceholders(player, placeholder);
return TAB.getInstance().getPlaceholderManager().findReplacement(placeholder, output).replace("%value%", output);
}
/**
* Returns value of defined property if found
* @param name - property name
* @param player - player to get property of
* @return value of specified property
*/
private String getProperty(String name, TabPlayer player) {
String propName = name.replace("_raw", "");
Property prop = player.getProperty(propName);
if (prop != null) {
if (name.endsWith("_raw")) {
return prop.getCurrentRawValue();
}
return prop.get();
}
return null;
}
}
@@ -1,7 +1,5 @@
package me.neznamy.tab.platforms.bukkit.features;
import java.util.Collections;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
@@ -14,59 +12,65 @@ import org.bukkit.plugin.java.JavaPlugin;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.bossbar.BossBar;
import me.neznamy.tab.platforms.bukkit.nms.PacketPlayOutEntityTeleport;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.features.bossbar.BossBarManagerImpl;
/**
* An additional class with additional code for <1.9 servers due to an entity being required
* An additional class with additional code for &lt;1.9 servers due to an entity being required
*/
public class WitherBossBar extends BossBarManagerImpl implements Listener {
//distance of wither in blocks
private static final int WITHER_DISTANCE = 60;
/**
* Constructs a new instance of the class
* @param plugin - plugin instance
*/
public WitherBossBar(JavaPlugin plugin) {
Bukkit.getPluginManager().registerEvents(this, plugin);
//when MC is on fullscreen, BossBar disappears after 1 second of not being seen
//when in a small window, it's about 100ms
TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder("%location%", 100, p -> ((Player)p.getPlayer()).getLocation());
addUsedPlaceholders(Collections.singletonList("%location%"));
}
@Override
public void load() {
super.load();
for (TabPlayer p : TAB.getInstance().getOnlinePlayers()) {
refresh(p, false);
}
}
@Override
public void refresh(TabPlayer p, boolean force) {
for (BossBar line : getRegisteredBossBars().values()) {
if (p.getVersion().getMinorVersion() > 8) continue; //sending VV packets to those
Location loc = ((Player) p.getPlayer()).getEyeLocation().add(((Player) p.getPlayer()).getEyeLocation().getDirection().normalize().multiply(WITHER_DISTANCE));
if (loc.getY() < 1) loc.setY(1);
p.sendCustomPacket(new PacketPlayOutEntityTeleport(line.getUniqueId().hashCode(), loc), TabConstants.PacketCategory.BOSSBAR_WITHER_TELEPORT);
}
}
@Override
public void unload() {
super.unload();
HandlerList.unregisterAll(this);
}
/**
* Respawning wither as respawn screen destroys all entities in client
*/
@EventHandler
public void onRespawn(PlayerRespawnEvent e) {
TAB.getInstance().getCPUManager().runMeasuredTask("processing PlayerRespawnEvent", this, TabConstants.CpuUsageCategory.PLAYER_RESPAWN, () -> detectBossBarsAndSend(TAB.getInstance().getPlayer(e.getPlayer().getUniqueId())));
}
/** Distance of the Wither in blocks */
private static final int WITHER_DISTANCE = 60;
/**
* Constructs a new instance of the class and registers events and task
*
* @param plugin
* plugin instance
*/
public WitherBossBar(JavaPlugin plugin) {
Bukkit.getPluginManager().registerEvents(this, plugin);
//when MC is on fullscreen, BossBar disappears after 1 second of not being seen
//when in a small window, it's about 100ms
TAB.getInstance().getCPUManager().startRepeatingMeasuredTask(100,
this, TabConstants.CpuUsageCategory.TELEPORTING_WITHER, this::teleport);
}
@Override
public void load() {
super.load();
teleport();
}
/**
* Updates Wither's location for all online players
*/
private void teleport() {
for (TabPlayer p : TAB.getInstance().getOnlinePlayers()) {
if (p.getVersion().getMinorVersion() > 8) continue; //sending VV packets to those
for (BossBar line : getRegisteredBossBars().values()) {
if (!line.containsPlayer(p)) continue;
Location loc = ((Player) p.getPlayer()).getEyeLocation().add(((Player) p.getPlayer()).getEyeLocation().getDirection().normalize().multiply(WITHER_DISTANCE));
if (loc.getY() < 1) loc.setY(1);
p.sendCustomPacket(new PacketPlayOutEntityTeleport(line.getUniqueId().hashCode(), loc), TabConstants.PacketCategory.BOSSBAR_WITHER_TELEPORT);
}
}
}
@Override
public void unload() {
super.unload();
HandlerList.unregisterAll(this);
}
/**
* Respawning wither as respawn screen destroys all entities in client
*/
@EventHandler
public void onRespawn(PlayerRespawnEvent e) {
TAB.getInstance().getCPUManager().runMeasuredTask(this, TabConstants.CpuUsageCategory.PLAYER_RESPAWN,
() -> detectBossBarsAndSend(TAB.getInstance().getPlayer(e.getPlayer().getUniqueId())));
}
}
@@ -1,16 +1,6 @@
package me.neznamy.tab.platforms.bukkit.features.unlimitedtags;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Pose;
import me.neznamy.tab.api.ArmorStand;
import me.neznamy.tab.api.Property;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.*;
import me.neznamy.tab.api.chat.EnumChatFormat;
import me.neznamy.tab.api.chat.IChatBaseComponent;
import me.neznamy.tab.api.protocol.TabPacket;
@@ -19,321 +9,362 @@ import me.neznamy.tab.platforms.bukkit.nms.PacketPlayOutEntityMetadata;
import me.neznamy.tab.platforms.bukkit.nms.PacketPlayOutEntityTeleport;
import me.neznamy.tab.platforms.bukkit.nms.PacketPlayOutSpawnEntityLiving;
import me.neznamy.tab.platforms.bukkit.nms.datawatcher.DataWatcher;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.TAB;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Pose;
import java.util.UUID;
/**
* A class representing an armor stand attached to a player (if the feature is enabled)
* A class representing an armor stand attached to a player
*/
public class BukkitArmorStand implements ArmorStand {
private final boolean alwaysVisible = TAB.getInstance().getConfiguration().isArmorStandsAlwaysVisible();
//entity id counter to pick unique entity IDs
private static int idCounter = 2000000000;
/** Entity id counter to pick unique entity ID for each armor stand */
private static int idCounter = 2000000000;
//NameTag feature
private final NameTagX manager = (NameTagX) TAB.getInstance().getFeatureManager().getFeature(TabConstants.Feature.UNLIMITED_NAME_TAGS);
/** NameTag feature */
private final BukkitNameTagX manager = (BukkitNameTagX) TabAPI.getInstance().getFeatureManager().getFeature(TabConstants.Feature.UNLIMITED_NAME_TAGS);
//owner of the armor stand
private final TabPlayer owner;
/** Armor stand manager which this armor stand belongs to */
private final BukkitArmorStandManager asm;
/** Owner of the armor stand */
private final TabPlayer owner;
//instance of Bukkit player
private final Player player;
/** Owner as a Bukkit player */
private final Player player;
//offset in blocks, 0 for original height
private double yOffset;
/** Offset in blocks, 0 for original height */
private double yOffset;
//entity ID of this armor stand
private final int entityId = idCounter++;
/** If offset is static, or dynamic based on other armor stands */
private final boolean staticOffset;
//unique ID of this armor stand
private final UUID uuid = UUID.randomUUID();
/** Entity ID of this armor stand */
private final int entityId = idCounter++;
//sneaking flag of armor stands
private boolean sneaking;
/** Unique ID of this armor stand */
private final UUID uuid = UUID.randomUUID();
//armor stand visibility
private boolean visible;
/** Sneaking flag of armor stands */
private boolean sneaking;
//property dedicated to this armor stand
private final Property property;
/** Armor stand visibility */
private boolean visible;
//if offset is static or dynamic based on other armor stands
private final boolean staticOffset;
/** Refresh property dedicated to this armor stand */
private final Property property;
//entity destroy packet
private final PacketPlayOutEntityDestroy destroyPacket = new PacketPlayOutEntityDestroy(entityId);
/** Entity destroy packet */
private final PacketPlayOutEntityDestroy destroyPacket = new PacketPlayOutEntityDestroy(entityId);
/**
* Constructs new instance with given parameters
* @param owner - armor stand owner
* @param property - property for armor stand's name
* @param yOffset - offset in blocks
* @param staticOffset - if offset is static or not
*/
public BukkitArmorStand(TabPlayer owner, Property property, double yOffset, boolean staticOffset) {
this.owner = owner;
this.staticOffset = staticOffset;
player = (Player) owner.getPlayer();
this.yOffset = yOffset;
this.property = property;
visible = getVisibility();
}
/**
* Constructs new instance with given parameters.
*
* @param asm
* Armor stand manager which this armor stand belongs to
* @param owner
* Owner of the armor stand
* @param propertyName
* Name of refresh property to use
* @param yOffset
* Offset in blocks
* @param staticOffset
* {@code true} if offset is static, {@code false} if not
*/
public BukkitArmorStand(BukkitArmorStandManager asm, TabPlayer owner, String propertyName, double yOffset, boolean staticOffset) {
this.asm = asm;
this.owner = owner;
this.staticOffset = staticOffset;
player = (Player) owner.getPlayer();
this.yOffset = yOffset;
this.property = owner.getProperty(propertyName);
visible = getVisibility();
sneaking = player.isSneaking();
}
@Override
public void refresh() {
visible = getVisibility();
updateMetadata();
}
@Override
public void refresh() {
visible = getVisibility();
updateMetadata();
}
@Override
public Property getProperty() {
return property;
}
@Override
public Property getProperty() {
return property;
}
@Override
public boolean hasStaticOffset() {
return staticOffset;
}
@Override
public boolean hasStaticOffset() {
return staticOffset;
}
@Override
public double getOffset() {
return yOffset;
}
@Override
public double getOffset() {
return yOffset;
}
@Override
public void setOffset(double offset) {
if (yOffset == offset) return;
yOffset = offset;
for (TabPlayer all : owner.getArmorStandManager().getNearbyPlayers()) {
all.sendCustomPacket(getTeleportPacket(all), TabConstants.PacketCategory.UNLIMITED_NAMETAGS_OFFSET_CHANGE);
}
}
@Override
public void setOffset(double offset) {
if (yOffset == offset) return;
yOffset = offset;
for (TabPlayer all : asm.getNearbyPlayers()) {
all.sendCustomPacket(getTeleportPacket(all), TabConstants.PacketCategory.UNLIMITED_NAMETAGS_OFFSET_CHANGE);
}
}
@Override
public void spawn(TabPlayer viewer) {
for (TabPacket packet : getSpawnPackets(viewer)) {
viewer.sendCustomPacket(packet, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_SPAWN);
}
}
@Override
public void spawn(TabPlayer viewer) {
for (TabPacket packet : getSpawnPackets(viewer)) {
viewer.sendCustomPacket(packet, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_SPAWN);
}
}
@Override
public void destroy() {
for (TabPlayer all : owner.getArmorStandManager().getNearbyPlayers()) all.sendCustomPacket(destroyPacket, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_DESPAWN);
}
@Override
public void destroy(TabPlayer viewer) {
viewer.sendCustomPacket(destroyPacket, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_DESPAWN);
}
@Override
public void destroy() {
for (TabPlayer all : asm.getNearbyPlayers()) all.sendCustomPacket(destroyPacket, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_DESPAWN);
}
@Override
public void destroy(TabPlayer viewer) {
viewer.sendCustomPacket(destroyPacket, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_DESPAWN);
}
@Override
public void teleport() {
for (TabPlayer all : owner.getArmorStandManager().getNearbyPlayers()) {
all.sendCustomPacket(getTeleportPacket(all), TabConstants.PacketCategory.UNLIMITED_NAMETAGS_TELEPORT);
}
}
@Override
public void teleport() {
for (TabPlayer all : asm.getNearbyPlayers()) {
all.sendCustomPacket(getTeleportPacket(all), TabConstants.PacketCategory.UNLIMITED_NAMETAGS_TELEPORT);
}
}
@Override
public void teleport(TabPlayer viewer) {
if (!owner.getArmorStandManager().isNearby(viewer) && viewer != owner) {
owner.getArmorStandManager().spawn(viewer);
} else {
viewer.sendCustomPacket(getTeleportPacket(viewer), TabConstants.PacketCategory.UNLIMITED_NAMETAGS_TELEPORT);
}
}
@Override
public void teleport(TabPlayer viewer) {
if (!asm.isNearby(viewer) && viewer != owner) {
asm.spawn(viewer);
} else {
viewer.sendCustomPacket(getTeleportPacket(viewer), TabConstants.PacketCategory.UNLIMITED_NAMETAGS_TELEPORT);
}
}
@Override
public void sneak(boolean sneaking) {
if (this.sneaking == sneaking) return; //idk
this.sneaking = sneaking;
for (TabPlayer viewer : owner.getArmorStandManager().getNearbyPlayers()) {
if (viewer.getVersion().getMinorVersion() == 14 && !alwaysVisible) {
//1.14.x client sided bug, de-spawning completely
if (sneaking) {
viewer.sendCustomPacket(destroyPacket, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_SNEAK);
} else {
spawn(viewer);
}
} else {
//respawning so there's no animation and it's instant
respawn(viewer);
}
}
}
@Override
public void sneak(boolean sneaking) {
if (this.sneaking == sneaking) return; //idk
this.sneaking = sneaking;
for (TabPlayer viewer : asm.getNearbyPlayers()) {
if (viewer.getVersion().getMinorVersion() == 14 && !manager.isArmorStandsAlwaysVisible()) {
//1.14.x client sided bug, de-spawning completely
if (sneaking) {
viewer.sendCustomPacket(destroyPacket, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_SNEAK);
} else {
spawn(viewer);
}
} else {
//respawning so there's no animation and it's instant
respawn(viewer);
}
}
}
@Override
public void updateVisibility(boolean force) {
boolean visibility = getVisibility();
if (visible != visibility || force) {
visible = visibility;
updateMetadata();
}
}
@Override
public void updateVisibility(boolean force) {
boolean visibility = getVisibility();
if (visible != visibility || force) {
refresh();
}
}
@Override
public int getEntityId() {
return entityId;
}
@Override
public int getEntityId() {
return entityId;
}
/**
* Returns teleport packet for specified viewer
* @param viewer - player to get location for
* @return teleport packet
*/
public PacketPlayOutEntityTeleport getTeleportPacket(TabPlayer viewer) {
return new PacketPlayOutEntityTeleport(entityId, getArmorStandLocationFor(viewer));
}
/**
* Returns teleport packet for specified viewer
*
* @param viewer
* player to get location for
* @return teleport packet
*/
public PacketPlayOutEntityTeleport getTeleportPacket(TabPlayer viewer) {
return new PacketPlayOutEntityTeleport(entityId, getArmorStandLocationFor(viewer));
}
/**
* Updates armor stand's metadata
*/
public void updateMetadata() {
for (TabPlayer viewer : owner.getArmorStandManager().getNearbyPlayers()) {
viewer.sendCustomPacket(new PacketPlayOutEntityMetadata(entityId, createDataWatcher(property.getFormat(viewer), viewer)), TabConstants.PacketCategory.UNLIMITED_NAMETAGS_METADATA);
}
}
/**
* Updates armor stand's metadata
*/
public void updateMetadata() {
for (TabPlayer viewer : asm.getNearbyPlayers()) {
viewer.sendCustomPacket(new PacketPlayOutEntityMetadata(entityId, createDataWatcher(property.getFormat(viewer), viewer)), TabConstants.PacketCategory.UNLIMITED_NAMETAGS_METADATA);
}
}
/**
* Returns general visibility rule for everyone with limited info
* @return true if armor stand should be visible, false if not
*/
public boolean getVisibility() {
if (owner.isDisguised() || manager.getVehicleManager().isOnBoat(owner)) return false;
if (alwaysVisible) return true;
return !owner.hasInvisibilityPotion() && owner.getGamemode() != 3 && !manager.hasHiddenNametag(owner) && property.get().length() > 0;
}
/**
* Returns general visibility rule for everyone with limited info
*
* @return {@code true} if armor stand should be visible, {@code false} if not
*/
public boolean getVisibility() {
if (manager.isArmorStandsAlwaysVisible()) return true;
if (owner.isDisguised() || manager.getVehicleManager().isOnBoat(owner)) return false;
return !owner.hasInvisibilityPotion() && owner.getGamemode() != 3 && !manager.hasHiddenNametag(owner) && property.get().length() > 0;
}
/**
* Returns general location where armor stand should be at time of calling
* @return Location where armor stand should be for everyone
*/
public Location getLocation() {
double x = player.getLocation().getX();
double y = getY() + yOffset + 2;
double z = player.getLocation().getZ();
if (player.isSleeping()) {
y -= 1.76;
} else {
if (TAB.getInstance().getServerVersion().getMinorVersion() >= 9) {
y -= (sneaking ? 0.45 : 0.18);
} else {
y -= (sneaking ? 0.30 : 0.18);
}
}
return new Location(null,x,y,z);
}
/**
* Returns location where armor stand should be at time of calling.
* This takes into account everything that affects height, including
* viewer's game version.
*
* @param viewer
* Player looking at the armor stand
* @return Location where armor stand should be for specified viewer
*/
public Location getLocation(TabPlayer viewer) {
double x = player.getLocation().getX();
double y = getY() + yOffset;
double z = player.getLocation().getZ();
if (!player.isSleeping()) {
if (sneaking) {
if (viewer.getVersion().getMinorVersion() >= 15) {
y += 1.37;
} else if (viewer.getVersion().getMinorVersion() >= 9) {
y += 1.52;
} else {
y += 1.7;
}
} else {
y += viewer.getVersion().getMinorVersion() >= 9 ? 1.8 : 1.84; // Normal
}
} else {
y += viewer.getVersion().getMinorVersion() >= 9 ? 0.2 : 0.26; // Sleeping
}
return new Location(null,x,y,z);
}
/**
* Returns Y where player is based on player's vehicle due to bukkit API bug
* @return correct player's Y
*/
protected double getY() {
//1.14+ server sided bug
Entity vehicle = player.getVehicle();
if (vehicle != null) {
if (vehicle.getType().toString().contains("HORSE")) { //covering all 3 horse types
return vehicle.getLocation().getY() + 0.85;
}
if (vehicle.getType().toString().equals("DONKEY")) { //1.11+
return vehicle.getLocation().getY() + 0.525;
}
if (vehicle.getType() == EntityType.PIG) {
return vehicle.getLocation().getY() + 0.325;
}
if (vehicle.getType().toString().equals("STRIDER")) { //1.16+
return vehicle.getLocation().getY() + 1.15;
}
}
//1.13+ swimming or 1.9+ flying with elytra
if (isSwimming() || (TAB.getInstance().getServerVersion().getMinorVersion() >= 9 && player.isGliding())) {
return player.getLocation().getY()-1.22;
}
return player.getLocation().getY();
}
/**
* Returns Y where player is based on player's vehicle due to bukkit API bug
*
* @return correct player's Y
*/
protected double getY() {
//1.14+ server sided bug
Entity vehicle = player.getVehicle();
if (vehicle != null) {
if (vehicle.getType().toString().contains("HORSE")) { //covering all 3 horse types
return vehicle.getLocation().getY() + 0.85;
}
if (vehicle.getType().toString().equals("DONKEY")) { //1.11+
return vehicle.getLocation().getY() + 0.525;
}
if (vehicle.getType() == EntityType.PIG) {
return vehicle.getLocation().getY() + 0.325;
}
if (vehicle.getType().toString().equals("STRIDER")) { //1.16+
return vehicle.getLocation().getY() + 1.15;
}
}
//1.13+ swimming or 1.9+ flying with elytra
if (isSwimming() || (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 9 && player.isGliding())) {
return player.getLocation().getY()-1.22;
}
return player.getLocation().getY();
}
private boolean isSwimming() {
if (TAB.getInstance().getServerVersion().getMinorVersion() >= 14 && player.getPose() == Pose.SWIMMING) return true;
return TAB.getInstance().getServerVersion().getMinorVersion() == 13 && player.isSwimming();
}
/**
* Returns {@code true} if owner is swimming, {@code false} if not
* @return {@code true} if owner is swimming, {@code false} if not
*/
private boolean isSwimming() {
if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 14 && player.getPose() == Pose.SWIMMING) return true;
return TabAPI.getInstance().getServerVersion().getMinorVersion() == 13 && player.isSwimming();
}
/**
* Creates data watcher with specified display name for viewer
* @param displayName - armor stand name
* @param viewer - player to apply checks against
* @return DataWatcher for viewer
*/
public DataWatcher createDataWatcher(String displayName, TabPlayer viewer) {
DataWatcher datawatcher = new DataWatcher();
/**
* Creates data watcher with specified display name for viewer
*
* @param displayName
* armor stand name
* @param viewer
* player to apply checks against
* @return DataWatcher for viewer
*/
public DataWatcher createDataWatcher(String displayName, TabPlayer viewer) {
DataWatcher datawatcher = new DataWatcher();
byte flag = 32; //invisible
if (sneaking) flag += (byte)2;
datawatcher.helper().setEntityFlags(flag);
datawatcher.helper().setCustomName(displayName, viewer.getVersion());
byte flag = 32; //invisible
if (sneaking) flag += (byte)2;
datawatcher.helper().setEntityFlags(flag);
datawatcher.helper().setCustomName(displayName, viewer.getVersion());
boolean visibility;
if (isNameVisiblyEmpty(displayName) || !((Player) viewer.getPlayer()).canSee(player) || manager.hasHiddenNametag(owner, viewer)) {
visibility = false;
} else {
visibility = visible;
}
datawatcher.helper().setCustomNameVisible(visibility);
boolean visibility;
if (isNameVisiblyEmpty(displayName) || !((Player) viewer.getPlayer()).canSee(player) ||
manager.hasHiddenNametag(owner, viewer) || manager.hasHiddenNameTagVisibilityView(viewer)) {
visibility = false;
} else {
visibility = visible;
}
datawatcher.helper().setCustomNameVisible(visibility);
if (viewer.getVersion().getMinorVersion() > 8 || manager.isMarkerFor18x()) datawatcher.helper().setArmorStandFlags((byte)16);
return datawatcher;
}
if (viewer.getVersion().getMinorVersion() > 8 || manager.isMarkerFor18x()) datawatcher.helper().setArmorStandFlags((byte)16);
return datawatcher;
}
/**
* Returns true if display name is in fact empty, for example only containing color codes
* @param displayName - string to check
* @return true if it's empty, false if not
*/
private boolean isNameVisiblyEmpty(String displayName) {
if (displayName.length() == 0) return true;
if (!displayName.startsWith(EnumChatFormat.COLOR_STRING) && !displayName.startsWith("&") && !displayName.startsWith("#")) return false;
String text = IChatBaseComponent.fromColoredText(displayName).toRawText();
if (text.contains(" ")) text = text.replace(" ", "");
return text.length() == 0;
}
/**
* Returns {@code true} if display name is in fact empty,
* for example only containing color codes, {@code false} if not.
*
* @param displayName
* string to check
* @return {@code true} if it's empty, {@code false} if not
*/
private boolean isNameVisiblyEmpty(String displayName) {
if (displayName.length() == 0) return true;
if (!displayName.startsWith(EnumChatFormat.COLOR_STRING) && !displayName.startsWith("&") && !displayName.startsWith("#")) return false;
String text = IChatBaseComponent.fromColoredText(displayName).toRawText();
if (text.contains(" ")) text = text.replace(" ", "");
return text.length() == 0;
}
/**
* Returns list of packets to send to make armor stand spawn with metadata
* @param viewer - viewer to apply relational placeholders for
* @return List of packets that spawn the armor stand
*/
public TabPacket[] getSpawnPackets(TabPlayer viewer) {
visible = getVisibility();
DataWatcher dataWatcher = createDataWatcher(property.getFormat(viewer), viewer);
if (TAB.getInstance().getServerVersion().getMinorVersion() >= 15) {
return new TabPacket[] {
new PacketPlayOutSpawnEntityLiving(entityId, uuid, EntityType.ARMOR_STAND, getArmorStandLocationFor(viewer), null),
new PacketPlayOutEntityMetadata(entityId, dataWatcher)
};
} else {
return new TabPacket[] {
new PacketPlayOutSpawnEntityLiving(entityId, uuid, EntityType.ARMOR_STAND, getArmorStandLocationFor(viewer), dataWatcher),
};
}
}
/**
* Returns list of packets to send to make armor stand spawn with metadata
*
* @param viewer
* viewer to apply relational placeholders for
* @return List of packets that spawn the armor stand
*/
public TabPacket[] getSpawnPackets(TabPlayer viewer) {
visible = getVisibility();
DataWatcher dataWatcher = createDataWatcher(property.getFormat(viewer), viewer);
if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 15) {
return new TabPacket[] {
new PacketPlayOutSpawnEntityLiving(entityId, uuid, EntityType.ARMOR_STAND, getArmorStandLocationFor(viewer), null),
new PacketPlayOutEntityMetadata(entityId, dataWatcher)
};
} else {
return new TabPacket[] {
new PacketPlayOutSpawnEntityLiving(entityId, uuid, EntityType.ARMOR_STAND, getArmorStandLocationFor(viewer), dataWatcher),
};
}
}
/**
* Returns location where armor stand should be for specified viewer
* @param viewer - player to get location for
* @return location of armor stand
*/
public Location getArmorStandLocationFor(TabPlayer viewer) {
return viewer.getVersion().getMinorVersion() == 8 && !manager.isMarkerFor18x() ? getLocation().clone().add(0,-2,0) : getLocation();
}
/**
* Returns location where armor stand should be for specified viewer
*
* @param viewer
* player to get location for
* @return location of armor stand
*/
public Location getArmorStandLocationFor(TabPlayer viewer) {
return viewer.getVersion().getMinorVersion() == 8 && !manager.isMarkerFor18x() ? getLocation(viewer).add(0,-2,0) : getLocation(viewer);
}
@Override
public void respawn(TabPlayer viewer) {
viewer.sendCustomPacket(destroyPacket, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_DESPAWN);
Runnable spawn = () -> spawn(viewer);
if (viewer.getVersion().getMinorVersion() == 8) {
//1.8.0 client sided bug
TAB.getInstance().getCPUManager().runTaskLater(50, "compensating for 1.8.0 bugs", manager, TabConstants.CpuUsageCategory.V1_8_0_BUG_COMPENSATION, spawn);
} else {
spawn.run();
}
}
@Override
public void respawn(TabPlayer viewer) {
viewer.sendCustomPacket(destroyPacket, TabConstants.PacketCategory.UNLIMITED_NAMETAGS_DESPAWN);
// 1.8.0 will not see entity that respawned in the same tick
// creating new delayed task every time someone sneaks can be abused and cause OOM
spawn(viewer);
}
}
@@ -0,0 +1,211 @@
package me.neznamy.tab.platforms.bukkit.features.unlimitedtags;
import me.neznamy.tab.api.ArmorStand;
import me.neznamy.tab.api.ArmorStandManager;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.shared.features.nametags.unlimited.NameTagX;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* A helper class for easy management of armor stands of a player
*/
public class BukkitArmorStandManager implements ArmorStandManager {
/** Map of registered armor stands by name */
private final Map<String, ArmorStand> armorStands = new LinkedHashMap<>();
/** Armor stands in an array for speed while iterating */
private ArmorStand[] armorStandArray = new ArmorStand[0];
/** Players in entity tracking range of owner */
private final List<TabPlayer> nearbyPlayers = new ArrayList<>();
/** Nearby players in an array for speed while iterating */
private TabPlayer[] nearbyPlayerArray = new TabPlayer[0];
/**
* Constructs new instance with given parameters and loads armor stands.
*
* @param nameTagX
* Main feature
* @param owner
* Owner of this armor stand manager
*/
public BukkitArmorStandManager(NameTagX nameTagX, TabPlayer owner) {
owner.setProperty(nameTagX, TabConstants.Property.NAMETAG, owner.getProperty(TabConstants.Property.TAGPREFIX).getCurrentRawValue()
+ owner.getProperty(TabConstants.Property.CUSTOMTAGNAME).getCurrentRawValue()
+ owner.getProperty(TabConstants.Property.TAGSUFFIX).getCurrentRawValue());
double height = 0;
for (String line : nameTagX.getDynamicLines()) {
addArmorStand(line, new BukkitArmorStand(this, owner, line, height, false));
height += 0.26;
}
for (Map.Entry<String, Object> line : nameTagX.getStaticLines().entrySet()) {
addArmorStand(line.getKey(), new BukkitArmorStand(this, owner, line.getKey(), Double.parseDouble(line.getValue().toString()), true));
}
fixArmorStandHeights();
}
/**
* Teleports armor stands to player's current location for specified viewer
*
* @param viewer
* player to teleport armor stands for
*/
public void teleport(TabPlayer viewer) {
for (ArmorStand a : armorStandArray) a.teleport(viewer);
}
/**
* Teleports armor stands to player's current location for all nearby players
*/
public void teleport() {
for (ArmorStand a : armorStandArray) a.teleport();
}
/**
* Returns array of nearby players
* @return array of nearby players
*/
public TabPlayer[] getNearbyPlayers(){
return nearbyPlayerArray;
}
/**
* Returns {@code true} if requested player is nearby, {@code false} if not
*
* @param viewer
* Player to check for
* @return {@code true} if player nearby, {@code false} if not
*/
public boolean isNearby(TabPlayer viewer) {
return nearbyPlayers.contains(viewer);
}
/**
* Returns {@code true} if manager contains armor stand with specified entity id, {@code false} if not
*
* @param entityId
* entity id
* @return {@code true} if armor stand with specified entity id exists, {@code false} if not
*/
public boolean hasArmorStandWithID(int entityId) {
for (ArmorStand a : armorStandArray) {
if (a.getEntityId() == entityId) {
return true;
}
}
return false;
}
/**
* Sets sneak value of armor stands to specified value
*
* @param sneaking
* new sneaking status
*/
public void sneak(boolean sneaking) {
for (ArmorStand a : armorStandArray) a.sneak(sneaking);
}
/**
* Performs respawn operation on all armor stands to skip teleport animation
*/
public void respawn() {
for (ArmorStand a : armorStandArray) {
for (TabPlayer viewer : nearbyPlayerArray) {
a.respawn(viewer);
}
}
}
/**
* Spawns all armor stands for specified viewer and adds them into nearby players
*
* @param viewer
* player to spawn armor stands for
*/
public void spawn(TabPlayer viewer) {
nearbyPlayers.add(viewer);
nearbyPlayerArray = nearbyPlayers.toArray(new TabPlayer[0]);
if (viewer.getVersion().getMinorVersion() < 8) return;
for (ArmorStand a : armorStandArray) a.spawn(viewer);
}
/**
* Fixes heights of all armor stands due to dynamic lines
*/
public void fixArmorStandHeights() {
double currentY = -0.26;
for (ArmorStand as : armorStandArray) {
if (as.hasStaticOffset()) continue;
if (as.getProperty().get().length() != 0) {
currentY += 0.26;
as.setOffset(currentY);
}
}
}
/**
* Adds armor stand to list and registers it to all nearby players
*
* @param name
* Unique identifier of the text line
* @param as
* Armor stand to add
*/
public void addArmorStand(String name, ArmorStand as) {
armorStands.put(name, as);
armorStandArray = armorStands.values().toArray(new ArmorStand[0]);
for (TabPlayer p : nearbyPlayerArray) as.spawn(p);
}
/**
* Removes specified player from list of nearby players
*
* @param viewer
* player to remove
*/
public void unregisterPlayer(TabPlayer viewer) {
if (nearbyPlayers.remove(viewer)) nearbyPlayerArray = nearbyPlayers.toArray(new TabPlayer[0]);
}
public void updateVisibility(boolean force) {
for (ArmorStand a : armorStandArray) a.updateVisibility(force);
}
/**
* Sends destroy packet of all armor stands to specified player and removes them from nearby players list
*
* @param viewer
* player to destroy armor stands for
*/
public void destroy(TabPlayer viewer) {
for (ArmorStand a : armorStandArray) a.destroy(viewer);
unregisterPlayer(viewer);
}
@Override
public void destroy() {
for (ArmorStand a : armorStandArray) a.destroy();
nearbyPlayers.clear();
nearbyPlayerArray = new TabPlayer[0];
}
@Override
public void refresh(boolean force) {
boolean fix = false;
for (ArmorStand as : armorStandArray) {
if (as.getProperty().update() || force) {
as.refresh();
fix = true;
}
}
if (fix) fixArmorStandHeights();
}
}
@@ -0,0 +1,188 @@
package me.neznamy.tab.platforms.bukkit.features.unlimitedtags;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.shared.features.nametags.unlimited.NameTagX;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.java.JavaPlugin;
/**
* The core class for unlimited NameTag mode on Bukkit
*/
public class BukkitNameTagX extends NameTagX {
/** Bukkit event listener */
private final EventListener eventListener = new EventListener(this);
/** Vehicle manager reference */
private final VehicleRefresher vehicleManager = new VehicleRefresher(this);
/**
* Constructs new instance with given parameter, loads config options, registers events
* and registers sub-features.
*
* @param plugin
* plugin instance
*/
public BukkitNameTagX(JavaPlugin plugin) {
super(BukkitArmorStandManager::new);
Bukkit.getPluginManager().registerEvents(eventListener, plugin);
TabAPI.getInstance().getFeatureManager().registerFeature(TabConstants.Feature.UNLIMITED_NAME_TAGS_PACKET_LISTENER, new PacketListener(this));
TabAPI.getInstance().getFeatureManager().registerFeature(TabConstants.Feature.UNLIMITED_NAME_TAGS_VEHICLE_REFRESHER, vehicleManager);
}
@Override
public void load() {
super.load();
for (TabPlayer all : TabAPI.getInstance().getOnlinePlayers()) {
if (isPlayerDisabled(all)) continue;
for (TabPlayer viewer : TabAPI.getInstance().getOnlinePlayers()) {
spawnArmorStands(viewer, all);
}
}
startVisibilityRefreshTask();
}
/**
* Starts task checking for player visibility to hide armor stands of invisible players.
*/
private void startVisibilityRefreshTask() {
TabAPI.getInstance().getThreadManager().startRepeatingMeasuredTask(500, this, TabConstants.CpuUsageCategory.REFRESHING_NAME_TAG_VISIBILITY, () -> {
for (TabPlayer p : TabAPI.getInstance().getOnlinePlayers()) {
if (isPlayerDisabled(p)) continue;
getArmorStandManager(p).updateVisibility(false);
}
});
}
@Override
public void unload() {
super.unload();
HandlerList.unregisterAll(eventListener);
}
@Override
public void onJoin(TabPlayer connectedPlayer) {
super.onJoin(connectedPlayer);
if (isPlayerDisabled(connectedPlayer)) return;
for (TabPlayer viewer : TabAPI.getInstance().getOnlinePlayers()) {
spawnArmorStands(viewer, connectedPlayer);
spawnArmorStands(connectedPlayer, viewer);
}
}
@Override
public boolean isOnBoat(TabPlayer player) {
return vehicleManager != null && vehicleManager.isOnBoat(player);
}
/**
* Spawns armor stands of target player to viewer if all requirements are met.
* These include players being in the same world, distance being less than 48 blocks
* and target player being visible to viewer.
*
* @param viewer
* Player viewing armor stands
* @param target
* Target player with armor stands
*/
private void spawnArmorStands(TabPlayer viewer, TabPlayer target) {
if (viewer.getVersion().getMinorVersion() < 8) return;
if (target == viewer || isPlayerDisabled(target)) return;
if (((Player) viewer.getPlayer()).getWorld() != ((Player) target.getPlayer()).getWorld()) return;
if (getDistance(viewer, target) <= 48 && ((Player)viewer.getPlayer()).canSee((Player)target.getPlayer()) && !target.isVanished())
getArmorStandManager(target).spawn(viewer);
}
@Override
public void setNameTagPreview(TabPlayer player, boolean status) {
if (status) {
getArmorStandManager(player).spawn(player);
} else {
getArmorStandManager(player).destroy(player);
}
}
@Override
public void resumeArmorStands(TabPlayer player) {
if (isPlayerDisabled(player)) return;
for (TabPlayer viewer : TabAPI.getInstance().getOnlinePlayers()) {
spawnArmorStands(viewer, player);
}
}
@Override
public void pauseArmorStands(TabPlayer player) {
getArmorStandManager(player).destroy();
}
@Override
public void updateNameTagVisibilityView(TabPlayer player) {
for (TabPlayer all : TabAPI.getInstance().getOnlinePlayers()) {
getArmorStandManager(all).updateVisibility(true);
}
}
@Override
public void onQuit(TabPlayer disconnectedPlayer) {
super.onQuit(disconnectedPlayer);
for (TabPlayer all : TabAPI.getInstance().getOnlinePlayers()) {
getArmorStandManager(all).unregisterPlayer(disconnectedPlayer);
}
armorStandManagerMap.get(disconnectedPlayer).destroy();
armorStandManagerMap.remove(disconnectedPlayer); // WeakHashMap doesn't clear this due to value referencing the key
}
@Override
public void onWorldChange(TabPlayer p, String from, String to) {
super.onWorldChange(p, from, to);
if (isUnlimitedDisabled(p.getServer(), to)) {
getDisabledUnlimitedPlayers().add(p);
updateTeamData(p);
} else if (getDisabledUnlimitedPlayers().remove(p)) {
updateTeamData(p);
}
if (isPreviewingNametag(p)) {
getArmorStandManager(p).spawn(p);
}
//for some reason this is needed for some users
for (TabPlayer viewer : TabAPI.getInstance().getOnlinePlayers()) {
if (viewer.getWorld().equals(from)) {
getArmorStandManager(p).destroy(viewer);
}
}
}
/**
* Returns flat distance between two players ignoring Y value
*
* @param player1
* first player
* @param player2
* second player
* @return flat distance in blocks
*/
private double getDistance(TabPlayer player1, TabPlayer player2) {
Location loc1 = ((Player) player1.getPlayer()).getLocation();
Location loc2 = ((Player) player2.getPlayer()).getLocation();
return Math.sqrt(Math.pow(loc1.getX()-loc2.getX(), 2) + Math.pow(loc1.getZ()-loc2.getZ(), 2));
}
@Override
public BukkitArmorStandManager getArmorStandManager(TabPlayer player) {
return (BukkitArmorStandManager) armorStandManagerMap.get(player);
}
/**
* Returns {@link #vehicleManager}
* @return {@link #vehicleManager}
*/
public VehicleRefresher getVehicleManager() {
return vehicleManager;
}
}
@@ -1,81 +1,58 @@
package me.neznamy.tab.platforms.bukkit.features.unlimitedtags;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.api.TabPlayer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.TAB;
/**
* The event listener part for securing proper functionality of armor stands
* Class listening to Bukkit events which the feature requires and are
* sufficient, without packets being requires.
*/
public class EventListener implements Listener {
//the NameTag feature handler
private final NameTagX feature;
/**
* Constructs new instance with given parameters
* @param feature - NameTag feature handler
*/
public EventListener(NameTagX feature) {
this.feature = feature;
}
/**
* Sneak event listener to de-spawn & spawn armor stands to skip animation
* @param e - sneak event
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onSneak(PlayerToggleSneakEvent e) {
TabPlayer p = TAB.getInstance().getPlayer(e.getPlayer().getUniqueId());
if (p == null || feature.isPlayerDisabled(p)) return;
TAB.getInstance().getCPUManager().runMeasuredTask("processing PlayerToggleSneakEvent", feature, TabConstants.CpuUsageCategory.PLAYER_SNEAK, () -> {
if (p.getArmorStandManager() != null) p.getArmorStandManager().sneak(e.isSneaking());
});
}
/**
* Respawning armor stands as respawn screen destroys all entities in client
* @param e - respawn event
*/
@EventHandler
public void onRespawn(PlayerRespawnEvent e) {
TAB.getInstance().getCPUManager().runMeasuredTask("processing PlayerRespawnEvent", feature, TabConstants.CpuUsageCategory.PLAYER_RESPAWN, () -> {
TabPlayer respawned = TAB.getInstance().getPlayer(e.getPlayer().getUniqueId());
if (feature.isPlayerDisabled(respawned)) return;
respawned.getArmorStandManager().teleport();
});
}
@EventHandler
public void onWorldChange(PlayerChangedWorldEvent e) {
TabPlayer p = TAB.getInstance().getPlayer(e.getPlayer().getUniqueId());
if (p == null || !p.isLoaded()) return;
long time = System.nanoTime();
String to = e.getPlayer().getWorld().getName();
if (feature.isDisabled(to)) {
feature.getPlayersInDisabledUnlimitedWorlds().add(p);
} else {
feature.getPlayersInDisabledUnlimitedWorlds().remove(p);
}
//TODO delete the block below
TabPlayer[] nearby = p.getArmorStandManager().getNearbyPlayers();
p.getArmorStandManager().destroy();
feature.loadArmorStands(p);
feature.getVehicleManager().loadPassengers(p);
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
if (viewer.getArmorStandManager() != null) viewer.getArmorStandManager().destroy(p);
if (!to.equals(viewer.getWorld())) continue;
for (TabPlayer player : nearby) {
if (player == viewer) feature.spawnArmorStands(p, viewer, true);
}
}
TAB.getInstance().getCPUManager().addTime(feature, TabConstants.CpuUsageCategory.WORLD_SWITCH, System.nanoTime()-time);
}
/** Reference to the main feature */
private final BukkitNameTagX feature;
/**
* Constructs new instance with given parameter
*
* @param feature
* NameTag feature handler
*/
public EventListener(BukkitNameTagX feature) {
this.feature = feature;
}
/**
* Sneak event listener to de-spawn and spawn armor stands to skip animation
*
* @param e
* sneak event
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onSneak(PlayerToggleSneakEvent e) {
TabPlayer p = TabAPI.getInstance().getPlayer(e.getPlayer().getUniqueId());
if (p == null || feature.isPlayerDisabled(p)) return;
TabAPI.getInstance().getThreadManager().runMeasuredTask(feature, TabConstants.CpuUsageCategory.PLAYER_SNEAK, () -> feature.getArmorStandManager(p).sneak(e.isSneaking()));
}
/**
* Respawning armor stands as respawn screen destroys all entities in client
*
* @param e
* respawn event
*/
@EventHandler
public void onRespawn(PlayerRespawnEvent e) {
TabAPI.getInstance().getThreadManager().runMeasuredTask(feature, TabConstants.CpuUsageCategory.PLAYER_RESPAWN, () -> {
TabPlayer respawned = TabAPI.getInstance().getPlayer(e.getPlayer().getUniqueId());
if (feature.isPlayerDisabled(respawned)) return;
feature.getArmorStandManager(respawned).teleport();
});
}
}
@@ -1,413 +0,0 @@
package me.neznamy.tab.platforms.bukkit.features.unlimitedtags;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.java.JavaPlugin;
import me.neznamy.tab.api.ArmorStand;
import me.neznamy.tab.api.ArmorStandManager;
import me.neznamy.tab.api.Property;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.api.team.UnlimitedNametagManager;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.features.nametags.NameTag;
/**
* The core class for unlimited NameTag mode
*/
public class NameTagX extends NameTag implements UnlimitedNametagManager {
//config options
private final boolean markerFor18x = TAB.getInstance().getConfiguration().getConfig().getBoolean("scoreboard-teams.unlimited-nametag-mode.use-marker-tag-for-1-8-x-clients", false);
private final boolean disableOnBoats = TAB.getInstance().getConfiguration().getConfig().getBoolean("scoreboard-teams.unlimited-nametag-mode.disable-on-boats", true);
private final double spaceBetweenLines = TAB.getInstance().getConfiguration().getConfig().getDouble("scoreboard-teams.unlimited-nametag-mode.space-between-lines", 0.22);
protected final List<String> disabledUnlimitedWorlds = TAB.getInstance().getConfiguration().getConfig().getStringList("scoreboard-teams.unlimited-nametag-mode.disable-in-worlds", new ArrayList<>());
private final List<String> dynamicLines = new ArrayList<>(TAB.getInstance().getConfiguration().getConfig().getStringList("scoreboard-teams.unlimited-nametag-mode.dynamic-lines", Arrays.asList(TabConstants.Property.ABOVENAME, TabConstants.Property.NAMETAG, TabConstants.Property.BELOWNAME, "another")));
private final Map<String, Object> staticLines = TAB.getInstance().getConfiguration().getConfig().getConfigurationSection("scoreboard-teams.unlimited-nametag-mode.static-lines");
//player data by entityId, used for better performance
private final Map<Integer, TabPlayer> entityIdMap = new ConcurrentHashMap<>();
//bukkit event listener
private final EventListener eventListener = new EventListener(this);
private final Set<TabPlayer> playersInDisabledUnlimitedWorlds = Collections.newSetFromMap(new WeakHashMap<>());
private final String[] disabledUnlimitedWorldsArray = disabledUnlimitedWorlds.toArray(new String[0]);
private final boolean unlimitedWorldWhitelistMode = disabledUnlimitedWorlds.contains("WHITELIST");
private final Set<TabPlayer> playersDisabledWithAPI = Collections.newSetFromMap(new WeakHashMap<>());
private final VehicleRefresher vehicleManager = new VehicleRefresher(this);
/**
* Constructs new instance with given parameters and loads config options
* @param plugin - plugin instance
*/
public NameTagX(JavaPlugin plugin) {
Collections.reverse(dynamicLines);
Bukkit.getPluginManager().registerEvents(eventListener, plugin);
TAB.getInstance().getFeatureManager().registerFeature(TabConstants.Feature.UNLIMITED_NAME_TAGS_PACKET_LISTENER, new PacketListener(this));
TAB.getInstance().getFeatureManager().registerFeature(TabConstants.Feature.UNLIMITED_NAME_TAGS_VEHICLE_REFRESHER, vehicleManager);
TAB.getInstance().debug(String.format("Loaded Unlimited NameTag feature with parameters markerFor18x=%s, disableOnBoats=%s, spaceBetweenLines=%s, disabledUnlimitedWorlds=%s",
markerFor18x, disableOnBoats, spaceBetweenLines, disabledUnlimitedWorlds));
}
@Override
public void load() {
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
getEntityIdMap().put(((Player) all.getPlayer()).getEntityId(), all);
updateProperties(all);
loadArmorStands(all);
if (isDisabled(all.getWorld())) {
playersInDisabledUnlimitedWorlds.add(all);
}
if (isPlayerDisabled(all)) continue;
vehicleManager.loadPassengers(all);
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
spawnArmorStands(all, viewer, false);
}
}
super.load();
startVisibilityRefreshTask();
}
private void startVisibilityRefreshTask() {
TAB.getInstance().getCPUManager().startRepeatingMeasuredTask(500, "refreshing NameTag visibility", this, TabConstants.CpuUsageCategory.REFRESHING_NAME_TAG_VISIBILITY, () -> {
for (TabPlayer p : TAB.getInstance().getOnlinePlayers()) {
if (isPlayerDisabled(p)) continue;
p.getArmorStandManager().updateVisibility(false);
}
});
}
@Override
public void unload() {
super.unload();
HandlerList.unregisterAll(eventListener);
for (TabPlayer p : TAB.getInstance().getOnlinePlayers()) {
if (p.getArmorStandManager() != null) p.getArmorStandManager().destroy();
}
getEntityIdMap().clear();
}
@Override
public void onJoin(TabPlayer connectedPlayer) {
if (isDisabled(connectedPlayer.getWorld()))
playersInDisabledUnlimitedWorlds.add(connectedPlayer);
super.onJoin(connectedPlayer);
getEntityIdMap().put(((Player) connectedPlayer.getPlayer()).getEntityId(), connectedPlayer);
loadArmorStands(connectedPlayer);
if (isPlayerDisabled(connectedPlayer)) return;
vehicleManager.loadPassengers(connectedPlayer);
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
spawnArmorStands(connectedPlayer, viewer, true);
}
}
/**
* Returns flat distance between two players ignoring Y value
* @param player1 - first player
* @param player2 - second player
* @return flat distance in blocks
*/
private double getDistance(TabPlayer player1, TabPlayer player2) {
Location loc1 = ((Player) player1.getPlayer()).getLocation();
Location loc2 = ((Player) player2.getPlayer()).getLocation();
return Math.sqrt(Math.pow(loc1.getX()-loc2.getX(), 2) + Math.pow(loc1.getZ()-loc2.getZ(), 2));
}
@Override
public void onQuit(TabPlayer disconnectedPlayer) {
super.onQuit(disconnectedPlayer);
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
if (all.getArmorStandManager() != null) all.getArmorStandManager().unregisterPlayer(disconnectedPlayer);
}
entityIdMap.remove(((Player) disconnectedPlayer.getPlayer()).getEntityId());
if (disconnectedPlayer.getArmorStandManager() != null) { //player was not loaded yet
disconnectedPlayer.getArmorStandManager().destroy();
TAB.getInstance().getCPUManager().runTaskLater(500, "processing onQuit", this, TabConstants.CpuUsageCategory.PLAYER_QUIT, () -> disconnectedPlayer.getArmorStandManager().destroy());
}
}
/**
* Spawns armor stands of specified owner to viewer and, mutually if set
* @param owner - armor stand owner
* @param viewer - player to spawn armor stands for
* @param sendMutually - if packets should be sent both ways
*/
public void spawnArmorStands(TabPlayer owner, TabPlayer viewer, boolean sendMutually) {
if (owner == viewer) return; //not displaying own armor stands
if (((Player) viewer.getPlayer()).getWorld() != ((Player) owner.getPlayer()).getWorld()) return; //different world
if (isPlayerDisabled(owner)) return;
if (getDistance(viewer, owner) <= 48) {
if (((Player)viewer.getPlayer()).canSee((Player)owner.getPlayer()) && !owner.isVanished()) owner.getArmorStandManager().spawn(viewer);
if (sendMutually && viewer.getArmorStandManager() != null && ((Player)owner.getPlayer()).canSee((Player)viewer.getPlayer())
&& !viewer.isVanished()) viewer.getArmorStandManager().spawn(owner);
}
}
/**
* Restarts and loads armor stands from config
* @param pl - player to load
*/
public void loadArmorStands(TabPlayer pl) {
pl.setArmorStandManager(new ArmorStandManager());
pl.setProperty(this, TabConstants.Property.NAMETAG, pl.getProperty(TabConstants.Property.TAGPREFIX).getCurrentRawValue()
+ pl.getProperty(TabConstants.Property.CUSTOMTAGNAME).getCurrentRawValue()
+ pl.getProperty(TabConstants.Property.TAGSUFFIX).getCurrentRawValue());
double height = 0;
for (String line : dynamicLines) {
Property p = pl.getProperty(line);
if (p.getCurrentRawValue().length() == 0) continue;
pl.getArmorStandManager().addArmorStand(line, new BukkitArmorStand(pl, p, height, false));
height += spaceBetweenLines;
}
for (Entry<String, Object> line : staticLines.entrySet()) {
Property p = pl.getProperty(line.getKey());
if (p.getCurrentRawValue().length() == 0) continue;
pl.getArmorStandManager().addArmorStand(line.getKey(), new BukkitArmorStand(pl, p, Double.parseDouble(line.getValue().toString()), true));
}
fixArmorStandHeights(pl);
}
/**
* Fixes heights of all armor stands of specified player due to dynamic lines
* @param p - player to fix armor stands heights for
*/
public void fixArmorStandHeights(TabPlayer p) {
double currentY = -spaceBetweenLines;
for (ArmorStand as : p.getArmorStandManager().getArmorStands()) {
if (as.hasStaticOffset()) continue;
if (as.getProperty().get().length() != 0) {
currentY += spaceBetweenLines;
as.setOffset(currentY);
}
}
}
@Override
public void refresh(TabPlayer refreshed, boolean force) {
super.refresh(refreshed, force);
if (isPlayerDisabled(refreshed)) return;
if (force) {
refreshed.getArmorStandManager().destroy();
loadArmorStands(refreshed);
vehicleManager.loadPassengers(refreshed);
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
if (viewer == refreshed) continue;
if (viewer.getWorld().equals(refreshed.getWorld())) {
refreshed.getArmorStandManager().spawn(viewer);
}
}
} else {
boolean fix = false;
for (ArmorStand as : refreshed.getArmorStandManager().getArmorStands()) {
if (as.getProperty().update()) {
as.refresh();
fix = true;
}
}
if (fix) fixArmorStandHeights(refreshed);
}
}
/**
* Updates raw values of properties for specified player
* @param p - player to update
*/
@Override
public boolean updateProperties(TabPlayer p) {
boolean changed = super.updateProperties(p);
if (p.loadPropertyFromConfig(this, TabConstants.Property.CUSTOMTAGNAME, p.getName())) changed = true;
if (p.setProperty(this, TabConstants.Property.NAMETAG, p.getProperty(TabConstants.Property.TAGPREFIX).getCurrentRawValue() +
p.getProperty(TabConstants.Property.CUSTOMTAGNAME).getCurrentRawValue() + p.getProperty(TabConstants.Property.TAGSUFFIX).getCurrentRawValue())) changed = true;
for (String property : dynamicLines) {
if (!property.equals(TabConstants.Property.NAMETAG) && p.loadPropertyFromConfig(this, property)) changed = true;
}
for (String property : staticLines.keySet()) {
if (!property.equals(TabConstants.Property.NAMETAG) && p.loadPropertyFromConfig(this, property)) changed = true;
}
return changed;
}
@Override
public String getFeatureName() {
return "Unlimited NameTags";
}
@Override
public boolean getTeamVisibility(TabPlayer p, TabPlayer viewer) {
if (p.hasInvisibilityPotion()) return false; //1.8.x client sided bug
if (vehicleManager == null) return false; //class is still being initialized and this was called from super constructor
return vehicleManager.isOnBoat(p) || isPlayerDisabled(p);
}
public Map<Integer, TabPlayer> getEntityIdMap() {
return entityIdMap;
}
public boolean isDisabled(String world) {
boolean contains = contains(disabledUnlimitedWorldsArray, world);
if (unlimitedWorldWhitelistMode) contains = !contains;
return contains;
}
public boolean isPlayerDisabled(TabPlayer p) {
return isDisabledPlayer(p) || playersInDisabledUnlimitedWorlds.contains(p) || hasTeamHandlingPaused(p) || hasDisabledArmorStands(p);
}
public boolean isMarkerFor18x() {
return markerFor18x;
}
public Set<TabPlayer> getPlayersInDisabledUnlimitedWorlds() {
return playersInDisabledUnlimitedWorlds;
}
public VehicleRefresher getVehicleManager() {
return vehicleManager;
}
@Override
public void pauseTeamHandling(TabPlayer player) {
if (teamHandlingPaused.contains(player)) return;
if (!isDisabledPlayer(player)) unregisterTeam(player);
teamHandlingPaused.add(player); //adding after, so unregisterTeam method runs
player.getArmorStandManager().destroy();
}
@Override
public void resumeTeamHandling(TabPlayer player) {
if (!teamHandlingPaused.contains(player)) return;
teamHandlingPaused.remove(player); //removing before, so registerTeam method runs
if (!isDisabledPlayer(player)) registerTeam(player);
if (!isPlayerDisabled(player)) {
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
spawnArmorStands(player, viewer, false);
}
}
}
@Override
public void disableArmorStands(TabPlayer player) {
if (playersDisabledWithAPI.contains(player)) return;
playersDisabledWithAPI.add(player);
player.getArmorStandManager().destroy();
updateTeamData(player);
}
@Override
public void enableArmorStands(TabPlayer player) {
if (!playersDisabledWithAPI.contains(player)) return;
playersDisabledWithAPI.remove(player);
if (!isPlayerDisabled(player)) {
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
spawnArmorStands(player, viewer, false);
}
}
updateTeamData(player);
}
@Override
public boolean hasDisabledArmorStands(TabPlayer player) {
return playersDisabledWithAPI.contains(player);
}
@Override
public void setPrefix(TabPlayer player, String prefix) {
player.getProperty(TabConstants.Property.TAGPREFIX).setTemporaryValue(prefix);
rebuildNameTagLine(player);
player.forceRefresh();
}
@Override
public void setSuffix(TabPlayer player, String suffix) {
player.getProperty(TabConstants.Property.TAGSUFFIX).setTemporaryValue(suffix);
rebuildNameTagLine(player);
player.forceRefresh();
}
@Override
public void resetPrefix(TabPlayer player) {
player.getProperty(TabConstants.Property.TAGPREFIX).setTemporaryValue(null);
rebuildNameTagLine(player);
player.forceRefresh();
}
@Override
public void resetSuffix(TabPlayer player) {
player.getProperty(TabConstants.Property.TAGSUFFIX).setTemporaryValue(null);
rebuildNameTagLine(player);
player.forceRefresh();
}
@Override
public void setName(TabPlayer player, String customName) {
player.getProperty(TabConstants.Property.CUSTOMTAGNAME).setTemporaryValue(customName);
rebuildNameTagLine(player);
player.forceRefresh();
}
@Override
public void setLine(TabPlayer player, String line, String value) {
player.getProperty(line).setTemporaryValue(value);
player.forceRefresh();
}
@Override
public void resetName(TabPlayer player) {
player.getProperty(TabConstants.Property.CUSTOMTAGNAME).setTemporaryValue(null);
rebuildNameTagLine(player);
player.forceRefresh();
}
@Override
public void resetLine(TabPlayer player, String line) {
player.getProperty(line).setTemporaryValue(null);
player.forceRefresh();
}
@Override
public String getCustomName(TabPlayer player) {
return player.getProperty(TabConstants.Property.CUSTOMTAGNAME).getTemporaryValue();
}
@Override
public String getCustomLineValue(TabPlayer player, String line) {
return player.getProperty(line).getTemporaryValue();
}
@Override
public String getOriginalName(TabPlayer player) {
return player.getProperty(TabConstants.Property.CUSTOMTAGNAME).getOriginalRawValue();
}
@Override
public String getOriginalLineValue(TabPlayer player, String line) {
return player.getProperty(line).getOriginalRawValue();
}
private void rebuildNameTagLine(TabPlayer player) {
player.setProperty(this, TabConstants.Property.NAMETAG, player.getProperty(TabConstants.Property.TAGPREFIX).getCurrentRawValue() +
player.getProperty(TabConstants.Property.CUSTOMTAGNAME).getCurrentRawValue() + player.getProperty(TabConstants.Property.TAGSUFFIX).getCurrentRawValue());
}
public boolean isDisableOnBoats() {
return disableOnBoats;
}
@Override
public List<String> getDefinedLines() {
List<String> lines = new ArrayList<>(dynamicLines);
lines.addAll(staticLines.keySet());
return lines;
}
}
@@ -1,126 +1,200 @@
package me.neznamy.tab.platforms.bukkit.features.unlimitedtags;
import java.util.List;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.api.TabFeature;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.platforms.bukkit.nms.NMSStorage;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.TAB;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* The packet listening part for securing proper functionality of armor stands
* The packet listening part for securing proper functionality of armor stands.
* Bukkit events are too unreliable and delayed/ahead which causes de-sync
* if trying to listen to move event.
* For entering/leaving tracking range there are no events and
* periodic / move-triggered distance checks would cause high CPU usage.
*/
public class PacketListener extends TabFeature {
//main feature
private final NameTagX nameTagX;
//nms storage
private final NMSStorage nms = NMSStorage.getInstance();
/** Reference to the main feature */
private final BukkitNameTagX nameTagX;
/**
* Constructs new instance with given parameters and loads config options
* @param nameTagX - main feature
*/
public PacketListener(NameTagX nameTagX) {
super(nameTagX.getFeatureName(), null);
this.nameTagX = nameTagX;
}
/** A player map by entity id, used for better performance */
private final Map<Integer, TabPlayer> entityIdMap = new ConcurrentHashMap<>();
/** Reference to NMS storage for quick access */
private final NMSStorage nms = NMSStorage.getInstance();
@Override
public boolean onPacketReceive(TabPlayer sender, Object packet) throws ReflectiveOperationException {
if (sender.getVersion().getMinorVersion() == 8 && nms.PacketPlayInUseEntity.isInstance(packet)) {
int entityId = nms.PacketPlayInUseEntity_ENTITY.getInt(packet);
TabPlayer attacked = null;
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
if (all.isLoaded() && all.getArmorStandManager().hasArmorStandWithID(entityId)) {
attacked = all;
break;
}
}
if (attacked != null && attacked != sender) {
nms.setField(packet, nms.PacketPlayInUseEntity_ENTITY, ((Player) attacked.getPlayer()).getEntityId());
}
}
return false;
}
/**
* Constructs new instance with given parameter
*
* @param nameTagX
* main feature
*/
public PacketListener(BukkitNameTagX nameTagX) {
super(nameTagX.getFeatureName(), null);
this.nameTagX = nameTagX;
}
@SuppressWarnings("unchecked")
@Override
public void onPacketSend(TabPlayer receiver, Object packet) throws ReflectiveOperationException {
if (receiver.getVersion().getMinorVersion() < 8) return;
if (!receiver.isLoaded() || nameTagX.isDisabledPlayer(receiver) || nameTagX.getPlayersInDisabledUnlimitedWorlds().contains(receiver)) return;
if (nms.PacketPlayOutEntity.isInstance(packet) && !nms.PacketPlayOutEntityLook.isInstance(packet)) { //ignoring head rotation only packets
onEntityMove(receiver, nms.PacketPlayOutEntity_ENTITYID.getInt(packet));
} else if (nms.PacketPlayOutEntityTeleport.isInstance(packet)) {
onEntityMove(receiver, nms.PacketPlayOutEntityTeleport_ENTITYID.getInt(packet));
} else if (nms.PacketPlayOutNamedEntitySpawn.isInstance(packet)) {
onEntitySpawn(receiver, nms.PacketPlayOutNamedEntitySpawn_ENTITYID.getInt(packet));
} else if (nms.PacketPlayOutEntityDestroy.isInstance(packet)) {
if (nms.getMinorVersion() >= 17) {
Object entities = nms.PacketPlayOutEntityDestroy_ENTITIES.get(packet);
if (entities instanceof List) {
onEntityDestroy(receiver, (List<Integer>) entities);
} else {
//1.17.0
onEntityDestroy(receiver, (int) entities);
}
} else {
onEntityDestroy(receiver, (int[]) nms.PacketPlayOutEntityDestroy_ENTITIES.get(packet));
}
}
}
@Override
public void load() {
for (TabPlayer all : TabAPI.getInstance().getOnlinePlayers()) {
entityIdMap.put(((Player) all.getPlayer()).getEntityId(), all);
}
}
/**
* Processes entity move packet
* @param receiver - packet receiver
* @param entityId - entity that moved
*/
private void onEntityMove(TabPlayer receiver, int entityId) {
TabPlayer pl = nameTagX.getEntityIdMap().get(entityId);
List<Entity> vehicleList;
if (pl != null) {
//player moved
if (nameTagX.isPlayerDisabled(pl)) return;
TAB.getInstance().getCPUManager().runMeasuredTask("processing EntityMove", nameTagX, TabConstants.CpuUsageCategory.PACKET_ENTITY_MOVE, () -> pl.getArmorStandManager().teleport(receiver));
} else if ((vehicleList = nameTagX.getVehicleManager().getVehicles().get(entityId)) != null){
//a vehicle carrying something moved
for (Entity entity : vehicleList) {
TabPlayer passenger = nameTagX.getEntityIdMap().get(entity.getEntityId());
if (passenger != null && passenger.getArmorStandManager() != null) {
TAB.getInstance().getCPUManager().runMeasuredTask("processing EntityMove", nameTagX, TabConstants.CpuUsageCategory.PACKET_ENTITY_MOVE_PASSENGER, () -> passenger.getArmorStandManager().teleport(receiver));
}
}
}
}
private void onEntitySpawn(TabPlayer receiver, int entityId) {
TabPlayer spawnedPlayer = nameTagX.getEntityIdMap().get(entityId);
if (spawnedPlayer != null && spawnedPlayer.isLoaded() && !nameTagX.isPlayerDisabled(spawnedPlayer)) {
TAB.getInstance().getCPUManager().runMeasuredTask("processing NamedEntitySpawn", nameTagX, TabConstants.CpuUsageCategory.PACKET_ENTITY_SPAWN, () -> spawnedPlayer.getArmorStandManager().spawn(receiver));
}
}
@Override
public void onJoin(TabPlayer connectedPlayer) {
entityIdMap.put(((Player) connectedPlayer.getPlayer()).getEntityId(), connectedPlayer);
}
private void onEntityDestroy(TabPlayer receiver, List<Integer> entities) {
for (int entity : entities) {
onEntityDestroy(receiver, entity);
}
}
private void onEntityDestroy(TabPlayer receiver, int... entities) {
for (int entity : entities) {
onEntityDestroy(receiver, entity);
}
}
private void onEntityDestroy(TabPlayer receiver, int entity) {
TabPlayer deSpawnedPlayer = nameTagX.getEntityIdMap().get(entity);
if (deSpawnedPlayer != null && deSpawnedPlayer.isLoaded() && !nameTagX.isPlayerDisabled(deSpawnedPlayer))
TAB.getInstance().getCPUManager().runMeasuredTask("processing EntityDestroy", nameTagX, TabConstants.CpuUsageCategory.PACKET_ENTITY_DESTROY, () -> deSpawnedPlayer.getArmorStandManager().destroy(receiver));
}
@Override
public void onQuit(TabPlayer disconnectedPlayer) {
entityIdMap.remove(((Player) disconnectedPlayer.getPlayer()).getEntityId());
}
@Override
public boolean onPacketReceive(TabPlayer sender, Object packet) throws ReflectiveOperationException {
if (sender.getVersion().getMinorVersion() == 8 && nms.PacketPlayInUseEntity.isInstance(packet)) {
int entityId = nms.PacketPlayInUseEntity_ENTITY.getInt(packet);
TabPlayer attacked = null;
for (TabPlayer all : TabAPI.getInstance().getOnlinePlayers()) {
if (all.isLoaded() && nameTagX.getArmorStandManager(all).hasArmorStandWithID(entityId)) {
attacked = all;
break;
}
}
if (attacked != null && attacked != sender) {
nms.setField(packet, nms.PacketPlayInUseEntity_ENTITY, ((Player) attacked.getPlayer()).getEntityId());
}
}
return false;
}
@SuppressWarnings("unchecked")
@Override
public void onPacketSend(TabPlayer receiver, Object packet) throws ReflectiveOperationException {
if (receiver.getVersion().getMinorVersion() < 8) return;
if (!receiver.isLoaded() || nameTagX.isDisabledPlayer(receiver) || nameTagX.getDisabledUnlimitedPlayers().contains(receiver)) return;
if (nms.PacketPlayOutEntity.isInstance(packet) && !nms.PacketPlayOutEntityLook.isInstance(packet)) { //ignoring head rotation only packets
onEntityMove(receiver, nms.PacketPlayOutEntity_ENTITYID.getInt(packet));
} else if (nms.PacketPlayOutEntityTeleport.isInstance(packet)) {
onEntityMove(receiver, nms.PacketPlayOutEntityTeleport_ENTITYID.getInt(packet));
} else if (nms.PacketPlayOutNamedEntitySpawn.isInstance(packet)) {
onEntitySpawn(receiver, nms.PacketPlayOutNamedEntitySpawn_ENTITYID.getInt(packet));
} else if (nms.PacketPlayOutEntityDestroy.isInstance(packet)) {
if (nms.getMinorVersion() >= 17) {
Object entities = nms.PacketPlayOutEntityDestroy_ENTITIES.get(packet);
if (entities instanceof List) {
onEntityDestroy(receiver, (List<Integer>) entities);
} else {
//1.17.0
onEntityDestroy(receiver, (int) entities);
}
} else {
onEntityDestroy(receiver, (int[]) nms.PacketPlayOutEntityDestroy_ENTITIES.get(packet));
}
}
}
/**
* Processes entity move packet. If entity ID belongs to a player,
* armor stands of that player are teleported to player who received the packet.
* If it belongs to a vehicle carrying a player, that player's armor stands are
* teleported as well.
*
* @param receiver
* packet receiver
* @param entityId
* entity that moved
*/
private void onEntityMove(TabPlayer receiver, int entityId) {
TabPlayer pl = entityIdMap.get(entityId);
List<Entity> vehicleList;
if (pl != null) {
//player moved
if (nameTagX.isPlayerDisabled(pl) || !pl.isLoaded()) return;
TabAPI.getInstance().getThreadManager().runMeasuredTask(nameTagX, TabConstants.CpuUsageCategory.PACKET_ENTITY_MOVE,
() -> nameTagX.getArmorStandManager(pl).teleport(receiver));
} else if ((vehicleList = nameTagX.getVehicleManager().getVehicles().get(entityId)) != null){
//a vehicle carrying something moved
for (Entity entity : vehicleList) {
TabPlayer passenger = entityIdMap.get(entity.getEntityId());
if (passenger != null && nameTagX.getArmorStandManager(passenger) != null) {
TabAPI.getInstance().getThreadManager().runMeasuredTask(nameTagX, TabConstants.CpuUsageCategory.PACKET_ENTITY_MOVE_PASSENGER,
() -> nameTagX.getArmorStandManager(passenger).teleport(receiver));
}
}
}
}
/**
* Processes named entity spawn packet and spawns armor stands if
* entity ID belongs to an online player.
*
* @param receiver
* packet receiver
* @param entityId
* spawned entity
*/
private void onEntitySpawn(TabPlayer receiver, int entityId) {
TabPlayer spawnedPlayer = entityIdMap.get(entityId);
if (spawnedPlayer != null && spawnedPlayer.isLoaded() && !nameTagX.isPlayerDisabled(spawnedPlayer)) {
TabAPI.getInstance().getThreadManager().runMeasuredTask(nameTagX, TabConstants.CpuUsageCategory.PACKET_ENTITY_SPAWN,
() -> nameTagX.getArmorStandManager(spawnedPlayer).spawn(receiver));
}
}
/**
* Processes entity destroy packet and destroys armor stands if
* entity ID belongs to an online player.
*
* @param receiver
* packet receiver
* @param entities
* de-spawned entities
*/
private void onEntityDestroy(TabPlayer receiver, List<Integer> entities) {
for (int entity : entities) {
onEntityDestroy(receiver, entity);
}
}
/**
* Processes entity destroy packet and destroys armor stands if
* entity ID belongs to an online player.
*
* @param receiver
* packet receiver
* @param entities
* de-spawned entities
*/
private void onEntityDestroy(TabPlayer receiver, int... entities) {
for (int entity : entities) {
onEntityDestroy(receiver, entity);
}
}
/**
* Processes entity destroy packet and destroys armor stands if
* entity ID belongs to an online player.
*
* @param receiver
* packet receiver
* @param entity
* de-spawned entity
*/
private void onEntityDestroy(TabPlayer receiver, int entity) {
TabPlayer deSpawnedPlayer = entityIdMap.get(entity);
if (deSpawnedPlayer != null && deSpawnedPlayer.isLoaded() && !nameTagX.isPlayerDisabled(deSpawnedPlayer)) {
BukkitArmorStandManager asm = nameTagX.getArmorStandManager(deSpawnedPlayer);
TabAPI.getInstance().getThreadManager().runMeasuredTask(nameTagX, TabConstants.CpuUsageCategory.PACKET_ENTITY_DESTROY,
() -> asm.destroy(receiver));
}
}
}
@@ -1,140 +1,154 @@
package me.neznamy.tab.platforms.bukkit.features.unlimitedtags;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabConstants;
import me.neznamy.tab.api.TabFeature;
import me.neznamy.tab.api.TabPlayer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import me.neznamy.tab.shared.TabConstants;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import me.neznamy.tab.api.TabFeature;
import me.neznamy.tab.api.TabPlayer;
import me.neznamy.tab.shared.TAB;
/**
* Sub-feature for unlimited name tag mode to secure
* proper functionality when using vehicles.
* A config option allows to disable the feature on boats,
* where it would cause a massive de-sync due to boat movement
* animation.
* Additionally, when entering a vehicle, no move packet is sent
* and therefore manual teleporting of armor stands is required.
*/
public class VehicleRefresher extends TabFeature {
//map of players currently in a vehicle
private final WeakHashMap<TabPlayer, Entity> playersInVehicle = new WeakHashMap<>();
//map of vehicles carrying players
private final Map<Integer, List<Entity>> vehicles = new ConcurrentHashMap<>();
//set of players currently on boats
private final Set<TabPlayer> playersOnBoats = Collections.newSetFromMap(new WeakHashMap<>());
private final NameTagX feature;
public VehicleRefresher(NameTagX feature) {
super(feature.getFeatureName(), "Refreshing vehicles");
this.feature = feature;
TAB.getInstance().getCPUManager().startRepeatingMeasuredTask(50, "processing player movement",
this, TabConstants.CpuUsageCategory.PROCESSING_PLAYER_MOVEMENT, () -> {
for (TabPlayer inVehicle : playersInVehicle.keySet()) {
inVehicle.getArmorStandManager().teleport();
// feature.getVehicleManager().processPassengers((Entity) inVehicle.getPlayer());
}
for (TabPlayer p : TAB.getInstance().getOnlinePlayers()) {
if (p.isPreviewingNametag()) {
p.getArmorStandManager().teleport(p);
}
}
});
addUsedPlaceholders(Collections.singletonList("%vehicle%"));
TAB.getInstance().getPlaceholderManager().registerPlayerPlaceholder("%vehicle%", 100, p -> ((Player)p.getPlayer()).getVehicle() == null ? "" : ((Player)p.getPlayer()).getVehicle());
}
/** Map of players currently in a vehicle */
private final WeakHashMap<TabPlayer, Entity> playersInVehicle = new WeakHashMap<>();
/** Map of vehicles carrying players */
private final Map<Integer, List<Entity>> vehicles = new ConcurrentHashMap<>();
/** set of players currently on boats */
private final Set<TabPlayer> playersOnBoats = Collections.newSetFromMap(new WeakHashMap<>());
@Override
public void load() {
for (TabPlayer p : TAB.getInstance().getOnlinePlayers()) {
Entity vehicle = ((Player)p.getPlayer()).getVehicle();
if (vehicle != null) {
vehicles.put(vehicle.getEntityId(), getPassengers(vehicle));
playersInVehicle.put(p, vehicle);
if (feature.isDisableOnBoats() && vehicle.getType() == EntityType.BOAT) {
playersOnBoats.add(p);
}
}
}
}
/** Reference to the main feature */
private final BukkitNameTagX feature;
@Override
public void refresh(TabPlayer p, boolean force) {
if (feature.isPlayerDisabled(p)) return;
Entity vehicle = ((Player)p.getPlayer()).getVehicle();
if (playersInVehicle.containsKey(p) && vehicle == null) {
//vehicle exit
vehicles.remove(playersInVehicle.get(p).getEntityId());
p.getArmorStandManager().teleport();
playersInVehicle.remove(p);
if (feature.isDisableOnBoats() && playersOnBoats.contains(p)) {
playersOnBoats.remove(p);
feature.updateTeamData(p);
}
}
if (!playersInVehicle.containsKey(p) && vehicle != null) {
//vehicle enter
vehicles.put(vehicle.getEntityId(), getPassengers(vehicle));
p.getArmorStandManager().respawn(); //making teleport instant instead of showing teleport animation
playersInVehicle.put(p, vehicle);
if (feature.isDisableOnBoats() && vehicle.getType() == EntityType.BOAT) {
playersOnBoats.add(p);
feature.updateTeamData(p);
}
}
}
/**
* Constructs new instance with given parameter and starts tasks.
*
* @param feature
* Main feature
*/
public VehicleRefresher(BukkitNameTagX feature) {
super(feature.getFeatureName(), "Refreshing vehicles");
this.feature = feature;
TabAPI.getInstance().getThreadManager().startRepeatingMeasuredTask(50,
this, TabConstants.CpuUsageCategory.PROCESSING_PLAYER_MOVEMENT, () -> {
for (TabPlayer inVehicle : playersInVehicle.keySet()) {
if (!inVehicle.isOnline() || feature.getArmorStandManager(inVehicle) == null) continue; // not removed from WeakHashMap yet
feature.getArmorStandManager(inVehicle).teleport();
}
for (TabPlayer p : TabAPI.getInstance().getOnlinePlayers()) {
if (feature.isPreviewingNametag(p)) {
feature.getArmorStandManager(p).teleport(p);
}
}
});
addUsedPlaceholders(Collections.singletonList(TabConstants.Placeholder.VEHICLE));
TabAPI.getInstance().getPlaceholderManager().registerPlayerPlaceholder(TabConstants.Placeholder.VEHICLE, 100, p -> ((Player)p.getPlayer()).getVehicle() == null ? "" : ((Player)p.getPlayer()).getVehicle());
}
public boolean isOnBoat(TabPlayer p) {
return playersOnBoats.contains(p);
}
@Override
public void load() {
for (TabPlayer p : TabAPI.getInstance().getOnlinePlayers()) {
Entity vehicle = ((Player)p.getPlayer()).getVehicle();
if (vehicle != null) {
vehicles.put(vehicle.getEntityId(), getPassengers(vehicle));
playersInVehicle.put(p, vehicle);
if (feature.isDisableOnBoats() && vehicle.getType().toString().contains("BOAT")) {
playersOnBoats.add(p);
}
}
}
}
@Override
public void onQuit(TabPlayer disconnectedPlayer) {
if (playersInVehicle.containsKey(disconnectedPlayer)) vehicles.remove(playersInVehicle.get(disconnectedPlayer).getEntityId());
}
public Map<Integer, List<Entity>> getVehicles() {
return vehicles;
}
/**
* Returns list of all passengers on specified vehicle
* @param vehicle - vehicle to check passengers of
* @return list of passengers
*/
@SuppressWarnings("deprecation")
public List<Entity> getPassengers(Entity vehicle){
if (TAB.getInstance().getServerVersion().getMinorVersion() >= 11) {
return vehicle.getPassengers();
} else {
if (vehicle.getPassenger() != null) {
return Collections.singletonList(vehicle.getPassenger());
} else {
return new ArrayList<>();
}
}
}
/**
* Loads all passengers riding this player and adds them to vehicle list
* @param p - player to load passengers of
*/
public void loadPassengers(TabPlayer p) {
Entity vehicle = ((Entity) p.getPlayer()).getVehicle();
if (vehicle == null) return;
vehicles.put(vehicle.getEntityId(), getPassengers(vehicle));
}
/**
* Teleports armor stands of all passengers on specified vehicle
* @param vehicle - entity to check passengers of
*/
public void processPassengers(Entity vehicle) {
for (Entity passenger : getPassengers(vehicle)) {
if (passenger instanceof Player) {
TAB.getInstance().getPlayer(passenger.getUniqueId()).getArmorStandManager().teleport();
}
processPassengers(passenger);
}
}
@Override
public void onJoin(TabPlayer connectedPlayer) {
Entity vehicle = ((Entity) connectedPlayer.getPlayer()).getVehicle();
if (vehicle != null) vehicles.put(vehicle.getEntityId(), getPassengers(vehicle));
}
@Override
public void onQuit(TabPlayer disconnectedPlayer) {
if (playersInVehicle.containsKey(disconnectedPlayer)) vehicles.remove(playersInVehicle.get(disconnectedPlayer).getEntityId());
for (List<Entity> entities : vehicles.values()) {
entities.remove((Player) disconnectedPlayer.getPlayer());
}
}
@Override
public void refresh(TabPlayer p, boolean force) {
if (feature.isPlayerDisabled(p)) return;
Entity vehicle = ((Player)p.getPlayer()).getVehicle();
if (playersInVehicle.containsKey(p) && vehicle == null) {
//vehicle exit
vehicles.remove(playersInVehicle.get(p).getEntityId());
feature.getArmorStandManager(p).teleport();
playersInVehicle.remove(p);
if (feature.isDisableOnBoats() && playersOnBoats.contains(p)) {
playersOnBoats.remove(p);
feature.updateTeamData(p);
}
}
if (!playersInVehicle.containsKey(p) && vehicle != null) {
//vehicle enter
vehicles.put(vehicle.getEntityId(), getPassengers(vehicle));
feature.getArmorStandManager(p).respawn(); //making teleport instant instead of showing teleport animation
playersInVehicle.put(p, vehicle);
if (feature.isDisableOnBoats() && vehicle.getType().toString().contains("BOAT")) {
playersOnBoats.add(p);
feature.updateTeamData(p);
}
}
}
/**
* Returns {@code true} if the player is in a boat, {@code false} if not
*
* @param p
* Player to check
* @return {@code true} if in a boat, {@code false} if not
*/
public boolean isOnBoat(TabPlayer p) {
return playersOnBoats.contains(p);
}
/**
* Returns vehicle map
*
* @return vehicle map
*/
public Map<Integer, List<Entity>> getVehicles() {
return vehicles;
}
/**
* Returns list of all passengers on specified vehicle
*
* @param vehicle
* vehicle to check passengers of
* @return list of passengers
*/
@SuppressWarnings("deprecation")
public List<Entity> getPassengers(Entity vehicle){
if (TabAPI.getInstance().getServerVersion().getMinorVersion() >= 11) {
return vehicle.getPassengers();
} else {
if (vehicle.getPassenger() != null) {
return Collections.singletonList(vehicle.getPassenger());
} else {
return Collections.emptyList();
}
}
}
}
Loaded 100 of 300 files, more files were not shown because too many files have changed in this diff. Show more