Compare commits

...

285 Commits
1.2 ... master

Author SHA1 Message Date
Preva1l
e398306cd7
enhancement: role weight fallback to luckperms placeholder (#238) 2024-12-16 20:00:25 +01:00
AlexDev_
9cb20be6e0 Fixed problem with regex 2024-12-15 22:10:10 +01:00
AlexDev_
a14c8eb2ea Fixed problem with placeholder replacements 2024-12-15 21:44:31 +01:00
AlexDev_
d47ee75d5b Added a delay on join 2024-12-13 23:19:48 +01:00
AlexDev_
e17d36deb5 Added more information about multi line strings 2024-12-12 16:38:13 +01:00
AlexDev_
6796f4402f Removed server displaynames section from docs 2024-12-04 20:36:48 +01:00
AlexDev_
5a46053117 Added 1.21.4 to workflows
Added 1.21.4 to Protocol765Adapter
2024-12-03 23:14:00 +01:00
AlexDev_
94760f7794 Changed velocity min build 2024-12-03 22:53:08 +01:00
AlexDev_
ce88b480a6 Fixed code style 2024-12-03 22:28:38 +01:00
AlexDev_
a55bd56364 Fixed typo 2024-12-03 22:19:38 +01:00
dependabot[bot]
7b8d55ba77
deps: bump org.projectlombok:lombok from 1.18.34 to 1.18.36 (#233)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.34 to 1.18.36.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.34...v1.18.36)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 22:08:57 +01:00
AlexDev_
24c29e632f
1.7.3 - Placeholders Replacements & 1.21.4 (#236)
* Added PlaceholderReplacements

* Removed server display names

* Fixed disconnect problem

* Added support for 1.21.4
2024-12-03 22:08:24 +01:00
dependabot[bot]
2ba208002a
deps: bump io.netty:netty-codec-http from 4.1.114.Final to 4.1.115.Final (#234)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.114.Final to 4.1.115.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.114.Final...netty-4.1.115.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-28 15:15:55 +00:00
AlexDev_
6c558fac3a Fixed problem when isRemoveNametags is true
Improved performance caching reflection fields
2024-11-19 16:27:03 +01:00
dependabot[bot]
67931d8d41
deps: bump com.gradleup.shadow from 8.3.3 to 8.3.5 (#232)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.3 to 8.3.5.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.3...8.3.5)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 23:02:04 +00:00
William
da946ac75a
build: support 1.21.3 in ci 2024-10-28 18:31:19 +00:00
William
d9dcf54a91
build: fix missing versionMetadata 2024-10-28 18:25:11 +00:00
William
724a9b2851
build: target PAPIProxyBridge 1.7 2024-10-28 18:19:46 +00:00
dependabot[bot]
848c681cbf
deps: bump it.unimi.dsi:fastutil from 8.5.14 to 8.5.15 (#229)
Bumps [it.unimi.dsi:fastutil](https://github.com/vigna/fastutil) from 8.5.14 to 8.5.15.
- [Changelog](https://github.com/vigna/fastutil/blob/master/CHANGES)
- [Commits](https://github.com/vigna/fastutil/compare/8.5.14...8.5.15)

---
updated-dependencies:
- dependency-name: it.unimi.dsi:fastutil
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 18:17:56 +00:00
William
6f140e4708
feat: Add support for Minecraft 1.21.2/3 (#228)
* First step for 1.21.2

* fix

* feat: start preparing 1.21.2 support

bumps gradle and various build deps

* build: now requires Velocity 3.4.0

* build: use Velocity 3.4.0 from maven

* refactor: cleanup, fix wrong protocol ver in 765

* refactor: minor code cleanup & reformat

* refactor: further code cleanup

* refactor: more minor refactoring work

* docs: document prerequisites for using the plugin message API

* Fixed team packet mapping problem
Fixed problems with SortingOrder packet
Changed scoreboard logic to skip team packets for 1.21.2+ players if nametag is empty

* docs: further grammar fixes to plugin message API docs

* refactor: adjust PPB version checking logic

* build: simplify PPB test logic

* refactor: remove unused code

* refactor: adjust formatting

* refactor: make nametag empty by default

* refactor: suppress warning

* fix: `ConfigurationException` deserializing minimum PPB version string

* refactor: remove unused import

* Bug fixes

* Removed tablist order from all TabPlayer instances when a player leaves

* Fixed problem with data structure

* Removed synchronized

* fix: subscriber order not taking effect

* refactor: minor code style tweaks

---------

Co-authored-by: AlexDev_ <56083016+alexdev03@users.noreply.github.com>
2024-10-28 18:17:36 +00:00
dependabot[bot]
73de08eea9
deps: bump org.bstats:bstats-velocity from 3.0.3 to 3.1.0 (#217)
Bumps [org.bstats:bstats-velocity](https://github.com/Bastian/bStats-Metrics) from 3.0.3 to 3.1.0.
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.3...v3.1.0)

---
updated-dependencies:
- dependency-name: org.bstats:bstats-velocity
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 20:37:11 +01:00
AlexDev_
cf7c92e28f
Fixed compilation problem 2024-10-02 12:42:26 +02:00
AlexDev_
cc548b19fa Updated papiproxybridge version
Added a check to correctly remove old entries after a player left a server
Fixed problem where PlayerChannelHandler couldn't be able to handle tablist entries before the creation of TabPlayer object.
2024-10-02 12:09:06 +02:00
JerryLin
4f2fe1ef3f
fix: tablist continuesly shows on tablist disabled server (#218) 2024-10-01 22:39:44 +02:00
AlexDev_
3865387b80
Update Config-File.md 2024-09-14 23:42:04 +02:00
AlexDev_
852c3e830c Changed Animations docs
Fixed condition problem
2024-08-23 21:32:08 +02:00
AlexDev_
5470d19378 Removed unused dependency 2024-08-21 11:45:18 +02:00
dependabot[bot]
43d05b73d7
deps: bump xyz.jpenilla.run-velocity from 2.3.0 to 2.3.1 (#211)
Bumps xyz.jpenilla.run-velocity from 2.3.0 to 2.3.1.

---
updated-dependencies:
- dependency-name: xyz.jpenilla.run-velocity
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-21 10:37:52 +01:00
dependabot[bot]
e59cbe6ff0
deps: bump org.bstats:bstats-velocity from 3.0.2 to 3.0.3 (#213)
Bumps [org.bstats:bstats-velocity](https://github.com/Bastian/bStats-Metrics) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.2...v3.0.3)

---
updated-dependencies:
- dependency-name: org.bstats:bstats-velocity
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-21 10:37:44 +01:00
AlexDev_
59c8b08290 Fixed tablist problem for some mods 2024-08-21 11:22:35 +02:00
AlexDev_
b4746dd483
Fixed regex problem (#209) 2024-08-11 15:53:15 +02:00
William
642e18b618
[ci skip] build: mark as compatible with 1.21.1 2024-08-11 13:33:48 +01:00
William
4d7ffd75db
Merge remote-tracking branch 'origin/master' 2024-08-11 13:32:20 +01:00
William
20370a34aa
build: bump to 1.7.1 2024-08-11 13:31:59 +01:00
AlexDev_
fc39861f33
refactor: Make MiniMessage the default formatter, fix bugs (#208)
* Fixed some problems
Changed default formatter to MINIMESAGE

* Removed debug message
2024-08-11 13:31:56 +01:00
William
77254f9228
build: use tokenMap in build 2024-08-11 13:23:48 +01:00
William
3a3f42f489
build: improve resiliency on uncloned build 2024-08-11 13:19:21 +01:00
William
9c04513ee9
Merge remote-tracking branch 'origin/master' 2024-08-11 13:17:05 +01:00
William
a0fd9e0d1e
docs: move multi-line string docs, remove extra header 2024-08-11 13:15:53 +01:00
dependabot[bot]
64060edcbd
deps: bump it.unimi.dsi:fastutil from 8.5.13 to 8.5.14 (#207)
Bumps [it.unimi.dsi:fastutil](https://github.com/vigna/fastutil) from 8.5.13 to 8.5.14.
- [Changelog](https://github.com/vigna/fastutil/blob/master/CHANGES)
- [Commits](https://github.com/vigna/fastutil/commits/8.5.14)

---
updated-dependencies:
- dependency-name: it.unimi.dsi:fastutil
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-11 13:10:20 +01:00
dependabot[bot]
5ae32291a9
deps: bump org.projectlombok:lombok from 1.18.32 to 1.18.34 (#203)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.32 to 1.18.34.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.32...v1.18.34)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-25 12:27:20 +01:00
dependabot[bot]
3f42c332f7
deps: bump io.netty:netty-codec-http from 4.1.110.Final to 4.1.112.Final (#206)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.110.Final to 4.1.112.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.110.Final...netty-4.1.112.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-25 12:26:20 +01:00
William
06fce2218b
ci: add bones publishing to CI 2024-07-25 12:10:56 +01:00
AlexDev_
ace3644111
feat: Add conditional & relational MiniPlaceholders support (#197)
* Added relational mini placeholders support
Fixed some problems
Removed 300ms delay after joining a server
Code refactor

* Updated MiniPlacehodlers dependency
Removed max team length for 1.18+ clients
Fixed problem of backend sending team packets for online players and added a warning message

* Added docs
Added more time/date placeholders

* Added mini condition system

* Fixed problem due to adventure string quoting

* Fixed problem in a rare use case

* Removed debug message

* Fixed conversations
Fixed packet unregistration problem

* Added docs
Fixed a problem

* Added yaml multi-line docs

* Changed docs

* Added papi support for conditions

* Cone clenaup

* Fixed placeholders in conditions

* Fixed conversations

* Fixed problems

* Fixed problems while using minedown or legacy
Added check for team packets tracker

* Fixed problems
Added support for hex colors in legacy formatter

* Fixed problems

* Fixed problem with header & footer

* Resolved conversations
2024-06-29 13:32:29 +01:00
William
06268521cf
docs: document command, update sidebar 2024-06-19 10:57:33 +01:00
William
1d05a1b34e
fix: wrong /velocitab permission node 2024-06-19 10:57:13 +01:00
William
84ae7a9437
feat: Add configuration for server links (#201)
* feat: add server URLs

* refactor: cleanup imports

* fix: only send server links to 1.21 clients

* feat: update server links on reload

* refactor: minor cleanup

* docs: add docs for server links

* fix: protocol version check issue

* Improved ServerUrl#resolve

---------

Co-authored-by: AlexDev_ <56083016+alexdev03@users.noreply.github.com>
2024-06-18 22:42:50 +01:00
dependabot[bot]
6f909fbec1
deps: bump io.netty:netty-codec-http from 4.1.110.Final to 4.1.111.Final (#200)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.110.Final to 4.1.111.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.110.Final...netty-4.1.111.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-18 16:07:47 +01:00
William
53c9acb322
[ci skip] build: update MC versions 2024-06-14 12:54:15 +01:00
William
2dba7f2852
feat: support Minecraft 1.21 2024-06-13 12:18:18 +01:00
William
c13d30b29a
refactor: improve /velocitab command messages 2024-06-09 14:04:03 +01:00
dependabot[bot]
1f54cf7ba4
deps: bump io.netty:netty-codec-http from 4.1.109.Final to 4.1.110.Final (#194)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.109.Final to 4.1.110.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.109.Final...netty-4.1.110.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 23:29:55 +01:00
William
fac0ccdcbb
docs: Polymer by Patbox is not compatible
I'm not sure why tbh.
2024-05-15 13:54:13 +01:00
dependabot[bot]
68c3199379
deps: bump net.kyori:adventure-nbt from 4.16.0 to 4.17.0 (#192)
Bumps [net.kyori:adventure-nbt](https://github.com/KyoriPowered/adventure) from 4.16.0 to 4.17.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.16.0...v4.17.0)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-nbt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 21:33:21 +01:00
AlexDev_
4770567a98
fix: duplicate entity ID issues with offline accounts (#190) 2024-05-07 11:13:42 +01:00
dependabot[bot]
b4dd2f4d8b
deps: bump xyz.jpenilla.run-velocity from 2.2.4 to 2.3.0 (#191)
Bumps xyz.jpenilla.run-velocity from 2.2.4 to 2.3.0.

---
updated-dependencies:
- dependency-name: xyz.jpenilla.run-velocity
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 18:52:14 +01:00
dependabot[bot]
d441d6f5e7
deps: bump xyz.jpenilla.run-velocity from 2.2.3 to 2.2.4 (#189)
Bumps xyz.jpenilla.run-velocity from 2.2.3 to 2.2.4.

---
updated-dependencies:
- dependency-name: xyz.jpenilla.run-velocity
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-01 16:57:05 +01:00
William
5f7f5a64be
docs: document Vanilla pet nametags functionality/workaround 2024-04-26 16:33:35 +01:00
Connor W
720fca942f
docs: document regex servers being supported (#187) 2024-04-25 01:29:21 +01:00
William278
8c02dfa296
docs: further improve Nametags docs 2024-04-24 10:40:11 +01:00
William278
4d98b24c02
docs: clarify Nametag limitations 2024-04-24 10:32:10 +01:00
William
cf9175297f
build: update CI pipelines for 1.20.5 2024-04-23 16:24:53 +01:00
William
c23fdd1ff6
feat: Add support for Minecraft 1.20.5 (#186)
* feat: support Minecraft 1.20.5

* build: bump to 1.6.5

* refactor: optimize imports

* docs: update about menu author credits

* docs: update velocity meta author credits

* docs: update URL

* refactor: use Minedown from new repo

* docs: shorten name of Plugin Message API docs page

* deps: bump minimum Velocity version to 380
2024-04-23 16:23:36 +01:00
dependabot[bot]
88dc2996e4
deps: bump org.apache.commons:commons-text from 1.11.0 to 1.12.0 (#185)
Bumps org.apache.commons:commons-text from 1.11.0 to 1.12.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 12:59:30 +01:00
dependabot[bot]
d6e71766a1
deps: bump io.netty:netty-codec-http from 4.1.108.Final to 4.1.109.Final (#184)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.108.Final to 4.1.109.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.108.Final...netty-4.1.109.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-18 22:32:17 +01:00
dependabot[bot]
ced1b5e4f0
deps: bump org.projectlombok:lombok from 1.18.30 to 1.18.32 (#181)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.30 to 1.18.32.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.30...v1.18.32)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-29 23:27:39 +00:00
dependabot[bot]
bc6c0c41b5
deps: bump io.netty:netty-codec-http from 4.1.107.Final to 4.1.108.Final (#182)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.107.Final to 4.1.108.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.107.Final...netty-4.1.108.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-29 23:25:46 +00:00
AlexDev_
b7c353a0ec
feat: add show_all_players_from_all_groups config option (#183)
Code refactor
Improved system that handles latency
2024-03-29 23:25:38 +00:00
William
5b5e40e7f8
deps: ConfigLib now versioned without the v 2024-03-16 19:55:21 +00:00
William
b118a8c134
deps: Target ConfigLib via Maven Central 2024-03-16 19:54:30 +00:00
AlexDev_
48b3b2af48
fix: inconsistencies when players kicked/redirected on servers (#180)
Added onlyListPlayersInSameServer inside groups
Removed onlyListPlayersInSameGroup from config
Fixed problems with regex for servers
Fixed other problems
2024-03-14 22:08:02 +00:00
AlexDev_
4e2749ac9e
Added regex system and fixed ghost players bug (#176)
* Added regex system for TabGroup's servers.
Fixed ghost player after kick/disconnect.

* Fixed config docs with missing entries

* Bumped version
2024-03-11 18:43:34 +00:00
AlexDev_
c0abf481c1
fix: various bugs, improve non-VT user handling (#170) 2024-03-01 00:52:50 +00:00
AlexDev_
3064aad4f3
docs: correct Plugin Message API docs (#169) 2024-02-27 22:04:49 +00:00
dependabot[bot]
39aaae2f30
deps: bump com.github.Exlll.ConfigLib:configlib-yaml (#168)
Bumps [com.github.Exlll.ConfigLib:configlib-yaml](https://github.com/Exlll/ConfigLib) from v4.4.0 to v4.5.0.
- [Release notes](https://github.com/Exlll/ConfigLib/releases)
- [Commits](https://github.com/Exlll/ConfigLib/compare/v4.4.0...v4.5.0)

---
updated-dependencies:
- dependency-name: com.github.Exlll.ConfigLib:configlib-yaml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 13:53:22 +00:00
dependabot[bot]
e90a8df5c7
deps: bump net.kyori:adventure-nbt from 4.15.0 to 4.16.0 (#167)
Bumps [net.kyori:adventure-nbt](https://github.com/KyoriPowered/adventure) from 4.15.0 to 4.16.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.15.0...v4.16.0)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-nbt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 13:53:15 +00:00
William
6a3d2abb8c
docs: add Plugin Message API Examples to sidebar 2024-02-19 18:04:18 +00:00
dependabot[bot]
763acb9f28
deps: bump io.netty:netty-codec-http from 4.1.106.Final to 4.1.107.Final (#163)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.106.Final to 4.1.107.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.106.Final...netty-4.1.107.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 18:02:27 +00:00
William
58742eb2a1
build: use gradle 8.6 2024-02-19 14:08:39 +00:00
William
86b5e3a374
build: bump to 1.6.3 2024-02-19 14:04:49 +00:00
AlexDev_
3d744ccefe
Added skip for compatibility check (#162) 2024-02-19 14:55:13 +01:00
dependabot[bot]
47296961e2
deps: bump it.unimi.dsi:fastutil from 8.5.12 to 8.5.13 (#158)
Bumps [it.unimi.dsi:fastutil](https://github.com/vigna/fastutil) from 8.5.12 to 8.5.13.
- [Changelog](https://github.com/vigna/fastutil/blob/master/CHANGES)
- [Commits](https://github.com/vigna/fastutil/commits)

---
updated-dependencies:
- dependency-name: it.unimi.dsi:fastutil
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-16 17:03:50 +00:00
AlexDev_
4efc5797b3
feat: add plugin message api, GROUP_PLAYERS_ONLINE placeholder (#157)
* Added plugin message api & added LOCAL_GROUP_PLAYERS_ONLINE placeholders

* Fixed conversations, added placeholders to docs and fixed a few bugs

* Solved conversation

* Fixed possible charset problem and moved channels to a map instead of a set

* Changed docs

* Fixed kick issue and fixed problem header/footer on join
2024-02-09 23:58:15 +00:00
dependabot[bot]
a5940e0315
deps: bump xyz.jpenilla.run-velocity from 2.2.2 to 2.2.3 (#155)
Bumps xyz.jpenilla.run-velocity from 2.2.2 to 2.2.3.

---
updated-dependencies:
- dependency-name: xyz.jpenilla.run-velocity
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-08 15:10:51 +00:00
William278
345ce7fa8a refactor: require PPB for PAPI placeholder fallback, add setting 2024-02-04 14:26:17 +00:00
dependabot[bot]
65e93781eb
ci: bump gradle/gradle-build-action from 2 to 3 (#150)
Bumps [gradle/gradle-build-action](https://github.com/gradle/gradle-build-action) from 2 to 3.
- [Release notes](https://github.com/gradle/gradle-build-action/releases)
- [Commits](https://github.com/gradle/gradle-build-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: gradle/gradle-build-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-03 13:00:50 +00:00
Boy0000
f47f5fc2fd
refactor: Improve /velocitab name (#154)
* feat: use StringArgumentType#greedyString in name-command argument to allow for special characters

* feat: handle name-command if no argument is given
2024-02-03 12:59:22 +00:00
Boy0000
65abbc1646
fix: let LuckPerms-Meta placeholder pass if Proxy returns blank (#153)
* fix: let LuckPerms-Meta placeholder pass if Proxy returns blank

* fix: let unset prefix & suffix also pass to bridge

* refactor: remove unnecessary sorting warning
2024-02-03 12:03:38 +00:00
AlexDev_
7caa185fc1
Improve config validator, add team collision rule setting (#152)
* Fixed tab problem on not handled servers

* Fixed config validator and added collisions parameter

* Fixed conversations
2024-02-01 23:03:40 +00:00
William
63ed22527b
build: bump ConfigLib to 4.4.0, fix file encoding 2024-01-26 21:08:35 +00:00
dependabot[bot]
fc605ce6d7
deps: bump io.netty:netty-codec-http from 4.1.105.Final to 4.1.106.Final (#147)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.105.Final to 4.1.106.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.105.Final...netty-4.1.106.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-24 22:47:02 +00:00
AlexDev_
e19d06ee18
Fixed some problems: (#149)
- kick problem
- missing permission to name subcommand
- check before clearing header & footer on not handled servers
2024-01-24 22:13:29 +01:00
dependabot[bot]
dda537662e
deps: bump net.william278:PAPIProxyBridge from 1.4.2 to 1.5 (#148)
Bumps net.william278:PAPIProxyBridge from 1.4.2 to 1.5.

---
updated-dependencies:
- dependency-name: net.william278:PAPIProxyBridge
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-24 19:38:46 +00:00
William278
e5406051bf refactor: Slightly tweak ScoreboardManager registration error 2024-01-24 19:37:15 +00:00
William278
384137a67c feat: Add Velocity version compatibility checking 2024-01-24 19:36:59 +00:00
AlexDev_
c4a07e1997
Fix regex problem (#146) 2024-01-21 14:31:13 +01:00
William278
7bd0ac3e0c [ci skip] docs: fixup wrong @since tags in API 2024-01-19 20:24:39 +00:00
William278
7b347bb43e refactor: optimize imports 2024-01-19 18:05:43 +00:00
William278
fec29dd057 refactor: slight compressNumber refactor 2024-01-19 17:16:04 +00:00
William278
247fc68a4a refactor: lastDiplayname -> lastDisplayName 2024-01-19 17:12:55 +00:00
William278
8bace563b4 build: bump to 1.6.1 2024-01-19 16:40:27 +00:00
William278
f66483fdb1 build: bump adventure-nbt 2024-01-19 16:38:34 +00:00
AlexDev_
f3ec35f8e9 Updated gradle wrapper version 2024-01-19 14:28:13 +01:00
AlexDev_
8cc6df6fc2 Updated packet class 2024-01-19 13:01:59 +01:00
AlexDev_
9e60fc0daa Fix for https://github.com/WiIIiam278/Velocitab/issues/144 2024-01-18 16:58:06 +01:00
AlexDev_
e496c99a52 Fixed rare problem while joining 2024-01-16 22:51:23 +01:00
William
b37c760033
build: update wiki action to use v4 2024-01-16 21:38:45 +00:00
dependabot[bot]
0b98eff66e
deps: bump io.netty:netty-codec-http to 4.1.105.Final (#141)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.103.Final to 4.1.105.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.103.Final...netty-4.1.105.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 21:37:59 +00:00
AlexDev_
89a1f7add3
refactor: internals refactor, fix logic, new configs, spectator fix (#138)
* Started refactor

* more work

* Bug fixes and more work

* Fixed task problem

* More work on providers + fixed relocation problem

* Added providers + relocated snakeyaml

* Fixed relocation problem + removed org.json

* maps instantiation refactored

* Fixed reload problem

* Fixed logic problem

* More work on refactoring PlayerTabList

* Using lombok for procteded values

* More work

* Fixed cache problem + more work on refactor

* Fix for https://github.com/WiIIiam278/Velocitab/issues/35

* fixed conversations

* Code refactor

* Fixed problem while using minimessage

* Added more javadocs and removed kick handling as velocity fixed that problem

* Added username_lower placeholder and removed useless libraries

* Updated docs

* Added option to remove spectator effect in tablist
2024-01-16 21:09:46 +00:00
William278
08501e84b8 build: Add run-velocity, use velocity-proxy from william278 2024-01-11 17:01:19 +00:00
dependabot[bot]
53bdeee74b
ci: bump actions/upload-artifact from 3 to 4 (#129)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-14 23:17:44 +00:00
William
d5622bb90d
docs: Use wiki-action v3 for now 2023-12-14 23:07:58 +00:00
William
f16dd54a7f
feat: Add support for Minecraft 1.20.3/1.20.4 (#126)
* docs: Minor comment tweak

* Prepare 1.20.3 support pending Velocity

* 1.20.3/1.20.4 & java 17 (#128)

* Improved PacketAdapter system + added support for 1.20.3/1.20.4

* Changed java version to 17, updated velocity dependencies, improved packet adapters & added support for 1.20.3/4.

* Fixed compile error with adventure

* deps: Bump `netty-codec-http` to 4.1.103

* ci: Upgrade dependabot config

* ci: Update CI & Docs with new requirements

* refactor: Rename `LUCK_PERMS_META` -> `LUCKPERMS_META`

* docs: Document `%luckperms_meta_(key)%`

---------

Co-authored-by: AlexDev_ <56083016+alexdev03@users.noreply.github.com>
2023-12-14 23:00:04 +00:00
AlexDev_
d72ad289ec
feat: Improve placeholder system, add luck_perms_meta placeholder (#125) 2023-12-14 22:41:00 +00:00
dependabot[bot]
4355dc064d
build(deps): bump io.netty:netty-codec-http (#127)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.101.Final to 4.1.103.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.101.Final...netty-4.1.103.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-13 18:18:36 +00:00
AlexDev_
3b1be4142f
Improved serializer system for packet adapters & fixed bugs with nametags (#123) 2023-11-30 15:15:18 +00:00
William
4d6621c3c1
Tidy up bits of logic, use class for Nametags (#122)
* Tidy up bits of logic, use record for Nametags

* Few more bits of cleanup

* Some feedback

* More feedback

* Fix `#prefix()` and `#suffix()` record calls

* Fixup logical error

* `nameTag` -> `nametag`

* Make TabPlayer#getNametag returns TabPlayer.Nametag

---------

Co-authored-by: AlexDev_ <alessandrodalfovo2003@gmail.com>
2023-11-20 14:36:04 +00:00
AlexDev_
2becf43845
Fix for various problems (#119)
* Fixed logic problems with vanish + added tab recalculate system when luckperms fires UserDataRecalculateEvent

* Fix for https://github.com/WiIIiam278/Velocitab/issues/120 .
Fix for rgb nametags with legacy formatter.
Fix for players with escape characters in their name.
Fix for when a player is kicked from a server while staying online, tablist wasn't updated for that player.
Fix for vanish, wrong variable used.
Fix for negative values as input for tab sorting, min value is now 0.
2023-11-14 16:58:33 +00:00
dependabot[bot]
e4df93ca3f
build(deps): bump io.netty:netty-codec-http (#118)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.100.Final to 4.1.101.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.100.Final...netty-4.1.101.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-10 10:58:46 +00:00
William
2e5bf0f74d
docs: Minor ref formatting updates 2023-11-09 15:55:00 +00:00
AlexDev_
b8698075de Added missing key to Config-File.md 2023-11-09 16:27:16 +01:00
AlexDev_
7eb8f2dbd1 Fix wrong access modifier 2023-11-09 16:16:51 +01:00
AlexDev_
3d3f3a3bfa
Changed PlayerAddedToTabEvent from record to normal class (#117) 2023-11-09 12:04:42 +01:00
AlexDev_
938ce9e077 Added VelocitabAPI#getServerGroup and fixed problem while un-vanishing 2023-11-08 16:59:40 +01:00
AlexDev_
83195d0e72
Fixed vanish problem (#116) 2023-11-08 10:00:59 +01:00
AlexDev_
7c61d82ce6 Fixed wrong method in docs 2023-11-07 16:02:09 +01:00
AlexDev_
3bb457f14e Docs fix 2023-11-06 11:28:19 +01:00
AlexDev_
c36e17b75e
Added PlayerAddedToTabEvent, improved PlayerTabList performance and more (#114)
Added PlayerAddedToTabEvent, improved PlayerTabList performance and added the possibility to reload the plugin without breaking the tab list. This is only for dev purposes. Bumped version to 1.5.2
Fixed a few problems.
2023-11-05 21:15:23 +01:00
dependabot[bot]
c82a0c75d8
build(deps): bump org.apache.commons:commons-text from 1.10.0 to 1.11.0 (#111)
Bumps org.apache.commons:commons-text from 1.10.0 to 1.11.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-03 01:01:42 +00:00
AlexDev_
7005ceccd7
Added a team exists check when vanishing/unvanishing a player (#110) 2023-10-25 19:54:01 +02:00
dependabot[bot]
e22dc45a21
build(deps): bump org.ajoberstar.grgit from 5.2.0 to 5.2.1 (#109)
Bumps [org.ajoberstar.grgit](https://github.com/ajoberstar/grgit) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/ajoberstar/grgit/releases)
- [Commits](https://github.com/ajoberstar/grgit/compare/5.2.0...5.2.1)

---
updated-dependencies:
- dependency-name: org.ajoberstar.grgit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 00:11:16 +01:00
William
8d3abf7ef2
docs: Update documentation on supported MC versions 2023-10-22 17:06:54 +01:00
William
3f4cb6c947
Update plugin description 2023-10-21 13:52:13 +01:00
AlexDev_
f03b8f1819
Added an option to remove name tags if prefix & suffix are empty (#108) 2023-10-21 12:58:38 +01:00
William
887359ddd0
docs: Fixup formatting mistake in Sorting 2023-10-19 15:11:08 +01:00
AlexDev_
d0c55b9112
docs: New API documentation corrections (#107)
* Updated docs

* Improved grammar

* Added more information about vanishing a player.
2023-10-19 15:07:50 +01:00
William
ad3183fca1
docs: Add API docs 2023-10-18 19:29:31 +01:00
William
0ab7a81501
[ci skip] Fix API badge on README 2023-10-18 19:04:45 +01:00
William
51d607a6bb
ci: add publishing to maven 2023-10-18 19:04:04 +01:00
William
3d15022565
docs: Update README 2023-10-18 18:59:10 +01:00
AlexDev_
b128f36efa
Added VelocitabAPI, Vanish integration system (#106)
* Added support for VanishIntegration

* Merged

* Added API and improved vanish system

* Fixed problem with API

* Fixed import problem

* Commit with requested changes

* first test

* Added NotNull missing annotations

* Fixed all requested changes

* Fixed logic problem

* Revert "first test"

This reverts commit 1be3c47d9c.

* Hide nametag if the prefix & suffix are empty.

* Fixes for conversations.

* Added missing @NotNull

* Adjust repo order; use `elytrium` over `exceptionflug`

---------

Co-authored-by: William <will27528@gmail.com>
2023-10-18 18:43:03 +01:00
AlexDev_
1e2aff4cf0
Added Sorting Manager (#105)
* Added SortingManager.

* Fixed initialization problem + added cache for team name

* Improved code readability

* Commit with the requested changes.

* Update src/main/java/net/william278/velocitab/Velocitab.java

Co-authored-by: William <will27528@gmail.com>

---------

Co-authored-by: William <will27528@gmail.com>
2023-10-15 13:04:10 +01:00
William278
3d82ebe809 [ci skip] Bump to 1.5.1 2023-10-13 10:48:36 +01:00
William278
48c2b11e30 docs: Slight nametag formatting fixes 2023-10-13 10:48:21 +01:00
AlexDev_
a79404a530
Improved sorting logic to handle both high and low values. (#103) 2023-10-13 10:08:30 +01:00
Katherine
9090631677
Add all possible protocols to Protocol403Adapter (#102)
Fixes 1.19.2 clients being instantly kicked

Signed-off-by: unilock <unilock@fennet.rentals>
2023-10-12 17:09:11 +01:00
William278
b07c8da4a6 ci: fix version types 2023-10-12 15:51:34 +01:00
William278
23daa22d5d ci: add release action 2023-10-12 15:47:12 +01:00
William278
a1c3b069a5 ci: 1.8.8 is supported too 2023-10-12 14:19:01 +01:00
William278
ce3bb9f954 ci: verbosely state every version
My fork of mc-publish that supports Hangar needs updating because v3.3 adds support for version ranges :(
2023-10-12 14:15:49 +01:00
William278
c05d16eda0 ci: improve version availability info 2023-10-12 14:03:21 +01:00
William278
b40a48a987 docs: Document new placeholders 2023-10-12 13:44:29 +01:00
William
8224cd0ff1
Fixes, logic simplification, update docs for nametags (#101) 2023-10-12 11:44:27 +01:00
William
1f1e69ebca
Add support for Minecraft 1.20.2 (#99)
* Add protocol mappings for 1.20.2

* Cleanup some exception handling

* ci: Mark 1.20.2 as supported

* Minor code formatting tweaks
2023-10-11 17:10:29 +01:00
dependabot[bot]
fd17560f2e
Bump io.netty:netty-codec-http from 4.1.99.Final to 4.1.100.Final (#98)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.99.Final to 4.1.100.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.99.Final...netty-4.1.100.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 19:11:18 +01:00
dependabot[bot]
65141d0e61
Bump net.william278:PAPIProxyBridge from 1.4 to 1.4.2 (#97)
Bumps [net.william278:PAPIProxyBridge](https://github.com/WiIIiam278/PAPIProxyBridge) from 1.4 to 1.4.2.
- [Release notes](https://github.com/WiIIiam278/PAPIProxyBridge/releases)
- [Commits](https://github.com/WiIIiam278/PAPIProxyBridge/compare/1.4...1.4.2)

---
updated-dependencies:
- dependency-name: net.william278:PAPIProxyBridge
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-07 11:29:14 +01:00
dependabot[bot]
43d36d3425
Bump io.netty:netty-codec-http from 4.1.98.Final to 4.1.99.Final (#96)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.98.Final to 4.1.99.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.98.Final...netty-4.1.99.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-29 22:37:34 +01:00
William278
5ad41e041f Use text block instead of concat in Settings 2023-09-25 15:21:54 +01:00
William278
7a558d4072 Fix trailing comma in Protocol403Adapter 2023-09-25 15:21:11 +01:00
William278
6c8b1b8147 Update config file with new sorting key 2023-09-25 15:20:34 +01:00
William278
2afbf38ee1 Add %server_group% and %server_group_index% placeholders 2023-09-25 15:19:32 +01:00
AlexDev_
cb8a50c24f
Sorting System with Placeholders (#94)
* Added regex check for placeholders to avoid useless requests.
Added support for custom nametags. Due to minecraft limit only legacy chatcolor are supported.
Team names now are unique, so 1 team can have max 1 player.
Fixed problem with luckperms event bus while reloading the plugin.

* Update src/main/java/net/william278/velocitab/config/Placeholder.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/hook/LuckPermsHook.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/config/Formatter.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java

Co-authored-by: William <will27528@gmail.com>

* Fixed problem while updating display names. Changed a few method signature as requested in pr. Applied changes of pr.

* Added support for placeholders as sorting system

* Code reformat

* Update logging, task scheduling and player rosters

Modified logging in the ScoreboardManager to represent playerNames as an array for readability. Ensured all tasks scheduled by Velocitab are canceled on proxy shutdown to prevent unwanted behavior. Reworked player roster management in PlayerTabList to correctly update player roles and decrease asynchronicity, enhancing performance and preventing possible race conditions.

* Fixed problems after merging with upstream, fixed problem with player team color on join.

* Fixed problems with pr-merge. Added sorting system with placeholders.

* Update src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/ScoreboardManager.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/ScoreboardManager.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/ScoreboardManager.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/config/Formatter.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/player/TabPlayer.java

Co-authored-by: William <will27528@gmail.com>

* Fix username replacement in scoreboard and code typo

This commit resolves two issues. Firstly, changed the variable that we split the nametag on in `ScoreboardManager` from a hardcoded string to the player's specific username. This rectifies an issue where incorrect splitting occurred if the username wasn't exactly "%username%". Secondly, fixed a miswritten method call in `Formatter` from '..legacySection()' to '.legacySection()', correcting a syntax error. Lastly, removed superfluous replacement in `TabPlayer's` getNametag method as it was already handled in `ScoreboardManager`.

* Reformat code

* Changed logic with only one plugin message request.

* Update src/main/java/net/william278/velocitab/sorting/SortingManager.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/hook/LuckPermsHook.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/ScoreboardManager.java

Co-authored-by: William <will27528@gmail.com>

* Fixed requested changes

* Changed docs

---------

Co-authored-by: William <will27528@gmail.com>
2023-09-25 15:10:45 +01:00
William
f3f86f54d0
Merge remote-tracking branch 'origin/master' 2023-09-22 18:04:02 +01:00
William
1cbcc94f1c
[ci skip] Bump to v1.5 2023-09-22 18:03:49 +01:00
dependabot[bot]
2bbe2fb830
Bump org.projectlombok:lombok from 1.18.28 to 1.18.30 (#92)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.28 to 1.18.30.
- [Release notes](https://github.com/projectlombok/lombok/releases)
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.28...v1.18.30)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-22 18:01:56 +01:00
dependabot[bot]
cc38bf604d
Bump io.netty:netty-codec-http from 4.1.97.Final to 4.1.98.Final (#93)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.97.Final to 4.1.98.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.97.Final...netty-4.1.98.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-22 18:00:15 +01:00
AlexDev_
75d9f32010
Added support for nametags and fixed a few problems (#84)
* Added regex check for placeholders to avoid useless requests.
Added support for custom nametags. Due to minecraft limit only legacy chatcolor are supported.
Team names now are unique, so 1 team can have max 1 player.
Fixed problem with luckperms event bus while reloading the plugin.

* Update src/main/java/net/william278/velocitab/config/Placeholder.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/hook/LuckPermsHook.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/config/Formatter.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java

Co-authored-by: William <will27528@gmail.com>

* Fixed problem while updating display names. Changed a few method signature as requested in pr. Applied changes of pr.

* Fixed problems after merging with upstream, fixed problem with player team color on join.

* Update src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/ScoreboardManager.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/ScoreboardManager.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/ScoreboardManager.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/config/Formatter.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/player/TabPlayer.java

Co-authored-by: William <will27528@gmail.com>

* Fix username replacement in scoreboard and code typo

This commit resolves two issues. Firstly, changed the variable that we split the nametag on in `ScoreboardManager` from a hardcoded string to the player's specific username. This rectifies an issue where incorrect splitting occurred if the username wasn't exactly "%username%". Secondly, fixed a miswritten method call in `Formatter` from '..legacySection()' to '.legacySection()', correcting a syntax error. Lastly, removed superfluous replacement in `TabPlayer's` getNametag method as it was already handled in `ScoreboardManager`.

---------

Co-authored-by: William <will27528@gmail.com>
2023-09-22 17:57:51 +01:00
AlexDev_
8ae25521dd
Fix encode error on 1.20.1 (#90)
* Added test debug

* Fix issue https://github.com/WiIIiam278/Velocitab/issues/89

* Removed ViaVersion problem message

* Re-Added final keyword on plugin parameter in UpdateTeamsPacket
2023-09-16 10:53:50 +01:00
Nikita Obrekht
90a26f15eb
Add sorting by server group (#88)
* Add sorting by group order and group name

* Fix sorting by server group

* Use order of groups instead of config option

* Remove redundant getServerGroup in SERVER_GROUP

* Update Sorting in docs
2023-09-15 17:54:25 +01:00
William278
c1682aeda9 Update about menu, minor code cleanup 2023-09-13 10:06:44 +03:00
dependabot[bot]
02dca8ba3f
Bump net.william278:PAPIProxyBridge from 1.3 to 1.4 (#85)
Bumps net.william278:PAPIProxyBridge from 1.3 to 1.4.

---
updated-dependencies:
- dependency-name: net.william278:PAPIProxyBridge
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 22:34:13 +03:00
AlexDev_
8d654d6b26
Added support for 1.8.x and 1.12.2 clients (#87)
* Added support for 1.12.2 players

* Added support for 1.8.x & fixed code style

* Moved VersionManager inside ScoreboardManager

* Code refactor

* Update src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java

Co-authored-by: William <will27528@gmail.com>

* Update src/main/java/net/william278/velocitab/packet/ProtocolAbstractAdapter.java

Co-authored-by: William <will27528@gmail.com>

* Added requested changes

* Code refactoring

---------

Co-authored-by: William <will27528@gmail.com>
2023-09-12 17:30:55 +03:00
William
1544e302b2
Add exceptionflug maven repo for distributing velocity-proxy
`maven.elytrium.net` is hosted in Russia and is extremely flakey nowerdays it seems.
2023-08-27 17:07:58 +01:00
AlexDev_
8349d7eb51
Added function to unregister packets on plugin disable. [Dev System Only] (#83)
* Added function to unregister packets on plugin disable.

* Update src/main/java/net/william278/velocitab/packet/PacketRegistration.java

Co-authored-by: William <will27528@gmail.com>

* Added null check + changed style

* Update src/main/java/net/william278/velocitab/Velocitab.java

Co-authored-by: William <will27528@gmail.com>

* Fix indentation problem

---------

Co-authored-by: William <will27528@gmail.com>
2023-08-27 17:06:43 +01:00
dependabot[bot]
962f54ea27
Bump io.netty:netty-codec-http from 4.1.96.Final to 4.1.97.Final (#82)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.96.Final to 4.1.97.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.96.Final...netty-4.1.97.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-23 18:35:50 +01:00
dependabot[bot]
90a30ea37b
Bump net.william278:annotaml from 2.0.5 to 2.0.7 (#79)
Bumps net.william278:annotaml from 2.0.5 to 2.0.7.

---
updated-dependencies:
- dependency-name: net.william278:annotaml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-19 13:55:30 +01:00
Apehum
14482fd43e
Handle DisconnectEvent only if login status is SUCCESSFUL_LOGIN (#80) 2023-08-19 13:55:22 +01:00
dependabot[bot]
97b3bf5deb
Bump net.william278:PAPIProxyBridge from 1.2.2 to 1.3 (#77)
Bumps net.william278:PAPIProxyBridge from 1.2.2 to 1.3.

---
updated-dependencies:
- dependency-name: net.william278:PAPIProxyBridge
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-05 10:29:31 +01:00
dependabot[bot]
936e1b1179
Bump io.netty:netty-codec-http from 4.1.94.Final to 4.1.96.Final (#75)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.94.Final to 4.1.96.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.94.Final...netty-4.1.96.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-27 19:37:33 +01:00
dependabot[bot]
70500b0ece
Bump net.william278:annotaml from 2.0.2 to 2.0.5 (#74)
Bumps net.william278:annotaml from 2.0.2 to 2.0.5.

---
updated-dependencies:
- dependency-name: net.william278:annotaml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-27 19:37:25 +01:00
William
0ca8d5185b
Fix wrong comment in config file 2023-07-25 12:27:38 +01:00
dependabot[bot]
37b60fa034
Bump io.netty:netty-codec-http from 4.1.93.Final to 4.1.94.Final (#67)
Bumps [io.netty:netty-codec-http](https://github.com/netty/netty) from 4.1.93.Final to 4.1.94.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.93.Final...netty-4.1.94.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-codec-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-23 15:10:18 +01:00
dependabot[bot]
8e239b9c72
Bump net.william278:PAPIProxyBridge from 1.2.1 to 1.2.2 (#66)
Bumps net.william278:PAPIProxyBridge from 1.2.1 to 1.2.2.

---
updated-dependencies:
- dependency-name: net.william278:PAPIProxyBridge
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-23 15:10:10 +01:00
William
084cdec697
docs: Update Config-File.md page 2023-06-15 21:45:58 +01:00
dependabot[bot]
8ba308fc4e
Bump net.william278:PAPIProxyBridge from 1.2 to 1.2.1 (#65)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 19:43:52 +01:00
William
cbfe0323cb
[ci skip] Mark compatible with 1.20.1 2023-06-12 18:49:07 +01:00
William
71efba9139
Fix wrong PAPI cache time 2023-06-12 14:25:18 +01:00
William
4a0895cc8b
Add a bit more validation to the PAPI cache time option 2023-06-12 13:17:01 +01:00
William
a55eda8e52
Add option to configure the PAPIProxyBridge cache time 2023-06-12 13:16:12 +01:00
William
07fd9c306a
Add role_display_name placeholder, add docs for sorting 2023-06-12 12:02:33 +01:00
William
4c2735337a
[ci skip] Use new repo for annotaml 2023-06-07 20:44:37 +01:00
William
48251da94f
Merge remote-tracking branch 'origin/master' 2023-06-07 20:37:33 +01:00
William
a5ecf7c637
Bump to 1.4.1, indicate support for 1.20 2023-06-07 20:37:18 +01:00
William
83623eead8
docs: Fix broken link 2023-06-07 09:05:54 +01:00
William
9c7b8ac3df
docs: Add custom logos to homepage 2023-05-29 14:18:08 +01:00
William
aaae3216c8
docs: Add custom logos to sidebar 2023-05-29 14:17:51 +01:00
William
7a1cc640b3
Create Custom-Logos.md
docs: Add docs for displaying custom logos
2023-05-29 14:17:05 +01:00
William
e0fea30feb
docs: Add clarity to update rate part of Animations page 2023-05-29 13:30:54 +01:00
William
49f6f61ba6
docs: Tweak language in Formatting 2023-05-29 13:27:17 +01:00
William
eb36002f36
Fix a few issues in Setup docs 2023-05-29 13:26:15 +01:00
William
04e51a5b64
Bump to v1.4.1 2023-05-29 13:22:23 +01:00
dependabot[bot]
c734a87100
Bump org.projectlombok:lombok from 1.18.26 to 1.18.28 (#59)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-29 13:21:46 +01:00
dependabot[bot]
6f6ba3b0bb
Bump io.netty:netty-codec-http from 4.1.92.Final to 4.1.93.Final (#60)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-29 13:21:40 +01:00
dependabot[bot]
ab11f18e60
Bump net.william278:DesertWell from 2.0.2 to 2.0.4 (#55)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-06 15:49:22 +01:00
William
8ba66e7b56
docs: Update Formatting documentation 2023-05-06 15:49:14 +01:00
dependabot[bot]
1f09621e9a
Bump io.netty:netty-codec-http from 4.1.91.Final to 4.1.92.Final (#49)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 21:00:58 +01:00
dependabot[bot]
f75cdca180
Bump org.ajoberstar.grgit from 5.0.0 to 5.2.0 (#48)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 21:00:51 +01:00
ironboundred
3c1a980e25
set minimal update rate (#52) 2023-05-01 20:50:59 +01:00
William
c5b09590c7
Tweak hangar channel config 2023-04-23 18:13:07 +01:00
William
772e5a5f0d
Use game versions for hangar, update title 2023-04-23 18:07:16 +01:00
William
401b438a1a
Test publishing to hangar with mc-publish 2023-04-23 16:44:34 +01:00
William
6e5976bc68
[ci skip] Add link to Hangar 2023-04-22 23:11:45 +01:00
William
8e7ba474ae
Remove redundant removeIf log when a player cannot be removed 2023-04-21 21:48:56 +01:00
William
f773fc1a39
docs: Add footer 2023-04-21 19:00:08 +01:00
William
6995df1995
docs: Add contribution invitation 2023-04-21 15:06:48 +01:00
William
65da8556e7
docs: Clarify config file 2023-04-21 15:04:03 +01:00
William
5fd9ad9ecd
Give write permissions 2023-04-21 15:01:03 +01:00
William
5f090b9f77
docs: Add links to Setup.md 2023-04-21 14:39:51 +01:00
William
83eeba89d3
docs: Update workflow 2023-04-21 14:37:35 +01:00
William
0cee3556f1
docs: Update Placeholders.md, tweak script 2023-04-21 14:28:46 +01:00
William
ecf0fdfe15
docs: Update Animations.md 2023-04-21 14:26:35 +01:00
William
a14ee0f7dc
Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.github/workflows/update_docs.yml
2023-04-21 14:24:24 +01:00
William
46b93203f7
docs: Tweak script 2023-04-21 14:23:58 +01:00
William
bed49aef64
docs: Update update_docs.yml 2023-04-21 14:19:38 +01:00
William
380335b14d
docs: Tweak docs script 2023-04-21 14:14:25 +01:00
William
97d872501e
docs: Update CI workflow 2023-04-21 14:12:32 +01:00
William
771bce99ec
docs: Update workflow 2023-04-21 14:11:34 +01:00
William
c9d150a11d
docs: Update workflow, add link to config file from setup 2023-04-21 14:02:59 +01:00
William
1926150717
Update read/write access perms for wiki 2023-04-21 13:59:43 +01:00
William
e98ffaf4a6
Add docs to main repo, docs update action 2023-04-21 13:57:24 +01:00
Adrian
548e47f85a
fix: Fixed NullPointerException in Player Join Listener (#47) 2023-04-21 09:30:02 +01:00
William
e422bf0840
Add plugin metrics and update checker (#46) 2023-04-21 00:22:07 +01:00
William
c48693d865
Bump to 1.4, remove protocolize from build script, update README 2023-04-19 10:37:06 +01:00
Adrian
d3d67cb613
Implement native packets handling and remove Protocolize dependency (#44) 2023-04-19 10:31:37 +01:00
William
4d586d28c3
Remove unneeded log when player remove does not occur 2023-04-18 15:24:57 +01:00
William
3c630dcc50
Remove unnecessary if check 2023-04-18 15:23:37 +01:00
William
5da7da8514
Add new contributors to credits 2023-04-17 21:06:40 +01:00
William
7e349d3393
Add option to disable sorting players, close #39 2023-04-17 20:38:24 +01:00
William
6e67325731
Bump PAPIProxyBridge to v1.2 2023-04-17 20:16:01 +01:00
William
adcdef358b
Update modrinth icon character 2023-04-17 16:30:39 +01:00
ironboundred
7712eaaf15
Check against offline users during permission recalc TAB update (#43) 2023-04-17 16:28:03 +01:00
William
1e3f163f2d
Bump minimessage, fix empty string used for concatenation 2023-04-17 16:06:57 +01:00
William
c6603da50a
Add license header, tweak build scripts 2023-04-17 16:04:53 +01:00
William
97df3812c5
[ci skip] Update README.md 2023-04-17 11:22:08 +01:00
William
2873182a2f
[ci skip] Wrap README header in header tag 2023-04-17 11:13:08 +01:00
ironboundred
dad64098b2
More checkes for offline players (#42) 2023-04-14 17:36:33 +01:00
FreeMonoid
99ce4e3f54
Handle ProtocolizePlayer absence before sending packets (#40) 2023-04-11 00:27:10 +01:00
FreeMonoid
3c7187cca0
Support legacy RGB color codes (#37) 2023-04-06 18:11:00 +01:00
dependabot[bot]
ec0e962761
Bump io.netty:netty-codec-http from 4.1.90.Final to 4.1.91.Final (#36)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-06 01:34:16 +01:00
Adrian
82ffd265a7
Improved Velocitab command using native Brigadier (#34) 2023-04-02 00:40:22 +01:00
ironboundred
b653d568f5
Reload when proxy reload event is fired (#33) 2023-04-02 00:39:54 +01:00
William
80e4aa45ac
[ci skip] Fix heading level in README.md 2023-04-01 00:48:52 +01:00
William
e76b2a0917
[ci skip] Add command to README.md 2023-04-01 00:48:17 +01:00
ironboundred
88d1c4f6cf
Reload and About Commands (#32) 2023-04-01 00:46:56 +01:00
William278
81d4a9a711 Bump to 1.3 2023-03-31 15:09:36 +01:00
William278
8cbaa6f70f Tweak header/footer index incrementation order 2023-03-31 15:09:05 +01:00
William278
18086fffa4 Track header/footer index per-player, clarify configuration, fix #29 2023-03-31 14:43:35 +01:00
William278
4c10d27b6b Cleanup unneccessary annotations 2023-03-31 14:22:00 +01:00
FreeMonoid
ef7e07c59d
Allow setting display name for servers (#30) 2023-03-31 14:21:17 +01:00
ironboundred
f14a92b432
Fix failed Dispatch Packet on server disconnect (#28) 2023-03-30 20:40:22 +01:00
William
95bc4669a8
Merge remote-tracking branch 'origin/master' 2023-03-23 22:15:40 +00:00
William
dbf5509a9a
Add only_list_players_in_same_group option (true by default) 2023-03-23 22:14:31 +00:00
ironboundred
10c8102e59
Add support for header and footer animations (#25) 2023-03-22 18:47:20 +00:00
William278
3001abfa17 Fix version metadata 2023-03-22 16:22:28 +00:00
William278
e1d9cec3f6 Bump shadow to 8.1.1 2023-03-22 16:22:13 +00:00
William278
e5a6e2e051 Release version 1.2.3 2023-03-22 16:21:32 +00:00
William278
c144f79caa Fix compatibility with 1.19.4 2023-03-22 16:21:19 +00:00
dependabot[bot]
74c28c149d
Bump io.netty:netty-codec-http from 4.1.89.Final to 4.1.90.Final (#20)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-19 13:33:33 +00:00
dependabot[bot]
f61dfe9da2
Bump net.kyori:adventure-text-minimessage from 4.12.0 to 4.13.0 (#21)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-19 13:33:27 +00:00
William278
8957fafb7a Bump protocolize to 2.2.6 2023-03-19 13:26:47 +00:00
William278
3549899c90 Release version 1.2.2 2023-03-19 13:16:41 +00:00
William
6007cdddb3
Fix LuckPerms role update not updating actual user in TAB list 2023-03-16 21:11:53 +00:00
William
8a4651fb5f
Fix parsing of enums in sortable element list in config file 2023-03-16 20:00:49 +00:00
William
829ab42e2e
Merge remote-tracking branch 'origin/master' 2023-03-15 17:15:18 +00:00
William
0cc1b54481
Mark 1.19.4 as supported (Protocolize 2.2.6) 2023-03-15 17:15:07 +00:00
William278
28c5398c6b Add sorting by server name, customise sort element order 2023-03-14 14:19:59 +00:00
William
68f17b14a1
[ci skip] Update README 2023-03-13 02:18:17 +00:00
William
29553d45f7
[ci skip] Update README 2023-03-13 02:16:50 +00:00
William
9ba2949899
[ci skip] Update README 2023-03-13 02:12:47 +00:00
William
77604d17e8
[ci skip] Update README.md 2023-03-12 22:34:48 +00:00
William
1b4587bf0b
Tweak unset header/footer values to work with MiniMessage by default 2023-03-12 19:26:36 +00:00
William
ee820967de
Fix double underscore escaping still happening with MiniMessage, refactor formatter 2023-03-12 19:23:48 +00:00
William
0805a3114d
Bump to 1.2.1 2023-03-12 18:18:19 +00:00
William
fb66fb44ff
Merge remote-tracking branch 'origin/master' 2023-03-12 18:15:22 +00:00
William
da1ead367a
Better handling for nullable collision rules 2023-03-12 18:14:57 +00:00
90 changed files with 8083 additions and 809 deletions

View File

@ -2,7 +2,18 @@
version: 2
updates:
- package-ecosystem: "gradle" # See documentation for possible values
directory: "/" # Location of package manifests
# CI workflow action updates
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
commit-message:
prefix: "ci"
# Gradle package updates
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "deps"

130
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,130 @@
# Builds, tests the project with Gradle and publishes to Modrinth & Hangar
name: CI Tests & Publish
on:
push:
branches: [ 'master' ]
paths-ignore:
- 'docs/**'
- 'workflows/**'
- 'README.md'
permissions:
contents: read
checks: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
- name: 'Set up JDK 17 📦'
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v3
with:
arguments: build publish
env:
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: 'Fetch Version Name 📝'
run: |
echo "::set-output name=VERSION_NAME::$(${{github.workspace}}/gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
id: fetch-version
- name: Get Version
run: |
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
- name: 'Publish to William278.net 🚀'
uses: WiIIiam278/bones-publish-action@v1
with:
api-key: ${{ secrets.BONES_API_KEY }}
project: 'velocitab'
channel: 'alpha'
version: ${{ env.version_name }}
changelog: ${{ github.event.head_commit.message }}
distro-names: |
velocity
distro-groups: |
velocity
distro-descriptions: |
Velocity
files: |
target/Velocitab-${{ env.version_name }}.jar
- name: 'Publish to Modrinth & Hangar 🧽'
uses: WiIIiam278/mc-publish@hangar
with:
modrinth-id: Q10irTG0
modrinth-featured: false
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
modrinth-version-type: alpha
hangar-id: William278/Velocitab
hangar-token: ${{ secrets.HANGAR_API_KEY }}
hangar-version-type: Alpha
hangar-game-versions: |
3.4
files: target/Velocitab-*.jar
name: Velocitab v${{ env.version_name }}
version: ${{ env.version_name }}
changelog: ${{ github.event.head_commit.message }}
loaders: |
velocity
dependencies: |
luckperms | suggests | *
papiproxybridge | suggests | *
miniplaceholders | suggests | *
game-versions: |
1.8
1.8.1
1.8.2
1.8.3
1.8.4
1.8.5
1.8.6
1.8.7
1.8.8
1.8.9
1.12.2
1.13
1.13.1
1.13.2
1.14
1.14.1
1.14.2
1.14.3
1.14.4
1.15
1.15.1
1.15.2
1.16
1.16.1
1.16.2
1.16.3
1.16.4
1.16.5
1.17
1.17.1
1.18
1.18.1
1.18.2
1.19
1.19.1
1.19.2
1.19.3
1.19.4
1.20
1.20.1
1.20.2
1.20.3
1.20.4
1.20.5
1.20.6
1.21
1.21.1
1.21.2
1.21.3
1.21.4
java: 17

View File

@ -1,61 +0,0 @@
# Builds, tests the project with Gradle
name: Java CI
on:
push:
branches: [ 'master' ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 16
uses: actions/setup-java@v3
with:
java-version: '16'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: build
- name: Query Version
run: |
echo "::set-output name=VERSION_NAME::$(${{github.workspace}}/gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
id: fetch-version
- name: Get Version
run: |
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
- name: Upload to Modrinth
uses: Kir-Antipov/mc-publish@v3.2
with:
modrinth-id: Q10irTG0
modrinth-featured: false
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
files: target/Velocitab-*.jar
name: Velocitab v${{ env.version_name }}
version: ${{ env.version_name }}
version-type: alpha
changelog: ${{ github.event.head_commit.message }}
loaders: |
velocity
dependencies: |
protocolize | depends | 2.2.5
luckperms | suggests | *
papiproxybridge | suggests | *
miniplaceholders | suggests | *
game-versions: |
1.16.5
1.17.1
1.18.2
1.19.2
1.19.3
java: 16
- name: Upload GitHub Artifact
uses: actions/upload-artifact@v2
with:
name: Velocitab Plugin
path: target/Velocitab-*.jar

View File

@ -1,4 +1,3 @@
# Carry out tests on pull requests
name: PR Tests
on:
@ -7,18 +6,20 @@ on:
permissions:
contents: read
checks: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 16
uses: actions/setup-java@v3
- name: 'Checkout for CI 🛎'
uses: actions/checkout@v4
- name: 'Set up JDK 17 📦'
uses: actions/setup-java@v4
with:
java-version: '16'
java-version: '17'
distribution: 'temurin'
- name: Test Pull Request
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
- name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v3
with:
arguments: build

119
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,119 @@
# Publishes a release to Modrinth and Hangar when a release is published on GitHub.
name: Release Test & Publish
on:
release:
types: [ published ]
permissions:
contents: read
checks: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
- name: 'Set up JDK 17 📦'
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v3
with:
arguments: build publish
env:
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: 'Publish to William278.net 🚀'
uses: WiIIiam278/bones-publish-action@v1
with:
api-key: ${{ secrets.BONES_API_KEY }}
project: 'velocitab'
channel: 'release'
version: ${{ github.event.release.tag_name }}
changelog: ${{ github.event.release.body }}
distro-names: |
velocity
distro-groups: |
velocity
distro-descriptions: |
Velocity
files: |
target/Velocitab-${{ github.event.release.tag_name }}.jar
- name: 'Publish to Modrinth & Hangar 🚰'
uses: WiIIiam278/mc-publish@hangar
with:
modrinth-id: Q10irTG0
modrinth-featured: true
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
modrinth-version-type: release
hangar-id: William278/Velocitab
hangar-token: ${{ secrets.HANGAR_API_KEY }}
hangar-version-type: Release
hangar-game-versions: |
3.4
files: target/Velocitab-*.jar
name: Velocitab v${{ github.event.release.tag_name }}
version: ${{ github.event.release.tag_name }}
changelog: ${{ github.event.release.body }}
loaders: |
velocity
dependencies: |
luckperms | suggests | *
papiproxybridge | suggests | *
miniplaceholders | suggests | *
game-versions: |
1.8
1.8.1
1.8.2
1.8.3
1.8.4
1.8.5
1.8.6
1.8.7
1.8.8
1.8.9
1.12.2
1.13
1.13.1
1.13.2
1.14
1.14.1
1.14.2
1.14.3
1.14.4
1.15
1.15.1
1.15.2
1.16
1.16.1
1.16.2
1.16.3
1.16.4
1.16.5
1.17
1.17.1
1.18
1.18.1
1.18.2
1.19
1.19.1
1.19.2
1.19.3
1.19.4
1.20
1.20.1
1.20.2
1.20.3
1.20.4
1.20.5
1.20.6
1.21
1.21.1
1.21.2
1.21.3
1.21.4
java: 17

25
.github/workflows/update_docs.yml vendored Normal file
View File

@ -0,0 +1,25 @@
# Update the GitHub Wiki documentation when a push is made to docs/
name: Update Docs
on:
push:
branches: [ 'master' ]
paths:
- 'docs/**'
- 'workflows/**'
tags-ignore:
- '*'
permissions:
contents: write
jobs:
deploy-wiki:
runs-on: ubuntu-latest
steps:
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
- name: 'Push Docs to Github Wiki 📄️'
uses: Andrew-Chen-Wang/github-wiki-action@v4
with:
path: 'docs'

16
HEADER Normal file
View File

@ -0,0 +1,16 @@
This file is part of Velocitab, licensed under the Apache License 2.0.
Copyright (c) William278 <will27528@gmail.com>
Copyright (c) contributors
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.

View File

@ -1,49 +1,63 @@
<!--suppress ALL -->
<p align="center">
<img src="images/banner.png" alt="Velocitab" />
<a href="https://modrinth.com/plugin/velocitab">
<img src="https://img.shields.io/modrinth/v/velocitab?color=%231bd96a&label=modrinth&logo=modrinth&logoColor=%23fffff" />
</a>
<a href="https://github.com/WiIIiam278/Velocitab/actions/workflows/java_ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/Velocitab/java_ci.yml?branch=master&logo=github"/>
<a href="https://github.com/WiIIiam278/Velocitab/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/Velocitab/ci.yml?branch=master&logo=github"/>
</a>
<a href="https://repo.william278.net/#/releases/net/william278/velocitab/">
<img src="https://repo.william278.net/api/badge/latest/releases/net/william278/velocitab?color=00fb9a&name=Maven&prefix=v" />
</a>
<a href="https://discord.gg/tVYhJfyDWG">
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
</a>
</a>
<br/>
<b>
<a href="https://modrinth.com/plugin/velocitab">Modrinth</a>
</b>
<b>
<a href="https://william278.net/docs/velocitab/setup">Setup</a>
</b>
<b>
<a href="https://william278.net/docs/velocitab/">Docs</a>
</b>
<b>
<a href="https://github.com/WiIIiam278/Velocitab/issues">Issues</a>
</b>
</p>
<br/>
A super-simple Velocity TAB menu plugin that uses scoreboard team client-bound packets to actually sort player lists without the need for a backend plugin.
**Velocitab** is a super-simple Velocity TAB menu plugin that uses scoreboard team client-bound packets to actually sort player lists without the need for a backend plugin. Supporting modern RGB formatting, animations, comprehensive placeholder support and defining multiple TAB menus for different groups of servers, Velocitab is a versatile plugin, useful for any Velocity-based proxy network.
![Showcase of different TAB menus made with Velocitab.png](images/showcase.png)
## Features
**⭐ Flexible list sorting** &mdash; Customizable TAB [list sorting](https://william278.net/docs/velocitab/sorting) based on user role, server, placeholder, and more.
**⭐ Versatile formatting support** &mdash; Make your TAB list beautiful with full RGB color support, supporting MiniMessage, MineDown and legacy [formatting modes](https://william278.net/docs/velocitab/formatting).
**⭐ Multiple TAB menus for different servers** &mdash; Create [server groups](https://william278.net/docs/velocitab/server-groups) and configure different TAB lists to show for each group!
**⭐ Animations support** &mdash; Add extra flair to your TAB list or display additional information by creating pretty looking [animations](https://william278.net/docs/velocitab/animations).
**⭐ Player nametags** &mdash; Customize how over-the-head [nametags](https://william278.net/docs/velocitab/nametags) look to help players stand out in-game.
**⭐ Full placeholder support** &mdash; Comes with a robust set of built-in [placeholders](https://william278.net/docs/velocitab/placeholders), MiniPlaceholders support, as well as PAPIProxyBridge for PlaceholderAPI support
**Ready?** [Get started in a new TAB&hellip;](https://william278.net/docs/velocitab/setup)
## Setup
Requires [Protocolize](https://github.com/Exceptionflug/protocolize) v2.2.5 to be installed on your proxy. [LuckPerms](https://luckperms.net) is also strongly recommended for prefix/suffix/role (and sorting) support.
We suggest installing [LuckPerms](https://luckperms.net) on your Velocity proxy and backend (Spigot, Paper, Fabric, etc.) servers for prefix/suffix formatting right out the box.
Simply download the latest release and place it in your Velocity plugins folder (along with Protocolize).
1. Turn off your Velocity proxy server
2. [Download](https://github.com/WiIIiam278/Velocitab/releases/latest) and place the plugin jar file in the `/plugins/` folder of your Velocity proxy server.
3. Start your Velocity proxy, and allow the plugin to generate its config file
4. Edit the [`config.yml` file](https://william278.net/docs/velocitab/config-file) to your liking
5. Restart your Velocity proxy again
## Configuration
Velocitab has a simple config file that lets you define a header, footer and format for the player list, as well as a set of servers you do not want to have the custom player list appear on (i.e. if you want certain backend servers to manage the tab list instead of the proxy).
### Formatting
Formatting is handled through [MineDown](https://github.com/Phoenix616/MineDown), supporting the full range of RGB colors and gradients. If you use my other plugin using MineDown, HuskChat, you'll feel right at home.
### Placeholders
You can include placeholders in the header, footer and player name format of the TAB list. The following placeholders are supported:
| Placeholder | Description | Example |
|--------------------------|-----------------------------------------------|--------------------|
| `%players_online%` | Players online on the proxy | `6` |
| `%max_players_online%` | Player capacity of the proxy | `500` |
| `%local_players_online%` | Players online on the server the player is on | `3` |
| `%current_date%` | Current real-world date of the server | `24 Feb 2023` |
| `%current_time%` | Current real-world time of the server | `21:45:32` |
| `%username%` | The player's username | `William278` |
| `%server%` | Name of the server the player is on | `alpha` |
| `%ping%` | Ping of the player (in ms) | `6` |
| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` |
| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` |
| `%role%` | The player's primary LuckPerms group | `admin` |
| `%debug_team_name%` | Internal team value, used for list sorting | `1_alpha_William2` |
Need to make a quick config change? You can use the in-game `/velocitab reload` (permission: `velocitab.command.reload`) command, though we recommend restarting your proxy server for any major changes.
## Building
To build Velocitab, simply run the following in the root of the repository:
To build Velocitab, run the following in the root of the repository:
```bash
./gradlew clean build
```
@ -57,8 +71,9 @@ Velocitab is licensed under the Apache 2.0 license.
## Links
* **[Website](https://william278.net/project/velocitab)** — Visit my website!
* **[Docs](https://william278.net/docs/velocitab)** — Read the plugin docs!
* **[Modrinth](https://modrinth.com/plugin/velocitab)** — View the plugin Modrinth page (Also: [Hangar](https://hangar.papermc.io/William278/Velocitab))
* **[Issues](https://github.com/WiIIiam278/Velocitab/issues)** — File a bug report or feature request
* **[Discord](https://discord.com/invite/tVYhJfyDWG)** — Get support, ask questions!
* **[GitHub](https://github.com/WiIIiam278/Velocitab)** — Check out the plugin source code!
---
&copy; [William278](https://william278.net/), 2023. Licensed under the Apache-2.0 License.
&copy; [William278](https://william278.net/), 2024. Licensed under the Apache-2.0 License.

View File

@ -1,87 +1,192 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'com.github.johnrengelman.shadow' version '8.1.0'
id 'org.ajoberstar.grgit' version '5.0.0'
id 'xyz.jpenilla.run-velocity' version '2.3.1'
id 'com.gradleup.shadow' version '8.3.5'
id 'org.cadixdev.licenser' version '0.6.1'
id 'org.ajoberstar.grgit' version '5.3.0'
id 'maven-publish'
id 'java'
}
group 'net.william278'
version "$ext.plugin_version-${versionMetadata()}"
version "$ext.plugin_version${versionMetadata()}"
description "$ext.plugin_description"
defaultTasks 'licenseFormat', 'build'
ext {
set 'version', version.toString()
set 'description', description.toString()
set 'velocity_api_version', velocity_api_version.toString()
set 'velocity_minimum_build', velocity_minimum_build.toString()
set 'papi_proxy_bridge_minimum_version', papi_proxy_bridge_minimum_version.toString()
}
repositories {
mavenCentral()
maven { url = 'https://repo.william278.net/velocity/' }
maven { url = 'https://repo.william278.net/releases/' }
maven { url = 'https://repo.william278.net/snapshots/' }
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://jitpack.io/' }
maven { url = 'https://repo.minebench.de/' }
maven { url = 'https://mvn.exceptionflug.de/repository/exceptionflug-public/' }
maven { url = 'https://jitpack.io' }
}
dependencies {
compileOnly 'com.velocitypowered:velocity-api:3.1.1'
compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT"
compileOnly "com.velocitypowered:velocity-proxy:${velocity_api_version}-SNAPSHOT"
compileOnly "net.william278:papiproxybridge:${papi_proxy_bridge_minimum_version}"
compileOnly 'io.netty:netty-codec-http:4.1.115.Final'
compileOnly 'org.projectlombok:lombok:1.18.36'
compileOnly 'net.luckperms:api:5.4'
compileOnly 'dev.simplix:protocolize-api:2.2.5'
compileOnly 'io.netty:netty-codec-http:4.1.89.Final'
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.0.0'
compileOnly 'net.william278:PAPIProxyBridge:1.0'
compileOnly 'org.projectlombok:lombok:1.18.26'
compileOnly 'net.kyori:adventure-text-minimessage:4.12.0'
compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.2.3'
compileOnly 'it.unimi.dsi:fastutil:8.5.15'
compileOnly 'net.kyori:adventure-nbt:4.17.0'
implementation 'org.apache.commons:commons-text:1.10.0'
implementation 'net.william278:Annotaml:2.0.1'
implementation 'dev.dejvokep:boosted-yaml:1.3.1'
implementation 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
implementation 'org.apache.commons:commons-text:1.12.0'
implementation 'net.william278:desertwell:2.0.4'
implementation 'net.william278:minedown:1.8.2'
implementation 'org.bstats:bstats-velocity:3.1.0'
implementation 'de.exlll:configlib-yaml:4.5.0'
implementation 'org.apache.commons:commons-jexl3:3.4.0'
implementation 'net.jodah:expiringmap:0.5.11'
annotationProcessor 'org.projectlombok:lombok:1.18.26'
annotationProcessor 'org.projectlombok:lombok:1.18.36'
}
processResources {
def tokenMap = rootProject.ext.properties
tokenMap.merge("grgit",'',(s, s2) -> s)
filesMatching(['**/*.json', '**/*.yml']) {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: rootProject.ext.properties
tokens: tokenMap
}
}
logger.lifecycle("Building Velocitab ${version} by William278")
license {
header = rootProject.file('HEADER')
include '**/*.java'
newLine = true
}
logger.lifecycle("Building Velocitab ${version} by William278")
version rootProject.version
archivesBaseName = "${rootProject.name}"
compileJava.options.encoding = 'UTF-8'
javadoc.options.encoding = 'UTF-8'
javadoc.options.addStringOption('Xdoclint:none', '-quiet')
java {
def javaVersion = JavaVersion.toVersion(javaVersion)
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
withSourcesJar()
withJavadocJar()
}
shadowJar {
relocate 'org.apache.commons.text', 'net.william278.velocitab.libraries.commons.text'
relocate 'org.apache.commons.lang3', 'net.william278.velocitab.libraries.commons.lang3'
relocate 'org.jetbrains', 'net.william278.velocitab.libraries'
relocate 'org.intellij', 'net.william278.velocitab.libraries'
relocate 'de.themoep', 'net.william278.velocitab.libraries'
relocate 'dev.dejvokep.boostedyaml', 'net.william278.velocitab.libraries'
relocate 'net.william278.annotaml', 'net.william278.velocitab.libraries.annotaml'
relocate 'net.william278.desertwell', 'net.william278.velocitab.libraries.desertwell'
relocate 'org.bstats', 'net.william278.velocitab.libraries.bstats'
relocate 'de.exlll.configlib', 'net.william278.velocitab.libraries.configlib'
relocate 'org.snakeyaml', 'net.william278.velocitab.libraries.snakeyaml'
relocate 'org.apache.commons.jexl3', 'net.william278.velocitab.libraries.commons.jexl3'
relocate 'org.apache.commons.logging', 'net.william278.velocitab.libraries.commons.logging'
relocate 'net.jodah.expiringmap', 'net.william278.velocitab.libraries.expiringmap'
dependencies {
//noinspection GroovyAssignabilityCheck
exclude dependency(':slf4j-api')
exclude dependency('org.json:json')
}
destinationDirectory.set(file("$rootDir/target"))
archiveClassifier.set('')
minimize() {
exclude dependency('commons-logging:commons-logging')
}
}
jar.dependsOn shadowJar
clean.delete "$rootDir/target"
publishing {
repositories {
if (System.getenv("RELEASES_MAVEN_USERNAME") != null) {
maven {
name = "william278-releases"
url = "https://repo.william278.net/releases"
credentials {
username = System.getenv("RELEASES_MAVEN_USERNAME")
password = System.getenv("RELEASES_MAVEN_PASSWORD")
}
authentication {
basic(BasicAuthentication)
}
}
}
if (System.getenv("SNAPSHOTS_MAVEN_USERNAME") != null) {
maven {
name = "william278-snapshots"
url = "https://repo.william278.net/snapshots"
credentials {
username = System.getenv("SNAPSHOTS_MAVEN_USERNAME")
password = System.getenv("SNAPSHOTS_MAVEN_PASSWORD")
}
authentication {
basic(BasicAuthentication)
}
}
}
}
publications {
mavenJava(MavenPublication) {
groupId = 'net.william278'
artifactId = 'velocitab'
version = "$rootProject.version"
artifact shadowJar
artifact javadocJar
artifact sourcesJar
}
}
}
tasks {
var papi = papi_proxy_bridge_minimum_version
runVelocity {
velocityVersion("${velocity_api_version}-SNAPSHOT")
downloadPlugins {
github ("WiIIiam278", "PAPIProxyBridge", "1.7.1", "PAPIProxyBridge-Velocity-1.7.1.jar")
modrinth ("miniplaceholders", "2.2.4")
}
}
}
@SuppressWarnings('GrMethodMayBeStatic')
def versionMetadata() {
// Get if there is a tag for this commit
// Require grgit
if (grgit == null) {
return '-unknown'
}
// If unclean, return the last commit hash with -indev
if (!grgit.status().clean) {
return '-' + grgit.head().abbreviatedId + '-indev'
}
// Otherwise if this matches a tag, return nothing
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
if (tag != null) {
return ''
}
// Otherwise, get the last commit hash and if it's a clean head
if (grgit == null) {
return System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
}
return grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
return '-' + grgit.head().abbreviatedId
}

78
docs/API-Examples.md Normal file
View File

@ -0,0 +1,78 @@
Velocitab provides an API for vanishing (hiding) and modifying the names of players as they appear in the TAB list for other players.
This page assumes you have read the general [[API]] introduction and that you have both imported Velocitab into your project, added it as a dependency and having an instance of `VelocitabAPI` available. For the following examples, an instance called `velocitabAPI` was used.
## 1. Vanishing/Un-vanishing a player
### 1.1 Vanishing a player
Use `VelocitabAPI#vanishPlayer` to vanish a player. This method takes a Velocity `Player` as a parameter.
This will hide a user from all TAB lists (they will not be shown). Note this will remove them at a packet level; Vanish plugins should use this API feature as a utility that forms part of their Vanish implementation.
Be sure to not remove the entry from TabList with Velocity API or direct packet as the packet would be sent twice and could cause a client-side bug.
This won't send an EntityRemovePacket so your vanish plugin should send it. On a backend server you can just use Player#hidePlayer and Player#showPlayer.
<details>
<summary>Example &mdash; Vanishing a player</summary>
```java
// Vanishing a proxy Player
velocitabAPI.vanishPlayer(player);
```
</details>
### 1.2 Un-vanishing a player
Use `VelocitabAPI#unVanishPlayer` to un-vanish a player. This method takes a Velocity `Player` as a parameter.
This will allow the user to be shown in all TAB lists again.
<details>
<summary>Example &mdash; Un-vanishing a player</summary>
```java
// Un-vanishing a proxy Player
velocitabAPI.unVanishPlayer(player);
```
</details>
### 1.3 Providing a Vanish Integration
You can provide a Vanish integration to provide a managed class to Vanish/Unvanish a player through the `VelocitabAPI#setVanishIntegration` instance.
## 2. Modifying a player's name
You can set a custom name for a player that will be displayed in `%name%` placeholders in the TAB list. This can be used to display a player's nickname, for example. This is done through `VelocitabAPI#setCustomPlayerName`, which accepts a Velocity `Player` and a `String` custom name.
This won't change the player's name in nametags and name list when you press T (key to open chat) and then press tab.
<details>
<summary>Example &mdash; Setting a custom name for a player</summary>
```java
// Setting a custom name for a proxy Player
velocitabAPI.setCustomPlayerName(player, "CustomName");
```
</details>
You can also use `VelocitabAPI#getCustomPlayerName` which accepts a Velocity `Player`, to get player's custom name, wrapped in an `Optional<String>` that will return the String of the player's custom name if one has been set (otherwise `Optional#empty`)
<details>
<summary>Example &mdash; Getting a player's custom name</summary>
```java
// Getting a player's custom name
Optional<String> customName = velocitabAPI.getCustomPlayerName(player);
```
</details>
## 3. Listening to PlayerAddedToTabEvent
You can listen to `PlayerAddedToTabEvent` to get notified when a player is added to a group TabList.
<details>
<summary>Example &mdash; Listening to PlayerAddedToTabEvent</summary>
```java
@Subscribe
public void onPlayerAddedToTab(PlayerAddedToTabEvent event) {
VelocitabAPI velocitabAPI = VelocitabAPI.getInstance();
velocitabAPI.setCustomPlayerName(event.player().getPlayer(), "CustomName");
}
```
</details>

158
docs/API.md Normal file
View File

@ -0,0 +1,158 @@
The Velocitab API provides methods for vanishing ("hiding") and modifying usernames on the TAB list.
The API is distributed on Maven through [repo.william278.net](https://repo.william278.net/#/releases/net/william278/velocitab/) and can be included in any Maven, Gradle, etc. project. JavaDocs are [available here](https://repo.william278.net/javadoc/releases/net/william278/velocitab/latest).
Velocitab also provides a plugin message API, which is documented in the [[Plugin Message API Examples]] page.
## Compatibility
[![Maven](https://repo.william278.net/api/badge/latest/releases/net/william278/velocitab?color=00fb9a&name=Maven&prefix=v)](https://repo.william278.net/#/releases/net/william278/velocitab/)
The Velocitab API shares version numbering with the plugin itself for consistency and convenience. Please note minor and patch plugin releases may make API additions and deprecations, but will not introduce breaking changes without notice.
| API Version | Velocitab Versions | Supported |
|:-----------:|:----------------------:|:---------:|
| v1.x | _v1.5.2&mdash;Current_ | ✅ |
## Table of contents
1. Adding the API to your project
2. Adding Velocitab as a dependency
3. Next steps
## API Introduction
### 1.1 Setup with Maven
<details>
<summary>Maven setup information</summary>
Add the repository to your `pom.xml` as per below. You can alternatively specify `/snapshots` for the repository containing the latest development builds (not recommended).
```xml
<repositories>
<repository>
<id>william278.net</id>
<url>https://repo.william278.net/releases</url>
</repository>
</repositories>
```
Add the dependency to your `pom.xml` as per below. Replace `VERSION` with the latest version of Velocitab (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/Velocitab?color=%23282828&label=%20&style=flat-square)
```xml
<dependency>
<groupId>net.william278</groupId>
<artifactId>velocitab</artifactId>
<version>VERSION</version>
<scope>provided</scope>
</dependency>
```
</details>
### 1.2 Setup with Gradle
<details>
<summary>Gradle setup information</summary>
Add the dependency as per below to your `build.gradle`. You can alternatively specify `/snapshots` for the repository containing the latest development builds (not recommended).
```groovy
allprojects {
repositories {
maven { url 'https://repo.william278.net/releases' }
}
}
```
Add the dependency as per below. Replace `VERSION` with the latest version of Velocitab (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/Velocitab?color=%23282828&label=%20&style=flat-square)
```groovy
dependencies {
compileOnly 'net.william278:velocitab:VERSION'
}
```
</details>
### 2. Adding Velocitab as a dependency
Add Velocitab as a dependency in your main class annotation:
```java
@Plugin(
id = "myplugin",
name = "My Plugin",
version = "0.1.0",
dependencies = {
@Dependency(id = "velocitab", optional = true)
}
)
public class MyPlugin {
// ...
}
```
<details>
<summary>Alternative method: Adding to `velocity-plugin.json`</summary>
```json
{
"dependencies": [
{
"id": "velocitab",
"optional": true
}
]
}
```
</details>
## 3. Creating a class to interface with the API
- Unless your plugin completely relies on Velocitab, you shouldn't put Velocitab API calls into your main class, otherwise if Velocitab is not installed you'll encounter `ClassNotFoundException`s
```java
public class VelocitabAPIHook {
public VelocitabAPIHook() {
// Ready to do stuff with the API
}
}
```
## 4. Checking if Velocitab is present and creating the hook
- Check to make sure the Velocitab plugin is present before instantiating the API hook class
```java
@Plugin(
id = "myplugin",
name = "My Plugin",
version = "0.1.0",
dependencies = {
@Dependency(id = "velocitab", optional = true)
}
)
public class MyPlugin {
public VelocitabAPIHook velocitabHook;
@Subscribe
public void onProxyInitialization(@NotNull ProxyInitializeEvent event) {
if (event.getProxy().getPluginManager().getPlugin("velocitab").isPresent()) {
velocitabHook = new VelocitabAPIHook();
}
}
}
```
## 5. Getting an instance of the API
- You can now get the API instance by calling `VelocitabAPI#getInstance()`
```java
import net.william278.velocitab.api.BukkitVelocitabAPI;
public class VelocitabAPIHook {
private final VelocitabAPI velocitabApi;
public VelocitabAPIHook() {
this.velocitabApi = VelocitabAPI.getInstance();
}
}
```
### 6. Next steps
Now that you've got everything ready, you can start doing stuff with the Velocitab API!
- [[API Examples]]
- See also: [[Plugin Message API]]

81
docs/Animations.md Normal file
View File

@ -0,0 +1,81 @@
Velocitab lets you create basic animations in the header and footer, which you can combine with some nice [[Formatting]] to create a slick TAB menu for your server. Note you cannot animate player name formats, only headers and footers.
## Creating basic animations
By default, Velocitab headers/footers are static; only containing a single frame of animation and only updating when a user joins/leaves your server, or when permissions are recalcualated by LuckPerms.
### Adding additional frames of animation
To add additional frames of animation to a header format for a [server group](server-groups), add it to the string list for that group. The example below uses the MineDown gradient fade feature to create a simple three-phase animation.
<details>
<summary>Basic header animation (config.yml)</summary>
```yaml
headers:
- '<rainbow>Running Velocitab by William278 & AlexDev_</rainbow>'
- '<rainbow:10>Running Velocitab by William278 & AlexDev_</rainbow>'
- '<rainbow:20>Running Velocitab by William278 & AlexDev_</rainbow>'
```
</details>
### Setting the frame rate
The `header_footer_update_rate` setting in your `tab_groups.yml` (different for each group) file&mdash;set to `0` by default&mdash;controls the length (in milliseconds&dagger;) between your TAB list being updated. On each update, the header or footer format will use the next frame in the list, looping back to the first after the last one has been displayed.
A good starting value for this could be `1000`, which is equivalent to one second. Once you've changed the value, use `/velocitab reload` to update the TAB menu in-game without restarting your proxy. Note the minimum update rate is `200` to avoid excessive network packet traffic, so values between `1`-`199` will be rounded up to `200`. If this value is set to `0` or below (as it is by default), the TAB menu will only update when a player joins or leaves, permissions are recalculated on LuckPerms, or the proxy is reloaded.
&dagger;`1ms = 1/1000th` of a second.
## Example: Rainbow fade
![Example rainbow fade animation GIF](https://user-images.githubusercontent.com/31187453/232607366-35d530dc-fb2a-419b-a345-3cc758baa6df.gif)
Wondering how to make something like the above example? Here's how! This example uses MineDown formatting and its' gradient fade feature to create a convincing rainbow fade that's pleasing on the eyes.
<details>
<summary>Example rainbow fade (config.yml)</summary>
Please note this is not a complete tab_groups file; you will need to add the relevant sections to the correct part in your own Velocitab `tab_groups.yml`.
```yaml
headers:
- '<rainbow>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:1>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:2>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:3>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:4>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:5>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:6>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:7>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:8>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:9>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:10>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:11>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:12>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:13>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:14>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:15>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:16>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:17>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:18>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:19>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:20>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:21>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:22>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:23>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:24>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:25>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:26>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:27>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:28>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:29>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
- '<rainbow:30>Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin</rainbow>'
footers:
- |
\n<gray>For Velocity proxy servers:</gray>
<gradient:#1bd96a:#6cffa9>https://modrinth.com/plugin/velocitab</gradient>
<gradient:#1bd96a:#6cffa9>https://william278.net/project/veloictab</gradient>'
format: '<gradient:#999:#fff>[%server%] &f%username%</gradient>'
header_footer_update_rate: 200
```
In config.yml
```yaml
formatter: MINIMESSAGE
```
</details>

40
docs/Commands.md Normal file
View File

@ -0,0 +1,40 @@
This page contains the usage table and associated permissions for the `/velocitab` command provided by Velocitab.
<table>
<thead>
<tr>
<th colspan="2">Command</th>
<th>Description</th>
<th>Permission</th>
</tr>
</thead>
<tbody>
<!-- /velocitab command -->
<tr>
<td rowspan="5"><code>/velocitab</code></td>
<td><code>/velocitab</code></td>
<td>View & manage plugin system information</td>
<td><code>velocitab.command</code></td>
</tr>
<tr>
<td><code>/velocitab about</code></td>
<td>View information about the plugin</td>
<td><code>velocitab.command.about</code></td>
</tr>
<tr>
<td><code>/velocitab reload</code></td>
<td>Reload the plugin configuration</td>
<td><code>velocitab.command.reload</code></td>
</tr>
<tr>
<td><code>/velocitab update</code></td>
<td>Check for plugin updates</td>
<td><code>velocitab.command.update</code></td>
</tr>
<tr>
<td><code>/velocitab name [value]</code></td>
<td>Change or reset your TAB display name</td>
<td><code>velocitab.command.name</code></td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,40 @@
In order to use these placeholders, install MiniPlaceholders on your Velocity proxy, set the `formatter_type`
to `MINIMESSAGE`, and ensure `enable_miniplaceholders_hook` is set to `true`.
Conditional placeholders allow you to display different values based on certain conditions. The format
is `<velocitab_rel_condition|<condition>|<true>|<false>>`.
Currently, this system is only available for the `format` and `nametag` fields in the tab groups configuration.
## Table of Conditional Placeholders
| Placeholder Example | Description | Example Output |
|--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| `<velocitab_rel_condition:%vault_eco_balance% >= 10:rich:poor>` | Checks if the player's vault balance is greater than or equal to 10. If true, displays "rich", else "poor". | `rich` or `poor` |
| `<velocitab_rel_condition:%player_health% < 10:Low Health:Healthy>` | Checks if the player's health is below 10. If true, displays "Low Health", else "Healthy". | `Low Health` or `Healthy` |
| `<velocitab_rel_condition:%player_ping% <= 50:Good Ping:Bad Ping>` | Checks if the player's ping is 50 or below. If true, displays "Good Ping", else "Bad Ping". | `Good Ping` or `Bad Ping` |
| `<velocitab_rel_condition:%player_level% >= 30:High Level:Low Level>` | Checks if the player's level is 30 or above. If true, displays "High Level", else "Low Level". | `High Level` or `Low Level` |
| `<velocitab_rel_condition:%player_exp% >= 1000:XP Master:XP Novice>` | Checks if the player has 1000 or more experience points. If true, displays "XP Master", else "XP Novice". | `XP Master` or `XP Novice` |
| `<velocitab_rel_condition:"%player_name%" == ''AlexDev_'' OR "%player_name%" == ''William278_'':VelocitabDev:>` | Checks if the player's name is either "AlexDev_" or "William278". If true, displays "Developer", else "NotDev". | `Developer` or `NotDev` |
| `<velocitab_rel_condition:startsWith(''%player_name%'', ''AlexDe''):IsAlex:NotAlex>` | Checks if the player's name starts with "AlexDe". If true, displays "IsAlex", else "NotAlex". | `IsAlex` or `NotAlex` |
| `<velocitab_rel_condition:endsWith(''%player_name%'', ''278''):EndsWith278:DoesNotEndWith278>` | Checks if the player's name ends with "278". If true, displays "EndsWith278", else "DoesNotEndWith278". | `EndsWith278` or `DoesNotEndWith278` |
| `<velocitab_rel_condition:"%player_gamemode%" == ''CREATIVE'':Creative Mode:Not Creative Mode>` | Checks if the player is in creative mode. If true, displays "Creative Mode", else "Not Creative Mode". | `Creative Mode` or `Not Creative Mode` |
| `<velocitab_rel_condition:"%player_world%" == ''nether'':In Nether:Not in Nether>` | Checks if the player is in the Nether. If true, displays "In Nether", else "Not in Nether". | `In Nether` or `Not in Nether` |
| `<velocitab_rel_condition:"%player_biome%" == "DESERT":In Desert:Not in Desert>` | Checks if the player is in a desert biome. If true, displays "In Desert", else "Not in Desert". | `In Desert` or `Not in Desert` |
| `<velocitab_rel_condition:''%player_gamemode%''.contains(''S''):Survival or Spectator:Not Survival or Spectator> ` | Checks if the player is in survival or spectator mode. If true, displays "Survival or Spectator", else "Not Survival or Spectator". | `Survival or Spectator` or `Not Survival or Spectator` |
| `<velocitab_rel_condition:%player_health% == %target_player_health%:Same health:Not same health> ` | Checks if the player's health is the same as the target player's health. If true, displays "Same health", else "Not same health". | `Same health` or `Not same health` |
**Note:** For string comparisons, use double quotes `" "` or single quotes `' '`. For numerical comparisons, quotes are
not needed.
Also if you use `'` for quotes, you need to escape them with `''`. The same applies for `"` and `""`. Example: `''%player_name%''` or `"'%player_name%'"`
In order to use papi placeholders for target you need to use `''%target_player_name%''` in order to get `''%player_name%''` replaced with the target player's name.
If you want to use `:` as a character in the condition or in the true/false value, you need to replace it with `?dp?`. Example: `<velocitab_rel_condition:%player_health% == %target_player_health%:Value?dp?True:Value?dp?False>`.
# Example
If you want to compare audience player's health with target player's health, you can use the following configuration:
```yaml
format: "<velocitab_rel_condition:%player_health% == %target_player_health%:Same health:Not same health>"
```
This is system is based on [JEXL](https://commons.apache.org/proper/commons-jexl/reference/examples.html) expressions.

136
docs/Config-File.md Normal file
View File

@ -0,0 +1,136 @@
This page contains configuration file references for Velocitab.
The config file is located in `/plugins/velocitab/config.yml` and the tab groups file is located in `/plugins/velocitab/tab_groups.yml`
## Example config
<details>
<summary>config.yml</summary>
```yaml
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ Velocitab Config ┃
# ┃ Developed by William278 ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
# ┣╸ Information: https://william278.net/project/velocitab
# ┗╸ Documentation: https://william278.net/docs/velocitab
# Check for updates on startup
check_for_updates: true
# Whether to remove nametag from players' heads if the nametag associated with their server group is empty.
remove_nametags: true
# Whether to disable header and footer if they are empty and let backend servers handle them.
disable_header_footer_if_empty: true
# Which text formatter to use (MINIMESSAGE, MINEDOWN or LEGACY)
formatter: MINIMESSAGE
# All servers which are not in other groups will be put in the fallback group.
# "false" will exclude them from Velocitab.
fallback_enabled: true
# The formats to use for the fallback group.
fallback_group: default
# Whether to show all players from all groups in the TAB list.
show_all_players_from_all_groups: false
# Whether to enable the PAPIProxyBridge hook for PAPI support
enable_papi_hook: true
# How long in seconds to cache PAPI placeholders for, in milliseconds. (0 to disable)
papi_cache_time: 30000
# If you are using MINIMESSAGE formatting, enable this to support MiniPlaceholders in formatting.
enable_mini_placeholders_hook: true
# Whether to send scoreboard teams packets. Required for player list sorting and nametag formatting.
# Turn this off if you're using scoreboard teams on backend servers.
send_scoreboard_packets: true
# If built-in placeholders return a blank string, fallback to Placeholder API equivalents.
# For example, if %prefix% returns a blank string, use %luckperms_prefix%. Requires PAPIProxyBridge.
fallback_to_papi_if_placeholder_blank: false
# Whether to sort players in the TAB list.
sort_players: true
# Remove gamemode spectator effect for other players in the TAB list.
remove_spectator_effect: false
# Whether to enable the Plugin Message API (allows backend plugins to perform certain operations)
enable_plugin_message_api: true
# Whether to force sending tab list packets to all players, even if a packet for that action has already been sent. This could fix issues with some mods.
force_sending_tab_list_packets: false
# A list of URLs that will be sent to display on player pause menus (Minecraft 1.21+ clients only).
# • Labels can be fully custom or built-in (one of 'bug_report', 'community_guidelines', 'support', 'status',
# 'feedback', 'community', 'website', 'forums', 'news', or 'announcements').
# • If you supply a url with a 'bug_report' label, it will be shown if the player is disconnected.
# • Specify a set of server groups each URL should be sent on. Use '*' to show a URL to all groups.
server_links:
- label: '<#00fb9a>About Velocitab</#00fb9a>'
url: 'https://william278.net/project/velocitab'
groups:
- '*'
```
</details>
## Example tab groups
<details>
<summary>tab_groups.yml</summary>
```yaml
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ Velocitab TabGroups ┃
# ┃ Developed by William278 ┃
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
# ┣╸ Information: https://william278.net/project/velocitab
# ┗╸ Documentation: https://william278.net/docs/velocitab
groups:
- name: default
headers:
- <rainbow:!2>Running Velocitab by William278 & AlexDev_</rainbow>
footers:
- <gray>There are currently %players_online%/%max_players_online% players online</gray>
format: <gray>[%server%] %prefix%%username%</gray>
nametag:
prefix: <white>%prefix%</white>
suffix: <white>%suffix%</white>
servers:
- skyblock
- minigames
- survival
- lobby
- prison
- creative
- hub
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
placeholder_replacements:
'%current_date_weekday_en-US%':
- placeholder: Monday
replacement: <red>Monday</red>
- placeholder: Tuesday
replacement: <gold>Tuesday</gold>
- placeholder: Else
replacement: <green>Other day</green>
collisions: false
header_footer_update_rate: 1000
placeholder_update_rate: 1000
only_list_players_in_same_server: false
```
</details>
## Details
### Server Groups
Which formatting and the header/footer to use for a player's TAB list is determined by the group of servers they are currently connected to. See [[Server Groups]] for more information.
### Formatting
Velocitab supports the full range of modern color formatting, including RGB colors and gradients, through either MineDown or MiniMessage syntax. See [[Formatting]] for more information.
## Nametags
As well as updating the text in the TAB menu, Velocitab supports updating player nametags (the text displayed above their heads). See [[Nametags]] for more information.
### Animations
Velocitab supports basic header and footer animations by adding multiple frames of animation and setting the update rate to a value greater than 0.
### Placeholders
You can use various placeholders that will be replaced with values (for example, `%username%`) in your config. Support for PlaceholderAPI is also available through [a bridge library plugin](https://modrinth.com/plugin/papiproxybridge), as is the component-based MiniPlaceholders for users of that plugin with the MiniMessage formatter. See [[Placeholders]] for more information.
### Server Links
For Minecraft 1.21+ clients, Velocitab supports specifying a list of URLs that will be sent to display in the player pause menu. See [[Server Links]] for more information.
### Placeholder Replacements
Velocitab supports replacing values of placeholders with other values. See [[Placeholders Replacements]] for more information.

47
docs/Custom-Logos.md Normal file
View File

@ -0,0 +1,47 @@
If you'd like to display a custom logo or image in your TAB menu, like the example listed on the project listing,&dagger; you will need to make use of a resource pack to retexture Minecraft's default Unicode characters.
<details>
<summary>Example: <i>Mine in Abyss</i> server TAB menu.</summary>
!["Mine In Abyss" server TAB menu, featuring a custom logo](https://github.com/WiIIiam278/Velocitab/assets/31187453/de3f24a7-cdff-4575-9b3d-446fb77a75c4)
</details>
## Creating a resource pack
To do this, you'll need to make a resource pack, and set it to be used on your servers as the Server Resource Pack &ddagger;. To do this:
1. Create a blank resource pack. Consult the [Minecraft Wiki](https://minecraft.fandom.com/wiki/Resource_pack) for making a super basic resource pack layout; this involves creating a `pack.mcmeta` for the correct Minecraft version and placing this inside the root of a directory.
2. Create the directory to store your logo texture: `/assets/server_name/textures/font`. Replace `server_name` with your server name (lower case, no spaces)
3. Place your logo in that directory. The logo can have a maximum size of 256x256 (though you can technically use multiple characters if you want a larger logo; feel free to experiment)
4. Create the `/assets/minecraft/font` directory.
5. Create `default.json`, which we will use to specify a unicode character to replace with your logo texture.
6. Add the following to your `default.json` file, which will replace the (non-existent) Unicode character `\ue238` with your logo. Remember to replace `server_name` with the server name you used earlier:
```json
{
"providers": [
{
"file": "server_name:font/logo.png",
"chars": [
"\ue238"
],
"height": 50,
"ascent": 35,
"type": "bitmap"
}
]
}
```
7. Save the file. Note that later on you may need to tweak the `height` and `ascent` values in the file to suit your logo's size.
8. For testing, install the resource pack locally by placing it in your `~/.minecraft/resourcepacks` folder and select your newly created pack in the Resource Packs menu.
9. Copy the Unicode character from [fileformat.info's unicode browsertest page](https://fileformat.info/info/unicode/char/e238/browsertest.htm) and paste it to chat in-game. Observe and verify that your logo looks correct.
## Applying your pack to your servers and Velocitab
10. In Velocitab, add your logo's Unicode character to the header section of one or more of your TAB menus. You may need to add multiple newlines (`\n`) after the Unicode character to add spacing between the header and player list. Use `/velocitab reload` to get it right.
11. Finally, set the pack as your server resource pack, by uploading it to a service that lets you supply a direct download link into your `server.properties` files. You should do this on all your backend (Spigot/Paper/Folia/Fabric, etc.) servers.
12. Restart everything and fine tune as necessary.
## The entire _Bee Movie_ as an animated TAB header
You totally could render the entire _Bee Movie_ in the TAB menu by retexturing a ton of impossible Unicode characters with this method, yes (at a low resolution, granted). Bare in mind there are limits on maximum server resource pack sizes, so you'd need to do some optimizations. Your Velocitab config file would also be very long. Have fun.
## References
&dagger; &mdash; Courtesy of [https://mineinabyss.com Mine in Abyss].
<br/>
&ddagger; &mdash; Taken from [this helpful Reddit comment](https://www.reddit.com/r/admincraft/comments/llrgty/comment/gnswdcz/) on [/r/admincraft](https://www.reddit.com/r/admincraft/) by [/u/MrPowerGamerBR](https://www.reddit.com/user/MrPowerGamerBR/).

80
docs/Formatting.md Normal file
View File

@ -0,0 +1,80 @@
Velocitab supports the full range of modern color formatting, including RGB colors and gradients. Both MiniMessage (_default_), MineDown and Legacy formatting are supported. To change which formatter is being used, change the `formatter` value in `config.yml` to `MINEDOWN`, `MINIMESSAGE` or `LEGACY` respectively.
Formatting is applied on header, footer and player text for each server group, and is applied after [[Placeholders]] have been inserted.
## MiniMessage syntax reference
MiniMessage is the default formatter type, enabled by setting `formatter` to `MINIMESSAGE` in `config.yml`. See the [MiniMessage Syntax Reference](https://docs.advntr.dev/minimessage/format.html) on the Adventure Docs for how to format text with it. Using MiniMessage as the formatter also allows compatibility for using MiniPlaceholders in text.
## MineDown syntax reference
MineDown formatting can be enabled by setting `formatter` to `MINEDOWN` in `config.yml`. See the [MineDown Syntax Reference](https://github.com/WiIIiam278/MineDown) on GitHub for the specification of how to format text with it.
## Legacy formatting
> **Warning:** The option for legacy formatting is provided only for backwards compatibility with other plugins. Please consider using the MineDown or MiniMessage options instead!
Legacy formatting can be enabled by setting `formatter` to `LEGACY` in `config.yml`. Legacy formatter supports Mojang color and formatting codes (e.g. `&d`, `&l`), Adventure-styled RGB color codes (e.g. `&#a25981`), as well as BungeeCord RGB color codes (e.g. `&x&a&2&5&9&8&1`). See the [LegacyComponentSerializer Syntax Reference](https://docs.advntr.dev/serializer/legacy.html) on the Adventure Docs for more technical details.
## Multi-line strings
In order to have a multi-line string in YAML, you can use the `|-` or `|` syntax. The `|-` syntax will remove last newline character, while the `|` syntax will keep it.
You can also use `\n` to add a newline character in a string.
### Example 1
```yaml
foo: |-
bar 1
bar 2
bar 3
```
is equivalent to
```yaml
foo: "bar 1\nbar 2\nbar 3"
```
### Example 2
```yaml
foo: |
bar 1
bar 2
bar 3
```
is equivalent to
```yaml
foo: "bar 1\nbar 2\nbar 3\n"
```
## List of multi lines strings
> **Note:** The examples above are generic examples on how yaml works in multi line. If you want to use multi line in headers & footers you need to provide a list of multi line strings like in the example below.
```yaml
headers:
- |
<rainbow:!2>Running Velocitab by William278 & AlexDev_</rainbow>
<gray>Second line of the first element</gray>
<yellow>Third line of the first element</yellow>
- |
<rainbow:!4>Running Velocitab by William278 & AlexDev_</rainbow>
<gray>Second line of the second element</gray>
<yellow>Third line of the second element</yellow>
footers:
- <gray>There are currently %players_online%/%max_players_online% players online</gray>
- |
<gray> Test 1 </gray>
<yellow> Test 2 </yellow>
```
In this example the header will switch between the 2 elements, but it will always display all the 3 lines.
The footer in this example will switch between 2 elements, the first one is just a simple string, the second element will display 2 lines since it's a multi line string
<figure style="text-align: center;">
<img src="https://i.imgur.com/YKu1RWi.gif" />
<figcaption>Example of a header and footer with multi line strings</figcaption>
</figure>

33
docs/Home.md Normal file
View File

@ -0,0 +1,33 @@
![Velocitab banner](https://raw.githubusercontent.com/WiIIiam278/Velocitab/master/images/banner.png)
Welcome to the plugin documentation for Velocitab. Velocitab is a super-simple Velocity TAB menu plugin that uses scoreboard team client-bound packets to actually sort player lists by role without the need for a backend plugin.
Please click through to the topic you wish to read about.
## Guides
* 📚 [[Setup]]
* 📄 [[Config File]]
## Documentation
* 🖥️ [[Commands]]
* 👥 [[Server Groups]]
* 🎨 [[Formatting]]
* 📛 [[Nametags]]
* 📊 [[Sorting]]
* ✍️ [[Placeholders]]
* 🔗 [[Relational Placeholders]]
* 🔀 [[Conditional Placeholders]]
* ✨ [[Animations]]
* 🖼️ [[Custom Logos]]
* 🔗 [[Server Links]]
* 📦 [[API]]
* 📝 [[API Examples]]
* 📝 [[Plugin Message API]]
## Links
* 💻 [GitHub](https://github.com/WiIIiam278/Velocitab)
* 📂 [Download](https://modrinth.com/plugin/velocitab)
* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG)
## Contribute to this documentation
This documentation is automatically generated from the source code and markdown files in the [Velocitab GitHub repository](https://github.com/WiIIiam278/Velocitab/tree/master/docs). If you wish to contribute to the documentation, please submit a pull request to the repository.

52
docs/Nametags.md Normal file
View File

@ -0,0 +1,52 @@
Velocitab supports formatting the nametags of players (the text displayed above their heads). This can be used to display a player's rank, group, or other information using placeholders. Please note some limitations apply.
![Nametags being updated by Velocitab in-game](https://raw.githubusercontent.com/WiIIiam278/Velocitab/master/images/nametags.png)
> **Note:** This feature requires sending Update Teams packets. `send_scoreboard_packets` must be enabled in the [`config.yml` file](config-file) for this to work. [More details...](sorting#compatibility-issues)
## Customizing nametags
You can configure nametags per-group using the `nametag` field in `tab_groups.yml`. Each group must have a nametag format associated with it, which will be applied to all players on servers in that group. Nametags are comprised of a prefix and suffix; the player's username will be displayed in-between.
<details>
<summary>Editing nametags (tab_groups.yml)</summary>
```yaml
nametag:
prefix: '&f%prefix%'
suffix: '&f%suffix%'
```
</details>
Only players on servers which are part of groups that specify nametag formats will have their nametag formatted. To disable nametag formatting, remove all groups from the `nametags` section of the config file (leaving it empty).
## Disabling nametags
If you don't want Velocitab to format player nametags, set `prefix` and `suffix` to an empty string in each tab group (e.g., `prefix: ''`). You should also set `remove_nametags` to `true` in the [`config.yml` file](config-file).
<details>
<summary>Remove nametags option (config.yml)</summary>
```yaml
remove_nametags: true
```
</details>
## Named pets
A feature of the game since Minecraft 1.9 is that pets given a nametag inherit their owner's team prefix/suffix/color. This is an intentional game feature, and not a bug. Since Velocitab uses team prefixes/colors for name tag formatting, pets will have their name formatted using their owner's prefix/suffix. A side effect of this, however, is that setting `remove_nametags` to `true` hides nametags on pets.
You can install the [PetNameFix](https://www.spigotmc.org/resources/petnamefix.109466/) plugin if you don't like this bit of Vanilla behaviour.
## Formatting limitations
Nametags must adhere to the following restrictions:
* Nametag prefixes and suffixes can contain full RGB formatting, but the color used in the player's name between the two (effectively, their "Scoreboard Team" color) is limited to the set of legacy color codes.
* Velocitab determines which color to use here based on the last color format used in the configured prefix (displayed before their name), downsampled from RGB if necessary.
* To control this, simply set the prefix format to end with a valid [team color](https://wiki.vg/Text_formatting#Colors) you want to use (e.g. `&4` for dark_red in Minedown formatting).
* Nametags cannot contain newlines (must be on a single line).
## Bypassing formatting restrictions
UnlimitedNameTags is a spigot plugin that lets you create nametags with unlimited length and lines.
You can use rgb colors and gradients in usernames.
In order to use this plugin you need to disable the nametag feature in Velocitab by setting `remove_nametags` to `true` in the [`config.yml` file](config-file) and put an empty nametag (empty prefix & suffix) in the [`tab_groups.yml` file](Server-Groups.md).
You can find the plugin on [BuiltByBit](https://builtbybit.com/resources/unlimitednametags.46172/?ref=38685) and on [SpigotMC](https://www.spigotmc.org/resources/unlimitednametags.117526/).
![UnlimitedNameTags](https://i.imgur.com/VvHtqlY.gif)

View File

@ -0,0 +1,88 @@
Velocitab supports placeholder replacements, which allow you to replace a placeholder with a different value. This is useful for things like changing the text of a date placeholder to a localized version, changing the text of a biome placeholder to a color or you can use a vanish placeholder to show a player's vanish status if the placeholder returns just a boolean (true/false).
## Configuring
Placeholder replacements are configured in the `placeholder_replacements` section of the every Tab Group.
You can specify a list of replacements for a placeholder, and the replacements will be applied in the order they are listed.
The replacements are specified as a list of objects with two properties: `placeholder` and `replacement`.
`placeholder` is the placeholder to replace, and `replacement` is the replacement text.
### Example section
```yaml
placeholder_replacements:
'%current_date_weekday_en-US%':
- placeholder: Monday
replacement: <red>Monday</red>
- placeholder: Tuesday
replacement: <gold>Tuesday</gold>
- placeholder: Else
replacement: <green>Other day</green>
'%player_world_type%':
- placeholder: Overworld
replacement: '<aqua>Overworld</aqua>'
- placeholder: Nether
replacement: '<red>Nether</red>'
- placeholder: End
replacement: '<yellow>End</yellow>'
'%player_biome%':
- placeholder: PLAINS
replacement: <red>Plains</red>
- placeholder: DESERT
replacement: <yellow>Desert</yellow>
- placeholder: RIVER
replacement: <aqua>River</aqua>
```
## Specified cases
### Vanish status
If you want to show a player's vanish status, for example, you can use the `%advancedvanish_is_vanished%` placeholder.
This placeholder returns a boolean value, so you can use it to show a player's vanish status.
For example, if you wanted to show a player's vanish status as a color, you could use the following replacements:
```yaml
placeholder_replacements:
'%advancedvanish_is_vanished%':
- placeholder: Yes
replacement: <red>Vanished</red>
- placeholder: No
replacement: <green>Not vanished</green>
```
### Else clause
If you don't want to specify every possible value for a placeholder, you can use the `ELSE` placeholder.
This placeholder will be replaced with the replacement text of the first replacement that doesn't have a placeholder.
For example, if you wanted to show the current date as a color, you could use the following replacements:
```yaml
placeholder_replacements:
'%current_date_weekday_en-US%':
- placeholder: Monday
replacement: <red>Monday</red>
- placeholder: Tuesday
replacement: <gold>Tuesday</gold>
- placeholder: ELSE
replacement: <green>Other day</green>
```
### Placeholder not present in a server
If you have a group with multiple servers, and you have a placeholder that is not present in one of the servers, you can use the `%<placeholder>%` as a placeholder it will handle the case where the placeholder is not present in the server.
```yaml
placeholder_replacements:
'%huskhomes_homes_count%':
- placeholder: '%huskhomes_homes_count%'
replacement: <red>No homes in this server</red>
```
If you want you can also set the replacement as an empty string, which will be replaced with the empty string.
```yaml
placeholder_replacements:
'%huskhomes_homes_count%':
- placeholder: '%huskhomes_homes_count%'
replacement: ''
```

50
docs/Placeholders.md Normal file
View File

@ -0,0 +1,50 @@
Velocitab supports a number of Placeholders that will be replaced with their respective proper values in-game. In addition to the set of provided default Placeholders, you can make use of PlaceholderAPI and MiniPlaceholder-provided placeholders through special hooks.
## Default placeholders
Placeholders can be included in the header, footer and player name format of the TAB list. The following placeholders are supported out of the box:
| Placeholder | Description | Example |
|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| `%players_online%` | Players online on the proxy | `6` |
| `%max_players_online%` | Player capacity of the proxy | `500` |
| `%local_players_online%` | Players online on the server the player is on | `3` |
| `%group_players_online_(name)%` | Players online on the group provided | `11` |
| `%group_players_online%` | Players online on player's group | `15` |
| `%current_date_day%` | Current day of the month | `14` |
| `%current_date_weekday%` | Current day of the week | `Wednesday` |
| `%current_date_weekday_(tag)%` | Current day of the week ([localized](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags)) `it-IT` as example | `Mercoledì` |
| `%current_date_month%` | Current month of the year | `06` |
| `%current_date_year%` | Current year | `2024` |
| `%current_date%` | Current real-world date of the server | `14/06/2023` |
| `%current_date_(tag)%` | Current real-world date ([localized](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags)) `en-US` as example | `06/14/2023` |
| `%current_time_hour%` | Current hour of the day | `21` |
| `%current_time_minute%` | Current minute of the hour | `45` |
| `%current_time_second%` | Current second of the minute | `32` |
| `%current_time%` | Current real-world time of the server | `21:45:32` |
| `%current_time_(tag)%` | Current real-world time ([localized](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags)) `en-US` as example | `9:45 PM` |
| `%username%` | The player's username | `William278` |
| `%username_lower%` | The player's username, in lowercase | `william278` |
| `%server%` | Name of the server the player is on | `alpha` |
| `%ping%` | Ping of the player (in ms) | `6` |
| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` |
| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` |
| `%role%` | The player's primary LuckPerms group name | `admin` |
| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` |
| `%role_weight%` | Comparable-formatted primary LuckPerms group weight | `100` |
| `%luckperms_meta_(key)%` | Formats a meta key from the user's LuckPerms group | (varies) |
| `%server_group%` | The name of the server group the player is on | `default` |
| `%server_group_index%` | Indexed order of the server group in the list | `0` |
| `%debug_team_name%` | (Debug) Player's team name, used for [[Sorting]] | `1alphaWilliam278` |
**Note:** `(tag)` stands for IETF language tag, used for localization of date and time placeholders. For example, `en-US` for American English, `fr-FR` for French, `it-IT` for Italian, etc.
You can find a list of common primary language subtags [here](https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags).
## PlaceholderAPI support
To use PlaceholderAPI placeholders in Velocitab, install the [PAPIProxyBridge](https://modrinth.com/plugin/papiproxybridge) library plugin on your Velocity proxy and all Minecraft spigot servers on your network, and ensure the PAPI hook option is enabled in your Velocitab [[Config File]]. You can then include PAPI placeholders in your formats as you would any of the default placeholders.
PlaceholderAPI placeholders are cached to reduce plugin message traffic. By default, placeholders are cached for 30 seconds (30000 milliseconds); if you wish to use PAPI placeholders that update more frequently, you can reduce the cache time in the Velocitab config.yml file by adjusting the `papi_cache_time` value.
## MiniPlaceholders support
If you are using MiniMessage [[Formatting]], you can use [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders) with Velocitab for MiniMessage-styled component placeholders provided by other proxy plugins. Install MiniPlaceholders on your Velocity proxy, set the `formatter_type` to `MINIMESSAGE` and ensure `enable_miniplaceholders_hook` is set to `true`
You can also use [Relational Placeholders](Relational-Placeholders).

View File

@ -0,0 +1,34 @@
Velocitab provides a plugin message API, to let you do things with Velocitab from your backend servers.
> **Note:** This feature requires sending Update Teams packets. `send_scoreboard_packets` must be enabled in the [`config.yml` file](config-file) for this to work. [More details...](sorting#compatibility-issues)
>
## Prerequisites
To use the Velocitab plugin message API, you must first turn it on and ensure the following:
* That `enable_plugin_message_api` and `send_scoreboard_packets` is set to `true` in your Velocitab [[config file]]
* That `bungee-plugin-message-channel` is set to `true` in your **Velocity proxy config** TOML (see [Velocity config reference](https://docs.papermc.io/velocity/configuration)).
## API Requests from Backend Plugins
### 1 Changing player's username in the TAB list
To change a player's username in the TAB list, you can send a plugin message on the channel `velocitab:update_custom_name` with a `customName` string, where `customName` is the new desired display name.
<details>
<summary>Example &mdash; Changing player's username in the TAB List</summary>
```java
player.sendPluginMessage(plugin, "velocitab:update_custom_name", "Steve".getBytes());
```
</details>
### 2 Update color of player's nametag
To change player's [nametag](nametags) color, you can send a plugin message on the channel `velocitab:update_team_color` with `teamColor` string, where `teamColor` is the new desired name tag color.
You can only use legacy color codes, for example `a` for green, `b` for aqua, etc. Please note this option overrides the color of the glow potion effect if set. [Check here](https://wiki.vg/index.php?title=Text_formatting&oldid=18983#Colors) for a list of supported colors (The value under the "Code" header on the table is what you need).
<details>
<summary>Example &mdash; Changing player's team color</summary>
```java
player.sendPluginMessage(plugin, "velocitab:update_team_color", "a".getBytes());
```
</details>

View File

@ -0,0 +1,26 @@
In order to use these placeholders, install MiniPlaceholders on your Velocity proxy, set the `formatter_type` to `MINIMESSAGE` and ensure `enable_miniplaceholders_hook` is set to `true`
In all examples target is the one that sees the message, and the audience is the one that is being seen.
Example:
My username is `William278` and I can see in tablist an audience player named `Player1`.
## Table of Placeholders
| Placeholder | Description | Example Usage |
|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------|
| `<velocitab_rel_who-is-seeing>` | Displays the username of the target player, used for debug | `William278` |
| `<velocitab_rel_perm:(permission):(value)>` | Checks if the target player has a specific permission and, if true parse value with the audience player's name. | See below |
| `<velocitab_rel_vanish>` | Checks if the audience player can see the target player, considering the vanish status. | `true` |
## Examples of `<velocitab_rel_perm:(permission):(value)>` Placeholder
**Note:** In the value, you can [Velocitab](Placeholders.md) placeholders or [MiniPlaceholders](https://github.com/MiniPlaceholders/MiniPlaceholders/wiki/Placeholders#proxy-expansion)
| Placeholder Example Usage | Description | Output Example |
|------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|--------------------------|
| `<velocitab_rel_perm:check.server:Target is on %server%!>` | Checks if the target player has the permission 'check.server' and displays the server of the audience player. | `Target is on survival!` |
| `<velocitab_rel_perm:clientcheck:<player_client>>` | Checks if the target player has the permission 'clientcheck' and displays the client of the audience player. | `LunarClient` |
| `<velocitab_rel_perm:pingcheck:<player_ping>>` | Checks if the target player has the permission 'pingcheck' and displays the ping of the audience player. | `23` |

220
docs/Server-Groups.md Normal file
View File

@ -0,0 +1,220 @@
Velocitab supports defining multiple server groups, each providing distinct formatting for players in the TAB list,
alongside unique headers and footers. This is useful if you wish to display different information in TAB depending on
the server a player is on. You can also set formatting to use for [[Nametags]] above players' heads per-group.
## Defining groups
Groups are defined in `tab_groups.yml`, as a list of TabGroup elements.
Every group must have a unique name, and a list of servers to include in the group. You can also define a list of
sorting placeholders to use when sorting players in the TAB list, and a header/footer update rate and placeholder update
rate to use for the group.
## Headers and footers
<details>
<summary>Example of headers and footers</summary>
```yaml
headers:
- '<rainbow>Running Velocitab by William278 & AlexDev_</rainbow>'
footers:
- '<gray>There are currently %players_online%/%max_players_online% players online</gray>'
```
</details>
You can define a list of headers and footers to use for each group. These will be cycled through at the rate defined
by `header_footer_update_rate` in milliseconds. If you only want to use one header/footer, you can define a single
element list. You can also use the `|` character to define a multi-line header/footer. See [[Animations]] for more
information.
## Formats
<details>
<summary>Example of format</summary>
```yaml
format: '<gray>[%server%] %prefix%%username%</gray>'
```
</details>
You can define a format to use for each group. This will be used to format the text of each player in the TAB list.
See [[Formatting]] for more information.
Player formats may only utilize one line.
## Nametags
<details>
<summary>Example of nametag</summary>
```yaml
nametag:
prefix: '<white>%prefix%</white>'
suffix: '<white>%suffix%</white>'
```
</details>
You can define a nametag to use for each group. This will be used to format the text above each player's head.
See [[Nametags]] for more information.
Player nametags may only utilize one line.
## Servers
<details>
<summary>Example of servers</summary>
```yaml
servers:
- lobby
- survival
- creative
- minigames
- skyblock
- prison
- hub
```
</details>
You can define a list of servers to include in each group.
The use of regex patterns is also valid since v1.6.4
<details>
<summary>Example regex pattern</summary>
```yaml
servers:
- ^lobby-\d+$
```
This will include all servers starting with `lobby-` and ending with any integer
</details>
## Sorting placeholders
<details>
<summary>Example of sorting placeholders</summary>
```yaml
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
```
</details>
You can define a list of sorting placeholders to use when sorting players in the TAB list. See [[Sorting]] for more
information.
## Header/footer update rate
<details>
<summary>Example of header/footer update rate</summary>
```yaml
header_footer_update_rate: 1000
```
</details>
You can define a header/footer update rate to use for each group, in milliseconds. This will determine how quickly the
headers and footers will cycle through in the TAB list. The default is 1000 milliseconds (1 second).
## Placeholder update rate
<details>
<summary>Example of placeholder update rate</summary>
```yaml
placeholder_update_rate: 1000
```
</details>
You can define a placeholder update rate to use for each group, in milliseconds. This will determine how quickly the
placeholders in the TAB list will update. The default is 1000 milliseconds (1 second).
## Example tab groups
<details>
<summary>Adding more groups</summary>
```yaml
groups:
- name: lobbies
headers:
- '<rainbow:!2>Running Velocitab by William278 & AlexDev_ on Lobbies!</rainbow>'
footers:
- '<gray>There are currently %players_online%/%max_players_online% players online</gray>'
format: '<gray>[%server%] %prefix%%username%</gray>'
servers:
- lobby
- hub
- minigames
- creative
- survival
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
header_footer_update_rate: 1000
placeholder_update_rate: 1000
- name: creative
headers:
- '<rainbow:!2>Running Velocitab by William278 & AlexDev_ on Creative!</rainbow>'
footers:
- '<gray>There are currently %players_online%/%max_players_online% players online</gray>'
format: '<gray>[%server%] %prefix%%username%</gray>'
servers:
- creative
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
header_footer_update_rate: 1000
placeholder_update_rate: 1000
- name: survival
headers:
- '<rainbow:!2>Running Velocitab by William278 & AlexDev_ on Survival!</rainbow>'
footers:
- '<gray>There are currently %players_online%/%max_players_online% players online</gray>'
format: '<gray>[%server%] %prefix%%username%</gray>'
servers:
- survival
sorting_placeholders:
- '%role_weight%'
- '%username_lower%'
header_footer_update_rate: 1000
placeholder_update_rate: 1000
```
</details>
See [[Placeholders]] for how to use placeholders in these formats, and [[Formatting]] for how to format text with
colors, and see [[Animations]] for how to create basic animations by adding more headers/footers to each group's list.
Note that some formatting limitations apply to nametags &mdash; [[Nametags]] for more information.
## Default group
If a player isn't connected to a server on your network, their TAB menu will be formatted as per the formats defined
by `fallback_group` set in `config.yml`, provided `fallback_enabled` is set to `true`.
If you don't want them to have their TAB handled at all by Velocitab, you can use this to disable Velocitab formatting
on certain servers altogether by disabling the `fallback_enabled` setting and excluding servers you do not wish to
format from being part of a group.
<details>
<summary>Example in config.yml</summary>
```yaml
# All servers which are not in other groups will be put in the fallback group.
# "false" will exclude them from Velocitab.
fallback_enabled: true
# The formats to use for the fallback group.
fallback_group: 'lobbies'
```
</details>

37
docs/Server-Links.md Normal file
View File

@ -0,0 +1,37 @@
> **Note:** This feature will only apply for users connecting with **Minecraft 1.21+** clients
Velocitab supports sending _Server Links_ to players, which will be displayed in the player pause menu by 1.21+ game clients. This can be useful for linking to your server's website, Discord, or other resources.
## Configuring
Server links are configured with the `server_links` section in your `config.yml` file. A link must have:
* A `url` field; a valid web URL to link to
* A `label` field, which is the text to display for the link. Labels can be::
* Fully formatted custom text. You may include placeholders and formatting valid for your chosen formatter.
* One of the following built-in label strings, which will be localized into the user's client language:
* `bug_report` - Will also be shown on the disconnection error screen.
* `community_guidelines`
* `support`
* `status`
* `feedback`
* `community`
* `website`
* `forums`
* `news`
* `announcements`
* A `groups` field, which is a list of server groups the link should be sent to connecting players on.
* Use `'*'` to show the link to all groups.
### Example section
```yaml
server_links:
- url: 'https://william278.net/project/velocitab'
label: 'website'
groups: ['*']
- url: 'https://william278.net/docs/velocitab'
label: 'Documentation'
groups: ['*']
- url: 'https://github.com/William278/Velocitab/issues'
label: 'bug_report' # This will use the bug report built-in label and also be shown on the player disconnect screen
groups: ['*']
```

28
docs/Setup.md Normal file
View File

@ -0,0 +1,28 @@
This page will walk you through installing Velocitab on a Velocity proxy server.
## Requirements
* A Velocity proxy server (running Velocity 3.4.0 or newer)
* Backend Minecraft servers. The following Minecraft server versions are fully supported:
- Minecraft 1.8&mdash;1.8.9
- Minecraft 1.12.2&mdash;latest
&dagger;_Servers that support clients with versions not listed are bit supported, as Velocitab relies on dispatching protocol-compatible packets and modern 1.16 RGB chat color formatting. Users attempting to connect on earlier versions may cause errors to display in your proxy server console._
## Installation
1. Download the latest version of [Velocitab](https://modrinth.com/plugin/velocitab)
2. Drag the plugin into the `/plugins/` folder on your Velocity proxy server
3. Download and install additional optional dependencies on your proxy and backend servers as needed:
1. If you'd like Velocitab to display user roles from [LuckPerms](https://luckperms.net/), ensure LuckPerms is installed on your Velocity proxy as well, and configured to synchronise role information over your database
2. If you'd like to use [PlaceholderAPI](https://www.spigotmc.org/resources/placeholderapi.6245/) placeholders, install [PAPIProxyBridge](https://modrinth.com/plugin/papiproxybridge) on both your Velocity proxy and Spigot-based Minecraft servers. Also ensure PlaceholderAPI is installed on your spigot servers
3. If you'd like to use [MiniPlaceholders](https://modrinth.com/plugin/miniplaceholders) placeholders, install MiniPlaceholders on your Velocity proxy.
4. Restart your proxy to let Velocitab generate its configuration file
5. Stop your proxy server, modify the [`config.yml`](config-file) file to your liking, and start your server
Velocitab should now be successfully installed on your proxy.
## Next Steps
* [Configuring Velocitab](config-file)
* [Using Placeholders](placeholders)
* [Text Formatting](formatting)
* [Server Groups](server-groups)
* [Using Animations](animations)

42
docs/Sorting.md Normal file
View File

@ -0,0 +1,42 @@
Velocitab can sort players in the TAB list by a number of "sorting elements." Sorting is enabled by default, and can be disabled with the `sort_players` option in the [`config.yml`](Config-File) file.
> **Note:** This feature requires sending Update Teams packets. `send_scoreboard_packets` must be enabled in the [`config.yml` file](config-file) for this to work. [More details...](#compatibility-issues)
## Sortable elements
To modify what players are sorted by, modify the `sorting_placeholders` list in the [`config.yml`](Config-File) file. This option accepts an ordered list; the first element in the list is what players will be sorted by first, with subsequent elements being used to break ties. The default sorting strategy is to sort first by `%role_weight%` followed by `%username%`.
<details>
<summary>Sort Players By&hellip; (config.yml)</summary>
```yaml
# Ordered list of elements by which players should be sorted. (Correct values are both internal placeholders and (if enabled) PAPI placeholders)
sort_players_by:
- %role_weight%
- %username%
```
</details>
### List of elements
The following sorting elements are supported:
| Sorting element | Description |
|:-----------------------:|----------------------------------------------------------------------------------------------|
| `Internal Placeholders` | [Check docs here](https://william278.net/docs/velocitab/placeholders#default-placeholders) |
| `PAPI Placeholders` | [Check docs here](https://william278.net/docs/velocitab/placeholders#placeholderapi-support) |
## Technical details
In Minecraft, the TAB list is sorted by the client; the server does not handle the actual display order of names in the list. Players are sorted first by the name of their scoreboard team, then by their name. This is why having a proxy TAB plugin sort players is a surprisingly complex feature request!
To get the client to correctly sort the TAB list, Velocitab sends fake scoreboard "Update Teams" packets to everyone in order to trick the client into thinking players on the server are members of a fake scoreboard team. The name of the fake team for sorting is based on a number of "sorting elements," which can be customized.
Velocitab has a few optimizations in place to reduce the number of packets sent; if you update frequently sorting element placeholders, do note this will lead to more packets being sent between clients and the proxy as the teams will need to be updated more regularly. This can lead to an observable increase in network traffic&mdash;listing fewer sorting elements in the `sort_players_by` section will reduce the number of packets sent.
## Compatibility issues
There are a few compatibility caveats to bear in mind with sorting players in the TAB list:
* If you're using scoreboard teams on your server, then this will interfere with Velocitab's fake team packets and cause sorting to break. Most modern Minecraft proxy network servers probably won't use this feature, since there are better and more powerful plugin alternatives for teaming players, but it's still important to bear in mind.
* Some mods can interfere with scoreboard team packets, particularly if they internally deal with managing packets or scoreboard teams.
* If you're using Fabric/Quilt servers on the backend: [Polymer by Patbox](https://github.com/Patbox/polymer) is incompatible. Polymer does a fair bit of networking stuff which seems to break our packet handling.
In these cases, you may need to disable the use of scoreboard packets through the `send_scoreboard_packets` option detailed earlier.

2
docs/_Footer.md Normal file
View File

@ -0,0 +1,2 @@
| This documentation is available via [william278.net](https://william278.net/docs/velocitab/) |
|----------------------------------------------------------------------------------------------|

25
docs/_Sidebar.md Normal file
View File

@ -0,0 +1,25 @@
## Guides
* 📚 [[Setup]]
* 📄 [[Config File]]
## Documentation
* 🖥️ [[Commands]]
* 👥 [[Server Groups]]
* 🎨 [[Formatting]]
* 📛 [[Nametags]]
* 📊 [[Sorting]]
* ✍️ [[Placeholders]]
* 🔗 [[Relational Placeholders]]
* 🔀 [[Conditional Placeholders]]
* 📝 [[Placeholders Replacements]]
* ✨ [[Animations]]
* 🖼️ [[Custom Logos]]
* 🔗 [[Server Links]]
* 📦 [[API]]
* 📝 [[API Examples]]
* 📝 [[Plugin Message API]]
## Links
* 💻 [GitHub](https://github.com/WiIIiam278/Velocitab)
* 📂 [Download](https://modrinth.com/plugin/velocitab)
* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG)

View File

@ -1,7 +1,12 @@
javaVersion=16
javaVersion=17
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true
plugin_version=1.2
plugin_archive=velocitab
plugin_version=1.7.3
plugin_archive=velocitab
plugin_description=A beautiful and versatile TAB list plugin for Velocity proxies
velocity_api_version=3.4.0
velocity_minimum_build=453
papi_proxy_bridge_minimum_version=1.7

Binary file not shown.

View File

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

41
gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +80,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,22 +131,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
@ -205,6 +214,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

35
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

BIN
images/nametags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
images/server-links.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
images/showcase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

View File

@ -1,140 +1,211 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab;
import com.google.inject.Inject;
import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import net.william278.annotaml.Annotaml;
import lombok.Getter;
import lombok.Setter;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.desertwell.util.Version;
import net.william278.velocitab.api.PluginMessageAPI;
import net.william278.velocitab.api.VelocitabAPI;
import net.william278.velocitab.commands.VelocitabCommand;
import net.william278.velocitab.config.ConfigProvider;
import net.william278.velocitab.config.Formatter;
import net.william278.velocitab.config.Settings;
import net.william278.velocitab.config.TabGroups;
import net.william278.velocitab.hook.Hook;
import net.william278.velocitab.hook.LuckPermsHook;
import net.william278.velocitab.hook.MiniPlaceholdersHook;
import net.william278.velocitab.hook.PapiHook;
import net.william278.velocitab.packet.PacketEventManager;
import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.providers.HookProvider;
import net.william278.velocitab.providers.LoggerProvider;
import net.william278.velocitab.providers.MetricProvider;
import net.william278.velocitab.providers.ScoreboardProvider;
import net.william278.velocitab.sorting.SortingManager;
import net.william278.velocitab.tab.PlayerTabList;
import net.william278.velocitab.vanish.VanishManager;
import org.bstats.velocity.Metrics;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.event.Level;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Plugin(id = "velocitab")
public class Velocitab {
@Getter
public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProvider, HookProvider, MetricProvider {
@Setter
private Settings settings;
@Setter
private TabGroups tabGroups;
private final ProxyServer server;
private final Logger logger;
private final Path dataDirectory;
private final Path configDirectory;
@Inject
private PluginContainer pluginContainer;
@Inject
private Metrics.Factory metricsFactory;
@Setter
private PlayerTabList tabList;
@Setter
private List<Hook> hooks;
@Setter
private ScoreboardManager scoreboardManager;
@Setter
private SortingManager sortingManager;
private VanishManager vanishManager;
private PacketEventManager packetEventManager;
private PluginMessageAPI pluginMessageAPI;
@Inject
public Velocitab(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) {
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path configDirectory) {
this.server = server;
this.logger = logger;
this.dataDirectory = dataDirectory;
this.configDirectory = configDirectory;
}
@Subscribe
public void onProxyInitialization(ProxyInitializeEvent event) {
loadSettings();
public void onProxyInitialization(@NotNull ProxyInitializeEvent event) {
checkCompatibility();
loadConfigs();
loadHooks();
prepareScoreboardManager();
prepareTabList();
prepareVanishManager();
prepareChannelManager();
prepareScoreboard();
registerCommands();
registerMetrics();
checkForUpdates();
prepareAPI();
logger.info("Successfully enabled Velocitab");
}
@NotNull
public ProxyServer getServer() {
return server;
@Subscribe
public void onProxyShutdown(@NotNull ProxyShutdownEvent event) {
disableScoreboardManager();
getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent);
getMiniPlaceholdersHook().ifPresent(MiniPlaceholdersHook::unregisterExpansion);
unregisterAPI();
logger.info("Successfully disabled Velocitab");
}
@NotNull
public Settings getSettings() {
return settings;
public Formatter getFormatter() {
return getSettings().getFormatter();
}
private void loadSettings() {
try {
settings = Annotaml.create(
new File(dataDirectory.toFile(), "config.yml"),
new Settings(this)
).get();
} catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
logger.error("Failed to load config file: " + e.getMessage(), e);
}
public void loadConfigs() {
loadSettings();
loadTabGroups();
}
private <H extends Hook> Optional<H> getHook(@NotNull Class<H> hookType) {
return hooks.stream()
.filter(hook -> hook.getClass().equals(hookType))
.map(hookType::cast)
.findFirst();
private void prepareVanishManager() {
this.vanishManager = new VanishManager(this);
}
public Optional<LuckPermsHook> getLuckPerms() {
return getHook(LuckPermsHook.class);
}
public Optional<PapiHook> getPapiHook() {
return getHook(PapiHook.class);
}
public Optional<MiniPlaceholdersHook> getMiniPlaceholdersHook() {
return getHook(MiniPlaceholdersHook.class);
}
private void loadHooks() {
this.hooks = new ArrayList<>();
Hook.AVAILABLE.forEach(availableHook -> availableHook.apply(this).ifPresent(hooks::add));
}
private void prepareScoreboardManager() {
this.scoreboardManager = new ScoreboardManager(this);
scoreboardManager.registerPacket();
private void prepareChannelManager() {
this.packetEventManager = new PacketEventManager(this);
}
@Override
@NotNull
public Velocitab getPlugin() {
return this;
}
@Override
public ScoreboardManager getScoreboardManager() {
return scoreboardManager;
}
@NotNull
public PlayerTabList getTabList() {
return tabList;
private void prepareAPI() {
VelocitabAPI.register(this);
if (settings.isEnablePluginMessageApi()) {
pluginMessageAPI = new PluginMessageAPI(this);
pluginMessageAPI.registerChannel();
getLogger().info("Registered Velocitab Plugin Message API");
}
getLogger().info("Registered Velocitab API");
}
private void prepareTabList() {
this.tabList = new PlayerTabList(this);
server.getEventManager().register(this, tabList);
private void unregisterAPI() {
VelocitabAPI.unregister();
if (pluginMessageAPI != null) {
pluginMessageAPI.unregisterChannel();
}
}
@NotNull
public TabPlayer getTabPlayer(@NotNull Player player) {
return new TabPlayer(player,
getLuckPerms().map(hook -> hook.getPlayerRole(player))
.orElse(Role.DEFAULT_ROLE),
getLuckPerms().map(LuckPermsHook::getHighestWeight)
.orElse(0));
}
public void log(@NotNull String message, @NotNull Throwable... exceptions) {
Arrays.stream(exceptions).findFirst().ifPresentOrElse(
exception -> logger.error(message, exception),
() -> logger.warn(message)
private void registerCommands() {
final BrigadierCommand command = new VelocitabCommand(this).command();
server.getCommandManager().register(
server.getCommandManager().metaBuilder(command).plugin(this).build(),
command
);
}
@NotNull
public PluginDescription getDescription() {
return pluginContainer.getDescription();
}
@NotNull
public Version getVelocityVersion() {
return Version.fromString(server.getVersion().getVersion(), "-");
}
@NotNull
public Version getVersion() {
return Version.fromString(getDescription().getVersion().orElseThrow(), "-");
}
private void checkForUpdates() {
if (!getSettings().isCheckForUpdates()) {
return;
}
getUpdateChecker().check().thenAccept(checked -> {
if (!checked.isUpToDate()) {
log(Level.WARN, "A new version of Velocitab is available: " + checked.getLatestVersion());
}
});
}
@NotNull
public UpdateChecker getUpdateChecker() {
return UpdateChecker.builder()
.currentVersion(getVersion())
.endpoint(UpdateChecker.Endpoint.MODRINTH)
.resource("velocitab")
.build();
}
}

View File

@ -0,0 +1,29 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.api;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public record PlayerAddedToTabEvent(@NotNull TabPlayer player, @NotNull Group group) {
}

View File

@ -0,0 +1,127 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.api;
import com.google.common.collect.Maps;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.packet.UpdateTeamsPacket;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
public class PluginMessageAPI {
private final Velocitab plugin;
private final Map<String, MinecraftChannelIdentifier> channels;
public PluginMessageAPI(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.channels = Maps.newHashMap();
}
public void registerChannel() {
Arrays.stream(PluginMessageRequest.values())
.map(PluginMessageRequest::name)
.map(s -> s.toLowerCase(Locale.ENGLISH))
.forEach(request -> {
final String channelName = "velocitab:" + request;
final MinecraftChannelIdentifier channel = MinecraftChannelIdentifier.from(channelName);
channels.put(channelName, channel);
plugin.getServer().getChannelRegistrar().register(channel);
});
plugin.getServer().getEventManager().register(plugin, PluginMessageEvent.class, this::onPluginMessage);
}
public void unregisterChannel() {
channels.forEach((name, channel) -> plugin.getServer().getChannelRegistrar().unregister(channel));
}
private void onPluginMessage(@NotNull PluginMessageEvent pluginMessageEvent) {
final Optional<MinecraftChannelIdentifier> channel = Optional.ofNullable(channels.get(pluginMessageEvent.getIdentifier().getId()));
if (channel.isEmpty()) {
return;
}
if (!(pluginMessageEvent.getSource() instanceof ServerConnection serverConnection)) {
return;
}
final Player player = serverConnection.getPlayer();
final Optional<TabPlayer> optionalTabPlayer = plugin.getTabList().getTabPlayer(player);
if (optionalTabPlayer.isEmpty()) {
return;
}
final TabPlayer tabPlayer = optionalTabPlayer.get();
if (!tabPlayer.isLoaded()) {
return;
}
final Optional<PluginMessageRequest> request = PluginMessageRequest.get(channel.get());
if (request.isEmpty()) {
return;
}
final String data = new String(pluginMessageEvent.getData());
handleAPIRequest(tabPlayer, request.get(), data);
}
private void handleAPIRequest(@NotNull TabPlayer tabPlayer, @NotNull PluginMessageRequest request, @NotNull String arg) {
switch (request) {
case UPDATE_CUSTOM_NAME -> {
tabPlayer.setCustomName(arg);
plugin.getTabList().updatePlayer(tabPlayer, true);
}
case UPDATE_TEAM_COLOR -> {
final String clean = arg.replaceAll("&", "").replaceAll("§", "");
if (clean.isEmpty()) {
return;
}
final char colorChar = clean.charAt(0);
final Optional<UpdateTeamsPacket.TeamColor> color = Arrays.stream(UpdateTeamsPacket.TeamColor.values())
.filter(teamColor -> teamColor.colorChar() == colorChar)
.findFirst();
color.ifPresent(teamColor -> {
tabPlayer.setTeamColor(teamColor);
plugin.getTabList().updatePlayer(tabPlayer, true);
});
}
}
}
private enum PluginMessageRequest {
UPDATE_CUSTOM_NAME,
UPDATE_TEAM_COLOR;
public static Optional<PluginMessageRequest> get(@NotNull MinecraftChannelIdentifier channelIdentifier) {
return Arrays.stream(values())
.filter(request -> request.name().equalsIgnoreCase(channelIdentifier.getName()))
.findFirst();
}
}
}

View File

@ -0,0 +1,240 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.api;
import com.velocitypowered.api.proxy.Player;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.PlayerTabList;
import net.william278.velocitab.vanish.VanishIntegration;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
/**
* The Velocitab API.
* <p>
* Retrieve an instance of the API class via {@link #getInstance()}.
*
* @since 1.5.1
*/
@SuppressWarnings("unused")
public class VelocitabAPI {
// Instance of the plugin
private final Velocitab plugin;
private static VelocitabAPI instance;
@ApiStatus.Internal
protected VelocitabAPI(@NotNull Velocitab plugin) {
this.plugin = plugin;
}
/**
* Entrypoint to the {@link VelocitabAPI} API - returns an instance of the API
*
* @return instance of the Velocitab API
* @since 1.5.1
*/
@NotNull
public static VelocitabAPI getInstance() {
if (instance == null) {
throw new NotRegisteredException();
}
return instance;
}
/**
* <b>(Internal use only)</b> - Register the API.
*
* @param plugin the plugin instance
* @hidden This method is for internal use only
* @since 1.5.1
*/
@ApiStatus.Internal
public static void register(@NotNull Velocitab plugin) {
instance = new VelocitabAPI(plugin);
}
/**
* <b>(Internal use only)</b> - Unregister the API.
*
* @hidden This method is for internal use only
* @since 1.5.1
*/
@ApiStatus.Internal
public static void unregister() {
instance = null;
}
/**
* Returns an option of {@link TabPlayer} instance for the given Velocity {@link Player}.
*
* @param player the Velocity player to get the {@link TabPlayer} instance for
* @return the {@link TabPlayer} instance for the given player or an empty optional if the player is not in a group server
* @since 1.5.1
*/
public Optional<TabPlayer> getUser(@NotNull Player player) {
return plugin.getTabList().getTabPlayer(player);
}
/**
* Sets the custom name for a player.
* This will only be visible in the tab list and not in the nametag.
*
* @param player The player for whom to set the custom name
* @param name The custom name to set
* @since 1.5.1
*/
public void setCustomPlayerName(@NotNull Player player, @Nullable String name) {
getUser(player).ifPresent(tabPlayer -> {
tabPlayer.setCustomName(name);
plugin.getTabList().updatePlayerDisplayName(tabPlayer);
});
}
/**
* Returns the custom name of the TabPlayer, if it has been set.
*
* @param player The player for whom to get the custom name
* @return An Optional object containing the custom name, or empty if no custom name has been set.
* @since 1.5.1
*/
public Optional<String> getCustomPlayerName(@NotNull Player player) {
return getUser(player).flatMap(TabPlayer::getCustomName);
}
/**
* Get the {@link PlayerTabList}, which handles the tab list for players across different server groups.
*
* @return the {@link PlayerTabList} global instance.
* @since 1.5.1
*/
@NotNull
public PlayerTabList getTabList() {
return plugin.getTabList();
}
/**
* Sets the VanishIntegration to use for determining whether the plugin should show a player in the tab list.
*
* @param vanishIntegration the VanishIntegration to set
* @since 1.5.1
*/
public void setVanishIntegration(@NotNull VanishIntegration vanishIntegration) {
plugin.getVanishManager().setIntegration(vanishIntegration);
}
/**
* Retrieves the VanishIntegration associated with the VelocitabAPI instance.
* This integration allows checking if a player can see another player and if a player is vanished.
*
* @return The VanishIntegration instance associated with the VelocitabAPI
* @since 1.5.1
*/
@NotNull
public VanishIntegration getVanishIntegration() {
return plugin.getVanishManager().getIntegration();
}
/**
* Vanishes the player by hiding them from the tab list and scoreboard if enabled.
*
* @param player The player to vanish
* @since 1.5.1
*/
public void vanishPlayer(@NotNull Player player) {
plugin.getVanishManager().vanishPlayer(player);
}
/**
* Un-vanishes the given player by showing them in the tab list and scoreboard if enabled.
*
* @param player The player to unvanish
* @since 1.5.1
*/
public void unVanishPlayer(@NotNull Player player) {
plugin.getVanishManager().unVanishPlayer(player);
}
/**
* Retrieves the server group that the given player is connected to.
*
* @param player the player for whom to retrieve the server group
* @return the name of the server group that the player is connected to,
* or a null value if the player is not connected to a server group
* @since 1.5.1
*/
@Nullable
public Group getServerGroup(@NotNull Player player) {
return getUser(player).map(TabPlayer::getGroup).orElse(null);
}
/**
* Retrieves a list of server groups.
*
* @return A list of Group objects representing server groups.
* @since 1.6.6
*/
@NotNull
public List<Group> getServerGroups() {
return plugin.getTabGroups().getGroups();
}
/**
* Retrieves an optional Group object with the given name.
*
* @param name The name of the group to retrieve.
* @return An optional Group object containing the group with the given name, or an empty optional if no group exists with that name.
* @since 1.6.6
*/
@NotNull
public Optional<Group> getGroup(@NotNull String name) {
return plugin.getTabGroups().getGroup(name);
}
public Optional<Group> getGroupFromServer(@NotNull String server) {
return plugin.getTabGroups().getGroupFromServer(server, plugin);
}
/**
* An exception indicating the Velocitab API was accessed before it was registered.
*
* @since 1.5.1
*/
static final class NotRegisteredException extends IllegalStateException {
private static final String MESSAGE = """
Could not access the Velocitab API as it has not yet been registered. This could be because:
1) Velocitab has failed to enable successfully
2) You are attempting to access Velocitab on plugin construction/before your plugin has enabled.
3) You have shaded Velocitab into your plugin jar and need to fix your maven/gradle/build script
to only include Velocitab as a dependency and not as a shaded dependency.""";
NotRegisteredException() {
super(MESSAGE);
}
}
}

View File

@ -0,0 +1,165 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.commands;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.william278.desertwell.about.AboutMenu;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class VelocitabCommand {
private static final TextColor MAIN_COLOR = TextColor.color(0x00FB9A);
private static final TextColor ERROR_COLOR = TextColor.color(0xFF7E5E);
private final AboutMenu aboutMenu;
private final Velocitab plugin;
public VelocitabCommand(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.aboutMenu = AboutMenu.builder()
.title(Component.text("Velocitab"))
.description(Component.text(plugin.getDescription().getDescription().orElseThrow()))
.version(plugin.getVersion())
.credits("Authors",
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"),
AboutMenu.Credit.of("AlexDev03").description("Click to visit GitHub").url("https://github.com/alexdev03"))
.credits("Contributors",
AboutMenu.Credit.of("Ironboundred").description("Code"),
AboutMenu.Credit.of("Emibergo02").description("Code"),
AboutMenu.Credit.of("FreeMonoid").description("Code"),
AboutMenu.Credit.of("4drian3d").description("Code"))
.buttons(
AboutMenu.Link.of("https://william278.net/docs/velocitab").text("Docs").icon(""),
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("").color(TextColor.color(0x6773f5)),
AboutMenu.Link.of("https://modrinth.com/plugin/velocitab").text("Modrinth").icon("").color(TextColor.color(0x589143)))
.build();
}
@NotNull
public BrigadierCommand command() {
final LiteralArgumentBuilder<CommandSource> builder = LiteralArgumentBuilder
.<CommandSource>literal("velocitab")
.executes(ctx -> {
sendAboutInfo(ctx.getSource());
return Command.SINGLE_SUCCESS;
})
.then(LiteralArgumentBuilder.<CommandSource>literal("about")
.executes(ctx -> {
sendAboutInfo(ctx.getSource());
return Command.SINGLE_SUCCESS;
})
)
.then(LiteralArgumentBuilder.<CommandSource>literal("name")
.requires(src -> hasPermission(src, "name"))
.then(RequiredArgumentBuilder.<CommandSource, String>argument("name", StringArgumentType.greedyString())
.requires(src -> src instanceof Player)
.executes(ctx -> {
final Player player = (Player) ctx.getSource();
final String name = StringArgumentType.getString(ctx, "name");
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
if (tabPlayer.isEmpty()) {
ctx.getSource().sendMessage(Component
.text("You can't update your TAB name from an untracked server!", ERROR_COLOR));
return Command.SINGLE_SUCCESS;
}
tabPlayer.get().setCustomName(name);
plugin.getTabList().updatePlayerDisplayName(tabPlayer.get());
ctx.getSource().sendMessage(Component
.text("Your TAB name has been updated!", MAIN_COLOR));
return Command.SINGLE_SUCCESS;
})
)
.requires(src -> src instanceof Player)
.executes(ctx -> {
final Player player = (Player) ctx.getSource();
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
if (tabPlayer.isEmpty()) {
ctx.getSource().sendMessage(Component
.text("You can't reset your TAB name from an untracked server!", ERROR_COLOR));
return Command.SINGLE_SUCCESS;
}
// If no custom name is applied, ask for argument
String customName = tabPlayer.get().getCustomName().orElse("");
if (customName.isEmpty() || customName.equals(player.getUsername())) {
ctx.getSource().sendMessage(Component
.text("You aren't using a custom name in TAB!", ERROR_COLOR));
return Command.SINGLE_SUCCESS;
}
tabPlayer.get().setCustomName(null);
plugin.getTabList().updatePlayerDisplayName(tabPlayer.get());
player.sendMessage(Component.text("Your name has been reset!", MAIN_COLOR));
return Command.SINGLE_SUCCESS;
})
)
.then(LiteralArgumentBuilder.<CommandSource>literal("reload")
.requires(src -> hasPermission(src, "reload"))
.executes(ctx -> {
plugin.loadConfigs();
plugin.getTabList().reloadUpdate();
ctx.getSource().sendMessage(Component.text("Velocitab has been reloaded!",
MAIN_COLOR));
return Command.SINGLE_SUCCESS;
})
)
.then(LiteralArgumentBuilder.<CommandSource>literal("update")
.requires(src -> hasPermission(src, "update"))
.executes(ctx -> {
plugin.getUpdateChecker().check().thenAccept(checked -> {
if (checked.isUpToDate()) {
ctx.getSource().sendMessage(Component.text("Velocitab is up to date! (Running v%s)"
.formatted(plugin.getVersion()), MAIN_COLOR));
return;
}
ctx.getSource().sendMessage(Component
.text("An update for Velocitab is available. Please update to %s"
.formatted(checked.getLatestVersion()), MAIN_COLOR));
});
return Command.SINGLE_SUCCESS;
})
);
return new BrigadierCommand(builder);
}
private boolean hasPermission(@NotNull CommandSource source, @NotNull String command) {
return source.hasPermission(String.join(".", "velocitab", "command", command));
}
private void sendAboutInfo(@NotNull CommandSource source) {
source.sendMessage(aboutMenu.toComponent());
}
}

View File

@ -0,0 +1,199 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import de.exlll.configlib.NameFormatters;
import de.exlll.configlib.YamlConfigurationProperties;
import de.exlll.configlib.YamlConfigurations;
import net.william278.desertwell.util.Version;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
/**
* Interface for getting and setting data from plugin configuration files
*
* @since 1.0
*/
public interface ConfigProvider {
@NotNull
YamlConfigurationProperties.Builder<?> YAML_CONFIGURATION_PROPERTIES = YamlConfigurationProperties.newBuilder()
.charset(StandardCharsets.UTF_8)
.outputNulls(true)
.inputNulls(false)
.setNameFormatter(NameFormatters.LOWER_UNDERSCORE);
@NotNull
Velocitab getPlugin();
/**
* Get the plugin settings, read from the config file
*
* @return the plugin settings
* @since 1.0
*/
@NotNull
Settings getSettings();
/**
* Set the plugin settings
*
* @param settings The settings to set
* @since 1.0
*/
void setSettings(@NotNull Settings settings);
/**
* Load the plugin settings from the config file
*
* @since 1.0
*/
default void loadSettings() {
setSettings(YamlConfigurations.update(
getConfigDirectory().resolve("config.yml"),
Settings.class,
YAML_CONFIGURATION_PROPERTIES.header(Settings.CONFIG_HEADER).build()
));
getSettings().validateConfig(getPlugin());
}
/**
* Get the tab groups
*
* @return the tab groups
* @since 1.0
*/
@NotNull
TabGroups getTabGroups();
/**
* Set the tab groups
*
* @param tabGroups The tab groups to set
* @since 1.0
*/
void setTabGroups(@NotNull TabGroups tabGroups);
/**
* Load the tab groups from the config file
*
* @since 1.0
*/
default void loadTabGroups() {
setTabGroups(YamlConfigurations.update(
getConfigDirectory().resolve("tab_groups.yml"),
TabGroups.class,
YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
));
getTabGroups().validateConfig(getPlugin());
}
/**
* Load the tab groups from the config file
*
* @since 1.0
*/
@NotNull
default Metadata getMetadata() {
final URL resource = ConfigProvider.class.getResource("/metadata.yml");
try (InputStream input = Objects.requireNonNull(resource, "Metadata file missing").openStream()) {
return YamlConfigurations.read(input, Metadata.class, YAML_CONFIGURATION_PROPERTIES.build());
} catch (IOException e) {
throw new IllegalStateException("Unable to load plugin metadata", e);
}
}
@SuppressWarnings("OptionalIsPresent")
default void checkCompatibility() {
if (getSkipCompatibilityCheck().orElse(false)) {
getPlugin().getLogger().warn("Skipping compatibility checks");
return;
}
// Validate Velocity platform version
final Metadata metadata = getMetadata();
final Version proxyVersion = getVelocityVersion();
metadata.validateApiVersion(proxyVersion);
metadata.validateBuild(proxyVersion);
// Validate PAPIProxyBridge hook version
final Optional<Version> papiProxyBridgeVersion = getPapiProxyBridgeVersion();
if (papiProxyBridgeVersion.isPresent()) {
metadata.validatePapiProxyBridgeVersion(papiProxyBridgeVersion.get());
}
}
@NotNull
default Optional<Boolean> getSkipCompatibilityCheck() {
return ManagementFactory.getRuntimeMXBean().getInputArguments().stream()
.filter(s -> s.startsWith("-Dvelocitab.skip-compatibility-check="))
.map(s -> s.substring(s.indexOf('=') + 1))
.filter(s -> s.equalsIgnoreCase("true") || s.equalsIgnoreCase("false"))
.map(Boolean::parseBoolean)
.findFirst();
}
default Optional<Version> getPapiProxyBridgeVersion() {
return getPlugin().getServer().getPluginManager()
.getPlugin("papiproxybridge").map(PluginContainer::getDescription)
.flatMap(PluginDescription::getVersion).map(Version::fromString);
}
@NotNull
Version getVelocityVersion();
/**
* Saves the tab groups to the "tab_groups.yml" config file.
* Uses the YamlConfigurations#save method to write the tab groups object to the specified config file path.
* This method assumes that the getConfigDirectory method returns a valid directory path.
*
* @throws IllegalStateException if the getConfigDirectory method returns null
* @since 1.0
*/
default void saveTabGroups() {
YamlConfigurations.save(
getConfigDirectory().resolve("tab_groups.yml"),
TabGroups.class,
getTabGroups(),
YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
);
}
/**
* Get the plugin config directory
*
* @return the plugin config directory
* @since 1.0
*/
@NotNull
Path getConfigDirectory();
}

View File

@ -0,0 +1,33 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
public interface ConfigValidator {
/**
* Validates the configuration settings.
* @throws IllegalStateException if the configuration is invalid
*/
void validateConfig(@NotNull Velocitab plugin) throws IllegalStateException;
}

View File

@ -0,0 +1,148 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import de.themoep.minedown.adventure.MineDown;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.util.QuadFunction;
import net.william278.velocitab.util.SerializationUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
/**
* Different formatting markup options for the TAB list
*/
@SuppressWarnings("unused")
public enum Formatter {
MINEDOWN(
(text, player, viewer, plugin) -> new MineDown(text).toComponent(),
MineDown::escape,
"MineDown",
(text) -> new MineDown(text).toComponent(),
(text) -> {
throw new UnsupportedOperationException("MineDown does not support serialization");
}
),
MINIMESSAGE(
(text, player, viewer, plugin) -> plugin.getMiniPlaceholdersHook()
.filter(hook -> player != null)
.map(hook -> hook.format(text, player.getPlayer(), viewer == null ? null : viewer.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(text)),
(text) -> MiniMessage.miniMessage().escapeTags(text),
"MiniMessage",
(text) -> MiniMessage.miniMessage().deserialize(text),
MiniMessage.miniMessage()::serialize
),
LEGACY(
(text, player, viewer, plugin) -> SerializationUtil.LEGACY_SERIALIZER.deserialize(text),
Function.identity(),
"Legacy Text",
SerializationUtil.LEGACY_SERIALIZER::deserialize,
SerializationUtil.LEGACY_SERIALIZER::serialize
);
/**
* Name of the formatter
*/
private final String name;
/**
* Function to apply formatting to a string
*/
private final QuadFunction<String, TabPlayer, TabPlayer, Velocitab, Component> formatter;
/**
* Function to escape formatting characters in a string
*/
private final Function<String, String> escaper;
private final Function<String, Component> emptyFormatter;
private final Function<Component, String> serializer;
Formatter(@NotNull QuadFunction<String, TabPlayer, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper,
@NotNull String name, @NotNull Function<String, Component> emptyFormatter, @NotNull Function<Component, String> serializer) {
this.formatter = formatter;
this.escaper = escaper;
this.name = name;
this.emptyFormatter = emptyFormatter;
this.serializer = serializer;
}
/**
* Formats the given text using a specific formatter.
*
* @param text The text to format
* @param player The TabPlayer object representing the player
* @param tabPlayer The TabPlayer object representing the viewer (can be null)
* @param plugin The Velocitab plugin instance
* @return The formatted Component object
* @throws NullPointerException if any of the parameters (text, player, plugin) is null
*/
@NotNull
public Component format(@NotNull String text, @NotNull TabPlayer player, @Nullable TabPlayer tabPlayer, @NotNull Velocitab plugin) {
return formatter.apply(text, player, tabPlayer, plugin);
}
/**
* Formats the given text using a specific formatter.
*
* @param text The text to format
* @param player The TabPlayer object representing the player
* @param plugin The Velocitab plugin instance
* @return The formatted Component object
* @throws NullPointerException if any of the parameters (text, player, plugin) is null
*/
@NotNull
public Component format(@NotNull String text, @NotNull TabPlayer player, @NotNull Velocitab plugin) {
return formatter.apply(text, player, null, plugin);
}
@NotNull
public String formatLegacySymbols(@NotNull String text, @NotNull TabPlayer player, @NotNull Velocitab plugin) {
return LegacyComponentSerializer.legacySection()
.serialize(format(text, player, plugin));
}
@NotNull
public Component deserialize(@NotNull String text) {
return emptyFormatter.apply(text);
}
@NotNull
public String escape(@NotNull String text) {
return escaper.apply(text);
}
@NotNull
public String getName() {
return name;
}
@NotNull
public String serialize(@NotNull Component component) {
return serializer.apply(component);
}
}

View File

@ -0,0 +1,172 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import com.google.common.collect.Sets;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.Nametag;
import org.apache.commons.text.StringEscapeUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.event.Level;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
@SuppressWarnings("unused")
public record Group(
String name,
List<String> headers,
List<String> footers,
String format,
Nametag nametag,
Set<String> servers,
List<String> sortingPlaceholders,
Map<String, List<PlaceholderReplacement>> placeholderReplacements,
boolean collisions,
int headerFooterUpdateRate,
int placeholderUpdateRate,
boolean onlyListPlayersInSameServer
) {
@NotNull
public String getHeader(int index) {
return headers.isEmpty() ? "" : StringEscapeUtils.unescapeJava(headers
.get(Math.max(0, Math.min(index, headers.size() - 1))));
}
@NotNull
public String getFooter(int index) {
return footers.isEmpty() ? "" : StringEscapeUtils.unescapeJava(footers
.get(Math.max(0, Math.min(index, footers.size() - 1))));
}
public boolean containsServer(@NotNull Velocitab plugin, @NotNull String serverName) {
return registeredServers(plugin).stream()
.anyMatch(registeredServer -> registeredServer.getServerInfo().getName().equalsIgnoreCase(serverName));
}
@NotNull
public Set<RegisteredServer> registeredServers(@NotNull Velocitab plugin) {
return registeredServers(plugin, true);
}
@NotNull
public Set<RegisteredServer> registeredServers(@NotNull Velocitab plugin, boolean includeAllPlayers) {
if ((includeAllPlayers && plugin.getSettings().isShowAllPlayersFromAllGroups()) ||
(isDefault(plugin) && plugin.getSettings().isFallbackEnabled())) {
return Sets.newHashSet(plugin.getServer().getAllServers());
}
return getRegexServers(plugin);
}
@NotNull
private Set<RegisteredServer> getRegexServers(@NotNull Velocitab plugin) {
final Set<RegisteredServer> totalServers = Sets.newHashSet();
for (String server : servers) {
try {
final Matcher matcher = Pattern.compile(server, Pattern.CASE_INSENSITIVE).matcher("");
plugin.getServer().getAllServers().stream()
.filter(registeredServer -> matcher.reset(registeredServer.getServerInfo().getName()).matches())
.forEach(totalServers::add);
} catch (PatternSyntaxException exception) {
plugin.log(Level.WARN, "Invalid regex pattern " + server + " in group " + name, exception);
plugin.getServer().getServer(server).ifPresent(totalServers::add);
}
}
return totalServers;
}
public boolean isDefault(@NotNull Velocitab plugin) {
return name.equals(plugin.getSettings().getFallbackGroup());
}
@NotNull
public Set<Player> getPlayers(@NotNull Velocitab plugin) {
Set<Player> players = Sets.newHashSet();
for (RegisteredServer server : registeredServers(plugin)) {
players.addAll(server.getPlayersConnected());
}
return players;
}
@NotNull
public Set<Player> getPlayers(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
return Sets.newHashSet(plugin.getServer().getAllPlayers());
}
if (onlyListPlayersInSameServer) {
return tabPlayer.getPlayer().getCurrentServer()
.map(s -> Sets.newHashSet(s.getServer().getPlayersConnected()))
.orElseGet(Sets::newHashSet);
}
return getPlayers(plugin);
}
/**
* Retrieves the set of TabPlayers associated with the given Velocitab plugin instance.
* If the plugin is configured to show all players from all groups, all players will be returned.
*
* @param plugin The Velocitab plugin instance.
* @return A set of TabPlayers.
*/
@NotNull
public Set<TabPlayer> getTabPlayers(@NotNull Velocitab plugin) {
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
return Sets.newHashSet(plugin.getTabList().getPlayers().values());
}
return plugin.getTabList().getPlayers()
.values()
.stream()
.filter(tabPlayer -> tabPlayer.getGroup().equals(this))
.collect(Collectors.toSet());
}
@NotNull
public Set<TabPlayer> getTabPlayers(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
return Sets.newHashSet(plugin.getTabList().getPlayers().values());
}
if (onlyListPlayersInSameServer) {
return plugin.getTabList().getPlayers()
.values()
.stream()
.filter(player -> player.getGroup().equals(this) && player.getServerName().equals(tabPlayer.getServerName()))
.collect(Collectors.toSet());
}
return getTabPlayers(plugin);
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Group group)) {
return false;
}
return name.equals(group.name);
}
}

View File

@ -0,0 +1,76 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.william278.desertwell.util.Version;
import org.jetbrains.annotations.NotNull;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@SuppressWarnings("unused")
@Configuration
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Metadata {
private String velocityApiVersion;
private int velocityMinimumBuild;
private String papiProxyBridgeMinimumVersion;
public void validateApiVersion(@NotNull Version version) {
if (version.compareTo(Version.fromString(velocityApiVersion)) < 0) {
final String serverVersion = version.toStringWithoutMetadata();
throw new IllegalStateException("Your Velocity API version (" + serverVersion + ") is not supported! " +
"Disabling Velocitab. Please update to at least Velocity v" + velocityApiVersion
+ " build #" + velocityMinimumBuild + " or newer.");
}
}
public void validateBuild(@NotNull Version version) {
int serverBuild = getBuildNumber(version.toString());
if (serverBuild < velocityMinimumBuild) {
throw new IllegalStateException("Your Velocity build version (#" + serverBuild + ") is not supported! " +
"Disabling Velocitab. Please update to at least Velocity v" + velocityApiVersion
+ " build #" + velocityMinimumBuild + " or newer.");
}
}
public void validatePapiProxyBridgeVersion(@NotNull Version version) {
if (version.compareTo(Version.fromString(papiProxyBridgeMinimumVersion)) < 0) {
final String serverVersion = version.toStringWithoutMetadata();
throw new IllegalStateException("Your PAPIProxyBridge version (" + serverVersion + ") is not supported! " +
"Disabling Velocitab. Please update to at least PAPIProxyBridge v" + papiProxyBridgeMinimumVersion + ".");
}
}
private int getBuildNumber(@NotNull String proxyVersion) {
final Matcher matcher = Pattern.compile(".*-b(\\d+).*").matcher(proxyVersion);
if (matcher.find(1)) {
return Integer.parseInt(matcher.group(1));
}
throw new IllegalArgumentException("No build number found for proxy version: " + proxyVersion);
}
}

View File

@ -1,15 +1,48 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import it.unimi.dsi.fastutil.Pair;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.hook.miniconditions.MiniConditionManager;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.Nametag;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.function.TriFunction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.event.Level;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public enum Placeholder {
@ -20,36 +53,279 @@ public enum Placeholder {
.map(RegisteredServer::getPlayersConnected)
.map(players -> Integer.toString(players.size()))
.orElse("")),
CURRENT_DATE((plugin, player) -> DateTimeFormatter.ofPattern("dd MMM yyyy").format(LocalDateTime.now())),
CURRENT_TIME((plugin, player) -> DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now())),
USERNAME((plugin, player) -> escape(player.getPlayer().getUsername())),
GROUP_PLAYERS_ONLINE((param, plugin, player) -> {
if (param.isEmpty()) {
return Integer.toString(player.getGroup().getPlayers(plugin).size());
}
return plugin.getTabGroups().getGroup(param)
.map(group -> Integer.toString(group.getPlayers(plugin).size()))
.orElse("Group " + param + " not found");
}),
CURRENT_DATE_DAY((plugin, player) -> DateTimeFormatter.ofPattern("dd").format(LocalDateTime.now())),
CURRENT_DATE_WEEKDAY((param, plugin, player) -> {
if (param.isEmpty()) {
return DateTimeFormatter.ofPattern("EEEE").format(LocalDateTime.now());
}
final String countryCode = param.toUpperCase();
final Locale locale = Locale.forLanguageTag(countryCode);
return DateTimeFormatter.ofPattern("EEEE").withLocale(locale).format(LocalDateTime.now());
}),
CURRENT_DATE_MONTH((plugin, player) -> DateTimeFormatter.ofPattern("MM").format(LocalDateTime.now())),
CURRENT_DATE_YEAR((plugin, player) -> DateTimeFormatter.ofPattern("yyyy").format(LocalDateTime.now())),
CURRENT_DATE((param, plugin, player) -> {
if (param.isEmpty()) {
return DateTimeFormatter.ofPattern("dd/MM/yyyy").format(LocalDateTime.now());
}
final String countryCode = param.toUpperCase();
final Locale locale = Locale.forLanguageTag(countryCode);
return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale).format(LocalDateTime.now());
}),
CURRENT_TIME_HOUR((plugin, player) -> DateTimeFormatter.ofPattern("HH").format(LocalDateTime.now())),
CURRENT_TIME_MINUTE((plugin, player) -> DateTimeFormatter.ofPattern("mm").format(LocalDateTime.now())),
CURRENT_TIME_SECOND((plugin, player) -> DateTimeFormatter.ofPattern("ss").format(LocalDateTime.now())),
CURRENT_TIME((param, plugin, player) -> {
if (param.isEmpty()) {
return DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now());
}
final String countryCode = param.toUpperCase();
final Locale locale = Locale.forLanguageTag(countryCode);
return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale).format(LocalTime.now());
}),
USERNAME((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername())),
USERNAME_LOWER((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername()).toLowerCase()),
SERVER((plugin, player) -> player.getServerName()),
PING((plugin, player) -> Long.toString(player.getPlayer().getPing())),
PREFIX((plugin, player) -> player.getRole().getPrefix().orElse("")),
SUFFIX((plugin, player) -> player.getRole().getSuffix().orElse("")),
ROLE((plugin, player) -> player.getRole().getName().orElse("")),
DEBUG_TEAM_NAME((plugin, player) -> escape(player.getTeamName()));
PREFIX((plugin, player) -> player.getRole().getPrefix()
.orElse(getPlaceholderFallback(plugin, "%luckperms_prefix%"))),
SUFFIX((plugin, player) -> player.getRole().getSuffix()
.orElse(getPlaceholderFallback(plugin, "%luckperms_suffix%"))),
ROLE((plugin, player) -> player.getRole().getName()
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName()
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_weight%"))),
SERVER_GROUP((plugin, player) -> player.getGroup().name()),
SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))),
DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse(""))),
LUCKPERMS_META((param, plugin, player) -> plugin.getLuckPermsHook()
.map(hook -> hook.getMeta(player.getPlayer(), param))
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_" + param + "%")));
private final BiFunction<Velocitab, TabPlayer, String> formatter;
private final static Pattern VELOCITAB_PATTERN = Pattern.compile("<velocitab_.*?>");
private final static Pattern TEST = Pattern.compile("<.*?>");
private final static Pattern CONDITION_REPLACER = Pattern.compile("<velocitab_rel_condition:[^:]*:");
private final static Pattern PLACEHOLDER_PATTERN = Pattern.compile("%.*?%", Pattern.DOTALL);
private final static String DELIMITER = ":::";
private final static Map<String, String> SYMBOL_SUBSTITUTES = Map.of(
"<", "*LESS*",
">", "*GREATER*"
);
private final static Map<String, String> SYMBOL_SUBSTITUTES_2 = Map.of(
"*LESS*", "*LESS2*",
"*GREATER*", "*GREATER2*"
);
private final static String VEL_PLACEHOLDER = "<vel";
private final static String VELOCITAB_PLACEHOLDER = "<velocitab_rel";
private final static String ELSE_PLACEHOLDER = "ELSE";
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> formatter) {
this.formatter = formatter;
/**
* Function to replace placeholders with a real value
*/
private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
private final boolean parameterised;
private final Pattern pattern;
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
this.parameterised = false;
this.replacer = (text, player, plugin) -> replacer.apply(player, plugin);
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "%");
}
public static CompletableFuture<String> format(@NotNull String format, @NotNull Velocitab plugin, @NotNull TabPlayer player) {
for (Placeholder placeholder : values()) {
format = format.replace("%" + placeholder.name().toLowerCase() + "%", placeholder.formatter.apply(plugin, player));
}
final String replaced = format;
return plugin.getPapiHook()
.map(hook -> hook.formatPapiPlaceholders(replaced, player.getPlayer()))
.orElse(CompletableFuture.completedFuture(replaced));
Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) {
this.parameterised = true;
this.replacer = parameterisedReplacer;
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]*%", Pattern.CASE_INSENSITIVE);
}
public static CompletableFuture<Nametag> replace(@NotNull Nametag nametag, @NotNull Velocitab plugin,
@NotNull TabPlayer player) {
return replace(nametag.prefix() + DELIMITER + nametag.suffix(), plugin, player)
.thenApply(s -> s.split(DELIMITER, 2))
.thenApply(v -> new Nametag(v[0], v.length > 1 ? v[1] : ""));
}
// Replace __ so that it is not seen as underline when the string is formatted.
@NotNull
private static String escape(String replace) {
return replace.replace("__", "_\\_");
private static String getPlaceholderFallback(@NotNull Velocitab plugin, @NotNull String fallback) {
if (plugin.getPAPIProxyBridgeHook().isPresent() && plugin.getSettings().isFallbackToPapiIfPlaceholderBlank()) {
return fallback;
}
return "";
}
@NotNull
public static Pair<String, Map<String, String>> replaceInternal(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
format = processRelationalPlaceholders(format, plugin);
return replacePlaceholders(format, plugin, player);
}
private static String processRelationalPlaceholders(@NotNull String format, @NotNull Velocitab plugin) {
if (plugin.getFormatter().equals(Formatter.MINIMESSAGE) && format.contains(VEL_PLACEHOLDER)) {
final Matcher conditionReplacer = CONDITION_REPLACER.matcher(format);
while (conditionReplacer.find()) {
final String search = conditionReplacer.group().split(":")[1];
String condition = search;
for (Map.Entry<String, String> entry : MiniConditionManager.REPLACE.entrySet()) {
condition = condition.replace(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : MiniConditionManager.REPLACE_2.entrySet()) {
condition = condition.replace(entry.getValue(), entry.getKey());
}
format = format.replace(search, condition);
}
final Matcher testMatcher = TEST.matcher(format);
while (testMatcher.find()) {
if (testMatcher.group().startsWith(VELOCITAB_PLACEHOLDER)) {
final Matcher second = TEST.matcher(testMatcher.group().substring(1));
while (second.find()) {
String s = second.group();
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
s = s.replace(entry.getKey(), entry.getValue());
}
format = format.replace(second.group(), s);
}
continue;
}
String s = testMatcher.group();
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
s = s.replace(entry.getKey(), entry.getValue());
}
format = format.replace(testMatcher.group(), s);
}
final Matcher velocitabRelationalMatcher = VELOCITAB_PATTERN.matcher(format);
while (velocitabRelationalMatcher.find()) {
final String relationalPlaceholder = velocitabRelationalMatcher.group().substring(1, velocitabRelationalMatcher.group().length() - 1);
String fixedString = relationalPlaceholder;
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES_2.entrySet()) {
fixedString = fixedString.replace(entry.getKey(), entry.getValue());
}
format = format.replace(relationalPlaceholder, fixedString);
}
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
format = format.replace(entry.getValue(), entry.getKey());
}
}
return format;
}
@NotNull
private static Pair<String, Map<String, String>> replacePlaceholders(@NotNull String format, @NotNull Velocitab plugin,
@Nullable TabPlayer player) {
final Map<String, String> replacedPlaceholders = Maps.newHashMap();
for (Placeholder placeholder : values()) {
final Matcher matcher = placeholder.pattern.matcher(format);
if (placeholder.parameterised) {
format = matcher.replaceAll(matchResult -> {
final String replacement = placeholder.replacer.apply(StringUtils.chop(matchResult.group().replace("%" + placeholder.name().toLowerCase(), "")
.replaceFirst("_", "")), plugin, player);
replacedPlaceholders.put(matchResult.group(), replacement);
return Matcher.quoteReplacement(replacement);
});
} else {
format = matcher.replaceAll(matchResult -> {
final String replacement = placeholder.replacer.apply(null, plugin, player);
replacedPlaceholders.put(matchResult.group(), replacement);
return Matcher.quoteReplacement(replacement);
});
}
}
return Pair.of(format, replacedPlaceholders);
}
@NotNull
private static String applyPlaceholderReplacements(@NotNull String text, @NotNull TabPlayer player,
@NotNull Map<String, String> parsed) {
for (final Map.Entry<String, List<PlaceholderReplacement>> entry : player.getGroup().placeholderReplacements().entrySet()) {
if (!parsed.containsKey(entry.getKey())) {
continue;
}
final String replaced = parsed.get(entry.getKey());
final Optional<PlaceholderReplacement> replacement = entry.getValue().stream()
.filter(r -> r.placeholder().equalsIgnoreCase(replaced))
.findFirst();
if (replacement.isPresent()) {
text = text.replace(entry.getKey(), replacement.get().replacement());
} else {
final Optional<PlaceholderReplacement> elseReplacement = entry.getValue().stream()
.filter(r -> r.placeholder().equalsIgnoreCase(ELSE_PLACEHOLDER))
.findFirst();
if (elseReplacement.isPresent()) {
text = text.replace(entry.getKey(), elseReplacement.get().replacement());
}
}
}
return applyPlaceholders(text, parsed);
}
public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin,
@NotNull TabPlayer player) {
if (format.equals(DELIMITER)) {
return CompletableFuture.completedFuture("");
}
final Pair<String, Map<String, String>> replaced = replaceInternal(format, plugin, player);
if (!PLACEHOLDER_PATTERN.matcher(replaced.first()).find()) {
return CompletableFuture.completedFuture(applyPlaceholderReplacements(format, player, replaced.second()));
}
final List<String> placeholders = extractPlaceholders(replaced.first());
return plugin.getPAPIProxyBridgeHook()
.map(hook -> hook.parsePlaceholders(placeholders, player.getPlayer())
.exceptionally(e -> {
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
return Map.of();
})
)
.orElse(CompletableFuture.completedFuture(Maps.newHashMap()))
.exceptionally(e -> {
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
return Map.of();
})
.thenApply(m -> applyPlaceholderReplacements(format, player, mergeMaps(m, replaced.second())));
}
@NotNull
private static String applyPlaceholders(@NotNull String text, @NotNull Map<String, String> replacements) {
for (Map.Entry<String, String> entry : replacements.entrySet()) {
text = text.replace(entry.getKey(), entry.getValue());
}
return text;
}
@NotNull
private static Map<String, String> mergeMaps(@NotNull Map<String, String> map1, @NotNull Map<String, String> map2) {
map1.putAll(map2);
return map1;
}
@NotNull
private static List<String> extractPlaceholders(@NotNull String text) {
final List<String> placeholders = Lists.newArrayList();
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
while (matcher.find()) {
placeholders.add(matcher.group());
}
return placeholders;
}
}

View File

@ -0,0 +1,25 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import org.jetbrains.annotations.NotNull;
public record PlaceholderReplacement(@NotNull String placeholder, @NotNull String replacement) {
}

View File

@ -0,0 +1,89 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import com.velocitypowered.api.util.ServerLink;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.net.URI;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public record ServerUrl(
@NotNull String label,
@NotNull String url,
@NotNull Set<String> groups
) {
public ServerUrl(@NotNull String label, @NotNull String url) {
this(label, url, Set.of("*"));
}
// Resolve the built-in label or format the custom label, then wrap as a Velocity ServerLink
@NotNull
CompletableFuture<ServerLink> getServerLink(@NotNull Velocitab plugin, @NotNull TabPlayer player) {
return getBuiltInLabel().map(
(type) -> CompletableFuture.completedFuture(ServerLink.serverLink(type, url()))
).orElseGet(
() -> Placeholder.replace(label(), plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
.thenApply(formatted -> ServerLink.serverLink(formatted, url()))
);
}
@NotNull
public static CompletableFuture<List<ServerLink>> resolve(@NotNull Velocitab plugin, @NotNull TabPlayer player,
@NotNull List<ServerUrl> urls) {
final List<CompletableFuture<ServerLink>> futures = new ArrayList<>();
for (ServerUrl url : urls) {
futures.add(url.getServerLink(plugin, player));
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join).toList());
}
private Optional<ServerLink.Type> getBuiltInLabel() {
final String label = label().replaceAll(" ", "_").toUpperCase(Locale.ENGLISH);
return Arrays.stream(ServerLink.Type.values()).filter(type -> type.name().equals(label)).findFirst();
}
// Validate a ServerUrl
void validate() throws IllegalStateException {
if (label().isEmpty()) {
throw new IllegalStateException("Server URL label cannot be empty");
}
if (url().isEmpty()) {
throw new IllegalStateException("Server URL cannot be empty");
}
if (groups().isEmpty()) {
throw new IllegalStateException("Server URL must have at least one group, or '*' to show on all groups");
}
try {
//noinspection ResultOfMethodCallIgnored
URI.create(url());
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Server URL is not a valid URI");
}
}
}

View File

@ -1,141 +1,126 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import de.themoep.minedown.adventure.MineDown;
import de.exlll.configlib.Comment;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.william278.annotaml.YamlComment;
import net.william278.annotaml.YamlFile;
import net.william278.annotaml.YamlKey;
import lombok.NoArgsConstructor;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.apache.commons.lang3.function.TriFunction;
import org.apache.commons.text.StringEscapeUtils;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
@YamlFile(header = """
Velocitab Config
Developed by William278
Placeholders: %players_online%, %max_players_online%, %local_players_online%, %current_date%, %current_time%, %username%, %server%, %ping%, %prefix%, %suffix%, %role%""")
public class Settings {
@YamlKey("headers")
private Map<String, String> headers = Map.of("default", "&rainbow&Running Velocitab by William278");
@SuppressWarnings("FieldMayBeFinal")
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Settings implements ConfigValidator {
@YamlKey("footers")
private Map<String, String> footers = Map.of("default", "[There are currently %players_online%/%max_players_online% players online](gray)");
public static final String CONFIG_HEADER = """
Velocitab Config
Developed by William278
Information: https://william278.net/project/velocitab
Documentation: https://william278.net/docs/velocitab""";
@YamlKey("formats")
private Map<String, String> formats = Map.of("default", "&7[%server%] &f%prefix%%username%");
@Comment("Check for updates on startup")
private boolean checkForUpdates = true;
@Getter
@YamlComment("Which text formatter to use (MINEDOWN or MINIMESSAGE)")
@YamlKey("formatting_type")
private Formatter formatter = Formatter.MINEDOWN;
@Comment("Whether to remove nametag from players' heads if the nametag associated with their server group is empty.")
private boolean removeNametags = false;
@Getter
@YamlKey("server_groups")
@YamlComment("The servers in each group of servers")
private Map<String, List<String>> serverGroups = Map.of("default", List.of("lobby1", "lobby2", "lobby3"));
@Comment("Whether to disable header and footer if they are empty and let backend servers handle them.")
private boolean disableHeaderFooterIfEmpty = true;
@Getter
@YamlKey("fallback_enabled")
@YamlComment("All servers which are not in other groups will be put in the fallback group.\n\"false\" will exclude them from Velocitab.")
@Comment("Which text formatter to use (MINIMESSAGE, MINEDOWN or LEGACY)")
private Formatter formatter = Formatter.MINIMESSAGE;
@Comment("All servers which are not in other groups will be put in the fallback group."
+ "\n\"false\" will exclude them from Velocitab.")
private boolean fallbackEnabled = true;
@Getter
@YamlKey("fallback_group")
@YamlComment("The formats to use for the fallback group.")
@Comment("The formats to use for the fallback group.")
private String fallbackGroup = "default";
@YamlKey("enable_papi_hook")
@Comment("Whether to show all players from all groups in the TAB list.")
private boolean showAllPlayersFromAllGroups = false;
@Comment("Whether to enable the PAPIProxyBridge hook for PAPI support")
private boolean enablePapiHook = true;
@YamlKey("enable_miniplaceholders_hook")
@YamlComment("If you are using MINIMESSAGE formatting, enable this to support MiniPlaceholders in formatting.")
@Comment("How long in seconds to cache PAPI placeholders for, in milliseconds. (0 to disable)")
private long papiCacheTime = 30000;
@Comment("If you are using MINIMESSAGE formatting, enable this to support MiniPlaceholders in formatting.")
private boolean enableMiniPlaceholdersHook = true;
@YamlKey("update_rate")
@YamlComment("How often to periodically update the TAB list, including header and footer, for all users.\nWill only update on player join/leave if set to 0.")
private int updateRate = 0;
@Comment("Whether to send scoreboard teams packets. Required for player list sorting and nametag formatting."
+ "\nTurn this off if you're using scoreboard teams on backend servers.")
private boolean sendScoreboardPackets = true;
public Settings(@NotNull Velocitab plugin) {
this.serverGroups = Map.of("default",
plugin.getServer().getAllServers().stream().map(server -> server.getServerInfo().getName()).toList()
);
}
@Comment("If built-in placeholders return a blank string, fallback to Placeholder API equivalents.\n"
+ "For example, if %prefix% returns a blank string, use %luckperms_prefix%. Requires PAPIProxyBridge.")
private boolean fallbackToPapiIfPlaceholderBlank = false;
@SuppressWarnings("unused")
public Settings() {
}
@Comment("Whether to sort players in the TAB list.")
private boolean sortPlayers = true;
@Comment("Remove gamemode spectator effect for other players in the TAB list.")
private boolean removeSpectatorEffect = true;
@Comment("Whether to enable the Plugin Message API (allows backend plugins to perform certain operations)")
private boolean enablePluginMessageApi = true;
@Comment("Whether to force sending tab list packets to all players, even if a packet for that action has already been sent. This could fix issues with some mods.")
private boolean forceSendingTabListPackets = false;
@Comment({"A list of links that will be sent to display on player pause menus (Minecraft 1.21+ clients only).",
"• Labels can be fully custom or built-in (one of 'bug_report', 'community_guidelines', 'support', 'status',",
" 'feedback', 'community', 'website', 'forums', 'news', or 'announcements').",
"• If you supply a url with a 'bug_report' label, it will be shown if the player is disconnected.",
"• Specify a set of server groups each URL should be sent on. Use '*' to show a URL to all groups."})
private List<ServerUrl> serverLinks = List.of(
new ServerUrl(
"<#00fb9a>About Velocitab</#00fb9a>",
"https://william278.net/project/velocitab"
)
);
@NotNull
public String getHeader(String serverGroup) {
return StringEscapeUtils.unescapeJava(
headers.getOrDefault(serverGroup, "&rainbow&Running Velocitab by William278"));
public List<ServerUrl> getUrlsForGroup(@NotNull Group group) {
return serverLinks.stream()
.filter(link -> link.groups().contains("*") || link.groups().contains(group.name()))
.toList();
}
@NotNull
public String getFooter(String serverGroup) {
return StringEscapeUtils.unescapeJava(
footers.getOrDefault(serverGroup, "[There are currently %players_online%/%max_players_online% players online](gray)"));
}
@NotNull
public String getFormat(String serverGroup) {
return StringEscapeUtils.unescapeJava(
formats.getOrDefault(serverGroup, "&7[%server%] &f%prefix%%username%"));
}
/**
* Get the server group that a server is in
*
* @param serverName The name of the server
* @return The server group that the server is in, or "default" if the server is not in a group
*/
public String getServerGroup(String serverName) {
return serverGroups.entrySet().stream()
.filter(entry -> entry.getValue().contains(serverName)).findFirst()
.map(Map.Entry::getKey).orElse(fallbackGroup);
}
public boolean isPapiHookEnabled() {
return enablePapiHook;
}
public boolean isMiniPlaceholdersHookEnabled() {
return enableMiniPlaceholdersHook;
}
public int getUpdateRate() {
return updateRate;
}
/**
* Different formatting markup options for the TAB list
*/
@SuppressWarnings("unused")
public enum Formatter {
MINEDOWN((text, player, plugin) -> new MineDown(text).toComponent()),
MINIMESSAGE((text, player, plugin) -> plugin.getMiniPlaceholdersHook()
.map(hook -> hook.format(text, player.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(text)));
private final TriFunction<String, TabPlayer, Velocitab, Component> formatter;
Formatter(@NotNull TriFunction<String, TabPlayer, Velocitab, Component> formatter) {
this.formatter = formatter;
}
@NotNull
public Component format(@NotNull String text, @NotNull TabPlayer player, @NotNull Velocitab plugin) {
return formatter.apply(text, player, plugin);
@Override
public void validateConfig(@NotNull Velocitab plugin) {
if (papiCacheTime < 0) {
throw new IllegalStateException("PAPI cache time must be greater than or equal to 0");
}
serverLinks.forEach(ServerUrl::validate);
}
}

View File

@ -0,0 +1,186 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.config;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.tab.Nametag;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@SuppressWarnings("FieldMayBeFinal")
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TabGroups implements ConfigValidator {
public static final String CONFIG_HEADER = """
Velocitab TabGroups
Developed by William278
Information: https://william278.net/project/velocitab
Documentation: https://william278.net/docs/velocitab""";
private static final Group DEFAULT_GROUP = new Group(
"default",
List.of("<rainbow:!2>Running Velocitab by William278 & AlexDev_</rainbow>"),
List.of("<gray>There are currently %players_online%/%max_players_online% players online</gray>"),
"<gray>[%server%] %prefix%%username%</gray>",
new Nametag("", ""),
Set.of("lobby", "survival", "creative", "minigames", "skyblock", "prison", "hub"),
List.of("%role_weight%", "%username_lower%"),
new LinkedHashMap<>() {{
put("%current_date_weekday_en-US%", List.of(
new PlaceholderReplacement("Monday", "<red>Monday</red>"),
new PlaceholderReplacement("Tuesday", "<gold>Tuesday</gold>"),
new PlaceholderReplacement("Else", "<green>Other day</green>")
));
}},
false,
1000,
1000,
false
);
public List<Group> groups = List.of(DEFAULT_GROUP);
@NotNull
@SuppressWarnings("unused")
public Group getGroupFromName(@NotNull String name) {
return groups.stream()
.filter(group -> group.name().equals(name))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No group with name %s found".formatted(name)));
}
public Optional<Group> getGroup(@NotNull String name) {
return groups.stream()
.filter(group -> group.name().equals(name))
.findFirst();
}
public Optional<Group> getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
final List<Group> groups = new ArrayList<>(this.groups);
final Optional<Group> defaultGroup = getGroup("default");
if (defaultGroup.isEmpty()) {
throw new IllegalStateException("No default group found");
}
// Ensure the default group is always checked last
groups.remove(defaultGroup.get());
groups.add(defaultGroup.get());
for (Group group : groups) {
if (group.registeredServers(plugin, false)
.stream()
.anyMatch(s -> s.getServerInfo().getName().equalsIgnoreCase(server))) {
return Optional.of(group);
}
}
if (!plugin.getSettings().isFallbackEnabled()) {
return Optional.empty();
}
return defaultGroup;
}
public int getPosition(@NotNull Group group) {
return groups.indexOf(group) + 1;
}
@Override
public void validateConfig(@NotNull Velocitab plugin) {
if (groups.isEmpty()) {
throw new IllegalStateException("No tab groups defined in config");
}
if (groups.stream().noneMatch(group -> group.name().equals("default"))) {
throw new IllegalStateException("No default tab group defined in config");
}
final Multimap<Group, String> missingKeys = getMissingKeys();
if (missingKeys.isEmpty()) {
return;
}
fixMissingKeys(plugin, missingKeys);
}
@NotNull
private Multimap<Group, String> getMissingKeys() {
final Multimap<Group, String> missingKeys = Multimaps.newSetMultimap(Maps.newHashMap(), HashSet::new);
for (Group group : groups) {
if (group.format() == null) {
missingKeys.put(group, "format");
}
if (group.nametag() == null) {
missingKeys.put(group, "nametag");
}
if (group.servers() == null) {
missingKeys.put(group, "servers");
}
if (group.sortingPlaceholders() == null) {
missingKeys.put(group, "sortingPlaceholders");
}
if (group.placeholderReplacements() == null) {
missingKeys.put(group, "placeholderReplacements");
}
}
return missingKeys;
}
private void fixMissingKeys(@NotNull Velocitab plugin, @NotNull Multimap<Group, String> missingKeys) {
missingKeys.forEach((group, keys) -> {
plugin.log("Missing required key(s) " + keys + " for group " + group.name());
plugin.log("Using default values for group " + group.name());
groups.remove(group);
group = new Group(
group.name(),
group.headers(),
group.footers(),
group.format() == null ? DEFAULT_GROUP.format() : group.format(),
group.nametag() == null ? DEFAULT_GROUP.nametag() : group.nametag(),
group.servers() == null ? DEFAULT_GROUP.servers() : group.servers(),
group.sortingPlaceholders() == null ? DEFAULT_GROUP.sortingPlaceholders() : group.sortingPlaceholders(),
group.placeholderReplacements() == null ? DEFAULT_GROUP.placeholderReplacements() : group.placeholderReplacements(),
group.collisions(),
group.headerFooterUpdateRate(),
group.placeholderUpdateRate(),
group.onlyListPlayersInSameServer()
);
groups.add(group);
});
plugin.saveTabGroups();
}
}

View File

@ -1,7 +1,27 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.hook;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.List;
import java.util.Optional;
@ -15,30 +35,30 @@ public abstract class Hook {
try {
plugin.log("Successfully hooked into LuckPerms");
return Optional.of(new LuckPermsHook(plugin));
} catch (Exception e) {
plugin.log("LuckPerms hook was not loaded: " + e.getMessage(), e);
} catch (Throwable e) {
plugin.log(Level.WARN, "LuckPerms hook was not loaded: " + e.getMessage(), e);
}
}
return Optional.empty();
}),
(plugin -> {
if (isPluginAvailable(plugin, "papiproxybridge") && plugin.getSettings().isPapiHookEnabled()) {
if (isPluginAvailable(plugin, "papiproxybridge") && plugin.getSettings().isEnablePapiHook()) {
try {
plugin.log("Successfully hooked into PAPIProxyBridge");
return Optional.of(new PapiHook(plugin));
} catch (Exception e) {
plugin.log("PAPIProxyBridge hook was not loaded: " + e.getMessage(), e);
return Optional.of(new PAPIProxyBridgeHook(plugin));
} catch (Throwable e) {
plugin.log(Level.WARN, "PAPIProxyBridge hook was not loaded: " + e.getMessage(), e);
}
}
return Optional.empty();
}),
(plugin -> {
if (isPluginAvailable(plugin, "miniplaceholders") && plugin.getSettings().isMiniPlaceholdersHookEnabled()) {
if (isPluginAvailable(plugin, "miniplaceholders") && plugin.getSettings().isEnableMiniPlaceholdersHook()) {
try {
plugin.log("Successfully hooked into MiniPlaceholders");
return Optional.of(new MiniPlaceholdersHook(plugin));
} catch (Exception e) {
plugin.log("MiniPlaceholders hook was not loaded: " + e.getMessage(), e);
} catch (Throwable e) {
plugin.log(Level.WARN, "MiniPlaceholders hook was not loaded: " + e.getMessage(), e);
}
}
return Optional.empty();

View File

@ -1,31 +1,65 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.hook;
import com.google.common.collect.Maps;
import com.velocitypowered.api.proxy.Player;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.event.EventSubscription;
import net.luckperms.api.event.user.UserDataRecalculateEvent;
import net.luckperms.api.model.group.Group;
import net.luckperms.api.model.user.User;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.PlayerTabList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.OptionalInt;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class LuckPermsHook extends Hook {
private int highestWeight = Role.DEFAULT_WEIGHT;
private final LuckPerms api;
private final EventSubscription<UserDataRecalculateEvent> event;
private final Map<UUID, Long> lastUpdate;
private boolean enabled;
public LuckPermsHook(@NotNull Velocitab plugin) throws IllegalStateException {
super(plugin);
this.api = LuckPermsProvider.get();
api.getEventBus().subscribe(plugin, UserDataRecalculateEvent.class, this::onLuckPermsGroupUpdate);
this.lastUpdate = Maps.newConcurrentMap();
this.event = api.getEventBus().subscribe(
plugin, UserDataRecalculateEvent.class, this::onLuckPermsGroupUpdate
);
this.enabled = true;
}
public void closeEvent() {
event.close();
this.enabled = false;
}
@NotNull
@ -38,50 +72,80 @@ public class LuckPermsHook extends Hook {
if (metaData.getPrimaryGroup() == null) {
return Role.DEFAULT_ROLE;
}
final Optional<Group> group = getGroup(metaData.getPrimaryGroup());
return new Role(
getWeight(metaData.getPrimaryGroup()).orElse(0),
group.map(this::getGroupWeight).orElse(Role.DEFAULT_WEIGHT),
metaData.getPrimaryGroup(),
group.map(Group::getDisplayName).orElse(metaData.getPrimaryGroup()),
metaData.getPrefix(),
metaData.getSuffix()
);
}
@Nullable
public String getMeta(@NotNull Player player, @NotNull String key) {
return getUser(player.getUniqueId()).getCachedData().getMetaData().getMetaValue(key);
}
public void onLuckPermsGroupUpdate(@NotNull UserDataRecalculateEvent event) {
// Prevent duplicate events
if (lastUpdate.getOrDefault(event.getUser().getUniqueId(), 0L) > System.currentTimeMillis() - 100) {
return;
}
lastUpdate.put(event.getUser().getUniqueId(), System.currentTimeMillis());
if (!enabled) {
return;
}
final PlayerTabList tabList = plugin.getTabList();
plugin.getServer().getPlayer(event.getUser().getUniqueId())
.ifPresent(player -> plugin.getServer().getScheduler()
.buildTask(plugin, () -> plugin.getTabList()
.onUpdate(new TabPlayer(
player,
getRoleFromMetadata(event.getData().getMetaData()),
getHighestWeight()
)))
.buildTask(plugin, () -> {
final Optional<TabPlayer> tabPlayerOptional = tabList.getTabPlayer(player);
if (tabPlayerOptional.isEmpty()) {
return;
}
final TabPlayer tabPlayer = tabPlayerOptional.get();
final Role oldRole = tabPlayer.getRole();
final Role newRole = getRoleFromMetadata(event.getUser().getCachedData().getMetaData());
if (oldRole.equals(newRole)) {
return;
}
tabPlayer.setRole(newRole);
tabList.updatePlayerDisplayName(tabPlayer);
tabList.getVanishTabList().recalculateVanishForPlayer(tabPlayer);
checkRoleUpdate(tabPlayer, oldRole);
})
.delay(500, TimeUnit.MILLISECONDS)
.schedule());
}
private OptionalInt getWeight(@Nullable String groupName) {
final Group group;
if (groupName == null || (group = api.getGroupManager().getGroup(groupName)) == null) {
return OptionalInt.empty();
// Get a group by name
private Optional<Group> getGroup(@Nullable String groupName) {
if (groupName == null) {
return Optional.empty();
}
return group.getWeight();
return Optional.ofNullable(api.getGroupManager().getGroup(groupName));
}
public int getHighestWeight() {
if (highestWeight == Role.DEFAULT_WEIGHT) {
api.getGroupManager().getLoadedGroups().forEach(group -> {
final OptionalInt weight = group.getWeight();
if (weight.isPresent() && weight.getAsInt() > highestWeight) {
highestWeight = weight.getAsInt();
}
});
}
return highestWeight;
// Get the weight of a group
private int getGroupWeight(@NotNull Group group) {
return group.getWeight().orElse(Role.DEFAULT_WEIGHT);
}
private User getUser(@NotNull UUID uuid) {
return api.getUserManager().getUser(uuid);
}
private void checkRoleUpdate(@NotNull TabPlayer player, @NotNull Role oldRole) {
if (oldRole.equals(player.getRole())) {
return;
}
plugin.getTabList().updatePlayer(player, false);
}
}

View File

@ -1,3 +1,22 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.hook;
import io.github.miniplaceholders.api.MiniPlaceholders;
@ -6,16 +25,28 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class MiniPlaceholdersHook extends Hook {
private final VelocitabMiniExpansion expansion;
public MiniPlaceholdersHook(@NotNull Velocitab plugin) {
super(plugin);
this.expansion = new VelocitabMiniExpansion(plugin);
expansion.registerExpansion();
}
@NotNull
public Component format(@NotNull String text, @NotNull Audience player) {
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getAudienceGlobalPlaceholders(player));
public Component format(@NotNull String text, @NotNull Audience player, @Nullable Audience viewer) {
if (viewer == null) {
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getAudienceGlobalPlaceholders(player));
}
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getRelationalGlobalPlaceholders(player, viewer));
}
public void unregisterExpansion() {
expansion.unregisterExpansion();
}
}

View File

@ -0,0 +1,61 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.hook;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.velocitypowered.api.proxy.Player;
import net.william278.papiproxybridge.api.PlaceholderAPI;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class PAPIProxyBridgeHook extends Hook {
private final PlaceholderAPI api;
public PAPIProxyBridgeHook(@NotNull Velocitab plugin) {
super(plugin);
this.api = PlaceholderAPI.createInstance();
this.api.setCacheExpiry(Math.max(0, plugin.getSettings().getPapiCacheTime()));
this.api.setRequestTimeout(1500);
}
public CompletableFuture<String> formatPlaceholders(@NotNull String input, @NotNull Player player) {
return api.formatPlaceholders(input, player.getUniqueId());
}
public CompletableFuture<Map<String, String>> parsePlaceholders(@NotNull List<String> input, @NotNull Player player) {
final Map<String, String> map = Maps.newConcurrentMap();
final List<CompletableFuture<String>> futures = Lists.newArrayList();
for (String s : input) {
final CompletableFuture<String> future = formatPlaceholders(s, player);
futures.add(future);
future.thenAccept(r -> map.put(s, r));
}
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).thenApply(v -> map);
}
}

View File

@ -1,24 +0,0 @@
package net.william278.velocitab.hook;
import com.velocitypowered.api.proxy.Player;
import net.william278.papiproxybridge.api.PlaceholderAPI;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public class PapiHook extends Hook {
private final PlaceholderAPI api;
public PapiHook(@NotNull Velocitab plugin) {
super(plugin);
this.api = PlaceholderAPI.getInstance();
}
public CompletableFuture<String> formatPapiPlaceholders(@NotNull String input, @NotNull Player player) {
return api.formatPlaceholders(input, player.getUniqueId());
}
}

View File

@ -0,0 +1,143 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.hook;
import com.velocitypowered.api.proxy.Player;
import io.github.miniplaceholders.api.Expansion;
import io.github.miniplaceholders.api.MiniPlaceholders;
import io.github.miniplaceholders.api.utils.TagsUtils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.hook.miniconditions.MiniConditionManager;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public class VelocitabMiniExpansion {
private final Velocitab plugin;
private final MiniConditionManager miniConditionManager;
private Expansion expansion;
public VelocitabMiniExpansion(Velocitab plugin) {
this.plugin = plugin;
this.miniConditionManager = new MiniConditionManager(plugin);
}
public void registerExpansion() {
final Expansion.Builder builder = Expansion.builder("velocitab");
builder.relationalPlaceholder("condition", ((a1, a2, queue, ctx) -> {
if (!(a2 instanceof Player target)) {
return TagsUtils.EMPTY_TAG;
}
if (!(a1 instanceof Player audience)) {
return TagsUtils.EMPTY_TAG;
}
return Tag.selfClosingInserting(miniConditionManager.checkConditions(target, audience, queue));
}));
builder.relationalPlaceholder("who-is-seeing", ((a1, a2, queue, ctx) -> {
if (!(a2 instanceof Player target)) {
return TagsUtils.EMPTY_TAG;
}
if (!(a1 instanceof Player)) {
return TagsUtils.EMPTY_TAG;
}
return Tag.selfClosingInserting(Component.text(target.getUsername()));
}));
builder.relationalPlaceholder("perm", ((a1, a2, queue, ctx) -> {
if (!(a2 instanceof Player target)) {
return TagsUtils.EMPTY_TAG;
}
if (!(a1 instanceof Player audience)) {
return TagsUtils.EMPTY_TAG;
}
final Optional<TabPlayer> targetOptional = plugin.getTabList().getTabPlayer(audience);
if (targetOptional.isEmpty()) {
return TagsUtils.EMPTY_TAG;
}
final TabPlayer targetPlayer = targetOptional.get();
if (!queue.hasNext()) {
return TagsUtils.EMPTY_TAG;
}
final String permission = queue.pop().value();
if (!queue.hasNext()) {
return TagsUtils.EMPTY_TAG;
}
if (!target.hasPermission(permission)) {
return TagsUtils.EMPTY_TAG;
}
final String value = fixValue(popAll(queue));
final String replaced = Placeholder.replaceInternal(value, plugin, targetPlayer).first();
return Tag.selfClosingInserting(MiniMessage.miniMessage().deserialize(replaced, MiniPlaceholders.getAudienceGlobalPlaceholders(audience)));
}));
builder.relationalPlaceholder("vanish", ((a1, otherAudience, queue, ctx) -> {
if (!(otherAudience instanceof Player target)) {
return TagsUtils.EMPTY_TAG;
}
if (!(a1 instanceof Player audience)) {
return TagsUtils.EMPTY_TAG;
}
return Tag.selfClosingInserting(Component.text(plugin.getVanishManager().getIntegration().canSee(audience.getUsername(), target.getUsername())));
}));
plugin.getLogger().info("Registered Velocitab MiniExpansion");
expansion = builder.build();
expansion.register();
}
public void unregisterExpansion() {
expansion.unregister();
}
@NotNull
private String popAll(@NotNull ArgumentQueue queue) {
final StringBuilder builder = new StringBuilder();
int i = 0;
while (queue.hasNext()) {
if (i > 0) {
builder.append(":");
}
builder.append(queue.pop().value());
i++;
}
return builder.toString();
}
@NotNull
private String fixValue(@NotNull String value) {
return value.replace("*LESS2*", "<").replace("*GREATER2*", ">");
}
}

View File

@ -0,0 +1,216 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.hook.miniconditions;
import com.google.common.collect.Lists;
import com.velocitypowered.api.proxy.Player;
import net.jodah.expiringmap.ExpiringMap;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.player.TabPlayer;
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.MapContext;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MiniConditionManager {
public final static Map<String, String> REPLACE = Map.of(
"\"", "-q-",
"'", "-a-"
);
public final static Map<String, String> REPLACE_2 = Map.of(
"*LESS3*", "<",
"*GREATER3*", ">",
"*LESS2*", "<",
"*GREATER2*", ">"
);
private final static Map<String, String> REPLACE_3 = Map.of(
"?dp?", ":"
);
private final Velocitab plugin;
private final JexlEngine jexlEngine;
private final JexlContext jexlContext;
private final Pattern targetPlaceholderPattern;
private final Pattern miniEscapeEndTags;
private final Map<String, Object> cachedExpressions;
public MiniConditionManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.jexlEngine = createJexlEngine();
this.jexlContext = createJexlContext();
this.targetPlaceholderPattern = Pattern.compile("%target_(\\w+)?%");
this.miniEscapeEndTags = Pattern.compile("</(\\w+)>");
this.cachedExpressions = ExpiringMap.builder()
.expiration(5, TimeUnit.MINUTES)
.build();
}
@NotNull
private JexlEngine createJexlEngine() {
return new JexlBuilder().create();
}
@NotNull
private JexlContext createJexlContext() {
final JexlContext jexlContext = new MapContext();
jexlContext.set("startsWith", new StartsWith());
jexlContext.set("endsWith", new EndsWith());
return jexlContext;
}
@NotNull
public Component checkConditions(@NotNull Player target, @NotNull Player audience, @NotNull ArgumentQueue queue) {
final List<String> parameters = collectParameters(queue);
if (parameters.isEmpty()) {
plugin.getLogger().warn("Empty condition");
return Component.empty();
}
String condition = decodeCondition(parameters.get(0));
if (parameters.size() < 3) {
plugin.getLogger().warn("Invalid condition: Missing true/false values for condition: {}", condition);
return Component.empty();
}
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(target);
if (tabPlayer.isEmpty()) {
return Component.empty();
}
condition = Placeholder.replaceInternal(condition, plugin, tabPlayer.get()).first();
final String falseValue = processFalseValue(parameters.get(2));
final String expression = buildExpression(condition);
return evaluateAndFormatCondition(expression, target, audience, parameters.get(1), falseValue);
}
@NotNull
private List<String> collectParameters(@NotNull ArgumentQueue queue) {
final List<String> parameters = Lists.newArrayList();
while (queue.hasNext()) {
String param = queue.pop().value();
for (Map.Entry<String, String> entry : REPLACE_2.entrySet()) {
param = param.replace(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : REPLACE_3.entrySet()) {
param = param.replace(entry.getKey(), entry.getValue());
}
parameters.add(param);
}
return parameters;
}
@NotNull
private String decodeCondition(@NotNull String condition) {
for (Map.Entry<String, String> entry : REPLACE.entrySet()) {
condition = condition.replace(entry.getValue(), entry.getKey());
condition = condition.replace(entry.getKey() + entry.getKey(), entry.getKey());
}
return condition;
}
@NotNull
private String processFalseValue(@NotNull String falseValue) {
final Matcher matcher = miniEscapeEndTags.matcher(falseValue);
if (matcher.find()) {
final String tag = matcher.group(1);
if (falseValue.startsWith("</" + tag + ">")) {
falseValue = falseValue.substring(tag.length() + 3);
}
}
return falseValue;
}
@NotNull
private String buildExpression(@NotNull String condition) {
return condition.replace("and", "&&").replace("or", "||")
.replace("AND", "&&").replace("OR", "||");
}
@NotNull
private Component evaluateAndFormatCondition(@NotNull String expression, @NotNull Player target, @NotNull Player audience, @NotNull String trueValue, @NotNull String falseValue) {
final String targetString = parseTargetPlaceholders(expression, target);
try {
final Object result = evaluateExpression(targetString);
if (result instanceof Boolean) {
final boolean boolResult = (Boolean) result;
final String value = boolResult ? trueValue : falseValue;
return plugin.getMiniPlaceholdersHook().orElseThrow().format(value, target, audience);
}
} catch (Exception e) {
plugin.getLogger().warn("Failed to evaluate condition: {} error: {}", expression, e.getMessage());
}
return Component.empty();
}
@NotNull
private Object evaluateExpression(@NotNull String expression) {
return cachedExpressions.computeIfAbsent(expression, key -> jexlEngine.createExpression(key).evaluate(jexlContext));
}
@NotNull
private String parseTargetPlaceholders(@NotNull String input, @NotNull Player target) {
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(target);
if (tabPlayer.isEmpty()) {
return input;
}
return targetPlaceholderPattern.matcher(input).replaceAll(match -> {
final String placeholder = match.group(1);
if (placeholder == null) {
return "";
}
final String text = "%" + placeholder + "%";
final Optional<String> placeholderValue = tabPlayer.get().getCachedPlaceholderValue(text);
return placeholderValue.orElse(text);
});
}
@SuppressWarnings("unused")
private static class StartsWith {
public boolean startsWith(String str, String prefix) {
return str != null && str.startsWith(prefix);
}
}
@SuppressWarnings("unused")
private static class EndsWith {
public boolean endsWith(String str, String suffix) {
return str != null && str.endsWith(suffix);
}
}
}

View File

@ -0,0 +1,91 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import com.velocitypowered.api.event.AwaitingEventExecutor;
import com.velocitypowered.api.event.EventTask;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.network.Connections;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.DefaultChannelPipeline;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
public class PacketEventManager {
private static final String KEY = "velocitab";
private final Velocitab plugin;
public PacketEventManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.loadPlayers();
this.loadListeners();
}
private void loadPlayers() {
plugin.getServer().getAllPlayers().forEach(this::injectPlayer);
}
private void loadListeners() {
plugin.getServer().getEventManager().register(plugin, PostLoginEvent.class,
(AwaitingEventExecutor<PostLoginEvent>) postLoginEvent -> EventTask.withContinuation(continuation -> {
injectPlayer(postLoginEvent.getPlayer());
continuation.resume();
}));
plugin.getServer().getEventManager().register(plugin, DisconnectEvent.class,
(AwaitingEventExecutor<DisconnectEvent>) disconnectEvent ->
disconnectEvent.getLoginStatus() == DisconnectEvent.LoginStatus.CONFLICTING_LOGIN
? null
: EventTask.async(() -> removePlayer(disconnectEvent.getPlayer())));
}
public void injectPlayer(@NotNull Player player) {
final PlayerChannelHandler handler = new PlayerChannelHandler(plugin, player);
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
removePlayer(player);
connectedPlayer.getConnection()
.getChannel()
.pipeline()
.addBefore(Connections.HANDLER, KEY, handler);
}
public void removePlayer(@NotNull Player player) {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
final Channel channel = connectedPlayer.getConnection().getChannel();
final ChannelHandler handler = channel.pipeline().get(KEY);
if (handler == null) {
return;
}
if (channel.pipeline() instanceof DefaultChannelPipeline defaultChannelPipeline) {
defaultChannelPipeline.removeIfExists(KEY);
return;
}
plugin.getLogger().warn("Failed to remove player {} from Velocitab packet handler {}",
player.getUsername(), channel.pipeline().getClass().getName());
}
}

View File

@ -0,0 +1,173 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import io.netty.util.collection.IntObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.*;
import java.util.function.Supplier;
// Based on VPacketEvents PacketRegistration API
public final class PacketRegistration<P extends MinecraftPacket> {
private final Class<P> packetClass;
private Supplier<P> packetSupplier;
private ProtocolUtils.Direction direction;
private StateRegistry stateRegistry;
private final List<StateRegistry.PacketMapping> mappings = new ArrayList<>();
public PacketRegistration<P> packetSupplier(final @NotNull Supplier<P> packetSupplier) {
this.packetSupplier = packetSupplier;
return this;
}
public PacketRegistration<P> direction(final ProtocolUtils.Direction direction) {
this.direction = direction;
return this;
}
public PacketRegistration<P> stateRegistry(final @NotNull StateRegistry stateRegistry) {
this.stateRegistry = stateRegistry;
return this;
}
public PacketRegistration<P> mapping(
final int id,
final ProtocolVersion version,
final boolean encodeOnly
) {
try {
final StateRegistry.PacketMapping mapping = (StateRegistry.PacketMapping) PACKET_MAPPING$map.invoke(
id, version, encodeOnly);
this.mappings.add(mapping);
} catch (Throwable t) {
throw new RuntimeException(t);
}
return this;
}
public void register() {
try {
final StateRegistry.PacketRegistry packetRegistry = direction == ProtocolUtils.Direction.CLIENTBOUND
? (StateRegistry.PacketRegistry) STATE_REGISTRY$clientBound.invoke(stateRegistry)
: (StateRegistry.PacketRegistry) STATE_REGISTRY$serverBound.invoke(stateRegistry);
PACKET_REGISTRY$register.invoke(
packetRegistry,
packetClass,
packetSupplier,
mappings.toArray(StateRegistry.PacketMapping[]::new)
);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
@SuppressWarnings("unchecked")
public void unregister() {
try {
final StateRegistry.PacketRegistry packetRegistry = direction == ProtocolUtils.Direction.CLIENTBOUND
? (StateRegistry.PacketRegistry) STATE_REGISTRY$clientBound.invoke(stateRegistry)
: (StateRegistry.PacketRegistry) STATE_REGISTRY$serverBound.invoke(stateRegistry);
Map<ProtocolVersion, StateRegistry.PacketRegistry.ProtocolRegistry> versions = (Map<ProtocolVersion, StateRegistry.PacketRegistry.ProtocolRegistry>) PACKET_REGISTRY$versions.invoke(packetRegistry);
versions.forEach((protocolVersion, protocolRegistry) -> {
try {
IntObjectMap<Supplier<?>> packetIdToSupplier = (IntObjectMap<Supplier<?>>) PACKET_REGISTRY$packetIdToSupplier.invoke(protocolRegistry);
Object2IntMap<Class<?>> packetClassToId = (Object2IntMap<Class<?>>) PACKET_REGISTRY$packetClassToId.invoke(protocolRegistry);
Set.copyOf(packetIdToSupplier.keySet()).stream()
.filter(supplier -> packetIdToSupplier.get(supplier).get().getClass().equals(packetClass))
.forEach(packetIdToSupplier::remove);
packetClassToId.values().intStream()
.filter(id -> Objects.equals(packetClassToId.getInt(packetClass), id))
.forEach(packetClassToId::removeInt);
} catch (Throwable t) {
throw new RuntimeException(t);
}
});
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
public static <P extends MinecraftPacket> PacketRegistration<P> of(Class<P> packetClass) {
return new PacketRegistration<>(packetClass);
}
private PacketRegistration(final @NotNull Class<P> packetClass) {
this.packetClass = packetClass;
}
private static final MethodHandle STATE_REGISTRY$clientBound;
private static final MethodHandle STATE_REGISTRY$serverBound;
private static final MethodHandle PACKET_REGISTRY$register;
private static final MethodHandle PACKET_REGISTRY$packetIdToSupplier;
private static final MethodHandle PACKET_REGISTRY$packetClassToId;
private static final MethodHandle PACKET_REGISTRY$versions;
private static final MethodHandle PACKET_MAPPING$map;
static {
final MethodHandles.Lookup lookup = MethodHandles.lookup();
try {
final MethodHandles.Lookup stateRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.class, lookup);
STATE_REGISTRY$clientBound = stateRegistryLookup.findGetter(
StateRegistry.class, "clientbound", StateRegistry.PacketRegistry.class);
STATE_REGISTRY$serverBound = stateRegistryLookup.findGetter(
StateRegistry.class, "serverbound", StateRegistry.PacketRegistry.class);
final MethodType mapType = MethodType.methodType(
StateRegistry.PacketMapping.class, Integer.TYPE, ProtocolVersion.class, Boolean.TYPE);
PACKET_MAPPING$map = stateRegistryLookup.findStatic(
StateRegistry.class, "map", mapType);
final MethodHandles.Lookup packetRegistryLookup = MethodHandles.privateLookupIn(
StateRegistry.PacketRegistry.class, lookup);
final MethodType registerType = MethodType.methodType(
void.class, Class.class, Supplier.class, StateRegistry.PacketMapping[].class);
PACKET_REGISTRY$register = packetRegistryLookup.findVirtual(
StateRegistry.PacketRegistry.class, "register", registerType);
PACKET_REGISTRY$versions = packetRegistryLookup.findGetter(
StateRegistry.PacketRegistry.class, "versions", Map.class);
final MethodHandles.Lookup protocolRegistryLookup = MethodHandles.privateLookupIn(
StateRegistry.PacketRegistry.ProtocolRegistry.class, lookup);
PACKET_REGISTRY$packetIdToSupplier = protocolRegistryLookup.findGetter(
StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetIdToSupplier", IntObjectMap.class);
PACKET_REGISTRY$packetClassToId = protocolRegistryLookup.findGetter(
StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetClassToId", Object2IntMap.class);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,137 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import lombok.RequiredArgsConstructor;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
@RequiredArgsConstructor
public class PlayerChannelHandler extends ChannelDuplexHandler {
private final Velocitab plugin;
private final Player player;
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof final UpdateTeamsPacket updateTeamsPacket && plugin.getSettings().isSendScoreboardPackets()) {
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
if (!scoreboardManager.handleTeams()) {
super.write(ctx, msg, promise);
return;
}
if (updateTeamsPacket.isRemoveTeam()) {
super.write(ctx, msg, promise);
return;
}
if (scoreboardManager.isInternalTeam(updateTeamsPacket.teamName())) {
super.write(ctx, msg, promise);
return;
}
if (!updateTeamsPacket.hasEntities()) {
super.write(ctx, msg, promise);
return;
}
if (updateTeamsPacket.entities().stream().noneMatch(entity -> plugin.getServer().getPlayer(entity).isPresent())) {
super.write(ctx, msg, promise);
return;
}
// Cancel packet if the backend is trying to send a team packet with an online player.
// This is to prevent conflicts with Velocitab teams.
plugin.getLogger().warn("Cancelled team \"{}\" packet from backend for player {}. " +
"We suggest disabling \"send_scoreboard_packets\" in Velocitab's config.yml file, " +
"but note this will disable TAB sorting",
updateTeamsPacket.teamName(), player.getUsername());
return;
}
if (!(msg instanceof final UpsertPlayerInfoPacket minecraftPacket)) {
super.write(ctx, msg, promise);
return;
}
try {
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
if (tabPlayer.isEmpty() && !isFutureTabPlayer()) {
super.write(ctx, msg, promise);
return;
}
if (plugin.getSettings().isRemoveSpectatorEffect() && minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE)) {
forceGameMode(minecraftPacket.getEntries());
}
//fix for duplicate entries
if (minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) {
minecraftPacket.getEntries().stream()
.filter(entry -> entry.getProfile() != null && !entry.getProfile().getId().equals(entry.getProfileId()))
.forEach(entry -> entry.setListed(false));
}
if (!minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER) && !minecraftPacket.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) {
super.write(ctx, msg, promise);
return;
}
if (minecraftPacket.getEntries().stream().allMatch(entry -> entry.getProfile() != null && entry.getProfile().getName().startsWith("CIT"))) {
super.write(ctx, msg, promise);
return;
}
super.write(ctx, msg, promise);
} catch (Exception e) {
plugin.getLogger().error("An error occurred while handling a packet", e);
super.write(ctx, msg, promise);
}
}
private void forceGameMode(@NotNull List<UpsertPlayerInfoPacket.Entry> entries) {
entries.stream()
.filter(entry -> entry.getProfileId() != null && entry.getGameMode() == 3 && !entry.getProfileId().equals(player.getUniqueId()))
.forEach(entry -> entry.setGameMode(0));
}
private boolean isFutureTabPlayer() {
final String serverName = player.getCurrentServer()
.map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("");
final Optional<Group> groupOptional = plugin.getTabList().getGroup(serverName);
return groupOptional.isPresent();
}
}

View File

@ -0,0 +1,126 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Adapter for handling the UpdateTeamsPacket for Minecraft 1.13.2-1.15.2
*/
@SuppressWarnings("DuplicatedCode")
public class Protocol404Adapter extends TeamsPacketAdapter {
private final GsonComponentSerializer serializer;
public Protocol404Adapter(@NotNull Velocitab plugin) {
super(plugin, Set.of(
ProtocolVersion.MINECRAFT_1_13,
ProtocolVersion.MINECRAFT_1_13_1,
ProtocolVersion.MINECRAFT_1_13_2,
ProtocolVersion.MINECRAFT_1_14,
ProtocolVersion.MINECRAFT_1_14_1,
ProtocolVersion.MINECRAFT_1_14_2,
ProtocolVersion.MINECRAFT_1_14_3,
ProtocolVersion.MINECRAFT_1_14_4,
ProtocolVersion.MINECRAFT_1_15,
ProtocolVersion.MINECRAFT_1_15_1,
ProtocolVersion.MINECRAFT_1_15_2
));
serializer = GsonComponentSerializer.colorDownsamplingGson();
}
public Protocol404Adapter(@NotNull Velocitab plugin, Set<ProtocolVersion> protocolVersions) {
super(plugin, protocolVersions);
serializer = GsonComponentSerializer.colorDownsamplingGson();
}
@Override
public void decode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
packet.teamName(ProtocolUtils.readString(byteBuf));
UpdateTeamsPacket.UpdateMode mode = UpdateTeamsPacket.UpdateMode.byId(byteBuf.readByte());
packet.mode(mode);
if (mode == UpdateTeamsPacket.UpdateMode.REMOVE_TEAM) {
return;
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.UPDATE_INFO) {
packet.displayName(readComponent(byteBuf));
packet.friendlyFlags(UpdateTeamsPacket.FriendlyFlag.fromBitMask(byteBuf.readByte()));
packet.nametagVisibility(UpdateTeamsPacket.NametagVisibility.byId(ProtocolUtils.readString(byteBuf)));
packet.collisionRule(UpdateTeamsPacket.CollisionRule.byId(ProtocolUtils.readString(byteBuf)));
packet.color(byteBuf.readByte());
packet.prefix(readComponent(byteBuf));
packet.suffix(readComponent(byteBuf));
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.ADD_PLAYERS || mode == UpdateTeamsPacket.UpdateMode.REMOVE_PLAYERS) {
int count = ProtocolUtils.readVarInt(byteBuf);
List<String> entities = new ArrayList<>();
for (int i = 0; i < count; i++) {
entities.add(ProtocolUtils.readString(byteBuf));
}
packet.entities(entities);
}
}
@Override
public void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
ProtocolUtils.writeString(byteBuf, packet.teamName());
UpdateTeamsPacket.UpdateMode mode = packet.mode();
byteBuf.writeByte(mode.id());
if (mode == UpdateTeamsPacket.UpdateMode.REMOVE_TEAM) {
return;
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.UPDATE_INFO) {
writeComponent(byteBuf, packet.displayName());
byteBuf.writeByte(UpdateTeamsPacket.FriendlyFlag.toBitMask(packet.friendlyFlags()));
ProtocolUtils.writeString(byteBuf, packet.nametagVisibility().id());
ProtocolUtils.writeString(byteBuf, packet.collisionRule().id());
byteBuf.writeByte(packet.color());
writeComponent(byteBuf, packet.prefix());
writeComponent(byteBuf, packet.suffix());
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.ADD_PLAYERS || mode == UpdateTeamsPacket.UpdateMode.REMOVE_PLAYERS) {
List<String> entities = packet.entities();
ProtocolUtils.writeVarInt(byteBuf, entities != null ? entities.size() : 0);
for (String entity : entities != null ? entities : new ArrayList<String>()) {
ProtocolUtils.writeString(byteBuf, entity);
}
}
}
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
ProtocolUtils.writeString(buf, serializer.serialize(component));
}
@NotNull
protected Component readComponent(@NotNull ByteBuf buf) {
return serializer.deserialize(ProtocolUtils.readString(buf));
}
}

View File

@ -0,0 +1,124 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Adapter for handling the UpdateTeamsPacket for Minecraft 1.8.x
*/
@SuppressWarnings("DuplicatedCode")
public class Protocol48Adapter extends TeamsPacketAdapter {
private final LegacyComponentSerializer serializer;
public Protocol48Adapter(@NotNull Velocitab plugin) {
super(plugin, Set.of(ProtocolVersion.MINECRAFT_1_8, ProtocolVersion.MINECRAFT_1_12_2));
serializer = LegacyComponentSerializer.legacySection();
}
@Override
public void decode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
packet.teamName(ProtocolUtils.readString(byteBuf));
UpdateTeamsPacket.UpdateMode mode = UpdateTeamsPacket.UpdateMode.byId(byteBuf.readByte());
packet.mode(mode);
if (mode == UpdateTeamsPacket.UpdateMode.REMOVE_TEAM) {
return;
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.UPDATE_INFO) {
packet.displayName(readComponent(byteBuf));
packet.prefix(readComponent(byteBuf));
packet.suffix(readComponent(byteBuf));
packet.friendlyFlags(UpdateTeamsPacket.FriendlyFlag.fromBitMask(byteBuf.readByte()));
packet.nametagVisibility(UpdateTeamsPacket.NametagVisibility.byId(ProtocolUtils.readString(byteBuf)));
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_12_2) >= 0) {
packet.collisionRule(UpdateTeamsPacket.CollisionRule.byId(ProtocolUtils.readString(byteBuf)));
}
packet.color(byteBuf.readByte());
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.ADD_PLAYERS || mode == UpdateTeamsPacket.UpdateMode.REMOVE_PLAYERS) {
int count = ProtocolUtils.readVarInt(byteBuf);
List<String> entities = new ArrayList<>();
for (int i = 0; i < count; i++) {
entities.add(ProtocolUtils.readString(byteBuf));
}
packet.entities(entities);
}
}
@Override
public void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion) {
ProtocolUtils.writeString(byteBuf, shrinkString(packet.teamName()));
UpdateTeamsPacket.UpdateMode mode = packet.mode();
byteBuf.writeByte(mode.id());
if (mode == UpdateTeamsPacket.UpdateMode.REMOVE_TEAM) {
return;
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.UPDATE_INFO) {
writeComponent(byteBuf, packet.displayName());
writeComponent(byteBuf, packet.prefix());
writeComponent(byteBuf, packet.suffix());
byteBuf.writeByte(UpdateTeamsPacket.FriendlyFlag.toBitMask(packet.friendlyFlags()));
ProtocolUtils.writeString(byteBuf, packet.nametagVisibility().id());
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_12_2) >= 0) {
ProtocolUtils.writeString(byteBuf, packet.collisionRule().id());
}
byteBuf.writeByte(packet.color());
}
if (mode == UpdateTeamsPacket.UpdateMode.CREATE_TEAM || mode == UpdateTeamsPacket.UpdateMode.ADD_PLAYERS || mode == UpdateTeamsPacket.UpdateMode.REMOVE_PLAYERS) {
List<String> entities = packet.entities();
ProtocolUtils.writeVarInt(byteBuf, entities != null ? entities.size() : 0);
for (String entity : entities != null ? entities : new ArrayList<String>()) {
ProtocolUtils.writeString(byteBuf, entity);
}
}
}
/**
* Returns a shortened version of the given string, with a maximum length of 16 characters.
* This is used to ensure that the team name, display name, prefix and suffix are not too long for the client.
*
* @param string the string to be shortened
* @return the shortened string
*/
@NotNull
private String shrinkString(@NotNull String string) {
return string.substring(0, Math.min(string.length(), 16));
}
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
ProtocolUtils.writeString(buf, shrinkString(serializer.serialize(component)));
}
@NotNull
protected Component readComponent(@NotNull ByteBuf buf) {
return serializer.deserialize(ProtocolUtils.readString(buf));
}
}

View File

@ -0,0 +1,70 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
/**
* Adapter for handling the UpdateTeamsPacket for Minecraft 1.16-1.20.2
*/
public class Protocol735Adapter extends Protocol404Adapter {
private final GsonComponentSerializer serializer;
public Protocol735Adapter(@NotNull Velocitab plugin) {
super(plugin, Set.of(
ProtocolVersion.MINECRAFT_1_16,
ProtocolVersion.MINECRAFT_1_16_1,
ProtocolVersion.MINECRAFT_1_16_2,
ProtocolVersion.MINECRAFT_1_16_3,
ProtocolVersion.MINECRAFT_1_16_4,
ProtocolVersion.MINECRAFT_1_17,
ProtocolVersion.MINECRAFT_1_17_1,
ProtocolVersion.MINECRAFT_1_18,
ProtocolVersion.MINECRAFT_1_18_2,
ProtocolVersion.MINECRAFT_1_19,
ProtocolVersion.MINECRAFT_1_19_1,
ProtocolVersion.MINECRAFT_1_19_3,
ProtocolVersion.MINECRAFT_1_19_4,
ProtocolVersion.MINECRAFT_1_20,
ProtocolVersion.MINECRAFT_1_20_2
));
serializer = GsonComponentSerializer.gson();
}
@Override
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
ProtocolUtils.writeString(buf, serializer.serialize(component));
}
@NotNull
protected Component readComponent(@NotNull ByteBuf buf) {
return serializer.deserialize(ProtocolUtils.readString(buf));
}
}

View File

@ -0,0 +1,61 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
/**
* Adapter for handling the UpdateTeamsPacket for Minecraft 1.20.3-1.21.2
*/
public class Protocol765Adapter extends Protocol404Adapter {
public Protocol765Adapter(@NotNull Velocitab plugin) {
super(plugin, Set.of(
ProtocolVersion.MINECRAFT_1_20_3,
ProtocolVersion.MINECRAFT_1_20_5,
ProtocolVersion.MINECRAFT_1_21,
ProtocolVersion.MINECRAFT_1_21_2,
ProtocolVersion.MINECRAFT_1_21_4
));
}
protected void writeComponent(@NotNull ByteBuf buf, @NotNull Component component) {
final BinaryTag tag = ComponentHolder.serialize(GsonComponentSerializer.gson().serializeToTree(component));
ProtocolUtils.writeBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, tag);
}
@NotNull
protected Component readComponent(@NotNull ByteBuf buf) {
return GsonComponentSerializer.gson().deserializeFromTree(ComponentHolder.deserialize(
ProtocolUtils.readBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, null)
));
}
}

View File

@ -1,77 +1,474 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import dev.simplix.protocolize.api.PacketDirection;
import dev.simplix.protocolize.api.Protocol;
import dev.simplix.protocolize.api.Protocolize;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.sorting.SortedSet;
import net.william278.velocitab.tab.Nametag;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.*;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import static com.velocitypowered.api.network.ProtocolVersion.*;
public class ScoreboardManager {
private PacketRegistration<UpdateTeamsPacket> packetRegistration;
private final Velocitab plugin;
private final Map<UUID, List<String>> createdTeams;
private final Map<UUID, Map<String, String>> roleMappings;
private final boolean teams;
private final Map<ProtocolVersion, TeamsPacketAdapter> versions;
@Getter
private final Map<UUID, String> createdTeams;
private final Map<String, Nametag> nametags;
private final Multimap<UUID, String> trackedTeams;
@Getter
private final SortedSet sortedTeams;
public ScoreboardManager(@NotNull Velocitab velocitab) {
public ScoreboardManager(@NotNull Velocitab velocitab, boolean teams) {
this.plugin = velocitab;
this.createdTeams = new HashMap<>();
this.roleMappings = new HashMap<>();
this.teams = teams;
this.createdTeams = Maps.newConcurrentMap();
this.nametags = Maps.newConcurrentMap();
this.versions = Maps.newHashMap();
this.trackedTeams = Multimaps.synchronizedMultimap(Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet));
this.sortedTeams = new SortedSet(Comparator.reverseOrder());
this.registerVersions();
}
public boolean handleTeams() {
return teams;
}
private void registerVersions() {
try {
final Protocol765Adapter protocol765Adapter = new Protocol765Adapter(plugin);
protocol765Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol765Adapter));
final Protocol735Adapter protocol735Adapter = new Protocol735Adapter(plugin);
protocol735Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol735Adapter));
final Protocol404Adapter protocol404Adapter = new Protocol404Adapter(plugin);
protocol404Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol404Adapter));
final Protocol48Adapter protocol48Adapter = new Protocol48Adapter(plugin);
protocol48Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol48Adapter));
} catch (NoSuchFieldError e) {
throw new IllegalStateException("Failed to register Scoreboard Teams packets." +
" Velocitab probably does not (yet) support your Proxy version.", e);
}
}
public boolean isInternalTeam(@NotNull String teamName) {
return nametags.containsKey(teamName);
}
public int getPosition(@NotNull String teamName) {
return sortedTeams.getPosition(teamName);
}
@NotNull
public TeamsPacketAdapter getPacketAdapter(@NotNull ProtocolVersion version) {
return Optional.ofNullable(versions.get(version))
.orElseThrow(() -> new IllegalArgumentException("No adapter found for protocol version " + version));
}
public void close() {
plugin.getServer().getAllPlayers().forEach(this::resetCache);
}
public void resetCache(@NotNull Player player) {
createdTeams.remove(player.getUniqueId());
roleMappings.remove(player.getUniqueId());
}
public void setRoles(@NotNull Player player, @NotNull Map<String, String> playerRoles) {
playerRoles.entrySet().stream()
.collect(Collectors.groupingBy(
Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
))
.forEach((role, players) -> updateRoles(player, role, players.toArray(new String[0])));
}
public void updateRoles(@NotNull Player player, @NotNull String role, @NotNull String... playerNames) {
if (!createdTeams.getOrDefault(player.getUniqueId(), List.of()).contains(role)) {
dispatchPacket(UpdateTeamsPacket.create(role, playerNames), player);
createdTeams.computeIfAbsent(player.getUniqueId(), k -> new ArrayList<>()).add(role);
roleMappings.computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()).put(player.getUsername(), role);
} else {
roleMappings.getOrDefault(player.getUniqueId(), Map.of())
.entrySet().stream()
.filter((entry) -> List.of(playerNames).contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
.forEach((playerName, oldRole) -> dispatchPacket(UpdateTeamsPacket.removeFromTeam(oldRole, playerName), player));
dispatchPacket(UpdateTeamsPacket.addToTeam(role, playerNames), player);
roleMappings.computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()).put(player.getUsername(), role);
final String team = createdTeams.remove(player.getUniqueId());
if (team != null) {
removeSortedTeam(team);
plugin.getTabList().getTabPlayer(player).ifPresent(tabPlayer ->
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), tabPlayer)
);
trackedTeams.removeAll(player.getUniqueId());
}
}
private void dispatchPacket(@NotNull UpdateTeamsPacket packet, @NotNull Player player) {
try {
Protocolize.playerProvider().player(player.getUniqueId()).sendPacket(packet);
} catch (Exception e) {
plugin.log("Failed to dispatch packet (is the client or server modded or using an illegal version?)", e);
public void resetCache(@NotNull Player player, @NotNull Group group) {
final String team = createdTeams.remove(player.getUniqueId());
if (team != null) {
removeSortedTeam(team);
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), group);
}
}
private void removeSortedTeam(@NotNull String teamName) {
final boolean result = sortedTeams.removeTeam(teamName);
if (!result) {
plugin.log(Level.ERROR, "Failed to remove team " + teamName + " from sortedTeams");
}
}
public void vanishPlayer(@NotNull TabPlayer tabPlayer) {
this.handleVanish(tabPlayer, true);
}
public void unVanishPlayer(@NotNull TabPlayer tabPlayer) {
this.handleVanish(tabPlayer, false);
}
private void handleVanish(@NotNull TabPlayer tabPlayer, boolean vanish) {
if (!plugin.getSettings().isSortPlayers()) {
return;
}
final Player player = tabPlayer.getPlayer();
final String teamName = createdTeams.get(player.getUniqueId());
if (teamName == null) {
return;
}
final Set<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
final Optional<Nametag> cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null));
cachedTag.ifPresent(nametag -> siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player)
.forEach(connected -> {
if (vanish && !plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername())) {
sendPacket(connected, UpdateTeamsPacket.removeTeam(plugin, teamName), isNameTagEmpty);
trackedTeams.remove(connected.getUniqueId(), teamName);
} else {
dispatchGroupCreatePacket(plugin, tabPlayer, teamName, nametag, player.getUsername());
}
})));
}
/**
* Updates the role of the player in the scoreboard.
*
* @param tabPlayer The TabPlayer object representing the player whose role will be updated.
* @param role The new role of the player. Must not be null.
* @param force Whether to force the update even if the player's nametag is the same.
*/
public CompletableFuture<Void> updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
final Player player = tabPlayer.getPlayer();
if (!player.isActive()) {
plugin.getTabList().removeOfflinePlayer(player);
return CompletableFuture.completedFuture(null);
}
final String name = player.getUsername();
final CompletableFuture<Void> future = new CompletableFuture<>();
tabPlayer.getNametag(plugin).thenAccept(newTag -> {
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
if (createdTeams.containsKey(player.getUniqueId())) {
dispatchGroupPacket(
UpdateTeamsPacket.removeTeam(plugin, createdTeams.get(player.getUniqueId())),
tabPlayer
);
}
final String oldRole = createdTeams.remove(player.getUniqueId());
if (oldRole != null) {
removeSortedTeam(oldRole);
}
createdTeams.put(player.getUniqueId(), role);
final boolean a = sortedTeams.addTeam(role);
if (!a) {
plugin.log(Level.ERROR, "Failed to add team " + role + " to sortedTeams");
}
this.nametags.put(role, newTag);
dispatchGroupCreatePacket(plugin, tabPlayer, role, newTag, name);
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) {
this.nametags.put(role, newTag);
dispatchGroupChangePacket(plugin, tabPlayer, role, newTag);
} else {
updatePlaceholders(tabPlayer);
}
future.complete(null);
}).exceptionally(e -> {
plugin.log(Level.ERROR, "Failed to update role for " + player.getUsername(), e);
return null;
});
return future;
}
public void updatePlaceholders(@NotNull TabPlayer tabPlayer) {
final Player player = tabPlayer.getPlayer();
final String role = createdTeams.get(player.getUniqueId());
if (role == null) {
return;
}
final Optional<Nametag> optionalNametag = Optional.ofNullable(nametags.get(role));
optionalNametag.ifPresent(nametag -> dispatchGroupChangePacket(plugin, tabPlayer, role, nametag));
}
public void resendAllTeams(@NotNull TabPlayer tabPlayer) {
if (!teams) {
return;
}
if (!plugin.getSettings().isSendScoreboardPackets()) {
return;
}
final Player player = tabPlayer.getPlayer();
final Set<Player> players = tabPlayer.getGroup().getPlayers(plugin, tabPlayer);
final Set<String> roles = Sets.newHashSet();
players.forEach(p -> {
if (p == player || !p.isActive()) {
return;
}
if (!plugin.getVanishManager().canSee(player.getUsername(), p.getUsername())) {
return;
}
final String role = createdTeams.getOrDefault(p.getUniqueId(), "");
if (role.isEmpty()) {
return;
}
final Optional<TabPlayer> optionalTabPlayer = plugin.getTabList().getTabPlayer(p);
if (optionalTabPlayer.isEmpty()) {
return;
}
final TabPlayer targetTabPlayer = optionalTabPlayer.get();
// Prevent duplicate packets
if (roles.contains(role)) {
return;
}
roles.add(role);
// Send packet
final Nametag tag = nametags.get(role);
if (tag != null) {
dispatchCreatePacket(plugin, targetTabPlayer, role, tag, tabPlayer, p.getUsername());
}
});
}
private void dispatchGroupCreatePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName, @NotNull Nametag nametag,
@NotNull String... teamMembers) {
if (!teams) {
return;
}
tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> {
if (!viewer.getPlayer().isActive()) {
return;
}
dispatchCreatePacket(plugin, tabPlayer, teamName, nametag, viewer, teamMembers);
});
}
private void dispatchCreatePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName, @NotNull Nametag nametag,
@NotNull TabPlayer viewer,
@NotNull String... teamMembers) {
if (!teams) {
return;
}
final boolean canSee = plugin.getVanishManager().canSee(viewer.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername());
if (!canSee) {
return;
}
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(plugin, tabPlayer, teamName, nametag, viewer, teamMembers);
trackedTeams.put(viewer.getPlayer().getUniqueId(), teamName);
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
sendPacket(viewer.getPlayer(), packet, isNameTagEmpty);
}
private void dispatchGroupChangePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName,
@NotNull Nametag nametag) {
if (!teams) {
return;
}
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> {
if (viewer == tabPlayer || !viewer.getPlayer().isActive()) {
return;
}
final boolean canSee = plugin.getVanishManager().canSee(viewer.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername());
if (!canSee) {
return;
}
// Prevent sending change nametag packets to players who are not tracking the team
if (!trackedTeams.containsEntry(viewer.getPlayer().getUniqueId(), teamName)) {
return;
}
final UpdateTeamsPacket packet = UpdateTeamsPacket.changeNametag(plugin, tabPlayer, teamName, viewer, nametag);
final Component prefix = packet.prefix();
final Component suffix = packet.suffix();
final Optional<Component[]> cached = tabPlayer.getRelationalNametag(viewer.getPlayer().getUniqueId());
// Skip if the nametag is the same as the cached one
if (cached.isPresent() && cached.get()[0].equals(prefix) && cached.get()[1].equals(suffix)) {
return;
}
tabPlayer.setRelationalNametag(viewer.getPlayer().getUniqueId(), prefix, suffix);
sendPacket(viewer.getPlayer(), packet, isNameTagEmpty);
});
}
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Group group) {
if (!teams) {
return;
}
final boolean isRemove = packet.isRemoveTeam();
final boolean isNameTagEmpty = group.nametag().isEmpty();
group.registeredServers(plugin).forEach(server -> server.getPlayersConnected().forEach(connected -> {
try {
sendPacket(connected, packet, isNameTagEmpty);
if (isRemove) {
trackedTeams.remove(connected.getUniqueId(), packet.teamName());
}
} catch (Throwable e) {
plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e);
}
}));
}
private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull TabPlayer tabPlayer) {
if (!teams) {
return;
}
final Player player = tabPlayer.getPlayer();
final Optional<ServerConnection> optionalServerConnection = player.getCurrentServer();
if (optionalServerConnection.isEmpty()) {
return;
}
final Set<Player> players = tabPlayer.getGroup().getPlayers(plugin);
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
players.forEach(connected -> {
try {
final boolean canSee = plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername());
if (!canSee) {
return;
}
sendPacket(connected, packet, isNameTagEmpty);
} catch (Throwable e) {
plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e);
}
});
}
private void sendPacket(@NotNull Player player, @NotNull UpdateTeamsPacket packet, boolean isNameTagEmpty) {
if (!player.isActive()) {
plugin.getTabList().removeOfflinePlayer(player);
return;
}
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2) && isNameTagEmpty) {
return;
}
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
connectedPlayer.getConnection().write(packet);
}
public void registerPacket() {
if (!teams) {
return;
}
try {
Protocolize.protocolRegistration().registerPacket(
UpdateTeamsPacket.MAPPINGS,
Protocol.PLAY,
PacketDirection.CLIENTBOUND,
UpdateTeamsPacket.class
);
} catch (Exception e) {
plugin.log("Failed to register UpdateTeamsPacket", e);
packetRegistration = PacketRegistration.of(UpdateTeamsPacket.class)
.direction(ProtocolUtils.Direction.CLIENTBOUND)
.packetSupplier(() -> new UpdateTeamsPacket(plugin))
.stateRegistry(StateRegistry.PLAY)
.mapping(0x3E, MINECRAFT_1_8, false)
.mapping(0x44, MINECRAFT_1_12_2, false)
.mapping(0x47, MINECRAFT_1_13, false)
.mapping(0x4B, MINECRAFT_1_14, false)
.mapping(0x4C, MINECRAFT_1_15, false)
.mapping(0x55, MINECRAFT_1_17, false)
.mapping(0x58, MINECRAFT_1_19_1, false)
.mapping(0x56, MINECRAFT_1_19_3, false)
.mapping(0x5A, MINECRAFT_1_19_4, false)
.mapping(0x5C, MINECRAFT_1_20_2, false)
.mapping(0x5E, MINECRAFT_1_20_3, false)
.mapping(0x60, MINECRAFT_1_20_5, false)
.mapping(0x67, MINECRAFT_1_21_2, false);
packetRegistration.register();
} catch (Throwable e) {
plugin.log(Level.ERROR, "Failed to register UpdateTeamsPacket", e);
}
}
public void unregisterPacket() {
if (packetRegistration == null) {
return;
}
try {
packetRegistration.unregister();
} catch (Throwable e) {
plugin.log(Level.ERROR, "Failed to unregister UpdateTeamsPacket", e);
}
}
/**
* Recalculates the vanish status for a specific player.
* This method updates the player's scoreboard to reflect the vanish status of another player.
*
* @param tabPlayer The TabPlayer object representing the player whose scoreboard will be updated.
* @param target The TabPlayer object representing the player whose vanish status will be reflected.
* @param canSee A boolean indicating whether the player can see the target player.
*/
public void recalculateVanishForPlayer(TabPlayer tabPlayer, TabPlayer target, boolean canSee) {
if (!teams) {
return;
}
final Player player = tabPlayer.getPlayer();
final String team = createdTeams.get(target.getPlayer().getUniqueId());
if (team == null) {
return;
}
final UpdateTeamsPacket removeTeam = UpdateTeamsPacket.removeTeam(plugin, team);
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty() && !plugin.getSettings().isRemoveNametags();
sendPacket(player, removeTeam, isNameTagEmpty);
trackedTeams.remove(player.getUniqueId(), team);
if (canSee) {
final Nametag tag = nametags.get(team);
if (tag != null) {
dispatchCreatePacket(plugin, tabPlayer, team, tag, target, target.getPlayer().getUsername());
}
}
}
}

View File

@ -0,0 +1,48 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
@Getter
@RequiredArgsConstructor
public abstract class TeamsPacketAdapter {
private final Velocitab plugin;
private final Set<ProtocolVersion> protocolVersions;
public abstract void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion);
public abstract void decode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion);
protected abstract void writeComponent(@NotNull ByteBuf buf, @NotNull Component component);
@NotNull
protected abstract Component readComponent(@NotNull ByteBuf buf);
}

View File

@ -1,135 +1,225 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.packet;
import dev.simplix.protocolize.api.PacketDirection;
import dev.simplix.protocolize.api.mapping.AbstractProtocolMapping;
import dev.simplix.protocolize.api.mapping.ProtocolIdMapping;
import dev.simplix.protocolize.api.packet.AbstractPacket;
import dev.simplix.protocolize.api.util.ProtocolUtil;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import lombok.*;
import lombok.experimental.Accessors;
import org.apache.commons.text.StringEscapeUtils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.Nametag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static dev.simplix.protocolize.api.util.ProtocolVersions.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Accessors(fluent = true)
public class UpdateTeamsPacket extends AbstractPacket {
protected static final List<ProtocolIdMapping> MAPPINGS = List.of(
AbstractProtocolMapping.rangedIdMapping(MINECRAFT_1_13, MINECRAFT_1_13_2, 0x47),
AbstractProtocolMapping.rangedIdMapping(MINECRAFT_1_14, MINECRAFT_1_14_4, 0x4B),
AbstractProtocolMapping.rangedIdMapping(MINECRAFT_1_15, MINECRAFT_1_16_5, 0x4C),
AbstractProtocolMapping.rangedIdMapping(MINECRAFT_1_17, MINECRAFT_1_19, 0x55),
AbstractProtocolMapping.rangedIdMapping(MINECRAFT_1_19_1, MINECRAFT_1_19_2, 0x58),
AbstractProtocolMapping.rangedIdMapping(MINECRAFT_1_19_3, MINECRAFT_LATEST, 0x56)
);
@SuppressWarnings("unused")
public class UpdateTeamsPacket implements MinecraftPacket {
private final Velocitab plugin;
private String teamName;
private UpdateMode mode;
private String displayName;
private Component displayName;
private List<FriendlyFlag> friendlyFlags;
private NameTagVisibility nameTagVisibility;
private NametagVisibility nametagVisibility;
private CollisionRule collisionRule;
private int color;
private String prefix;
private String suffix;
private Component prefix;
private Component suffix;
private List<String> entities;
@NotNull
public static UpdateTeamsPacket create(@NotNull String teamName, @NotNull String... teamMembers) {
return new UpdateTeamsPacket()
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
.mode(UpdateMode.CREATE_TEAM)
.displayName(getChatString(teamName))
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
.nameTagVisibility(NameTagVisibility.ALWAYS)
.collisionRule(CollisionRule.ALWAYS)
.color(15)
.prefix(getChatString(""))
.suffix(getChatString(""))
.entities(Arrays.asList(teamMembers));
public UpdateTeamsPacket(@NotNull Velocitab plugin) {
this.plugin = plugin;
}
public boolean isRemoveTeam() {
return mode == UpdateMode.REMOVE_TEAM;
}
public boolean hasEntities() {
return entities != null && !entities.isEmpty();
}
@NotNull
public static UpdateTeamsPacket addToTeam(@NotNull String teamName, @NotNull String... teamMembers) {
return new UpdateTeamsPacket()
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
protected static UpdateTeamsPacket create(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName, @NotNull Nametag nametag,
@NotNull TabPlayer viewer,
@NotNull String... teamMembers) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName)
.mode(UpdateMode.CREATE_TEAM)
.displayName(Component.empty())
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
.collisionRule(tabPlayer.getGroup().collisions() ? CollisionRule.ALWAYS : CollisionRule.NEVER)
.color(getLastColor(tabPlayer, nametag.prefix(), plugin))
.prefix(nametag.getPrefixComponent(plugin, tabPlayer, viewer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer, viewer))
.entities(Arrays.asList(teamMembers));
}
private static boolean isNametagPresent(@NotNull Nametag nametag, @NotNull Velocitab plugin) {
if (!plugin.getSettings().isRemoveNametags()) {
return true;
}
return !nametag.prefix().isEmpty() || !nametag.suffix().isEmpty();
}
@NotNull
protected static UpdateTeamsPacket changeNametag(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
@NotNull String teamName, @NotNull TabPlayer viewer,
@NotNull Nametag nametag) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName)
.mode(UpdateMode.UPDATE_INFO)
.displayName(Component.empty())
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
.collisionRule(tabPlayer.getGroup().collisions() ? CollisionRule.ALWAYS : CollisionRule.NEVER)
.color(getLastColor(tabPlayer, nametag.prefix(), plugin))
.prefix(nametag.getPrefixComponent(plugin, tabPlayer, viewer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer, viewer));
}
@NotNull
protected static UpdateTeamsPacket addToTeam(@NotNull Velocitab plugin, @NotNull String teamName,
@NotNull String... teamMembers) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName)
.mode(UpdateMode.ADD_PLAYERS)
.entities(Arrays.asList(teamMembers));
}
@NotNull
public static UpdateTeamsPacket removeFromTeam(@NotNull String teamName, @NotNull String... teamMembers) {
return new UpdateTeamsPacket()
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
protected static UpdateTeamsPacket removeFromTeam(@NotNull Velocitab plugin, @NotNull String teamName,
@NotNull String... teamMembers) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName)
.mode(UpdateMode.REMOVE_PLAYERS)
.entities(Arrays.asList(teamMembers));
}
@Override
public void read(ByteBuf byteBuf, PacketDirection packetDirection, int i) {
teamName = ProtocolUtil.readString(byteBuf);
mode = UpdateMode.byId(byteBuf.readByte());
if (mode == UpdateMode.REMOVE_TEAM) {
return;
}
if (mode == UpdateMode.CREATE_TEAM || mode == UpdateMode.UPDATE_INFO) {
displayName = ProtocolUtil.readString(byteBuf);
friendlyFlags = FriendlyFlag.fromBitMask(byteBuf.readByte());
nameTagVisibility = NameTagVisibility.byId(ProtocolUtil.readString(byteBuf));
collisionRule = CollisionRule.byId(ProtocolUtil.readString(byteBuf));
color = byteBuf.readByte();
prefix = ProtocolUtil.readString(byteBuf);
suffix = ProtocolUtil.readString(byteBuf);
}
if (mode == UpdateMode.CREATE_TEAM || mode == UpdateMode.ADD_PLAYERS || mode == UpdateMode.REMOVE_PLAYERS) {
int entityCount = ProtocolUtil.readVarInt(byteBuf);
entities = new ArrayList<>(entityCount);
for (int j = 0; j < entityCount; j++) {
entities.add(ProtocolUtil.readString(byteBuf));
}
}
}
@Override
public void write(ByteBuf byteBuf, PacketDirection packetDirection, int i) {
ProtocolUtil.writeString(byteBuf, teamName);
byteBuf.writeByte(mode.id());
if (mode == UpdateMode.REMOVE_TEAM) {
return;
}
if (mode == UpdateMode.CREATE_TEAM || mode == UpdateMode.UPDATE_INFO) {
ProtocolUtil.writeString(byteBuf, displayName);
byteBuf.writeByte(FriendlyFlag.toBitMask(friendlyFlags));
ProtocolUtil.writeString(byteBuf, nameTagVisibility.id());
ProtocolUtil.writeString(byteBuf, collisionRule.id());
byteBuf.writeByte(color);
ProtocolUtil.writeString(byteBuf, prefix);
ProtocolUtil.writeString(byteBuf, suffix);
}
if (mode == UpdateMode.CREATE_TEAM || mode == UpdateMode.ADD_PLAYERS || mode == UpdateMode.REMOVE_PLAYERS) {
ProtocolUtil.writeVarInt(byteBuf, entities != null ? entities.size() : 0);
for (String entity : entities != null ? entities : new ArrayList<String>()) {
ProtocolUtil.writeString(byteBuf, entity);
}
}
}
@NotNull
private static String getChatString(@NotNull String string) {
return "{\"text\":\"" + StringEscapeUtils.escapeJson(string) + "\"}";
protected static UpdateTeamsPacket removeTeam(@NotNull Velocitab plugin, @NotNull String teamName) {
return new UpdateTeamsPacket(plugin)
.teamName(teamName)
.mode(UpdateMode.REMOVE_TEAM);
}
public static int getLastColor(@NotNull TabPlayer tabPlayer, @Nullable String text, @NotNull Velocitab plugin) {
if (tabPlayer.getTeamColor() != null) {
text = "&" + tabPlayer.getTeamColor().colorChar();
}
if (text == null) {
return 15;
}
//add 1 random char at the end to make sure the last color is always found
text = text + "z";
//serialize & deserialize to downsample rgb to legacy
final Component component = plugin.getFormatter().deserialize(text);
text = LegacyComponentSerializer.legacyAmpersand().serialize(component);
final int lastFormatIndex = text.lastIndexOf("&");
if (lastFormatIndex == -1 || lastFormatIndex == text.length() - 1) {
return 15;
}
final String last = text.substring(lastFormatIndex, lastFormatIndex + 2);
return TeamColor.getColorId(last.charAt(1));
}
//Style-codes are handled as white
public enum TeamColor {
BLACK('0', 0),
DARK_BLUE('1', 1),
DARK_GREEN('2', 2),
DARK_AQUA('3', 3),
DARK_RED('4', 4),
DARK_PURPLE('5', 5),
GOLD('6', 6),
GRAY('7', 7),
DARK_GRAY('8', 8),
BLUE('9', 9),
GREEN('a', 10),
AQUA('b', 11),
RED('c', 12),
LIGHT_PURPLE('d', 13),
YELLOW('e', 14),
WHITE('f', 15),
OBFUSCATED('k', 16),
BOLD('f', 17),
STRIKETHROUGH('f', 18),
UNDERLINED('f', 19),
ITALIC('f', 20),
RESET('r', 21);
@Getter
private final char colorChar;
private final int id;
TeamColor(char colorChar, int id) {
this.colorChar = colorChar;
this.id = id;
}
public static int getColorId(char var) {
return Arrays.stream(values())
.filter(color -> color.colorChar == var)
.map(c -> c.id).findFirst()
.orElse(15);
}
}
@Override
public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
scoreboardManager.getPacketAdapter(protocolVersion).decode(byteBuf, this, protocolVersion);
}
@Override
public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
scoreboardManager.getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion);
}
@Override
public boolean handle(MinecraftSessionHandler minecraftSessionHandler) {
return false;
}
public enum UpdateMode {
@ -149,6 +239,7 @@ public class UpdateTeamsPacket extends AbstractPacket {
return id;
}
@Nullable
public static UpdateMode byId(byte id) {
return Arrays.stream(values())
.filter(mode -> mode.id == id)
@ -183,7 +274,7 @@ public class UpdateTeamsPacket extends AbstractPacket {
}
}
public enum NameTagVisibility {
public enum NametagVisibility {
ALWAYS("always"),
NEVER("never"),
HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"),
@ -191,19 +282,21 @@ public class UpdateTeamsPacket extends AbstractPacket {
private final String id;
NameTagVisibility(String id) {
NametagVisibility(@NotNull String id) {
this.id = id;
}
@NotNull
public String id() {
return id;
}
public static NameTagVisibility byId(String id) {
return Arrays.stream(values())
@NotNull
public static NametagVisibility byId(@Nullable String id) {
return id == null ? ALWAYS : Arrays.stream(values())
.filter(visibility -> visibility.id.equals(id))
.findFirst()
.orElse(null);
.orElse(ALWAYS);
}
}
@ -215,19 +308,21 @@ public class UpdateTeamsPacket extends AbstractPacket {
private final String id;
CollisionRule(String id) {
CollisionRule(@NotNull String id) {
this.id = id;
}
@NotNull
public String id() {
return id;
}
public static CollisionRule byId(String id) {
return Arrays.stream(values())
@NotNull
public static CollisionRule byId(@Nullable String id) {
return id == null ? ALWAYS : Arrays.stream(values())
.filter(rule -> rule.id.equals(id))
.findFirst()
.orElse(null);
.orElse(ALWAYS);
}
}
}

View File

@ -1,31 +1,59 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.player;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.Optional;
@RequiredArgsConstructor
public class Role implements Comparable<Role> {
public static final int DEFAULT_WEIGHT = 0;
public static final Role DEFAULT_ROLE = new Role(DEFAULT_WEIGHT, null, null, null);
public static final int DEFAULT_WEIGHT = -1;
public static final Role DEFAULT_ROLE = new Role(DEFAULT_WEIGHT, null, null, null, null);
@Getter
private final int weight;
@Nullable
private final String name;
@Nullable
private final String displayName;
@Nullable
private final String prefix;
@Nullable
private final String suffix;
public Role(int weight, @Nullable String name, @Nullable String prefix, @Nullable String suffix) {
this.weight = weight;
this.name = name;
this.prefix = prefix;
this.suffix = suffix;
}
@Override
public int compareTo(@NotNull Role o) {
return weight - o.weight;
return Double.compare(weight, o.weight);
}
public Optional<String> getName() {
return Optional.ofNullable(name);
}
public Optional<String> getDisplayName() {
return Optional.ofNullable(displayName).or(this::getName);
}
public Optional<String> getPrefix() {
@ -36,12 +64,23 @@ public class Role implements Comparable<Role> {
return Optional.ofNullable(suffix);
}
public Optional<String> getName() {
return Optional.ofNullable(name);
@NotNull
protected Optional<String> getWeightString() {
if (weight == -1) {
return Optional.empty();
}
return Optional.of(Integer.toString(weight));
}
@NotNull
protected String getWeightString(int highestWeight) {
return String.format("%0" + (highestWeight + "").length() + "d", highestWeight - weight);
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
final Role role = (Role) obj;
return weight == role.weight &&
Objects.equals(name, role.name) &&
Objects.equals(displayName, role.displayName) &&
Objects.equals(prefix, role.prefix) &&
Objects.equals(suffix, role.suffix);
}
}

View File

@ -1,64 +1,269 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.player;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.velocitypowered.api.proxy.Player;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.packet.UpdateTeamsPacket;
import net.william278.velocitab.tab.Nametag;
import net.william278.velocitab.tab.PlayerTabList;
import org.apache.commons.lang3.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Getter
@ToString
public final class TabPlayer implements Comparable<TabPlayer> {
private final Player player;
private final Role role;
private final int highestWeight;
public TabPlayer(@NotNull Player player, @NotNull Role role, int highestWeight) {
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%");
private static final String PLACEHOLDER_DELIMITER = "<-DELIMITER->";
private final Velocitab plugin;
private final Player player;
@Setter
private Role role;
private int headerIndex = 0;
private int footerIndex = 0;
// Each TabPlayer contains the components for each TabPlayer it's currently viewing this player
private final Map<UUID, Component> relationalDisplayNames;
private final Map<UUID, Component[]> relationalNametags;
private final Map<String, String> cachedPlaceholders;
private final Map<UUID, Integer> cachedListOrders;
private String lastDisplayName;
private Component lastHeader;
private Component lastFooter;
private String teamName;
@Setter
private int listOrder = -1;
@Nullable
@Setter
private UpdateTeamsPacket.TeamColor teamColor;
@Nullable
@Setter
private String customName;
@Nullable
@Setter
private String lastServer;
@NotNull
@Setter
private Group group;
@Setter
private boolean loaded;
public TabPlayer(@NotNull Velocitab plugin, @NotNull Player player,
@NotNull Role role, @NotNull Group group) {
this.plugin = plugin;
this.player = player;
this.role = role;
this.highestWeight = highestWeight;
this.group = group;
this.relationalDisplayNames = Maps.newConcurrentMap();
this.relationalNametags = Maps.newConcurrentMap();
this.cachedPlaceholders = Maps.newConcurrentMap();
this.cachedListOrders = Maps.newConcurrentMap();
}
@NotNull
public Player getPlayer() {
return player;
}
@NotNull
public Role getRole() {
return role;
public Optional<String> getRoleWeightString() {
return getRole().getWeightString();
}
/**
* Get the server name the player is currently on.
* Isn't affected by server aliases defined in the config.
*
* @return The server name
*/
@NotNull
public String getServerName() {
return player.getCurrentServer()
.map(serverConnection -> serverConnection.getServerInfo().getName())
.orElse("Unknown");
.orElse(ObjectUtils.firstNonNull(lastServer, "unknown"));
}
/**
* Get the ordinal position of the TAB server group this player is connected to
*
* @param plugin instance of the {@link Velocitab} plugin
* @return The ordinal position of the server group
*/
public int getServerGroupPosition(@NotNull Velocitab plugin) {
return plugin.getTabGroups().getPosition(group);
}
@NotNull
public CompletableFuture<Component> getDisplayName(@NotNull Velocitab plugin) {
final String serverGroup = plugin.getSettings().getServerGroup(getServerName());
return Placeholder.format(plugin.getSettings().getFormat(serverGroup), plugin, this)
.thenApply(formatted -> plugin.getSettings().getFormatter().format(formatted, this, plugin));
public CompletableFuture<String> getDisplayName(@NotNull Velocitab plugin) {
final String format = formatGroup();
return Placeholder.replace(format, plugin, this)
.thenApply(d -> cacheDisplayName(d, format));
}
@NotNull
public String getTeamName() {
return role.getWeightString(highestWeight) + role.getName().map(name -> "-" + name).orElse("");
private String formatGroup() {
final Set<String> placeholders = Sets.newHashSet();
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(group.format());
while (matcher.find()) {
placeholders.add("%" + matcher.group(1) + "%");
}
return String.join(PLACEHOLDER_DELIMITER, placeholders);
}
public void sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
tabList.getHeader(this).thenAccept(header -> tabList.getFooter(this)
.thenAccept(footer -> player.sendPlayerListHeaderAndFooter(header, footer)));
@NotNull
private String cacheDisplayName(@NotNull String placeholders, @NotNull String keys) {
String displayName = group.format();
final String[] placeholderArray = placeholders.split(PLACEHOLDER_DELIMITER);
final String[] keyArray = keys.split(PLACEHOLDER_DELIMITER);
for (int i = 0; i < placeholderArray.length; i++) {
final String placeholder = keyArray[i];
final String value = placeholderArray[i];
cachedPlaceholders.put(placeholder, value);
displayName = displayName.replace(placeholder, value);
}
displayName = Placeholder.replaceInternal(displayName, plugin, this).first();
return lastDisplayName = displayName;
}
@NotNull
public CompletableFuture<Nametag> getNametag(@NotNull Velocitab plugin) {
return Placeholder.replace(group.nametag(), plugin, this);
}
@NotNull
public CompletableFuture<String> getTeamName(@NotNull Velocitab plugin) {
return plugin.getSortingManager().getTeamName(this)
.thenApply(teamName -> this.teamName = teamName);
}
public Optional<String> getLastTeamName() {
return Optional.ofNullable(teamName);
}
public CompletableFuture<Void> sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
return tabList.getHeader(this).thenCompose(header -> tabList.getFooter(this).thenAccept(footer -> {
final boolean disabled = plugin.getSettings().isDisableHeaderFooterIfEmpty();
if (disabled) {
if ((!Component.empty().equals(header) && !header.equals(lastHeader)) ||
(!Component.empty().equals(footer) && !footer.equals(lastFooter))) {
lastHeader = header;
lastFooter = footer;
player.sendPlayerListHeaderAndFooter(header, footer);
}
} else {
if (!header.equals(lastHeader) || !footer.equals(lastFooter)) {
lastHeader = header;
lastFooter = footer;
player.sendPlayerListHeaderAndFooter(header, footer);
}
}
}));
}
public void incrementIndexes() {
incrementHeaderIndex();
incrementFooterIndex();
}
public void incrementHeaderIndex() {
headerIndex++;
if (headerIndex >= group.headers().size()) {
headerIndex = 0;
}
}
public void incrementFooterIndex() {
footerIndex++;
if (footerIndex >= group.footers().size()) {
footerIndex = 0;
}
}
public void setRelationalDisplayName(@NotNull UUID target, @NotNull Component displayName) {
relationalDisplayNames.put(target, displayName);
}
public void unsetRelationalDisplayName(@NotNull UUID target) {
relationalDisplayNames.remove(target);
}
public Optional<Component> getRelationalDisplayName(@NotNull UUID target) {
return Optional.ofNullable(relationalDisplayNames.get(target));
}
public void setRelationalNametag(@NotNull UUID target, @NotNull Component prefix, @NotNull Component suffix) {
relationalNametags.put(target, new Component[]{prefix, suffix});
}
public void unsetRelationalNametag(@NotNull UUID target) {
relationalNametags.remove(target);
}
public void unsetTabListOrder(@NotNull UUID target) {
cachedListOrders.remove(target);
}
public Optional<Component[]> getRelationalNametag(@NotNull UUID target) {
return Optional.ofNullable(relationalNametags.get(target));
}
public void clearCachedData() {
loaded = false;
relationalDisplayNames.clear();
relationalNametags.clear();
lastHeader = null;
lastFooter = null;
role = Role.DEFAULT_ROLE;
teamName = null;
cachedListOrders.clear();
}
/**
* Returns the custom name of the TabPlayer, if it has been set.
*
* @return An Optional object containing the custom name, or empty if no custom name has been set.
*/
public Optional<String> getCustomName() {
return Optional.ofNullable(customName);
}
@Override
public int compareTo(@NotNull TabPlayer o) {
final int roleDifference = role.compareTo(o.role);
if (roleDifference == 0) {
if (roleDifference <= 0) {
return player.getUsername().compareTo(o.player.getUsername());
}
return roleDifference;
@ -68,4 +273,8 @@ public final class TabPlayer implements Comparable<TabPlayer> {
public boolean equals(Object obj) {
return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId());
}
public Optional<String> getCachedPlaceholderValue(@NotNull String placeholder) {
return Optional.ofNullable(cachedPlaceholders.get(placeholder));
}
}

View File

@ -0,0 +1,106 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.providers;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.hook.Hook;
import net.william278.velocitab.hook.LuckPermsHook;
import net.william278.velocitab.hook.MiniPlaceholdersHook;
import net.william278.velocitab.hook.PAPIProxyBridgeHook;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public interface HookProvider {
/**
* Retrieves the list of hooks associated with the HookProvider.
*
* @return The list of hooks associated with the HookProvider.
*/
List<Hook> getHooks();
/**
* Sets the list of hooks associated with the HookProvider.
*
* @param hooks The list of hooks to set.
*/
void setHooks(List<Hook> hooks);
/**
* Retrieves the instance of the Velocitab plugin.
*
* @return The instance of the Velocitab plugin.
*/
Velocitab getPlugin();
/**
* Loads the hooks associated with the HookProvider.
*/
default void loadHooks() {
List<Hook> hooks = new ArrayList<>();
Hook.AVAILABLE.forEach(availableHook -> availableHook.apply(getPlugin()).ifPresent(hooks::add));
setHooks(hooks);
}
/**
* Retrieves a hook of the specified type from the list of hooks associated with the HookProvider.
*
* @param hookType The class object representing the type of the hook to retrieve.
* @param <H> The type of the hook to retrieve.
* @return An Optional containing the hook of the specified type, or an empty Optional if the hook is not found.
*/
private <H extends Hook> Optional<H> getHook(@NotNull Class<H> hookType) {
return getHooks().stream()
.filter(hook -> hook.getClass().equals(hookType))
.map(hookType::cast)
.findFirst();
}
/**
* Retrieves the LuckPermsHook from the list of hooks associated with the HookProvider.
*
* @return An Optional containing the LuckPermsHook, or an empty Optional if it is not found.
*/
default Optional<LuckPermsHook> getLuckPermsHook() {
return getHook(LuckPermsHook.class);
}
/**
* Retrieves the PAPIProxyBridgeHook from the list of hooks associated with the HookProvider.
*
* @return An Optional containing the PAPIProxyBridgeHook, or an empty Optional if it is not found.
*/
default Optional<PAPIProxyBridgeHook> getPAPIProxyBridgeHook() {
return getHook(PAPIProxyBridgeHook.class);
}
/**
* Retrieves the MiniPlaceholdersHook from the list of hooks associated with the HookProvider.
*
* @return An Optional containing the MiniPlaceholdersHook, or an empty Optional if it is not found.
*/
default Optional<MiniPlaceholdersHook> getMiniPlaceholdersHook() {
return getHook(MiniPlaceholdersHook.class);
}
}

View File

@ -0,0 +1,71 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.providers;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.event.Level;
public interface LoggerProvider {
/**
* Retrieves the logger for the corresponding class.
*
* @return the logger for the class
*/
@NotNull
Logger getLogger();
/**
* Logs a message with the specified log level.
*
* @param level the log level
* @param message the log message
* @param exceptions the exceptions associated with the log message (optional)
*/
default void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... exceptions) {
switch (level) {
case ERROR -> {
if (exceptions.length > 0) {
getLogger().error(message, exceptions[0]);
} else {
getLogger().error(message);
}
}
case WARN -> {
if (exceptions.length > 0) {
getLogger().warn(message, exceptions[0]);
} else {
getLogger().warn(message);
}
}
case DEBUG -> getLogger().debug(message);
case INFO -> getLogger().info(message);
}
}
/**
* Logs a message with the specified log level.
*/
default void log(@NotNull String message) {
this.log(Level.INFO, message);
}
}

View File

@ -0,0 +1,62 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.providers;
import net.william278.velocitab.Velocitab;
import org.bstats.charts.SimplePie;
import org.bstats.velocity.Metrics;
public interface MetricProvider {
int METRICS_ID = 18247;
/**
* Retrieves the Metrics Factory used by the MetricProvider.
*
* @return The Metrics Factory used by the MetricProvider.
*/
Metrics.Factory getMetricsFactory();
/**
* Retrieves the Velocitab plugin instance.
*
* @return The Velocitab plugin instance.
*/
Velocitab getPlugin();
/**
* Registers metrics for the Velocitab plugin using the Metrics library.
* This method adds custom charts to the metrics object, which include information such as:
* - Whether player sorting is enabled or disabled
* - The type of formatter being used
* - Whether LuckPerms hook is present
* - Whether PAPIProxyBridge hook is present
* - Whether MiniPlaceholders hook is present
*/
default void registerMetrics() {
final Metrics metrics = getMetricsFactory().make(this, METRICS_ID);
metrics.addCustomChart(new SimplePie("sort_players", () -> getPlugin().getSettings().isSortPlayers() ? "Enabled" : "Disabled"));
metrics.addCustomChart(new SimplePie("formatter_type", () -> getPlugin().getFormatter().getName()));
metrics.addCustomChart(new SimplePie("using_luckperms", () -> getPlugin().getLuckPermsHook().isPresent() ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("using_papiproxybridge", () -> getPlugin().getPAPIProxyBridgeHook().isPresent() ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("using_miniplaceholders", () -> getPlugin().getMiniPlaceholdersHook().isPresent() ? "Yes" : "No"));
}
}

View File

@ -0,0 +1,110 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.providers;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.sorting.SortingManager;
import net.william278.velocitab.tab.PlayerTabList;
import java.util.concurrent.TimeUnit;
public interface ScoreboardProvider {
/**
* Retrieves the Velocitab plugin instance.
*
* @return The Velocitab plugin instance.
*/
Velocitab getPlugin();
/**
* Retrieves the scoreboard manager
* @return The scoreboard manager
*/
ScoreboardManager getScoreboardManager();
/**
* Sets the scoreboard manager.
*
* @param scoreboardManager The scoreboard manager to be set.
*/
void setScoreboardManager(ScoreboardManager scoreboardManager);
/**
* Retrieves the tab list for the player.
*
* @return The PlayerTabList object representing the tab list for the player.
*/
PlayerTabList getTabList();
/**
* Sets the tab list for the player.
*
* @param tabList The PlayerTabList object representing the tab list to be set for the player.
*/
void setTabList(PlayerTabList tabList);
/**
* Returns the SortingManager instance.
*
* @return The SortingManager instance.
*/
SortingManager getSortingManager();
/**
* Sets the sorting manager for the ScoreboardProvider.
*
* @param sortingManager The sorting manager to be set.
*/
void setSortingManager(SortingManager sortingManager);
/**
* Prepares the scoreboard by initializing the necessary components.
* This method is responsible for setting up the scoreboard manager, player tab list,
* scheduler tasks, and sorting manager.
*
*/
default void prepareScoreboard() {
final ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin(), getPlugin().getSettings().isSendScoreboardPackets());
setScoreboardManager(scoreboardManager);
scoreboardManager.registerPacket();
final PlayerTabList tabList = new PlayerTabList(getPlugin());
setTabList(tabList);
getPlugin().getServer().getEventManager().register(this, tabList);
getPlugin().getServer().getScheduler().buildTask(this, tabList::load).delay(1, TimeUnit.SECONDS).schedule();
final SortingManager sortingManager = new SortingManager(getPlugin());
setSortingManager(sortingManager);
}
/**
* Disables the ScoreboardManager and closes the tab list for the player.
*/
default void disableScoreboardManager() {
getScoreboardManager().close();
getScoreboardManager().unregisterPacket();
getTabList().close();
}
}

View File

@ -0,0 +1,74 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.sorting;
import com.google.common.collect.Maps;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListSet;
public class SortedSet {
private final ConcurrentSkipListSet<String> sortedTeams;
private final Map<String, Integer> positionMap;
public SortedSet(@NotNull Comparator<String> comparator) {
sortedTeams = new ConcurrentSkipListSet<>(comparator);
positionMap = Maps.newConcurrentMap();
}
public synchronized boolean addTeam(@NotNull String teamName) {
final boolean result = sortedTeams.add(teamName);
if (!result) {
return false;
}
updatePositions();
return true;
}
public synchronized boolean removeTeam(@NotNull String teamName) {
final boolean result = sortedTeams.remove(teamName);
if (!result) {
return false;
}
updatePositions();
return true;
}
private synchronized void updatePositions() {
int index = 0;
positionMap.clear();
for (final String team : sortedTeams) {
positionMap.put(team, index);
index++;
}
}
public synchronized int getPosition(@NotNull String teamName) {
return positionMap.getOrDefault(teamName, -1);
}
@Override
public String toString() {
return sortedTeams.toString();
}
}

View File

@ -0,0 +1,112 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.sorting;
import com.google.common.collect.Lists;
import com.velocitypowered.api.network.ProtocolVersion;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class SortingManager {
private final Velocitab plugin;
private static final String DELIMITER = ":::";
private static final Pattern NUMBER_PATTERN = Pattern.compile("^-?[0-9]\\d*(\\.\\d+)?$");
public SortingManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
}
@NotNull
public CompletableFuture<String> getTeamName(@NotNull TabPlayer player) {
if (!plugin.getSettings().isSortPlayers()) {
return CompletableFuture.completedFuture("");
}
return Placeholder.replace(String.join(DELIMITER, player.getGroup().sortingPlaceholders()), plugin, player)
.thenApply(s -> Arrays.asList(s.split(DELIMITER)))
.thenApply(v -> v.stream().map(s -> adaptValue(s, player)).collect(Collectors.toList()))
.thenApply(v -> handleList(player, v));
}
@NotNull
private String handleList(@NotNull TabPlayer player, @NotNull List<String> values) {
String result = String.join("", values);
if (result.length() > 12 && isLongTeamNotAllowed(player)) {
result = result.substring(0, 12);
}
result += player.getPlayer().getUniqueId().toString().substring(0, 4); // Make unique
return result;
}
private boolean isLongTeamNotAllowed(@NotNull TabPlayer player) {
return !player.getGroup().getPlayers(plugin, player).stream()
.allMatch(t -> t.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_18));
}
@NotNull
private String adaptValue(@NotNull String value, @NotNull TabPlayer player) {
if (value.isEmpty()) {
return "";
}
if (NUMBER_PATTERN.matcher(value).matches()) {
double parsed = Double.parseDouble(value);
parsed = Math.max(0, parsed);
return compressNumber(Integer.MAX_VALUE / 4d - parsed);
}
if (value.length() > 6 && isLongTeamNotAllowed(player)) {
return value.substring(0, 4);
}
return value;
}
@NotNull
public String compressNumber(double number) {
int wholePart = (int) number;
final char decimalChar = (char) ((number - wholePart) * Character.MAX_VALUE);
final List<Character> charList = Lists.newArrayList();
while (wholePart > 0) {
char digit = (char) (wholePart % Character.MAX_VALUE);
charList.add(0, digit);
wholePart /= Character.MAX_VALUE;
}
if (charList.isEmpty()) {
charList.add((char) 0);
}
return charList.stream().map(String::valueOf).collect(Collectors.joining()) + decimalChar;
}
}

View File

@ -0,0 +1,40 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.tab;
import com.velocitypowered.api.scheduler.ScheduledTask;
import org.jetbrains.annotations.Nullable;
public record GroupTasks(@Nullable ScheduledTask updateTask, @Nullable ScheduledTask headerFooterTask,
@Nullable ScheduledTask latencyTask) {
public void cancel() {
if (updateTask != null) {
updateTask.cancel();
}
if (headerFooterTask != null) {
headerFooterTask.cancel();
}
if (latencyTask != null) {
latencyTask.cancel();
}
}
}

View File

@ -0,0 +1,57 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.tab;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
/**
* Represents a nametag to be displayed above a player, with prefix and suffix
*/
public record Nametag(@NotNull String prefix, @NotNull String suffix) {
@NotNull
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
final String formatted = Placeholder.replaceInternal(prefix, plugin, tabPlayer).first();
return plugin.getFormatter().format(formatted, tabPlayer, target, plugin);
}
@NotNull
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
final String formatted = Placeholder.replaceInternal(suffix, plugin, tabPlayer).first();
return plugin.getFormatter().format(formatted, tabPlayer, target, plugin);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Nametag other)) {
return false;
}
return (prefix.equals(other.prefix)) && (suffix.equals(other.suffix));
}
public boolean isEmpty() {
return prefix.isEmpty() && suffix.isEmpty();
}
}

View File

@ -1,208 +1,624 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.tab;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
import com.google.common.collect.Maps;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.player.TabList;
import com.velocitypowered.api.proxy.player.TabListEntry;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.proxy.tablist.KeyedVelocityTabList;
import com.velocitypowered.proxy.tablist.VelocityTabList;
import lombok.AccessLevel;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.api.PlayerAddedToTabEvent;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.config.ServerUrl;
import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.event.Level;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER;
/**
* The main class for tracking the server TAB list for a map of {@link TabPlayer}s
*/
public class PlayerTabList {
private final Velocitab plugin;
private final ConcurrentLinkedQueue<TabPlayer> players;
private final ConcurrentLinkedQueue<String> fallbackServers;
@Getter
private final VanishTabList vanishTabList;
@Getter(value = AccessLevel.PUBLIC)
private final Map<UUID, TabPlayer> players;
private final TaskManager taskManager;
private final Map<Class<?>, Field> entriesFields;
public PlayerTabList(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.players = new ConcurrentLinkedQueue<>();
this.fallbackServers = new ConcurrentLinkedQueue<>();
this.vanishTabList = new VanishTabList(plugin, this);
this.players = Maps.newConcurrentMap();
this.taskManager = new TaskManager(plugin);
this.entriesFields = Maps.newHashMap();
this.reloadUpdate();
this.registerListener();
this.ensureDisplayNameTask();
this.registerFields();
}
// If the update time is set to 0 do not schedule the updater
if (plugin.getSettings().getUpdateRate() > 0) {
this.updatePeriodically(plugin.getSettings().getUpdateRate());
// VelocityTabListLegacy is not supported
private void registerFields() {
final Class<KeyedVelocityTabList> keyedVelocityTabListClass = KeyedVelocityTabList.class;
final Class<VelocityTabList> velocityTabListClass = VelocityTabList.class;
try {
final Field entriesField = keyedVelocityTabListClass.getDeclaredField("entries");
entriesField.setAccessible(true);
this.entriesFields.put(keyedVelocityTabListClass, entriesField);
} catch (NoSuchFieldException e) {
plugin.log(Level.ERROR, "Failed to register KeyedVelocityTabList field", e);
}
try {
final Field entriesField = velocityTabListClass.getDeclaredField("entries");
entriesField.setAccessible(true);
this.entriesFields.put(velocityTabListClass, entriesField);
} catch (NoSuchFieldException e) {
plugin.log(Level.ERROR, "Failed to register VelocityTabList field", e);
}
}
@SuppressWarnings("UnstableApiUsage")
@Subscribe
public void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
final Player joined = event.getPlayer();
plugin.getScoreboardManager().resetCache(joined);
// Remove the player from the tracking list if they are switching servers
if (event.getPreviousServer() == null) {
players.removeIf(player -> player.getPlayer().getUniqueId().equals(joined.getUniqueId()));
}
// Get the servers in the group from the joined server name
// If the server is not in a group, use fallback
Optional<List<String>> serversInGroup = getSiblings(joined.getCurrentServer()
.map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("?"));
// If the server is not in a group, use fallback.
// If fallback is disabled, permit the player to switch excluded servers without header or footer override
if (serversInGroup.isEmpty() && !this.fallbackServers.contains(event.getPreviousServer().getServerInfo().getName())) {
event.getPlayer().sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
return;
}
// Add the player to the tracking list
final TabPlayer tabPlayer = plugin.getTabPlayer(joined);
players.add(tabPlayer);
// Update lists
plugin.getServer().getScheduler()
.buildTask(plugin, () -> {
final TabList tabList = joined.getTabList();
final Map<String, String> playerRoles = new HashMap<>();
for (TabPlayer player : players) {
if (serversInGroup.isPresent() && !serversInGroup.get().contains(player.getServerName())) {
continue; // Skip players on other servers
}
playerRoles.put(player.getPlayer().getUsername(), player.getTeamName());
tabList.getEntries().stream()
.filter(e -> e.getProfile().getId().equals(player.getPlayer().getUniqueId())).findFirst()
.ifPresentOrElse(
entry -> player.getDisplayName(plugin).thenAccept(entry::setDisplayName),
() -> createEntry(player, tabList).thenAccept(tabList::addEntry)
);
addPlayerToTabList(player, tabPlayer);
player.sendHeaderAndFooter(this);
}
plugin.getScoreboardManager().setRoles(joined, playerRoles);
})
.delay(500, TimeUnit.MILLISECONDS)
.schedule();
}
@NotNull
private CompletableFuture<TabListEntry> createEntry(@NotNull TabPlayer player, @NotNull TabList tabList) {
return player.getDisplayName(plugin).thenApply(name -> TabListEntry.builder()
.profile(player.getPlayer().getGameProfile())
.displayName(name)
.latency(0)
.tabList(tabList)
.build());
}
private void addPlayerToTabList(@NotNull TabPlayer player, @NotNull TabPlayer newPlayer) {
if (newPlayer.getPlayer().getUniqueId().equals(player.getPlayer().getUniqueId())) {
return;
}
player.getPlayer()
.getTabList().getEntries().stream()
.filter(e -> e.getProfile().getId().equals(newPlayer.getPlayer().getUniqueId())).findFirst()
.ifPresentOrElse(
entry -> newPlayer.getDisplayName(plugin).thenAccept(entry::setDisplayName),
() -> createEntry(newPlayer, player.getPlayer().getTabList())
.thenAccept(entry -> player.getPlayer().getTabList().addEntry(entry))
);
plugin.getScoreboardManager().updateRoles(
player.getPlayer(),
newPlayer.getTeamName(),
newPlayer.getPlayer().getUsername()
);
}
@Subscribe
public void onPlayerQuit(@NotNull DisconnectEvent event) {
// Remove the player from the tracking list
players.removeIf(player -> player.getPlayer().getUniqueId().equals(event.getPlayer().getUniqueId()));
// Remove the player from the tab list of all other players
plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(event.getPlayer().getUniqueId()));
// Update the tab list of all players
plugin.getServer().getScheduler()
.buildTask(plugin, () -> players.forEach(player -> {
player.getPlayer().getTabList().removeEntry(event.getPlayer().getUniqueId());
player.sendHeaderAndFooter(this);
}))
.delay(500, TimeUnit.MILLISECONDS)
.schedule();
}
public void onUpdate(@NotNull TabPlayer tabPlayer) {
players.forEach(player -> tabPlayer.getDisplayName(plugin).thenAccept(displayName -> {
player.getPlayer().getTabList().getEntries().stream()
.filter(e -> e.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId())).findFirst()
.ifPresent(entry -> entry.setDisplayName(displayName));
plugin.getScoreboardManager().updateRoles(player.getPlayer(),
tabPlayer.getTeamName(), tabPlayer.getPlayer().getUsername());
}));
}
public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) {
return Placeholder.format(plugin.getSettings().getHeader(
plugin.getSettings().getServerGroup(player.getServerName())), plugin, player)
.thenApply(header -> plugin.getSettings().getFormatter().format(header, player, plugin));
}
public CompletableFuture<Component> getFooter(@NotNull TabPlayer player) {
return Placeholder.format(plugin.getSettings().getFooter(
plugin.getSettings().getServerGroup(player.getServerName())), plugin, player)
.thenApply(footer -> plugin.getSettings().getFormatter().format(footer, player, plugin));
}
// Update the tab list periodically
private void updatePeriodically(int updateRate) {
plugin.getServer().getScheduler()
.buildTask(plugin, () -> {
if (players.isEmpty()) {
return;
}
players.forEach(player -> {
this.onUpdate(player);
player.sendHeaderAndFooter(this);
});
})
.repeat(updateRate, TimeUnit.MILLISECONDS)
.schedule();
private void registerListener() {
plugin.getServer().getEventManager().register(plugin, new TabListListener(plugin, this));
}
/**
* Get the servers in the same group as the given server
* If the server is not in a group, use fallback
* If fallback is disabled, return empty
* Retrieves a TabPlayer object corresponding to the given Player object.
*
* @param serverName The server name
* @return The servers in the same group as the given server, empty if the server is not in a group and fallback is disabled
* @param player The Player object for which to retrieve the corresponding TabPlayer.
* @return An Optional object containing the TabPlayer if found, or an empty Optional if not found.
*/
@NotNull
public Optional<List<String>> getSiblings(String serverName) {
return plugin.getSettings().getServerGroups().values().stream()
.filter(servers -> servers.contains(serverName))
.findFirst()
.or(() -> {
if (!plugin.getSettings().isFallbackEnabled()) {
return Optional.empty();
}
if (!this.fallbackServers.contains(serverName)) {
this.fallbackServers.add(serverName);
}
return Optional.of(this.fallbackServers.stream().toList());
});
public Optional<TabPlayer> getTabPlayer(@NotNull Player player) {
return Optional.ofNullable(players.get(player.getUniqueId()));
}
/**
* Retrieves a TabPlayer object corresponding to the given UUID.
*
* @param uuid The UUID of the player for which to retrieve the corresponding TabPlayer.
* @return An Optional object containing the TabPlayer if found, or an empty Optional if not found.
*/
public Optional<TabPlayer> getTabPlayer(@NotNull UUID uuid) {
return Optional.ofNullable(players.get(uuid));
}
/**
* Loads the tab list for all players connected to the server.
* Removes the player's entry from the tab list of all other players on the same group servers.
*/
public void load() {
plugin.getServer().getAllPlayers().forEach(p -> {
final Optional<ServerConnection> server = p.getCurrentServer();
if (server.isEmpty()) {
return;
}
final String serverName = server.get().getServerInfo().getName();
final @NotNull Optional<Group> group = getGroup(serverName);
if (group.isEmpty()) {
return;
}
joinPlayer(p, group.get());
});
}
/**
* Closes the tab list for all players connected to the server.
* Removes the player's entry from the tab list of all other players on the same group servers.
*/
public void close() {
taskManager.cancelAllTasks();
plugin.getServer().getAllPlayers().forEach(p -> {
final Optional<ServerConnection> server = p.getCurrentServer();
if (server.isEmpty()) return;
final TabPlayer tabPlayer = players.get(p.getUniqueId());
if (tabPlayer == null) {
return;
}
final Set<RegisteredServer> serversInGroup = tabPlayer.getGroup().registeredServers(plugin);
if (serversInGroup.isEmpty()) {
return;
}
serversInGroup.remove(server.get().getServer());
serversInGroup.forEach(s -> s.getPlayersConnected().forEach(t -> t.getTabList().removeEntry(p.getUniqueId())));
});
}
protected void clearCachedData(@NotNull Player player) {
players.values().forEach(p -> {
p.unsetRelationalDisplayName(player.getUniqueId());
p.unsetRelationalNametag(player.getUniqueId());
p.unsetTabListOrder(player.getUniqueId());
});
}
protected void joinPlayer(@NotNull Player joined, @NotNull Group group) {
// Add the player to the tracking list if they are not already listed
final Optional<TabPlayer> tabPlayerOptional = getTabPlayer(joined);
if (tabPlayerOptional.isPresent()) {
tabPlayerOptional.get().clearCachedData();
tabPlayerOptional.get().setGroup(group);
tabPlayerOptional.get().setRole(plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(joined)).orElse(Role.DEFAULT_ROLE));
}
final TabPlayer tabPlayer = tabPlayerOptional.orElseGet(() -> createTabPlayer(joined, group));
final String serverName = getServerName(joined);
// Store last server, so it's possible to have the last server on disconnect
tabPlayer.setLastServer(serverName);
// Send server URLs (1.21 clients)
sendPlayerServerLinks(tabPlayer);
// Set the player as not loaded until the display name is set
tabPlayer.getDisplayName(plugin).thenAccept(d -> {
if (d == null) {
plugin.log(Level.ERROR, "Failed to get display name for " + joined.getUsername());
return;
}
handleDisplayLoad(tabPlayer);
}).exceptionally(throwable -> {
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
joined.getUsername(), joined.getUniqueId()), throwable);
return null;
});
}
private void handleDisplayLoad(@NotNull TabPlayer tabPlayer) {
final Player joined = tabPlayer.getPlayer();
final Group group = tabPlayer.getGroup();
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
players.putIfAbsent(joined.getUniqueId(), tabPlayer);
tabPlayer.sendHeaderAndFooter(this)
.thenAccept(v -> tabPlayer.setLoaded(true))
.exceptionally(throwable -> {
plugin.log(Level.ERROR, String.format("Failed to send header and footer for %s (UUID: %s)",
joined.getUsername(), joined.getUniqueId()), throwable);
return null;
});
final Set<TabPlayer> tabPlayers = group.getTabPlayers(plugin, tabPlayer);
updateTabListOnJoin(tabPlayer, group, tabPlayers, isVanished);
}
private void updateTabListOnJoin(@NotNull TabPlayer tabPlayer, @NotNull Group group,
@NotNull Set<TabPlayer> tabPlayers, boolean isJoinedVanished) {
final Player joined = tabPlayer.getPlayer();
final String serverName = getServerName(joined);
final Set<UUID> uuids = tabPlayers.stream().map(p -> p.getPlayer().getUniqueId()).collect(Collectors.toSet());
List.copyOf(tabPlayer.getPlayer().getTabList().getEntries()).forEach(entry -> {
if (!uuids.contains(entry.getProfile().getId())) {
tabPlayer.getPlayer().getTabList().removeEntry(entry.getProfile().getId());
}
});
for (final TabPlayer iteratedPlayer : tabPlayers) {
final Player player = iteratedPlayer.getPlayer();
final String username = player.getUsername();
final boolean isPlayerVanished = plugin.getVanishManager().isVanished(username);
if (group.onlyListPlayersInSameServer() && !serverName.equals(getServerName(player))) {
continue;
}
// Update lists regarding the joined player
checkVisibilityAndUpdateName(iteratedPlayer, tabPlayer, isJoinedVanished);
// Update lists regarding the iterated player
if (iteratedPlayer != tabPlayer) {
checkVisibilityAndUpdateName(tabPlayer, iteratedPlayer, isPlayerVanished);
}
iteratedPlayer.sendHeaderAndFooter(this);
}
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
scoreboardManager.resendAllTeams(tabPlayer);
updateSorting(tabPlayer, false);
fixDuplicateEntries(joined);
// Fire event without listening for result
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group));
}
private void checkVisibilityAndUpdateName(@NotNull TabPlayer observedPlayer, @NotNull TabPlayer observableTabPlayer,
boolean isObservablePlayerVanished) {
final UUID observableUUID = observableTabPlayer.getPlayer().getUniqueId();
final String observedUsername = observedPlayer.getPlayer().getUsername();
final String observableUsername = observableTabPlayer.getPlayer().getUsername();
final TabList observableTabPlayerTabList = observableTabPlayer.getPlayer().getTabList();
if (isObservablePlayerVanished && !plugin.getVanishManager().canSee(observableUsername, observedUsername) &&
!observableUUID.equals(observedPlayer.getPlayer().getUniqueId())) {
observableTabPlayerTabList.removeEntry(observedPlayer.getPlayer().getUniqueId());
} else {
updateDisplayName(observedPlayer, observableTabPlayer);
}
}
@NotNull
private String getServerName(@NotNull Player player) {
return player.getCurrentServer()
.map(serverConnection -> serverConnection.getServerInfo().getName())
.orElse("");
}
@NotNull
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer,
@NotNull Component single, @NotNull String toParse) {
if (plugin.getMiniPlaceholdersHook().isEmpty()) {
return single;
}
return plugin.getFormatter().format(toParse, player, viewer, plugin);
}
@NotNull
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull String toParse) {
final Component single = plugin.getFormatter().format(toParse, player, viewer, plugin);
return getRelationalPlaceholder(player, viewer, single, toParse);
}
@SuppressWarnings("unchecked")
private void fixDuplicateEntries(@NotNull Player target) {
try {
final Optional<Field> optionalField = Optional.ofNullable(this.entriesFields.get(target.getTabList().getClass()));
if (optionalField.isEmpty()) {
return;
}
final Field entriesField = optionalField.get();
final Map<UUID, TabListEntry> entries = (Map<UUID, TabListEntry>) entriesField.get(target.getTabList());
entries.entrySet().stream()
.filter(entry -> entry.getValue().getProfile() != null)
.filter(entry -> entry.getValue().getProfile().getId().equals(target.getUniqueId()))
.filter(entry -> !entry.getKey().equals(target.getUniqueId()))
.forEach(entry -> target.getTabList().removeEntry(entry.getKey()));
} catch (Throwable error) {
plugin.log(Level.ERROR, "Failed to fix duplicate entries for class " + target.getTabList().getClass().getName(), error);
}
}
protected void removePlayer(@NotNull Player target) {
removePlayer(target, null);
}
/**
* Remove a player from the tab list
*
* @param uuid {@link UUID} of the {@link TabPlayer player} to remove
*/
protected void removeTabListUUID(@NotNull UUID uuid) {
getPlayers().forEach((key, value) -> value.getPlayer().getTabList().getEntry(uuid).ifPresent(
entry -> value.getPlayer().getTabList().removeEntry(uuid)
));
}
protected void removePlayer(@NotNull Player target, @Nullable RegisteredServer server) {
final UUID uuid = target.getUniqueId();
plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(uuid));
final Set<Player> currentServerPlayers = Optional.ofNullable(server)
.map(RegisteredServer::getPlayersConnected)
.map(HashSet::new)
.orElseGet(HashSet::new);
currentServerPlayers.add(target);
// Update the tab list of all players
plugin.getServer().getScheduler()
.buildTask(plugin, () -> getPlayers().values().stream()
.filter(p -> currentServerPlayers.isEmpty() || !currentServerPlayers.contains(p.getPlayer()))
.forEach(player -> {
player.getPlayer().getTabList().removeEntry(uuid);
player.sendHeaderAndFooter(this);
updatePlayerDisplayName(player);
}))
.delay(250, TimeUnit.MILLISECONDS)
.schedule();
// Delete player team
plugin.getScoreboardManager().resetCache(target);
//remove player from tab list cache
getPlayers().remove(uuid);
}
@NotNull
protected TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull Component displayName) {
return TabListEntry.builder()
.profile(player.getPlayer().getGameProfile())
.displayName(displayName)
.latency(Math.max((int) player.getPlayer().getPing(), 0))
.tabList(tabList)
.build();
}
@NotNull
protected TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull TabPlayer viewer) {
if (!viewer.getPlayer().getTabList().equals(tabList)) {
throw new IllegalArgumentException("TabList of viewer is not the same as the TabList of the entry");
}
final Component single = plugin.getFormatter().format(player.getLastDisplayName(), player, viewer, plugin);
final Component displayName = getRelationalPlaceholder(player, viewer, single, player.getGroup().format());
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
return TabListEntry.builder()
.profile(player.getPlayer().getGameProfile())
.displayName(displayName)
.latency(Math.max((int) player.getPlayer().getPing(), 0))
.tabList(tabList)
.build();
}
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer) {
final Component displayName = getRelationalPlaceholder(player, viewer, player.getLastDisplayName());
updateDisplayName(player, viewer, displayName);
}
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull Component displayName) {
final Optional<Component> cached = player.getRelationalDisplayName(viewer.getPlayer().getUniqueId());
if (cached.isPresent() && cached.get().equals(displayName) &&
viewer.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
.flatMap(TabListEntry::getDisplayNameComponent).map(displayName::equals)
.orElse(false)
) {
return;
}
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
viewer.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
.ifPresentOrElse(
entry -> entry.setDisplayName(displayName),
() -> viewer.getPlayer().getTabList()
.addEntry(createEntry(player, viewer.getPlayer().getTabList(), displayName))
);
}
@NotNull
public TabPlayer createTabPlayer(@NotNull Player player, @NotNull Group group) {
return new TabPlayer(plugin, player,
plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE),
group
);
}
// Update a player's name in the tab list and scoreboard team
public void updatePlayer(@NotNull TabPlayer tabPlayer, boolean force) {
if (!tabPlayer.getPlayer().isActive()) {
removeOfflinePlayer(tabPlayer.getPlayer());
return;
}
updateSorting(tabPlayer, force);
}
private void updateSorting(@NotNull TabPlayer tabPlayer, boolean force) {
tabPlayer.getTeamName(plugin).thenAccept(teamName -> {
if (teamName.isBlank()) {
return;
}
plugin.getScoreboardManager().updateRole(tabPlayer, teamName, force).thenAccept(v -> {
final int order = plugin.getScoreboardManager().getPosition(teamName);
if (order == -1) {
plugin.log(Level.ERROR, "Failed to get position for " + tabPlayer.getPlayer().getUsername());
return;
}
tabPlayer.setListOrder(order);
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
players.forEach(p -> recalculateSortingForPlayer(p, players));
});
});
}
public void sendPlayerServerLinks(@NotNull TabPlayer player) {
if (player.getPlayer().getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21)) {
return;
}
final List<ServerUrl> urls = plugin.getSettings().getUrlsForGroup(player.getGroup());
ServerUrl.resolve(plugin, player, urls).thenAccept(player.getPlayer()::setServerLinks);
}
public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) {
tabPlayer.getDisplayName(plugin).thenAccept(displayName -> {
if (displayName == null) {
plugin.log(Level.ERROR, "Failed to get display name for " + tabPlayer.getPlayer().getUsername());
return;
}
final Component single = plugin.getFormatter().format(displayName, tabPlayer, plugin);
final boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername());
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
players.forEach(player -> {
if (isVanished && !plugin.getVanishManager().canSee(player.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) {
return;
}
final Component relationalPlaceholder = getRelationalPlaceholder(tabPlayer, player, single, displayName);
updateDisplayName(tabPlayer, player, relationalPlaceholder);
});
});
}
public void checkCorrectDisplayName(@NotNull TabPlayer tabPlayer) {
if (!tabPlayer.isLoaded()) {
return;
}
final boolean bypass = plugin.getSettings().isForceSendingTabListPackets();
players.values()
.stream()
.filter(TabPlayer::isLoaded)
.forEach(player -> player.getPlayer().getTabList().getEntry(tabPlayer.getPlayer().getUniqueId())
.ifPresent(entry -> {
final Optional<Component> displayNameOptional = tabPlayer.getRelationalDisplayName(player.getPlayer().getUniqueId());
if (displayNameOptional.isEmpty()) {
return;
}
final Component lastDisplayName = displayNameOptional.get();
if (bypass || entry.getDisplayNameComponent().isEmpty() || !lastDisplayName.equals(entry.getDisplayNameComponent().get())) {
entry.setDisplayName(lastDisplayName);
}
}));
}
// Update the display names of all listed players
public void updateDisplayNames() {
players.values().forEach(this::updatePlayerDisplayName);
}
public void checkCorrectDisplayNames() {
players.values().forEach(this::checkCorrectDisplayName);
}
public void ensureDisplayNameTask() {
plugin.getServer().getScheduler()
.buildTask(plugin, this::checkCorrectDisplayNames)
.delay(1, TimeUnit.SECONDS)
.repeat(2, TimeUnit.SECONDS)
.schedule();
}
// Get the component for the TAB list header
public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) {
final String header = player.getGroup().getHeader(player.getHeaderIndex());
return Placeholder.replace(header, plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
}
// Get the component for the TAB list footer
public CompletableFuture<Component> getFooter(@NotNull TabPlayer player) {
final String footer = player.getGroup().getFooter(player.getFooterIndex());
return Placeholder.replace(footer, plugin, player)
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
}
/**
* Update the TAB list for all players when a plugin or proxy reload is performed
*/
public void reloadUpdate() {
taskManager.cancelAllTasks();
plugin.getTabGroups().getGroups().forEach(taskManager::updatePeriodically);
if (players.isEmpty()) {
return;
}
// If the update time is set to 0 do not schedule the updater
players.values().forEach(player -> {
final Optional<ServerConnection> server = player.getPlayer().getCurrentServer();
if (server.isEmpty()) {
return;
}
final String serverName = server.get().getServerInfo().getName();
final Optional<Group> group = getGroup(serverName);
if (group.isEmpty()) {
return;
}
player.setGroup(group.get());
this.sendPlayerServerLinks(player);
this.updatePlayer(player, true);
player.sendHeaderAndFooter(this);
});
updateDisplayNames();
}
@NotNull
public Optional<Group> getGroup(@NotNull String serverName) {
return plugin.getTabGroups().getGroupFromServer(serverName, plugin);
}
public void removeOldEntry(@NotNull Group group, @NotNull UUID uuid) {
final Set<TabPlayer> players = group.getTabPlayers(plugin);
players.forEach(player -> player.getPlayer().getTabList().removeEntry(uuid));
}
/**
* Remove an offline player from the list of tracked TAB players
*
* @param player The player to remove
*/
public void removeOfflinePlayer(@NotNull Player player) {
players.remove(player.getUniqueId());
}
/**
* Whether the player can use server-side specified TAB list ordering (Minecraft 1.21.2+)
*
* @param tabPlayer player to check
* @return {@code true} if the user is on Minecraft 1.21.2+; {@code false}
*/
private boolean hasListOrder(@NotNull TabPlayer tabPlayer) {
return tabPlayer.getPlayer().getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2);
}
private void updateSorting(@NotNull TabPlayer tabPlayer, @NotNull UUID uuid, int position) {
if (!tabPlayer.getPlayer().getTabList().containsEntry(uuid)) {
return;
}
if (tabPlayer.getCachedListOrders().containsKey(uuid) && tabPlayer.getCachedListOrders().get(uuid) == position) {
return;
}
tabPlayer.getCachedListOrders().put(uuid, position);
final UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(UPDATE_LIST_ORDER);
final UpsertPlayerInfoPacket.Entry entry = new UpsertPlayerInfoPacket.Entry(uuid);
entry.setListOrder(position);
packet.addEntry(entry);
((ConnectedPlayer) tabPlayer.getPlayer()).getConnection().write(packet);
}
private String getPlayerName(UUID uuid) {
return plugin.getServer().getPlayer(uuid).map(Player::getUsername).orElse("Unknown");
}
public synchronized void recalculateSortingForPlayer(@NotNull TabPlayer tabPlayer, @NotNull Set<TabPlayer> players) {
if (!hasListOrder(tabPlayer)) {
return;
}
players.forEach(p -> {
final int order = p.getListOrder();
updateSorting(tabPlayer, p.getPlayer().getUniqueId(), order);
});
}
}

View File

@ -0,0 +1,195 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.tab;
import com.google.common.collect.Sets;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.player.KickedFromServerEvent;
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.ServerInfo;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* The TabListListener class is responsible for handling events related to the player tab list.
*/
@SuppressWarnings("unused")
public class TabListListener {
private final Velocitab plugin;
private final PlayerTabList tabList;
// Set of UUIDs of users who just left the game - fixes packet delay problem on Minecraft 1.8.x
private final Set<UUID> justQuit;
public TabListListener(@NotNull Velocitab plugin, @NotNull PlayerTabList tabList) {
this.plugin = plugin;
this.tabList = tabList;
this.justQuit = Sets.newConcurrentHashSet();
}
@Subscribe
public void onKick(@NotNull KickedFromServerEvent event) {
event.getPlayer().getTabList().getEntries().stream()
.filter(entry -> entry.getProfile() != null && !entry.getProfile().getId().equals(event.getPlayer().getUniqueId()))
.forEach(entry -> event.getPlayer().getTabList().removeEntry(entry.getProfile().getId()));
event.getPlayer().getTabList().clearHeaderAndFooter();
if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer) {
tabList.removePlayer(event.getPlayer());
} else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer redirectPlayer) {
tabList.removePlayer(event.getPlayer(), redirectPlayer.getServer());
} else if (event.getResult() instanceof KickedFromServerEvent.Notify notify) {
return;
}
event.getPlayer().getTabList().removeEntry(event.getPlayer().getUniqueId());
event.getPlayer().getTabList().clearHeaderAndFooter();
justQuit.add(event.getPlayer().getUniqueId());
plugin.getServer().getScheduler().buildTask(plugin,
() -> justQuit.remove(event.getPlayer().getUniqueId()))
.delay(300, TimeUnit.MILLISECONDS)
.schedule();
}
@SuppressWarnings("UnstableApiUsage")
@Subscribe
public void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
final Player joined = event.getPlayer();
final String serverName = joined.getCurrentServer()
.map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("");
final Optional<Group> previousGroup = tabList.getTabPlayer(joined)
.map(TabPlayer::getGroup);
// Get the group the player should now be in
final @NotNull Optional<Group> groupOptional = tabList.getGroup(serverName);
final boolean isDefault = groupOptional.map(g -> g.isDefault(plugin)).orElse(true);
// Removes cached relational data of the joined player from all other players
plugin.getTabList().clearCachedData(joined);
if (!plugin.getSettings().isShowAllPlayersFromAllGroups() && previousGroup.isPresent()
&& (groupOptional.isPresent() && !previousGroup.get().equals(groupOptional.get())
|| groupOptional.isEmpty())) {
tabList.removeOldEntry(previousGroup.get(), joined.getUniqueId());
}
// If the server is not in a group, use fallback.
// If fallback is disabled, permit the player to switch excluded servers without a header or footer override
if (isDefault && !plugin.getSettings().isFallbackEnabled() && !groupOptional.map(g -> g.containsServer(plugin, serverName)).orElse(false)) {
final Optional<TabPlayer> tabPlayer = tabList.getTabPlayer(joined);
if (tabPlayer.isEmpty()) {
return;
}
if (event.getPreviousServer() == null) {
return;
}
final Component header = tabPlayer.get().getLastHeader();
final Component footer = tabPlayer.get().getLastFooter();
plugin.getServer().getScheduler().buildTask(plugin, () -> {
final Component currentHeader = joined.getPlayerListHeader();
final Component currentFooter = joined.getPlayerListFooter();
if ((header.equals(currentHeader) && footer.equals(currentFooter)) ||
(currentHeader.equals(Component.empty()) && currentFooter.equals(Component.empty()))
) {
joined.sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
joined.getCurrentServer().ifPresent(serverConnection -> serverConnection.getServer().getPlayersConnected().forEach(player ->
player.getTabList().getEntry(joined.getUniqueId())
.ifPresent(entry -> entry.setDisplayName(Component.text(joined.getUsername())))));
}
}).delay(500, TimeUnit.MILLISECONDS).schedule();
tabList.getPlayers().remove(event.getPlayer().getUniqueId());
return;
}
if (groupOptional.isEmpty()) {
return;
}
final Group group = groupOptional.get();
plugin.getScoreboardManager().resetCache(joined, group);
final int delay = justQuit.contains(joined.getUniqueId()) ? 100 : 250;
plugin.getServer().getScheduler().buildTask(plugin,
() -> tabList.joinPlayer(joined, group))
.delay(delay, TimeUnit.MILLISECONDS)
.schedule();
}
@SuppressWarnings("deprecation")
@Subscribe(order = PostOrder.CUSTOM, priority = Short.MIN_VALUE)
public void onPlayerQuit(@NotNull DisconnectEvent event) {
if (event.getLoginStatus() == DisconnectEvent.LoginStatus.CONFLICTING_LOGIN) {
return;
}
if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) {
checkDelayedDisconnect(event);
return;
}
// Remove the player from the tab list of all other players
tabList.removePlayer(event.getPlayer());
}
private void checkDelayedDisconnect(@NotNull DisconnectEvent event) {
final Player player = event.getPlayer();
plugin.getServer().getScheduler().buildTask(plugin, () -> {
final Optional<Player> actualPlayer = plugin.getServer().getPlayer(player.getUniqueId());
if (actualPlayer.isPresent() && !actualPlayer.get().equals(player)) {
return;
}
if (player.getCurrentServer().isPresent()) {
return;
}
tabList.removeOfflinePlayer(player);
tabList.removeTabListUUID(event.getPlayer().getUniqueId());
}).delay(750, TimeUnit.MILLISECONDS).schedule();
}
@Subscribe
public void proxyReload(@NotNull ProxyReloadEvent event) {
plugin.loadConfigs();
tabList.reloadUpdate();
plugin.log("Velocitab has been reloaded!");
}
}

View File

@ -0,0 +1,121 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.tab;
import com.google.common.collect.Maps;
import com.velocitypowered.api.scheduler.ScheduledTask;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class TaskManager {
private final Velocitab plugin;
private final Map<Group, GroupTasks> groupTasks;
public TaskManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.groupTasks = Maps.newConcurrentMap();
}
protected void cancelAllTasks() {
groupTasks.values().forEach(GroupTasks::cancel);
groupTasks.clear();
}
protected void updatePeriodically(@NotNull Group group) {
ScheduledTask headerFooterTask = null;
ScheduledTask updateTask = null;
ScheduledTask latencyTask;
if (group.headerFooterUpdateRate() > 0) {
headerFooterTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateGroupPlayers(group, false, true))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.headerFooterUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
}
if (group.placeholderUpdateRate() > 0) {
updateTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateGroupPlayers(group, true, false))
.delay(1, TimeUnit.SECONDS)
.repeat(Math.max(200, group.placeholderUpdateRate()), TimeUnit.MILLISECONDS)
.schedule();
}
latencyTask = plugin.getServer().getScheduler()
.buildTask(plugin, () -> updateLatency(group))
.delay(1, TimeUnit.SECONDS)
.repeat(3, TimeUnit.SECONDS)
.schedule();
groupTasks.put(group, new GroupTasks(headerFooterTask, updateTask, latencyTask));
}
/**
* Updates the players in the given group.
*
* @param group The group whose players should be updated.
* @param all Whether to update all player properties, or just the header and footer.
* @param incrementIndexes Whether to increment the header and footer indexes.
*/
private void updateGroupPlayers(@NotNull Group group, boolean all, boolean incrementIndexes) {
final Set<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
if (groupPlayers.isEmpty()) {
return;
}
groupPlayers.stream()
.filter(player -> player.getPlayer().isActive())
.forEach(player -> {
if (incrementIndexes) {
player.incrementIndexes();
}
if (all) {
plugin.getTabList().updatePlayer(player, false);
}
player.sendHeaderAndFooter(plugin.getTabList());
});
if (all) {
plugin.getTabList().updateDisplayNames();
}
}
private void updateLatency(@NotNull Group group) {
final Set<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
if (groupPlayers.isEmpty()) {
return;
}
groupPlayers.stream()
.filter(player -> player.getPlayer().isActive())
.forEach(player -> {
final int latency = (int) player.getPlayer().getPing();
final Set<TabPlayer> players = group.getTabPlayers(plugin, player);
players.forEach(p -> p.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
.ifPresent(entry -> entry.setLatency(Math.max(latency, 0))));
});
}
}

View File

@ -0,0 +1,111 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.tab;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.player.TabListEntry;
import lombok.RequiredArgsConstructor;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.UUID;
/**
* The VanishTabList handles the tab list for vanished players
*/
@RequiredArgsConstructor
public class VanishTabList {
private final Velocitab plugin;
private final PlayerTabList tabList;
public void vanishPlayer(@NotNull TabPlayer tabPlayer) {
tabList.getPlayers().values().forEach(p -> {
if (p.getPlayer().equals(tabPlayer.getPlayer())) {
return;
}
if (!plugin.getVanishManager().canSee(p.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) {
p.getPlayer().getTabList().removeEntry(tabPlayer.getPlayer().getUniqueId());
}
});
}
public void unVanishPlayer(@NotNull TabPlayer tabPlayer) {
final UUID uuid = tabPlayer.getPlayer().getUniqueId();
tabList.getPlayers().values().forEach(p -> {
if (p.getPlayer().equals(tabPlayer.getPlayer())) {
return;
}
if (!p.getPlayer().getTabList().containsEntry(uuid)) {
tabList.createEntry(tabPlayer, p.getPlayer().getTabList(), p);
} else {
tabList.updateDisplayName(tabPlayer, p);
}
});
}
/**
* Recalculates the visibility of players in the tab list for the given player.
* If tabPlayer can see the player, the player will be added to the tab list.
*
* @param tabPlayer The TabPlayer object representing the player for whom to recalculate the tab list visibility.
*/
public void recalculateVanishForPlayer(@NotNull TabPlayer tabPlayer) {
final Player player = tabPlayer.getPlayer();
plugin.getServer().getAllPlayers().forEach(p -> {
if (p.equals(player)) {
return;
}
final Optional<TabPlayer> targetOptional = tabList.getTabPlayer(p);
if (targetOptional.isEmpty()) {
return;
}
final TabPlayer target = targetOptional.get();
final String serverName = target.getServerName();
if (tabPlayer.getGroup().onlyListPlayersInSameServer()
&& !tabPlayer.getServerName().equals(serverName)) {
return;
}
final boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) ||
plugin.getVanishManager().canSee(player.getUsername(), p.getUsername());
if (!canSee) {
player.getTabList().removeEntry(p.getUniqueId());
plugin.getScoreboardManager().recalculateVanishForPlayer(tabPlayer, target, false);
} else {
if (!player.getTabList().containsEntry(p.getUniqueId())) {
final TabListEntry tabListEntry = tabList.createEntry(target, player.getTabList(), tabPlayer);
player.getTabList().addEntry(tabListEntry);
plugin.getScoreboardManager().recalculateVanishForPlayer(tabPlayer, target, true);
}
}
});
}
}

View File

@ -0,0 +1,36 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.util;
@FunctionalInterface
public interface QuadFunction<T, U, V, S, R> {
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @param v the third function argument
* @param s the fourth function argument
* @return the function result
*/
R apply(T t, U u, V v, S s);
}

View File

@ -0,0 +1,32 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.util;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
public final class SerializationUtil {
public final static LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder()
.hexCharacter('#')
.character('&')
.hexColors()
.build();
}

View File

@ -0,0 +1,39 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.vanish;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class DefaultVanishIntegration implements VanishIntegration {
@Override
public boolean canSee(@NotNull String name, @NotNull String otherName) {
return true;
}
@Override
public boolean isVanished(@NotNull String name) {
return false;
}
}

View File

@ -0,0 +1,30 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.vanish;
import org.jetbrains.annotations.NotNull;
public interface VanishIntegration {
boolean canSee(@NotNull String name, @NotNull String otherName);
boolean isVanished(@NotNull String name);
}

View File

@ -0,0 +1,77 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* 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.
*/
package net.william278.velocitab.vanish;
import com.velocitypowered.api.proxy.Player;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public class VanishManager {
private final Velocitab plugin;
private VanishIntegration integration;
public VanishManager(@NotNull Velocitab plugin) {
this.plugin = plugin;
setIntegration(new DefaultVanishIntegration());
}
public void setIntegration(@NotNull VanishIntegration integration) {
this.integration = integration;
}
@NotNull
public VanishIntegration getIntegration() {
return integration;
}
public boolean canSee(@NotNull String name, @NotNull String otherName) {
return integration.canSee(name, otherName);
}
public boolean isVanished(@NotNull String name) {
return integration.isVanished(name);
}
public void vanishPlayer(@NotNull Player player) {
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
if (tabPlayer.isEmpty()) {
plugin.log("Failed to vanish player " + player.getUsername() + " as they are not in the tab list");
return;
}
plugin.getTabList().getVanishTabList().vanishPlayer(tabPlayer.get());
plugin.getScoreboardManager().vanishPlayer(tabPlayer.get());
}
public void unVanishPlayer(@NotNull Player player) {
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
if (tabPlayer.isEmpty()) {
plugin.log("Failed to un-vanish player " + player.getUsername() + " as they are not in the tab list");
return;
}
plugin.getTabList().getVanishTabList().unVanishPlayer(tabPlayer.get());
plugin.getScoreboardManager().unVanishPlayer(tabPlayer.get());
}
}

View File

@ -0,0 +1,3 @@
velocity_api_version: '${velocity_api_version}'
velocity_minimum_build: ${velocity_minimum_build}
papi_proxy_bridge_minimum_version: '${papi_proxy_bridge_minimum_version}'

View File

@ -2,16 +2,13 @@
"id": "velocitab",
"name": "Velocitab",
"version": "${version}",
"description": "A super-simple (sorted!) Velocity TAB menu plugin",
"url": "https://william278.net",
"description": "${description}",
"url": "https://william278.net/project/velocitab",
"authors": [
"William278"
"William278",
"AlexDev03"
],
"dependencies": [
{
"id": "protocolize",
"optional": false
},
{
"id": "luckperms",
"optional": true