forked from Upstream/Velocitab
Compare commits
105 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e398306cd7 | ||
|
9cb20be6e0 | ||
|
a14c8eb2ea | ||
|
d47ee75d5b | ||
|
e17d36deb5 | ||
|
6796f4402f | ||
|
5a46053117 | ||
|
94760f7794 | ||
|
ce88b480a6 | ||
|
a55bd56364 | ||
|
7b8d55ba77 | ||
|
24c29e632f | ||
|
2ba208002a | ||
|
6c558fac3a | ||
|
67931d8d41 | ||
|
da946ac75a | ||
|
d9dcf54a91 | ||
|
724a9b2851 | ||
|
848c681cbf | ||
|
6f140e4708 | ||
|
73de08eea9 | ||
|
cf7c92e28f | ||
|
cc548b19fa | ||
|
4f2fe1ef3f | ||
|
3865387b80 | ||
|
852c3e830c | ||
|
5470d19378 | ||
|
43d05b73d7 | ||
|
e59cbe6ff0 | ||
|
59c8b08290 | ||
|
b4746dd483 | ||
|
642e18b618 | ||
|
4d7ffd75db | ||
|
20370a34aa | ||
|
fc39861f33 | ||
|
77254f9228 | ||
|
3a3f42f489 | ||
|
9c04513ee9 | ||
|
a0fd9e0d1e | ||
|
64060edcbd | ||
|
5ae32291a9 | ||
|
3f42c332f7 | ||
|
06fce2218b | ||
|
ace3644111 | ||
|
06268521cf | ||
|
1d05a1b34e | ||
|
84ae7a9437 | ||
|
6f909fbec1 | ||
|
53c9acb322 | ||
|
2dba7f2852 | ||
|
c13d30b29a | ||
|
1f54cf7ba4 | ||
|
fac0ccdcbb | ||
|
68c3199379 | ||
|
4770567a98 | ||
|
b4dd2f4d8b | ||
|
d441d6f5e7 | ||
|
5f7f5a64be | ||
|
720fca942f | ||
|
8c02dfa296 | ||
|
4d98b24c02 | ||
|
cf9175297f | ||
|
c23fdd1ff6 | ||
|
88dc2996e4 | ||
|
d6e71766a1 | ||
|
ced1b5e4f0 | ||
|
bc6c0c41b5 | ||
|
b7c353a0ec | ||
|
5b5e40e7f8 | ||
|
b118a8c134 | ||
|
48b3b2af48 | ||
|
4e2749ac9e | ||
|
c0abf481c1 | ||
|
3064aad4f3 | ||
|
39aaae2f30 | ||
|
e90a8df5c7 | ||
|
6a3d2abb8c | ||
|
763acb9f28 | ||
|
58742eb2a1 | ||
|
86b5e3a374 | ||
|
3d744ccefe | ||
|
47296961e2 | ||
|
4efc5797b3 | ||
|
a5940e0315 | ||
|
345ce7fa8a | ||
|
65e93781eb | ||
|
f47f5fc2fd | ||
|
65abbc1646 | ||
|
7caa185fc1 | ||
|
63ed22527b | ||
|
fc605ce6d7 | ||
|
e19d06ee18 | ||
|
dda537662e | ||
|
e5406051bf | ||
|
384137a67c | ||
|
c4a07e1997 | ||
|
7bd0ac3e0c | ||
|
7b347bb43e | ||
|
fec29dd057 | ||
|
247fc68a4a | ||
|
8bace563b4 | ||
|
f66483fdb1 | ||
|
f3ec35f8e9 | ||
|
8cc6df6fc2 | ||
|
9e60fc0daa |
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: build publish
|
||||
env:
|
||||
@ -38,6 +38,22 @@ jobs:
|
||||
- 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:
|
||||
@ -49,7 +65,7 @@ jobs:
|
||||
hangar-token: ${{ secrets.HANGAR_API_KEY }}
|
||||
hangar-version-type: Alpha
|
||||
hangar-game-versions: |
|
||||
3.3
|
||||
3.4
|
||||
files: target/Velocitab-*.jar
|
||||
name: Velocitab v${{ env.version_name }}
|
||||
version: ${{ env.version_name }}
|
||||
@ -104,10 +120,11 @@ jobs:
|
||||
1.20.2
|
||||
1.20.3
|
||||
1.20.4
|
||||
java: 17
|
||||
- name: 'Upload GitHub Artifact 📦'
|
||||
uses: actions/upload-artifact@v4
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: Velocitab
|
||||
path: target/Velocitab-*.jar
|
||||
1.20.5
|
||||
1.20.6
|
||||
1.21
|
||||
1.21.1
|
||||
1.21.2
|
||||
1.21.3
|
||||
1.21.4
|
||||
java: 17
|
2
.github/workflows/pr_tests.yml
vendored
2
.github/workflows/pr_tests.yml
vendored
@ -20,6 +20,6 @@ jobs:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: build
|
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@ -21,12 +21,28 @@ jobs:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v2
|
||||
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:
|
||||
@ -38,7 +54,7 @@ jobs:
|
||||
hangar-token: ${{ secrets.HANGAR_API_KEY }}
|
||||
hangar-version-type: Release
|
||||
hangar-game-versions: |
|
||||
3.3
|
||||
3.4
|
||||
files: target/Velocitab-*.jar
|
||||
name: Velocitab v${{ github.event.release.tag_name }}
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
@ -93,9 +109,11 @@ jobs:
|
||||
1.20.2
|
||||
1.20.3
|
||||
1.20.4
|
||||
java: 17
|
||||
- name: 'Upload GitHub Artifacts 📦'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Velocitab
|
||||
path: target/Velocitab-*.jar
|
||||
1.20.5
|
||||
1.20.6
|
||||
1.21
|
||||
1.21.1
|
||||
1.21.2
|
||||
1.21.3
|
||||
1.21.4
|
||||
java: 17
|
@ -76,4 +76,4 @@ Velocitab is licensed under the Apache 2.0 license.
|
||||
* **[Discord](https://discord.com/invite/tVYhJfyDWG)** — Get support, ask questions!
|
||||
|
||||
---
|
||||
© [William278](https://william278.net/), 2023. Licensed under the Apache-2.0 License.
|
||||
© [William278](https://william278.net/), 2024. Licensed under the Apache-2.0 License.
|
||||
|
91
build.gradle
91
build.gradle
@ -1,10 +1,10 @@
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
plugins {
|
||||
id 'xyz.jpenilla.run-velocity' version '2.2.2'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
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.2.1'
|
||||
id 'org.ajoberstar.grgit' version '5.3.0'
|
||||
id 'maven-publish'
|
||||
id 'java'
|
||||
}
|
||||
@ -17,44 +17,51 @@ 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://s01.oss.sonatype.org/content/repositories/snapshots/' }
|
||||
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||
maven { url = 'https://repo.william278.net/releases/' }
|
||||
maven { url = 'https://repo.william278.net/velocity/' }
|
||||
maven { url = 'https://jitpack.io/' }
|
||||
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://repo.minebench.de/' }
|
||||
maven { url = 'https://maven.elytrium.net/repo/' }
|
||||
maven { url = 'https://mvn.exceptionflug.de/repository/exceptionflug-public/' }
|
||||
maven { url = 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'com.velocitypowered:velocity-api:3.3.0-SNAPSHOT'
|
||||
compileOnly 'com.velocitypowered:velocity-proxy:3.3.0-SNAPSHOT'
|
||||
compileOnly 'io.netty:netty-codec-http:4.1.105.Final'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||
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 'io.github.miniplaceholders:miniplaceholders-api:2.0.0'
|
||||
compileOnly 'net.william278:PAPIProxyBridge:1.4.2'
|
||||
compileOnly 'it.unimi.dsi:fastutil:8.5.12'
|
||||
compileOnly 'net.kyori:adventure-nbt:4.14.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.11.0'
|
||||
implementation 'net.william278:DesertWell:2.0.4'
|
||||
implementation 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
implementation 'org.bstats:bstats-velocity:3.0.2'
|
||||
implementation 'com.github.Exlll.ConfigLib:configlib-yaml:v4.3.0'
|
||||
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.30'
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +72,6 @@ license {
|
||||
}
|
||||
|
||||
logger.lifecycle("Building Velocitab ${version} by William278")
|
||||
|
||||
version rootProject.version
|
||||
archivesBaseName = "${rootProject.name}"
|
||||
|
||||
@ -91,6 +97,10 @@ shadowJar {
|
||||
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 {
|
||||
exclude dependency(':slf4j-api')
|
||||
@ -100,7 +110,9 @@ shadowJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
|
||||
minimize()
|
||||
minimize() {
|
||||
exclude dependency('commons-logging:commons-logging')
|
||||
}
|
||||
}
|
||||
jar.dependsOn shadowJar
|
||||
clean.delete "$rootDir/target"
|
||||
@ -148,22 +160,33 @@ publishing {
|
||||
}
|
||||
|
||||
tasks {
|
||||
var papi = papi_proxy_bridge_minimum_version
|
||||
runVelocity {
|
||||
velocityVersion("3.3.0-SNAPSHOT")
|
||||
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
|
||||
}
|
@ -2,6 +2,8 @@ The Velocitab API provides methods for vanishing ("hiding") and modifying userna
|
||||
|
||||
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
|
||||
[](https://repo.william278.net/#/releases/net/william278/velocitab/)
|
||||
|
||||
@ -152,4 +154,5 @@ public class VelocitabAPIHook {
|
||||
|
||||
### 6. Next steps
|
||||
Now that you've got everything ready, you can start doing stuff with the Velocitab API!
|
||||
- [[API Examples]]
|
||||
- [[API Examples]]
|
||||
- See also: [[Plugin Message API]]
|
@ -11,9 +11,9 @@ To add additional frames of animation to a header format for a [server group](se
|
||||
|
||||
```yaml
|
||||
headers:
|
||||
- '&rainbow&Running Velocitab by William278'
|
||||
- '&rainbow:10&Running Velocitab by William278'
|
||||
- '&rainbow:20&Running Velocitab by William278'
|
||||
- '<rainbow>Running Velocitab by William278 & AlexDev_</rainbow>'
|
||||
- '<rainbow:10>Running Velocitab by William278 & AlexDev_</rainbow>'
|
||||
- '<rainbow:20>Running Velocitab by William278 & AlexDev_</rainbow>'
|
||||
```
|
||||
</details>
|
||||
|
||||
@ -35,47 +35,47 @@ Wondering how to make something like the above example? Here's how! This example
|
||||
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\n'
|
||||
- '&rainbow:2&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:4&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:6&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:8&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:10&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:12&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:14&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:16&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:18&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:20&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:22&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:24&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:26&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:28&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:30&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:32&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:34&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:36&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:38&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:40&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:42&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:44&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:46&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:48&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:50&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:52&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:54&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:56&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:58&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '&rainbow:60&Velocitab ⭐ A super-simple (sorted!) Velocity TAB menu plugin\n'
|
||||
- '<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&7For Velocity proxy servers:
|
||||
bd96a-#6cffa9&https://modrinth.com/plugin/velocitab
|
||||
bd96a-#6cffa9&https://william278.net/project/veloictab'
|
||||
format: 'ϧ-#fff&[%server%] &f%username%'
|
||||
\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: MINEDOWN
|
||||
formatter: MINIMESSAGE
|
||||
```
|
||||
</details>
|
||||
|
40
docs/Commands.md
Normal file
40
docs/Commands.md
Normal 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>
|
40
docs/Conditional-Placeholders.md
Normal file
40
docs/Conditional-Placeholders.md
Normal 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.
|
@ -17,32 +17,47 @@ The config file is located in `/plugins/velocitab/config.yml` and the tab groups
|
||||
check_for_updates: true
|
||||
# Whether to remove nametag from players' heads if the nametag associated with their server group is empty.
|
||||
remove_nametags: true
|
||||
# Which text formatter to use (MINEDOWN, MINIMESSAGE, or LEGACY)
|
||||
formatter: MINEDOWN
|
||||
# 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
|
||||
# Only show other players on a server that is part of the same server group as the player.
|
||||
only_list_players_in_same_group: true
|
||||
# Define custom names to be shown in the TAB list for specific server names.
|
||||
# If no custom display name is provided for a server, its original name will be used.
|
||||
server_display_names:
|
||||
very-long-server-name: VLSN
|
||||
# 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: 200
|
||||
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>
|
||||
@ -62,28 +77,38 @@ remove_spectator_effect: false
|
||||
# ┗╸ Documentation: https://william278.net/docs/velocitab
|
||||
|
||||
groups:
|
||||
- name: default
|
||||
headers:
|
||||
- '&rainbow&Running Velocitab by William278'
|
||||
footers:
|
||||
- '[There are currently %players_online%/%max_players_online% players online](gray)'
|
||||
format: '&7[%server%] &f%prefix%%username%'
|
||||
nametag:
|
||||
prefix: '&f%prefix%'
|
||||
suffix: '&f%suffix%'
|
||||
servers:
|
||||
- lobby
|
||||
- survival
|
||||
- creative
|
||||
- minigames
|
||||
- skyblock
|
||||
- prison
|
||||
- hub
|
||||
sorting_placeholders:
|
||||
- '%role_weight%'
|
||||
- '%username_lower%'
|
||||
header_footer_update_rate: 1000
|
||||
placeholder_update_rate: 1000
|
||||
- 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>
|
||||
@ -102,4 +127,10 @@ As well as updating the text in the TAB menu, Velocitab supports updating player
|
||||
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.
|
||||
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.
|
@ -1,14 +1,80 @@
|
||||
Velocitab supports the full range of modern color formatting, including RGB colors and gradients. Both MineDown (_default_), MiniMessage 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.
|
||||
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.
|
||||
|
||||
## MineDown syntax reference
|
||||
MineDown is the default formatter type, enabled by setting `formatter` to `MINEDOWN` in `config.yml`. See the [MineDown Syntax Reference](https://github.com/Phoenix616/MineDown) on GitHub for the specification of how to format text with it.
|
||||
|
||||
## MiniMessage syntax reference
|
||||
MiniMessage formatting can be 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.
|
||||
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>
|
||||
|
||||
|
@ -9,15 +9,20 @@ Please click through to the topic you wish to read about.
|
||||
* 📄 [[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)
|
||||
|
@ -4,33 +4,49 @@ Velocitab supports formatting the nametags of players (the text displayed above
|
||||
|
||||
> **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)
|
||||
|
||||
## Setting name tags
|
||||
You can configure nametags per-group using the `nametags` section of the config file. Each group should have one nametag format associated with it, which will be applied to all players on servers in that group.
|
||||
## 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(s) to display above players' heads for each server group. Set to empty to disable.
|
||||
# Nametag formats must contain a %username%. Docs: https://william278.net/docs/velocitab/nametags
|
||||
nametag:
|
||||
prefix: '&f%prefix%'
|
||||
suffix: '&f%suffix%'
|
||||
|
||||
# (...)
|
||||
|
||||
# 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
|
||||
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).
|
||||
|
||||
## Removing name tags
|
||||
In order to remove nametags, you must set `prefix` and `suffix` to empty. After that be sure to set `remove_nametags` to `true` in the [`config.yml` file](config-file).
|
||||
## 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:
|
||||
* Only legacy colors can be used in username formats. If RGB colors are specified, they will automatically be downsampled to the nearest legacy color. This is a limitation of the scoreboard team system.
|
||||
* Nametags cannot contain newlines (must be on a single line).
|
||||
* 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/).
|
||||
|
||||

|
||||
|
||||
|
88
docs/Placeholders-Replacements.md
Normal file
88
docs/Placeholders-Replacements.md
Normal 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: ''
|
||||
```
|
@ -3,40 +3,42 @@ Velocitab supports a number of Placeholders that will be replaced with their res
|
||||
## 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` |
|
||||
| `%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` |
|
||||
| `%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` |
|
||||
| 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` |
|
||||
|
||||
### Customising server display names
|
||||
You can make use of the `server_display_names` feature in `config.yml` to customise how server display name appear when using the `%server%` placeholder. In the below example, if a user is connected to a server with the name "`very-long-server-`name" and the player name format for the group that server belongs to includes a `%server%` placeholder, the placeholder would be replaced with "`VSLN`" instead of the full server name.
|
||||
**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).
|
||||
|
||||
<details>
|
||||
<summary>Server display names (config.yml)</summary>
|
||||
|
||||
```yaml
|
||||
# Define custom names to be shown in the TAB list for specific server names.
|
||||
# If no custom display name is provided for a server, its original name will be used.
|
||||
server_display_names:
|
||||
very-long-server-name: VLSN
|
||||
```
|
||||
</details>
|
||||
|
||||
## 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.
|
||||
@ -44,4 +46,5 @@ To use PlaceholderAPI placeholders in Velocitab, install the [PAPIProxyBridge](h
|
||||
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`
|
||||
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).
|
34
docs/Plugin-Message-API.md
Normal file
34
docs/Plugin-Message-API.md
Normal 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 — 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 — Changing player's team color</summary>
|
||||
|
||||
```java
|
||||
player.sendPluginMessage(plugin, "velocitab:update_team_color", "a".getBytes());
|
||||
```
|
||||
</details>
|
26
docs/Relational-Placeholders.md
Normal file
26
docs/Relational-Placeholders.md
Normal 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` |
|
||||
|
||||
|
@ -17,9 +17,9 @@ rate to use for the group.
|
||||
|
||||
```yaml
|
||||
headers:
|
||||
- '&rainbow&Running Velocitab by William278'
|
||||
- '<rainbow>Running Velocitab by William278 & AlexDev_</rainbow>'
|
||||
footers:
|
||||
- '[There are currently %players_online%/%max_players_online% players online](gray)'
|
||||
- '<gray>There are currently %players_online%/%max_players_online% players online</gray>'
|
||||
```
|
||||
|
||||
</details>
|
||||
@ -35,7 +35,7 @@ information.
|
||||
<summary>Example of format</summary>
|
||||
|
||||
```yaml
|
||||
format: '&7[%server%] &f%prefix%%username%'
|
||||
format: '<gray>[%server%] %prefix%%username%</gray>'
|
||||
```
|
||||
|
||||
</details>
|
||||
@ -51,8 +51,8 @@ Player formats may only utilize one line.
|
||||
|
||||
```yaml
|
||||
nametag:
|
||||
prefix: '&f%prefix%'
|
||||
suffix: '&f%suffix%'
|
||||
prefix: '<white>%prefix%</white>'
|
||||
suffix: '<white>%suffix%</white>'
|
||||
```
|
||||
|
||||
</details>
|
||||
@ -80,6 +80,18 @@ Player nametags may only utilize one line.
|
||||
</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
|
||||
|
||||
@ -136,10 +148,10 @@ placeholders in the TAB list will update. The default is 1000 milliseconds (1 se
|
||||
groups:
|
||||
- name: lobbies
|
||||
headers:
|
||||
- '&rainbow&Running Velocitab by William278 on Lobbies!'
|
||||
- '<rainbow:!2>Running Velocitab by William278 & AlexDev_ on Lobbies!</rainbow>'
|
||||
footers:
|
||||
- '[There are currently %players_online%/%max_players_online% players online](gray)'
|
||||
format: '&7[%server%] &f%prefix%%username%'
|
||||
- '<gray>There are currently %players_online%/%max_players_online% players online</gray>'
|
||||
format: '<gray>[%server%] %prefix%%username%</gray>'
|
||||
servers:
|
||||
- lobby
|
||||
- hub
|
||||
@ -153,10 +165,10 @@ groups:
|
||||
placeholder_update_rate: 1000
|
||||
- name: creative
|
||||
headers:
|
||||
- '&rainbow&Running Velocitab by William278 on Creative!'
|
||||
- '<rainbow:!2>Running Velocitab by William278 & AlexDev_ on Creative!</rainbow>'
|
||||
footers:
|
||||
- '[There are currently %players_online%/%max_players_online% players online](gray)'
|
||||
format: '&7[%server%] &f%prefix%%username%'
|
||||
- '<gray>There are currently %players_online%/%max_players_online% players online</gray>'
|
||||
format: '<gray>[%server%] %prefix%%username%</gray>'
|
||||
servers:
|
||||
- creative
|
||||
sorting_placeholders:
|
||||
@ -166,10 +178,10 @@ groups:
|
||||
placeholder_update_rate: 1000
|
||||
- name: survival
|
||||
headers:
|
||||
- '&rainbow&Running Velocitab by William278 on Survival!'
|
||||
- '<rainbow:!2>Running Velocitab by William278 & AlexDev_ on Survival!</rainbow>'
|
||||
footers:
|
||||
- '[There are currently %players_online%/%max_players_online% players online](gray)'
|
||||
format: '&7[%server%] &f%prefix%%username%'
|
||||
- '<gray>There are currently %players_online%/%max_players_online% players online</gray>'
|
||||
format: '<gray>[%server%] %prefix%%username%</gray>'
|
||||
servers:
|
||||
- survival
|
||||
sorting_placeholders:
|
||||
@ -205,4 +217,4 @@ fallback_enabled: true
|
||||
fallback_group: 'lobbies'
|
||||
```
|
||||
|
||||
</details>
|
||||
</details>
|
||||
|
37
docs/Server-Links.md
Normal file
37
docs/Server-Links.md
Normal 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: ['*']
|
||||
```
|
@ -1,7 +1,7 @@
|
||||
This page will walk you through installing Velocitab on a Velocity proxy server.
|
||||
|
||||
## Requirements
|
||||
* A Velocity proxy server (running Velocity 3.3.0 or newer)
|
||||
* 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—1.8.9
|
||||
- Minecraft 1.12.2—latest
|
||||
|
@ -37,6 +37,6 @@ There are a few compatibility caveats to bear in mind with sorting players in th
|
||||
|
||||
* 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.
|
||||
* Sending fake scoreboard team packets might not work correctly on some Minecraft server implementations such as [Quilt](https://quiltmc.org/).
|
||||
* 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.
|
||||
|
@ -3,15 +3,21 @@
|
||||
* 📄 [[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)
|
||||
|
@ -3,6 +3,10 @@ javaVersion=17
|
||||
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.daemon=true
|
||||
|
||||
plugin_version=1.6
|
||||
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
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
41
gradlew
vendored
@ -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
35
gradlew.bat
vendored
@ -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/server-links.png
Normal file
BIN
images/server-links.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
@ -29,11 +29,11 @@ import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
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;
|
||||
@ -42,14 +42,15 @@ 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.packet.ScoreboardManager;
|
||||
import net.william278.velocitab.hook.MiniPlaceholdersHook;
|
||||
import net.william278.velocitab.packet.PacketEventManager;
|
||||
import net.william278.velocitab.sorting.SortingManager;
|
||||
import net.william278.velocitab.tab.PlayerTabList;
|
||||
import net.william278.velocitab.packet.ScoreboardManager;
|
||||
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;
|
||||
@ -58,16 +59,16 @@ import org.slf4j.event.Level;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Plugin(id = "velocitab")
|
||||
@SuppressWarnings("unused")
|
||||
@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 configDirectory;
|
||||
@ -85,6 +86,7 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
private SortingManager sortingManager;
|
||||
private VanishManager vanishManager;
|
||||
private PacketEventManager packetEventManager;
|
||||
private PluginMessageAPI pluginMessageAPI;
|
||||
|
||||
@Inject
|
||||
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path configDirectory) {
|
||||
@ -95,6 +97,7 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
|
||||
@Subscribe
|
||||
public void onProxyInitialization(@NotNull ProxyInitializeEvent event) {
|
||||
checkCompatibility();
|
||||
loadConfigs();
|
||||
loadHooks();
|
||||
prepareVanishManager();
|
||||
@ -109,10 +112,10 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
|
||||
@Subscribe
|
||||
public void onProxyShutdown(@NotNull ProxyShutdownEvent event) {
|
||||
server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel);
|
||||
disableScoreboardManager();
|
||||
getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent);
|
||||
VelocitabAPI.unregister();
|
||||
getMiniPlaceholdersHook().ifPresent(MiniPlaceholdersHook::unregisterExpansion);
|
||||
unregisterAPI();
|
||||
logger.info("Successfully disabled Velocitab");
|
||||
}
|
||||
|
||||
@ -135,17 +138,31 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Velocitab getPlugin() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Optional<ScoreboardManager> getScoreboardManager() {
|
||||
return Optional.ofNullable(scoreboardManager);
|
||||
@Override
|
||||
public ScoreboardManager getScoreboardManager() {
|
||||
return scoreboardManager;
|
||||
}
|
||||
|
||||
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 unregisterAPI() {
|
||||
VelocitabAPI.unregister();
|
||||
if (pluginMessageAPI != null) {
|
||||
pluginMessageAPI.unregisterChannel();
|
||||
}
|
||||
}
|
||||
|
||||
private void registerCommands() {
|
||||
@ -161,6 +178,11 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
return pluginContainer.getDescription();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Version getVelocityVersion() {
|
||||
return Version.fromString(server.getVersion().getVersion(), "-");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Version getVersion() {
|
||||
return Version.fromString(getDescription().getVersion().orElseThrow(), "-");
|
||||
|
127
src/main/java/net/william278/velocitab/api/PluginMessageAPI.java
Normal file
127
src/main/java/net/william278/velocitab/api/PluginMessageAPI.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -29,12 +29,15 @@ 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 class.
|
||||
* The Velocitab API.
|
||||
* <p>
|
||||
* Retrieve an instance of the API class via {@link #getInstance()}.
|
||||
*
|
||||
* @since 1.5.1
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class VelocitabAPI {
|
||||
@ -51,8 +54,8 @@ public class VelocitabAPI {
|
||||
/**
|
||||
* Entrypoint to the {@link VelocitabAPI} API - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
* @since 1.5.2
|
||||
* @return instance of the Velocitab API
|
||||
* @since 1.5.1
|
||||
*/
|
||||
@NotNull
|
||||
public static VelocitabAPI getInstance() {
|
||||
@ -66,7 +69,8 @@ public class VelocitabAPI {
|
||||
* <b>(Internal use only)</b> - Register the API.
|
||||
*
|
||||
* @param plugin the plugin instance
|
||||
* @since 3.0
|
||||
* @hidden This method is for internal use only
|
||||
* @since 1.5.1
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void register(@NotNull Velocitab plugin) {
|
||||
@ -75,6 +79,9 @@ public class VelocitabAPI {
|
||||
|
||||
/**
|
||||
* <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() {
|
||||
@ -86,7 +93,7 @@ public class VelocitabAPI {
|
||||
*
|
||||
* @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 2.0
|
||||
* @since 1.5.1
|
||||
*/
|
||||
public Optional<TabPlayer> getUser(@NotNull Player player) {
|
||||
return plugin.getTabList().getTabPlayer(player);
|
||||
@ -98,6 +105,7 @@ public class VelocitabAPI {
|
||||
*
|
||||
* @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 -> {
|
||||
@ -111,15 +119,17 @@ public class VelocitabAPI {
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link PlayerTabList} handles the tab list for all players on the server groups.
|
||||
* 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() {
|
||||
@ -127,9 +137,10 @@ public class VelocitabAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the VanishIntegration for the VelocitabAPI.
|
||||
* 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);
|
||||
@ -140,6 +151,7 @@ public class VelocitabAPI {
|
||||
* 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() {
|
||||
@ -150,6 +162,7 @@ public class VelocitabAPI {
|
||||
* 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);
|
||||
@ -159,6 +172,7 @@ public class VelocitabAPI {
|
||||
* 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);
|
||||
@ -170,13 +184,45 @@ public class VelocitabAPI {
|
||||
* @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 = """
|
||||
|
@ -36,20 +36,23 @@ 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(final @NotNull 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("Author",
|
||||
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
|
||||
.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("AlexDev03").description("Code"),
|
||||
AboutMenu.Credit.of("Ironboundred").description("Code"),
|
||||
AboutMenu.Credit.of("Emibergo02").description("Code"),
|
||||
AboutMenu.Credit.of("FreeMonoid").description("Code"),
|
||||
@ -76,51 +79,73 @@ public final class VelocitabCommand {
|
||||
})
|
||||
)
|
||||
.then(LiteralArgumentBuilder.<CommandSource>literal("name")
|
||||
.then(RequiredArgumentBuilder.<CommandSource, String>argument("name", StringArgumentType.word())
|
||||
.requires(src -> hasPermission(src, "name"))
|
||||
.then(RequiredArgumentBuilder.<CommandSource, String>argument("name", StringArgumentType.greedyString())
|
||||
.requires(src -> src instanceof Player)
|
||||
.executes(ctx -> {
|
||||
if (!(ctx.getSource() instanceof Player player)) {
|
||||
ctx.getSource().sendMessage(Component.text("You must be a player to use this command!", MAIN_COLOR));
|
||||
return Command.SINGLE_SUCCESS;
|
||||
}
|
||||
|
||||
String name = StringArgumentType.getString(ctx, "name");
|
||||
Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
|
||||
|
||||
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 must in a correct server!", MAIN_COLOR));
|
||||
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 -> src.hasPermission("velocitab.command.reload"))
|
||||
.requires(src -> hasPermission(src, "reload"))
|
||||
.executes(ctx -> {
|
||||
plugin.loadConfigs();
|
||||
plugin.getTabList().reloadUpdate();
|
||||
ctx.getSource().sendMessage(Component.text(
|
||||
"Velocitab has been reloaded!",
|
||||
ctx.getSource().sendMessage(Component.text("Velocitab has been reloaded!",
|
||||
MAIN_COLOR));
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
)
|
||||
.then(LiteralArgumentBuilder.<CommandSource>literal("update")
|
||||
.requires(src -> src.hasPermission("velocitab.command.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" + plugin.getVersion() + ")", MAIN_COLOR));
|
||||
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 " + checked.getLatestVersion(), MAIN_COLOR));
|
||||
.text("An update for Velocitab is available. Please update to %s"
|
||||
.formatted(checked.getLatestVersion()), MAIN_COLOR));
|
||||
});
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
@ -129,7 +154,12 @@ public final class VelocitabCommand {
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,12 +19,23 @@
|
||||
|
||||
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
|
||||
@ -35,8 +46,14 @@ 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
|
||||
*
|
||||
@ -65,7 +82,7 @@ public interface ConfigProvider {
|
||||
Settings.class,
|
||||
YAML_CONFIGURATION_PROPERTIES.header(Settings.CONFIG_HEADER).build()
|
||||
));
|
||||
getSettings().validateConfig();
|
||||
getSettings().validateConfig(getPlugin());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,10 +113,79 @@ public interface ConfigProvider {
|
||||
TabGroups.class,
|
||||
YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
|
||||
));
|
||||
getTabGroups().validateConfig();
|
||||
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
|
||||
|
@ -19,12 +19,15 @@
|
||||
|
||||
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() throws IllegalStateException;
|
||||
void validateConfig(@NotNull Velocitab plugin) throws IllegalStateException;
|
||||
|
||||
}
|
||||
|
@ -25,8 +25,10 @@ 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 org.apache.commons.lang3.function.TriFunction;
|
||||
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;
|
||||
|
||||
@ -36,26 +38,33 @@ import java.util.function.Function;
|
||||
@SuppressWarnings("unused")
|
||||
public enum Formatter {
|
||||
MINEDOWN(
|
||||
(text, player, plugin) -> new MineDown(text).toComponent(),
|
||||
(text) -> text.replace("__", "_\\_"),
|
||||
(text, player, viewer, plugin) -> new MineDown(text).toComponent(),
|
||||
MineDown::escape,
|
||||
"MineDown",
|
||||
(text) -> new MineDown(text).toComponent()
|
||||
(text) -> new MineDown(text).toComponent(),
|
||||
(text) -> {
|
||||
throw new UnsupportedOperationException("MineDown does not support serialization");
|
||||
}
|
||||
),
|
||||
MINIMESSAGE(
|
||||
(text, player, plugin) -> plugin.getMiniPlaceholdersHook()
|
||||
.map(hook -> hook.format(text, player.getPlayer()))
|
||||
(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)
|
||||
(text) -> MiniMessage.miniMessage().deserialize(text),
|
||||
MiniMessage.miniMessage()::serialize
|
||||
),
|
||||
LEGACY(
|
||||
(text, player, plugin) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text),
|
||||
(text, player, viewer, plugin) -> SerializationUtil.LEGACY_SERIALIZER.deserialize(text),
|
||||
Function.identity(),
|
||||
"Legacy Text",
|
||||
(text) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text)
|
||||
SerializationUtil.LEGACY_SERIALIZER::deserialize,
|
||||
SerializationUtil.LEGACY_SERIALIZER::serialize
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Name of the formatter
|
||||
*/
|
||||
@ -64,24 +73,50 @@ public enum Formatter {
|
||||
/**
|
||||
* Function to apply formatting to a string
|
||||
*/
|
||||
private final TriFunction<String, TabPlayer, Velocitab, Component> formatter;
|
||||
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 TriFunction<String, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper,
|
||||
@NotNull String name, @NotNull Function<String, Component> emptyFormatter) {
|
||||
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, plugin);
|
||||
return formatter.apply(text, player, null, plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -91,7 +126,7 @@ public enum Formatter {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component emptyFormat(@NotNull String text) {
|
||||
public Component deserialize(@NotNull String text) {
|
||||
return emptyFormatter.apply(text);
|
||||
}
|
||||
|
||||
@ -105,4 +140,9 @@ public enum Formatter {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String serialize(@NotNull Component component) {
|
||||
return serializer.apply(component);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,33 +19,39 @@
|
||||
|
||||
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 de.exlll.configlib.Comment;
|
||||
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.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
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,
|
||||
List<String> servers,
|
||||
Set<String> servers,
|
||||
List<String> sortingPlaceholders,
|
||||
@Comment("""
|
||||
How often in milliseconds to periodically update the TAB list, including header and footer, for all users.
|
||||
If set to 0, TAB will be updated on player join/leave instead. (1s = 1000ms)
|
||||
The minimal update rate is 200ms, anything lower will automatically be set to 200ms.""")
|
||||
Map<String, List<PlaceholderReplacement>> placeholderReplacements,
|
||||
boolean collisions,
|
||||
int headerFooterUpdateRate,
|
||||
int placeholderUpdateRate
|
||||
int placeholderUpdateRate,
|
||||
boolean onlyListPlayersInSameServer
|
||||
) {
|
||||
|
||||
@NotNull
|
||||
@ -60,25 +66,49 @@ public record Group(
|
||||
.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 List<RegisteredServer> registeredServers(Velocitab plugin) {
|
||||
if (isDefault() && plugin.getSettings().isFallbackEnabled()) {
|
||||
return new ArrayList<>(plugin.getServer().getAllServers());
|
||||
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 servers.stream()
|
||||
.map(plugin.getServer()::getServer)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public boolean isDefault() {
|
||||
return name.equals("default");
|
||||
return getRegexServers(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<Player> getPlayers(Velocitab plugin) {
|
||||
List<Player> players = new ArrayList<>();
|
||||
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());
|
||||
}
|
||||
@ -86,16 +116,54 @@ public record Group(
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<TabPlayer> getTabPlayers(Velocitab plugin) {
|
||||
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))
|
||||
.toList();
|
||||
.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(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof Group group)) {
|
||||
return false;
|
||||
}
|
||||
|
76
src/main/java/net/william278/velocitab/config/Metadata.java
Normal file
76
src/main/java/net/william278/velocitab/config/Metadata.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -19,18 +19,26 @@
|
||||
|
||||
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;
|
||||
@ -45,23 +53,84 @@ 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())),
|
||||
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.getServerDisplayName(plugin)),
|
||||
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("")),
|
||||
ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName().orElse("")),
|
||||
ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()),
|
||||
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()
|
||||
LUCKPERMS_META((param, plugin, player) -> plugin.getLuckPermsHook()
|
||||
.map(hook -> hook.getMeta(player.getPlayer(), param))
|
||||
.orElse(""));
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_" + param + "%")));
|
||||
|
||||
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";
|
||||
|
||||
/**
|
||||
* Function to replace placeholders with a real value
|
||||
@ -69,8 +138,6 @@ public enum Placeholder {
|
||||
private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
|
||||
private final boolean parameterised;
|
||||
private final Pattern pattern;
|
||||
private final static Pattern checkPlaceholders = Pattern.compile("%.*?%");
|
||||
private final static String DELIMITER = ":::";
|
||||
|
||||
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
|
||||
this.parameterised = false;
|
||||
@ -81,7 +148,7 @@ public enum Placeholder {
|
||||
Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) {
|
||||
this.parameterised = true;
|
||||
this.replacer = parameterisedReplacer;
|
||||
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]+%", Pattern.CASE_INSENSITIVE);
|
||||
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]*%", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
public static CompletableFuture<Nametag> replace(@NotNull Nametag nametag, @NotNull Velocitab plugin,
|
||||
@ -91,6 +158,125 @@ public enum Placeholder {
|
||||
.thenApply(v -> new Nametag(v[0], v.length > 1 ? v[1] : ""));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
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) {
|
||||
|
||||
@ -98,35 +284,48 @@ public enum Placeholder {
|
||||
return CompletableFuture.completedFuture("");
|
||||
}
|
||||
|
||||
for (Placeholder placeholder : values()) {
|
||||
Matcher matcher = placeholder.pattern.matcher(format);
|
||||
if (placeholder.parameterised) {
|
||||
// Replace the placeholder with the result of the replacer function with the parameter
|
||||
format = matcher.replaceAll(matchResult ->
|
||||
placeholder.replacer.apply(StringUtils.chop(matchResult.group().replace("%" + placeholder.name().toLowerCase(), ""))
|
||||
, plugin, player));
|
||||
} else {
|
||||
// Replace the placeholder with the result of the replacer function
|
||||
format = matcher.replaceAll(matchResult -> placeholder.replacer.apply(null, plugin, player));
|
||||
}
|
||||
|
||||
}
|
||||
final String replaced = format;
|
||||
|
||||
if (!checkPlaceholders.matcher(replaced).find()) {
|
||||
return CompletableFuture.completedFuture(replaced);
|
||||
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.formatPlaceholders(replaced, player.getPlayer())
|
||||
.map(hook -> hook.parsePlaceholders(placeholders, player.getPlayer())
|
||||
.exceptionally(e -> {
|
||||
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
|
||||
return replaced;
|
||||
return Map.of();
|
||||
})
|
||||
)
|
||||
.orElse(CompletableFuture.completedFuture(replaced)).exceptionally(e -> {
|
||||
.orElse(CompletableFuture.completedFuture(Maps.newHashMap()))
|
||||
.exceptionally(e -> {
|
||||
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
|
||||
return replaced;
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
89
src/main/java/net/william278/velocitab/config/ServerUrl.java
Normal file
89
src/main/java/net/william278/velocitab/config/ServerUrl.java
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -24,6 +24,7 @@ import de.exlll.configlib.Configuration;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
@ -34,7 +35,7 @@ import java.util.Map;
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Settings implements ConfigValidator{
|
||||
public class Settings implements ConfigValidator {
|
||||
|
||||
public static final String CONFIG_HEADER = """
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
@ -50,8 +51,11 @@ public class Settings implements ConfigValidator{
|
||||
@Comment("Whether to remove nametag from players' heads if the nametag associated with their server group is empty.")
|
||||
private boolean removeNametags = false;
|
||||
|
||||
@Comment("Which text formatter to use (MINEDOWN, MINIMESSAGE, or LEGACY)")
|
||||
private Formatter formatter = Formatter.MINEDOWN;
|
||||
@Comment("Whether to disable header and footer if they are empty and let backend servers handle them.")
|
||||
private boolean disableHeaderFooterIfEmpty = true;
|
||||
|
||||
@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.")
|
||||
@ -60,12 +64,8 @@ public class Settings implements ConfigValidator{
|
||||
@Comment("The formats to use for the fallback group.")
|
||||
private String fallbackGroup = "default";
|
||||
|
||||
@Comment("Only show other players on a server that is part of the same server group as the player.")
|
||||
private boolean onlyListPlayersInSameGroup = true;
|
||||
|
||||
@Comment("Define custom names to be shown in the TAB list for specific server names."
|
||||
+ "\nIf no custom display name is provided for a server, its original name will be used.")
|
||||
private Map<String, String> serverDisplayNames = Map.of("very-long-server-name", "VLSN");
|
||||
@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;
|
||||
@ -80,25 +80,47 @@ public class Settings implements ConfigValidator{
|
||||
+ "\nTurn this off if you're using scoreboard teams on backend servers.")
|
||||
private boolean sendScoreboardPackets = true;
|
||||
|
||||
@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;
|
||||
|
||||
@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;
|
||||
|
||||
/**
|
||||
* Get display name for the server
|
||||
*
|
||||
* @param serverName The server name
|
||||
* @return The display name, or the server name if no display name is defined
|
||||
*/
|
||||
@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 getServerDisplayName(@NotNull String serverName) {
|
||||
return serverDisplayNames.getOrDefault(serverName, serverName);
|
||||
public List<ServerUrl> getUrlsForGroup(@NotNull Group group) {
|
||||
return serverLinks.stream()
|
||||
.filter(link -> link.groups().contains("*") || link.groups().contains(group.name()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig() {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,14 +19,18 @@
|
||||
|
||||
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.List;
|
||||
import java.util.*;
|
||||
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
@Getter
|
||||
@ -42,36 +46,66 @@ public class TabGroups implements ConfigValidator {
|
||||
┣╸ Information: https://william278.net/project/velocitab
|
||||
┗╸ Documentation: https://william278.net/docs/velocitab""";
|
||||
|
||||
public List<Group> groups = List.of(
|
||||
new Group(
|
||||
"default",
|
||||
List.of("&rainbow&Running Velocitab by William278"),
|
||||
List.of("[There are currently %players_online%/%max_players_online% players online](gray)"),
|
||||
"&7[%server%] &f%prefix%%username%",
|
||||
new Nametag("&f%prefix%", "&f%suffix%"),
|
||||
List.of("lobby", "survival", "creative", "minigames", "skyblock", "prison", "hub"),
|
||||
List.of("%role_weight%", "%username_lower%"),
|
||||
1000,
|
||||
1000
|
||||
)
|
||||
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 " + name + " found"));
|
||||
.orElseThrow(() -> new IllegalStateException("No group with name %s found".formatted(name)));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Group getGroupFromServer(@NotNull String server) {
|
||||
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.servers().contains(server)) {
|
||||
return group;
|
||||
if (group.registeredServers(plugin, false)
|
||||
.stream()
|
||||
.anyMatch(s -> s.getServerInfo().getName().equalsIgnoreCase(server))) {
|
||||
return Optional.of(group);
|
||||
}
|
||||
}
|
||||
return getGroupFromName("default");
|
||||
|
||||
if (!plugin.getSettings().isFallbackEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return defaultGroup;
|
||||
}
|
||||
|
||||
public int getPosition(@NotNull Group group) {
|
||||
@ -80,12 +114,73 @@ public class TabGroups implements ConfigValidator {
|
||||
|
||||
|
||||
@Override
|
||||
public void validateConfig() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
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;
|
||||
@ -34,7 +35,6 @@ import net.william278.velocitab.tab.PlayerTabList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@ -45,18 +45,21 @@ public class LuckPermsHook extends Hook {
|
||||
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();
|
||||
this.lastUpdate = new HashMap<>();
|
||||
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
|
||||
@ -92,6 +95,10 @@ public class LuckPermsHook extends Hook {
|
||||
}
|
||||
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()
|
||||
@ -103,7 +110,12 @@ public class LuckPermsHook extends Hook {
|
||||
|
||||
final TabPlayer tabPlayer = tabPlayerOptional.get();
|
||||
final Role oldRole = tabPlayer.getRole();
|
||||
tabPlayer.setRole(getRoleFromMetadata(event.getData().getMetaData()));
|
||||
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);
|
||||
|
@ -25,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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,11 +19,15 @@
|
||||
|
||||
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 {
|
||||
@ -41,4 +45,17 @@ public class PAPIProxyBridgeHook extends Hook {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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*", ">");
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -19,7 +19,6 @@
|
||||
|
||||
package net.william278.velocitab.packet;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.velocitypowered.api.event.AwaitingEventExecutor;
|
||||
import com.velocitypowered.api.event.EventTask;
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
@ -27,31 +26,20 @@ 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 com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
||||
import io.netty.channel.Channel;
|
||||
import lombok.Getter;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.DefaultChannelPipeline;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PacketEventManager {
|
||||
|
||||
private static final String KEY = "velocitab";
|
||||
private static final String CITIZENS_PREFIX = "CIT";
|
||||
|
||||
private final Velocitab plugin;
|
||||
@Getter
|
||||
private final Set<UUID> velocitabEntries;
|
||||
|
||||
public PacketEventManager(@NotNull Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.velocitabEntries = Sets.newConcurrentHashSet();
|
||||
this.loadPlayers();
|
||||
this.loadListeners();
|
||||
}
|
||||
@ -87,32 +75,17 @@ public class PacketEventManager {
|
||||
public void removePlayer(@NotNull Player player) {
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
|
||||
final Channel channel = connectedPlayer.getConnection().getChannel();
|
||||
if (channel.pipeline().get(KEY) != null) {
|
||||
channel.pipeline().remove(KEY);
|
||||
final ChannelHandler handler = channel.pipeline().get(KEY);
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleEntry(@NotNull UpsertPlayerInfo packet, @NotNull Player player) {
|
||||
final List<TabPlayer> toUpdate = packet.getEntries().stream()
|
||||
.filter(entry -> entry.getProfile() != null)
|
||||
.filter(entry -> !entry.getProfile().getName().startsWith(CITIZENS_PREFIX))
|
||||
.filter(entry -> velocitabEntries.stream().noneMatch(uuid -> uuid.equals(entry.getProfile().getId())))
|
||||
.map(entry -> entry.getProfile().getId())
|
||||
.map(id -> plugin.getTabList().getTabPlayer(id))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.toList();
|
||||
|
||||
if (toUpdate.isEmpty()) {
|
||||
if (channel.pipeline() instanceof DefaultChannelPipeline defaultChannelPipeline) {
|
||||
defaultChannelPipeline.removeIfExists(KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
toUpdate.forEach(tabPlayer -> packet.getEntries().stream()
|
||||
.filter(entry -> entry.getProfile() != null)
|
||||
.filter(entry -> entry.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId()))
|
||||
.findFirst()
|
||||
.ifPresent(entry -> entry.setDisplayName(
|
||||
new ComponentHolder(player.getProtocolVersion(), tabPlayer.getLastDisplayname()))));
|
||||
plugin.getLogger().warn("Failed to remove player {} from Velocitab packet handler {}",
|
||||
player.getUsername(), channel.pipeline().getClass().getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,10 +30,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
// Based on VPacketEvents PacketRegistration API
|
||||
@ -105,7 +102,7 @@ public final class PacketRegistration<P extends MinecraftPacket> {
|
||||
try {
|
||||
IntObjectMap<Supplier<?>> packetIdToSupplier = (IntObjectMap<Supplier<?>>) PACKET_REGISTRY$packetIdToSupplier.invoke(protocolRegistry);
|
||||
Object2IntMap<Class<?>> packetClassToId = (Object2IntMap<Class<?>>) PACKET_REGISTRY$packetClassToId.invoke(protocolRegistry);
|
||||
packetIdToSupplier.keySet().stream()
|
||||
Set.copyOf(packetIdToSupplier.keySet()).stream()
|
||||
.filter(supplier -> packetIdToSupplier.get(supplier).get().getClass().equals(packetClass))
|
||||
.forEach(packetIdToSupplier::remove);
|
||||
packetClassToId.values().intStream()
|
||||
@ -141,20 +138,31 @@ public final class PacketRegistration<P extends MinecraftPacket> {
|
||||
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);
|
||||
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 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 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);
|
||||
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) {
|
||||
|
@ -20,15 +20,21 @@
|
||||
package net.william278.velocitab.packet;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo;
|
||||
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 {
|
||||
@ -38,33 +44,94 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
|
||||
|
||||
@Override
|
||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
||||
if (!(msg instanceof final UpsertPlayerInfo minecraftPacket)) {
|
||||
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(UpsertPlayerInfo.Action.UPDATE_GAME_MODE)) {
|
||||
forceGameMode(minecraftPacket.getEntries());
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (!minecraftPacket.containsAction(UpsertPlayerInfo.Action.ADD_PLAYER) && !minecraftPacket.containsAction(UpsertPlayerInfo.Action.UPDATE_LISTED)) {
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
if (minecraftPacket.getEntries().stream().allMatch(entry -> entry.getProfile() != null && entry.getProfile().getName().startsWith("CIT"))) {
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().error("An error occurred while handling a packet", e);
|
||||
super.write(ctx, msg, promise);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getPacketEventManager().handleEntry(minecraftPacket, player);
|
||||
super.write(ctx, msg, promise);
|
||||
}
|
||||
|
||||
private void forceGameMode(@NotNull List<UpsertPlayerInfo.Entry> entries) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,10 @@ public class Protocol404Adapter extends TeamsPacketAdapter {
|
||||
private final GsonComponentSerializer serializer;
|
||||
|
||||
public Protocol404Adapter(@NotNull Velocitab plugin) {
|
||||
super(plugin, Set.of(ProtocolVersion.MINECRAFT_1_13_2,
|
||||
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,
|
||||
@ -55,7 +58,34 @@ public class Protocol404Adapter extends TeamsPacketAdapter {
|
||||
|
||||
public Protocol404Adapter(@NotNull Velocitab plugin, Set<ProtocolVersion> protocolVersions) {
|
||||
super(plugin, protocolVersions);
|
||||
serializer = null;
|
||||
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
|
||||
@ -84,8 +114,13 @@ public class Protocol404Adapter extends TeamsPacketAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeComponent(ByteBuf buf, Component component) {
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -44,6 +44,35 @@ public class Protocol48Adapter extends TeamsPacketAdapter {
|
||||
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()));
|
||||
@ -75,6 +104,7 @@ public class Protocol48Adapter extends TeamsPacketAdapter {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@ -83,7 +113,12 @@ public class Protocol48Adapter extends TeamsPacketAdapter {
|
||||
return string.substring(0, Math.min(string.length(), 16));
|
||||
}
|
||||
|
||||
protected void writeComponent(ByteBuf buf, Component component) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -58,8 +58,13 @@ public class Protocol735Adapter extends Protocol404Adapter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeComponent(ByteBuf buf, Component component) {
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
@ -32,19 +32,30 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Adapter for handling the UpdateTeamsPacket for Minecraft 1.20.3-1.20.4
|
||||
* 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_3,
|
||||
ProtocolVersion.MINECRAFT_1_20_5,
|
||||
ProtocolVersion.MINECRAFT_1_21,
|
||||
ProtocolVersion.MINECRAFT_1_21_2,
|
||||
ProtocolVersion.MINECRAFT_1_21_4
|
||||
));
|
||||
}
|
||||
|
||||
protected void writeComponent(ByteBuf buf, Component component) {
|
||||
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)
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,6 +20,8 @@
|
||||
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;
|
||||
@ -28,14 +30,18 @@ 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.concurrent.CompletableFuture;
|
||||
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.*;
|
||||
|
||||
@ -43,34 +49,57 @@ public class ScoreboardManager {
|
||||
|
||||
private PacketRegistration<UpdateTeamsPacket> packetRegistration;
|
||||
private final Velocitab plugin;
|
||||
private final Set<TeamsPacketAdapter> versions;
|
||||
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.teams = teams;
|
||||
this.createdTeams = Maps.newConcurrentMap();
|
||||
this.nametags = Maps.newConcurrentMap();
|
||||
this.versions = Sets.newHashSet();
|
||||
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 {
|
||||
versions.add(new Protocol765Adapter(plugin));
|
||||
versions.add(new Protocol735Adapter(plugin));
|
||||
versions.add(new Protocol404Adapter(plugin));
|
||||
versions.add(new Protocol48Adapter(plugin));
|
||||
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 packet adapters. Try to update velocity to latest build", 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 versions.stream()
|
||||
.filter(adapter -> adapter.getProtocolVersions().contains(version))
|
||||
.findFirst()
|
||||
return Optional.ofNullable(versions.get(version))
|
||||
.orElseThrow(() -> new IllegalArgumentException("No adapter found for protocol version " + version));
|
||||
}
|
||||
|
||||
@ -81,18 +110,29 @@ public class ScoreboardManager {
|
||||
public void resetCache(@NotNull Player player) {
|
||||
final String team = createdTeams.remove(player.getUniqueId());
|
||||
if (team != null) {
|
||||
final TabPlayer tabPlayer = plugin.getTabList().getTabPlayer(player).orElseThrow();
|
||||
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), tabPlayer);
|
||||
removeSortedTeam(team);
|
||||
plugin.getTabList().getTabPlayer(player).ifPresent(tabPlayer ->
|
||||
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), tabPlayer)
|
||||
);
|
||||
trackedTeams.removeAll(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@ -111,16 +151,19 @@ public class ScoreboardManager {
|
||||
if (teamName == null) {
|
||||
return;
|
||||
}
|
||||
final List<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
|
||||
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 -> {
|
||||
final UpdateTeamsPacket packet = vanish ? UpdateTeamsPacket.removeTeam(plugin, teamName) :
|
||||
UpdateTeamsPacket.create(plugin, tabPlayer, teamName, nametag, player.getUsername());
|
||||
siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player)
|
||||
.filter(p -> vanish && !plugin.getVanishManager().canSee(p.getUsername(), player.getUsername()))
|
||||
.forEach(connected -> dispatchPacket(packet, connected)));
|
||||
});
|
||||
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());
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -130,14 +173,14 @@ public class ScoreboardManager {
|
||||
* @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 void updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
|
||||
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;
|
||||
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())) {
|
||||
@ -146,40 +189,56 @@ public class ScoreboardManager {
|
||||
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);
|
||||
dispatchGroupPacket(
|
||||
UpdateTeamsPacket.create(plugin, tabPlayer, role, newTag, name),
|
||||
tabPlayer
|
||||
);
|
||||
dispatchGroupCreatePacket(plugin, tabPlayer, role, newTag, name);
|
||||
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) {
|
||||
this.nametags.put(role, newTag);
|
||||
dispatchGroupPacket(
|
||||
UpdateTeamsPacket.changeNametag(plugin, tabPlayer, role, newTag),
|
||||
tabPlayer
|
||||
);
|
||||
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 List<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
|
||||
final List<Player> players = siblings.stream()
|
||||
.map(RegisteredServer::getPlayersConnected)
|
||||
.flatMap(Collection::stream)
|
||||
.toList();
|
||||
final Set<Player> players = tabPlayer.getGroup().getPlayers(plugin, tabPlayer);
|
||||
|
||||
final List<String> roles = new ArrayList<>();
|
||||
final Set<String> roles = Sets.newHashSet();
|
||||
players.forEach(p -> {
|
||||
if (p == player || !p.isActive()) {
|
||||
return;
|
||||
@ -194,6 +253,13 @@ public class ScoreboardManager {
|
||||
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;
|
||||
@ -203,33 +269,93 @@ public class ScoreboardManager {
|
||||
// Send packet
|
||||
final Nametag tag = nametags.get(role);
|
||||
if (tag != null) {
|
||||
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(
|
||||
plugin, tabPlayer, role, tag, p.getUsername()
|
||||
);
|
||||
dispatchPacket(packet, player);
|
||||
dispatchCreatePacket(plugin, targetTabPlayer, role, tag, tabPlayer, p.getUsername());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchPacket(@NotNull UpdateTeamsPacket packet, @NotNull Player player) {
|
||||
if (!player.isActive()) {
|
||||
plugin.getTabList().removeOfflinePlayer(player);
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
|
||||
connectedPlayer.getConnection().write(packet);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e);
|
||||
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 {
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected;
|
||||
connectedPlayer.getConnection().write(packet);
|
||||
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);
|
||||
}
|
||||
@ -237,45 +363,66 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
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 List<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
|
||||
siblings.forEach(server -> server.getPlayersConnected().forEach(connected -> {
|
||||
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;
|
||||
}
|
||||
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected;
|
||||
connectedPlayer.getConnection().write(packet);
|
||||
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 {
|
||||
packetRegistration = PacketRegistration.of(UpdateTeamsPacket.class)
|
||||
.direction(ProtocolUtils.Direction.CLIENTBOUND)
|
||||
.packetSupplier(() -> new UpdateTeamsPacket(plugin))
|
||||
.stateRegistry(StateRegistry.PLAY)
|
||||
.mapping(0x3E, MINECRAFT_1_8, true)
|
||||
.mapping(0x44, MINECRAFT_1_12_2, true)
|
||||
.mapping(0x47, MINECRAFT_1_13, true)
|
||||
.mapping(0x4B, MINECRAFT_1_14, true)
|
||||
.mapping(0x4C, MINECRAFT_1_15, true)
|
||||
.mapping(0x55, MINECRAFT_1_17, true)
|
||||
.mapping(0x58, MINECRAFT_1_19_1, true)
|
||||
.mapping(0x56, MINECRAFT_1_19_3, true)
|
||||
.mapping(0x5A, MINECRAFT_1_19_4, true)
|
||||
.mapping(0x5C, MINECRAFT_1_20_2, true)
|
||||
.mapping(0x5E, MINECRAFT_1_20_3, true);
|
||||
.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);
|
||||
@ -302,6 +449,9 @@ public class ScoreboardManager {
|
||||
* @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) {
|
||||
@ -309,15 +459,14 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
final UpdateTeamsPacket removeTeam = UpdateTeamsPacket.removeTeam(plugin, team);
|
||||
dispatchPacket(removeTeam, player);
|
||||
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) {
|
||||
final UpdateTeamsPacket addTeam = UpdateTeamsPacket.create(
|
||||
plugin, tabPlayer, team, tag, target.getPlayer().getUsername()
|
||||
);
|
||||
dispatchPacket(addTeam, player);
|
||||
dispatchCreatePacket(plugin, tabPlayer, team, tag, target, target.getPlayer().getUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,11 @@ public abstract class TeamsPacketAdapter {
|
||||
|
||||
public abstract void encode(@NotNull ByteBuf byteBuf, @NotNull UpdateTeamsPacket packet, @NotNull ProtocolVersion protocolVersion);
|
||||
|
||||
protected abstract void writeComponent(ByteBuf buf, Component component);
|
||||
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);
|
||||
|
||||
}
|
||||
|
@ -36,7 +36,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@ -64,21 +63,29 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean isRemoveTeam() {
|
||||
return mode == UpdateMode.REMOVE_TEAM;
|
||||
}
|
||||
|
||||
public boolean hasEntities() {
|
||||
return entities != null && !entities.isEmpty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static UpdateTeamsPacket create(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
|
||||
@NotNull String teamName,
|
||||
@NotNull Nametag nametag,
|
||||
@NotNull String teamName, @NotNull Nametag nametag,
|
||||
@NotNull TabPlayer viewer,
|
||||
@NotNull String... teamMembers) {
|
||||
return new UpdateTeamsPacket(plugin)
|
||||
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.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(CollisionRule.ALWAYS)
|
||||
.color(getLastColor(nametag.prefix(), plugin))
|
||||
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
|
||||
.suffix(nametag.getSuffixComponent(plugin, tabPlayer))
|
||||
.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));
|
||||
}
|
||||
|
||||
@ -92,25 +99,25 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
|
||||
@NotNull
|
||||
protected static UpdateTeamsPacket changeNametag(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer,
|
||||
@NotNull String teamName,
|
||||
@NotNull String teamName, @NotNull TabPlayer viewer,
|
||||
@NotNull Nametag nametag) {
|
||||
return new UpdateTeamsPacket(plugin)
|
||||
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.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(CollisionRule.ALWAYS)
|
||||
.color(getLastColor(nametag.prefix(), plugin))
|
||||
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
|
||||
.suffix(nametag.getSuffixComponent(plugin, tabPlayer));
|
||||
.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.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.teamName(teamName)
|
||||
.mode(UpdateMode.ADD_PLAYERS)
|
||||
.entities(Arrays.asList(teamMembers));
|
||||
}
|
||||
@ -119,7 +126,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
protected static UpdateTeamsPacket removeFromTeam(@NotNull Velocitab plugin, @NotNull String teamName,
|
||||
@NotNull String... teamMembers) {
|
||||
return new UpdateTeamsPacket(plugin)
|
||||
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.teamName(teamName)
|
||||
.mode(UpdateMode.REMOVE_PLAYERS)
|
||||
.entities(Arrays.asList(teamMembers));
|
||||
}
|
||||
@ -127,11 +134,15 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
@NotNull
|
||||
protected static UpdateTeamsPacket removeTeam(@NotNull Velocitab plugin, @NotNull String teamName) {
|
||||
return new UpdateTeamsPacket(plugin)
|
||||
.teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName)
|
||||
.teamName(teamName)
|
||||
.mode(UpdateMode.REMOVE_TEAM);
|
||||
}
|
||||
|
||||
public static int getLastColor(@Nullable String text, @NotNull Velocitab plugin) {
|
||||
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;
|
||||
}
|
||||
@ -140,10 +151,10 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
text = text + "z";
|
||||
|
||||
//serialize & deserialize to downsample rgb to legacy
|
||||
Component component = plugin.getFormatter().emptyFormat(text);
|
||||
final Component component = plugin.getFormatter().deserialize(text);
|
||||
text = LegacyComponentSerializer.legacyAmpersand().serialize(component);
|
||||
|
||||
int lastFormatIndex = text.lastIndexOf("&");
|
||||
final int lastFormatIndex = text.lastIndexOf("&");
|
||||
if (lastFormatIndex == -1 || lastFormatIndex == text.length() - 1) {
|
||||
return 15;
|
||||
}
|
||||
@ -177,6 +188,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
ITALIC('f', 20),
|
||||
RESET('r', 21);
|
||||
|
||||
@Getter
|
||||
private final char colorChar;
|
||||
private final int id;
|
||||
|
||||
@ -195,17 +207,14 @@ public class UpdateTeamsPacket implements MinecraftPacket {
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException("Operation not supported");
|
||||
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
|
||||
scoreboardManager.getPacketAdapter(protocolVersion).decode(byteBuf, this, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
final Optional<ScoreboardManager> optionalManager = plugin.getScoreboardManager();
|
||||
|
||||
if (optionalManager.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
optionalManager.get().getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion);
|
||||
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
|
||||
scoreboardManager.getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -20,13 +20,17 @@
|
||||
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 int DEFAULT_WEIGHT = -1;
|
||||
public static final Role DEFAULT_ROLE = new Role(DEFAULT_WEIGHT, null, null, null, null);
|
||||
@Getter
|
||||
private final int weight;
|
||||
@ -39,14 +43,6 @@ public class Role implements Comparable<Role> {
|
||||
@Nullable
|
||||
private final String suffix;
|
||||
|
||||
public Role(int weight, @Nullable String name, @Nullable String displayName, @Nullable String prefix, @Nullable String suffix) {
|
||||
this.weight = weight;
|
||||
this.name = name;
|
||||
this.displayName = displayName;
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull Role o) {
|
||||
return Double.compare(weight, o.weight);
|
||||
@ -69,8 +65,22 @@ public class Role implements Comparable<Role> {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected String getWeightString() {
|
||||
return Integer.toString(weight);
|
||||
protected Optional<String> getWeightString() {
|
||||
if (weight == -1) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(Integer.toString(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);
|
||||
}
|
||||
}
|
||||
|
@ -19,32 +19,58 @@
|
||||
|
||||
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 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;
|
||||
private Component lastDisplayname;
|
||||
// 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;
|
||||
@ -57,14 +83,20 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
@Setter
|
||||
private boolean loaded;
|
||||
|
||||
public TabPlayer(@NotNull Player player, @NotNull Role role, @NotNull Group group) {
|
||||
public TabPlayer(@NotNull Velocitab plugin, @NotNull Player player,
|
||||
@NotNull Role role, @NotNull Group group) {
|
||||
this.plugin = plugin;
|
||||
this.player = player;
|
||||
this.role = role;
|
||||
this.group = group;
|
||||
this.relationalDisplayNames = Maps.newConcurrentMap();
|
||||
this.relationalNametags = Maps.newConcurrentMap();
|
||||
this.cachedPlaceholders = Maps.newConcurrentMap();
|
||||
this.cachedListOrders = Maps.newConcurrentMap();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getRoleWeightString() {
|
||||
public Optional<String> getRoleWeightString() {
|
||||
return getRole().getWeightString();
|
||||
}
|
||||
|
||||
@ -91,23 +123,39 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
return plugin.getTabGroups().getPosition(group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name of the server the player is currently on.
|
||||
* Affected by server aliases defined in the config.
|
||||
*
|
||||
* @param plugin The plugin instance
|
||||
* @return The display name of the server
|
||||
*/
|
||||
@NotNull
|
||||
public String getServerDisplayName(@NotNull Velocitab plugin) {
|
||||
return plugin.getSettings().getServerDisplayName(getServerName());
|
||||
public CompletableFuture<String> getDisplayName(@NotNull Velocitab plugin) {
|
||||
final String format = formatGroup();
|
||||
return Placeholder.replace(format, plugin, this)
|
||||
.thenApply(d -> cacheDisplayName(d, format));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CompletableFuture<Component> getDisplayName(@NotNull Velocitab plugin) {
|
||||
return Placeholder.replace(group.format(), plugin, this)
|
||||
.thenApply(formatted -> plugin.getFormatter().format(formatted, this, plugin))
|
||||
.thenApply(c -> this.lastDisplayname = c);
|
||||
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);
|
||||
}
|
||||
|
||||
@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
|
||||
@ -126,8 +174,23 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
|
||||
return tabList.getHeader(this).thenCompose(header -> tabList.getFooter(this)
|
||||
.thenAccept(footer -> player.sendPlayerListHeaderAndFooter(header, footer)));
|
||||
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() {
|
||||
@ -149,6 +212,45 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
@ -161,7 +263,7 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
@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;
|
||||
@ -172,18 +274,7 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TabPlayer{" +
|
||||
"player=" + player +
|
||||
", role=" + role +
|
||||
", headerIndex=" + headerIndex +
|
||||
", footerIndex=" + footerIndex +
|
||||
", lastDisplayname=" + lastDisplayname +
|
||||
", teamName='" + teamName + '\'' +
|
||||
", lastServer='" + lastServer + '\'' +
|
||||
", group=" + group.name() +
|
||||
", loaded=" + loaded +
|
||||
'}';
|
||||
public Optional<String> getCachedPlaceholderValue(@NotNull String placeholder) {
|
||||
return Optional.ofNullable(cachedPlaceholders.get(placeholder));
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ public interface LoggerProvider {
|
||||
*
|
||||
* @return the logger for the class
|
||||
*/
|
||||
@NotNull
|
||||
Logger getLogger();
|
||||
|
||||
/**
|
||||
@ -55,6 +56,7 @@ public interface LoggerProvider {
|
||||
getLogger().warn(message);
|
||||
}
|
||||
}
|
||||
case DEBUG -> getLogger().debug(message);
|
||||
case INFO -> getLogger().info(message);
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,8 @@ public interface MetricProvider {
|
||||
|
||||
/**
|
||||
* Retrieves the Velocitab plugin instance.
|
||||
* @return
|
||||
*
|
||||
* @return The Velocitab plugin instance.
|
||||
*/
|
||||
Velocitab getPlugin();
|
||||
|
||||
|
@ -24,7 +24,6 @@ import net.william278.velocitab.packet.ScoreboardManager;
|
||||
import net.william278.velocitab.sorting.SortingManager;
|
||||
import net.william278.velocitab.tab.PlayerTabList;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public interface ScoreboardProvider {
|
||||
@ -37,11 +36,10 @@ public interface ScoreboardProvider {
|
||||
Velocitab getPlugin();
|
||||
|
||||
/**
|
||||
* Retrieves the optional scoreboard manager.
|
||||
*
|
||||
* @return An {@code Optional} object that may contain a {@code ScoreboardManager} instance.
|
||||
* Retrieves the scoreboard manager
|
||||
* @return The scoreboard manager
|
||||
*/
|
||||
Optional<ScoreboardManager> getScoreboardManager();
|
||||
ScoreboardManager getScoreboardManager();
|
||||
|
||||
/**
|
||||
* Sets the scoreboard manager.
|
||||
@ -85,11 +83,9 @@ public interface ScoreboardProvider {
|
||||
*
|
||||
*/
|
||||
default void prepareScoreboard() {
|
||||
if (getPlugin().getSettings().isSendScoreboardPackets()) {
|
||||
ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin());
|
||||
setScoreboardManager(scoreboardManager);
|
||||
scoreboardManager.registerPacket();
|
||||
}
|
||||
final ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin(), getPlugin().getSettings().isSendScoreboardPackets());
|
||||
setScoreboardManager(scoreboardManager);
|
||||
scoreboardManager.registerPacket();
|
||||
|
||||
final PlayerTabList tabList = new PlayerTabList(getPlugin());
|
||||
setTabList(tabList);
|
||||
@ -105,10 +101,8 @@ public interface ScoreboardProvider {
|
||||
* Disables the ScoreboardManager and closes the tab list for the player.
|
||||
*/
|
||||
default void disableScoreboardManager() {
|
||||
if (getScoreboardManager().isPresent() && getPlugin().getSettings().isSendScoreboardPackets()) {
|
||||
getScoreboardManager().get().close();
|
||||
getScoreboardManager().get().unregisterPacket();
|
||||
}
|
||||
getScoreboardManager().close();
|
||||
getScoreboardManager().unregisterPacket();
|
||||
|
||||
getTabList().close();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -19,22 +19,24 @@
|
||||
|
||||
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 org.slf4j.event.Level;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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;
|
||||
@ -48,7 +50,7 @@ public class SortingManager {
|
||||
|
||||
return Placeholder.replace(String.join(DELIMITER, player.getGroup().sortingPlaceholders()), plugin, player)
|
||||
.thenApply(s -> Arrays.asList(s.split(DELIMITER)))
|
||||
.thenApply(v -> v.stream().map(this::adaptValue).collect(Collectors.toList()))
|
||||
.thenApply(v -> v.stream().map(s -> adaptValue(s, player)).collect(Collectors.toList()))
|
||||
.thenApply(v -> handleList(player, v));
|
||||
}
|
||||
|
||||
@ -56,9 +58,8 @@ public class SortingManager {
|
||||
private String handleList(@NotNull TabPlayer player, @NotNull List<String> values) {
|
||||
String result = String.join("", values);
|
||||
|
||||
if (result.length() > 12) {
|
||||
if (result.length() > 12 && isLongTeamNotAllowed(player)) {
|
||||
result = result.substring(0, 12);
|
||||
plugin.log(Level.WARN, "Sorting element list is too long, truncating to 16 characters");
|
||||
}
|
||||
|
||||
result += player.getPlayer().getUniqueId().toString().substring(0, 4); // Make unique
|
||||
@ -66,19 +67,24 @@ public class SortingManager {
|
||||
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) {
|
||||
private String adaptValue(@NotNull String value, @NotNull TabPlayer player) {
|
||||
if (value.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (value.matches("^-?[0-9]\\d*(\\.\\d+)?$")) {
|
||||
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) {
|
||||
if (value.length() > 6 && isLongTeamNotAllowed(player)) {
|
||||
return value.substring(0, 4);
|
||||
}
|
||||
|
||||
@ -89,13 +95,11 @@ public class SortingManager {
|
||||
public String compressNumber(double number) {
|
||||
int wholePart = (int) number;
|
||||
final char decimalChar = (char) ((number - wholePart) * Character.MAX_VALUE);
|
||||
final List<Character> charList = new ArrayList<>();
|
||||
final List<Character> charList = Lists.newArrayList();
|
||||
|
||||
while (wholePart > 0) {
|
||||
char digit = (char) (wholePart % Character.MAX_VALUE);
|
||||
|
||||
charList.add(0, digit);
|
||||
|
||||
wholePart /= Character.MAX_VALUE;
|
||||
}
|
||||
|
||||
|
40
src/main/java/net/william278/velocitab/tab/GroupTasks.java
Normal file
40
src/main/java/net/william278/velocitab/tab/GroupTasks.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -21,6 +21,7 @@ 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;
|
||||
|
||||
@ -30,13 +31,15 @@ import org.jetbrains.annotations.NotNull;
|
||||
public record Nametag(@NotNull String prefix, @NotNull String suffix) {
|
||||
|
||||
@NotNull
|
||||
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
|
||||
return plugin.getFormatter().format(prefix, tabPlayer, plugin);
|
||||
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) {
|
||||
return plugin.getFormatter().format(suffix, tabPlayer, plugin);
|
||||
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
|
||||
@ -47,4 +50,8 @@ public record Nametag(@NotNull String prefix, @NotNull String suffix) {
|
||||
return (prefix.equals(other.prefix)) && (suffix.equals(other.suffix));
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return prefix.isEmpty() && suffix.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,15 +19,18 @@
|
||||
|
||||
package net.william278.velocitab.tab;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
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.RegisteredServer;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
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;
|
||||
@ -35,35 +38,65 @@ 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.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
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
|
||||
* The main class for tracking the server TAB list for a map of {@link TabPlayer}s
|
||||
*/
|
||||
public class PlayerTabList {
|
||||
|
||||
private final Velocitab plugin;
|
||||
@Getter
|
||||
private final VanishTabList vanishTabList;
|
||||
@Getter(value = AccessLevel.PUBLIC)
|
||||
private final Map<UUID, TabPlayer> players;
|
||||
private final Map<Group, ScheduledTask> placeholderTasks;
|
||||
private final Map<Group, ScheduledTask> headerFooterTasks;
|
||||
private final TaskManager taskManager;
|
||||
private final Map<Class<?>, Field> entriesFields;
|
||||
|
||||
public PlayerTabList(@NotNull Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.vanishTabList = new VanishTabList(plugin, this);
|
||||
this.players = Maps.newConcurrentMap();
|
||||
this.placeholderTasks = Maps.newConcurrentMap();
|
||||
this.headerFooterTasks = Maps.newConcurrentMap();
|
||||
this.taskManager = new TaskManager(plugin);
|
||||
this.entriesFields = Maps.newHashMap();
|
||||
this.reloadUpdate();
|
||||
this.registerListener();
|
||||
this.ensureDisplayNameTask();
|
||||
this.registerFields();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerListener() {
|
||||
@ -97,17 +130,17 @@ public class PlayerTabList {
|
||||
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 Group group = getGroup(serverName);
|
||||
final boolean isDefault = !group.servers().contains(serverName);
|
||||
|
||||
if (isDefault && !plugin.getSettings().isFallbackEnabled()) {
|
||||
if (server.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
joinPlayer(p, group);
|
||||
final String serverName = server.get().getServerInfo().getName();
|
||||
final @NotNull Optional<Group> group = getGroup(serverName);
|
||||
if (group.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
joinPlayer(p, group.get());
|
||||
});
|
||||
}
|
||||
|
||||
@ -116,10 +149,7 @@ public class PlayerTabList {
|
||||
* Removes the player's entry from the tab list of all other players on the same group servers.
|
||||
*/
|
||||
public void close() {
|
||||
placeholderTasks.values().forEach(ScheduledTask::cancel);
|
||||
placeholderTasks.clear();
|
||||
headerFooterTasks.values().forEach(ScheduledTask::cancel);
|
||||
headerFooterTasks.clear();
|
||||
taskManager.cancelAllTasks();
|
||||
plugin.getServer().getAllPlayers().forEach(p -> {
|
||||
final Optional<ServerConnection> server = p.getCurrentServer();
|
||||
if (server.isEmpty()) return;
|
||||
@ -129,7 +159,7 @@ public class PlayerTabList {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<RegisteredServer> serversInGroup = new ArrayList<>(tabPlayer.getGroup().registeredServers(plugin));
|
||||
final Set<RegisteredServer> serversInGroup = tabPlayer.getGroup().registeredServers(plugin);
|
||||
if (serversInGroup.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@ -139,81 +169,39 @@ public class PlayerTabList {
|
||||
});
|
||||
}
|
||||
|
||||
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 TabPlayer tabPlayer = getTabPlayer(joined).orElseGet(() -> createTabPlayer(joined, group));
|
||||
tabPlayer.setGroup(group);
|
||||
players.putIfAbsent(joined.getUniqueId(), tabPlayer);
|
||||
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);
|
||||
|
||||
//store last server, so it's possible to have the last server on disconnect
|
||||
tabPlayer.setLastServer(joined.getCurrentServer().map(ServerConnection::getServerInfo).map(ServerInfo::getName).orElse(""));
|
||||
|
||||
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
|
||||
final boolean isDefault = group.isDefault();
|
||||
final boolean isFallback = isDefault && plugin.getSettings().isFallbackEnabled();
|
||||
// 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;
|
||||
}
|
||||
|
||||
joined.getTabList().getEntry(joined.getUniqueId())
|
||||
.ifPresentOrElse(e -> e.setDisplayName(d),
|
||||
() -> joined.getTabList().addEntry(createEntry(tabPlayer, joined.getTabList(), d)));
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
// Update lists
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> {
|
||||
final TabList tabList = joined.getTabList();
|
||||
for (final TabPlayer player : players.values()) {
|
||||
// Skip players on other servers if the setting is enabled
|
||||
if (plugin.getSettings().isOnlyListPlayersInSameGroup()
|
||||
&& !isFallback &&
|
||||
!group.servers().contains(player.getServerName())
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// check if current player can see the joined player
|
||||
if (!isVanished || plugin.getVanishManager().canSee(player.getPlayer().getUsername(), joined.getUsername())) {
|
||||
addPlayerToTabList(player, tabPlayer, d);
|
||||
} else {
|
||||
player.getPlayer().getTabList().removeEntry(joined.getUniqueId());
|
||||
}
|
||||
// check if joined player can see current player
|
||||
if ((plugin.getVanishManager().isVanished(player.getPlayer().getUsername()) &&
|
||||
!plugin.getVanishManager().canSee(joined.getUsername(), player.getPlayer().getUsername())) && player.getPlayer() != joined) {
|
||||
tabList.removeEntry(player.getPlayer().getUniqueId());
|
||||
} else {
|
||||
tabList.getEntry(player.getPlayer().getUniqueId()).ifPresentOrElse(
|
||||
entry -> player.getDisplayName(plugin).thenAccept(entry::setDisplayName)
|
||||
.exceptionally(throwable -> {
|
||||
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
|
||||
player.getPlayer().getUsername(), player.getPlayer().getUniqueId()), throwable);
|
||||
return null;
|
||||
}),
|
||||
() -> createEntry(player, tabList).thenAccept(tabList::addEntry)
|
||||
);
|
||||
}
|
||||
|
||||
player.sendHeaderAndFooter(this);
|
||||
}
|
||||
|
||||
plugin.getScoreboardManager().ifPresent(s -> {
|
||||
s.resendAllTeams(tabPlayer);
|
||||
tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(tabPlayer, t, false));
|
||||
});
|
||||
|
||||
// Fire event without listening for result
|
||||
plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group));
|
||||
})
|
||||
.delay(300, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
handleDisplayLoad(tabPlayer);
|
||||
}).exceptionally(throwable -> {
|
||||
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
|
||||
joined.getUsername(), joined.getUniqueId()), throwable);
|
||||
@ -221,51 +209,210 @@ public class PlayerTabList {
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
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 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(0)
|
||||
.latency(Math.max((int) player.getPlayer().getPing(), 0))
|
||||
.tabList(tabList)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void addPlayerToTabList(@NotNull TabPlayer player, @NotNull TabPlayer newPlayer, @NotNull Component displayName) {
|
||||
if (newPlayer.getPlayer().getUniqueId().equals(player.getPlayer().getUniqueId())) {
|
||||
@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;
|
||||
}
|
||||
|
||||
plugin.getPacketEventManager().getVelocitabEntries().add(newPlayer.getPlayer().getUniqueId());
|
||||
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> plugin.getPacketEventManager().getVelocitabEntries().remove(newPlayer.getPlayer().getUniqueId()))
|
||||
.delay(500, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
|
||||
player.getPlayer()
|
||||
.getTabList().getEntries().stream()
|
||||
.filter(e -> e.getProfile().getId().equals(newPlayer.getPlayer().getUniqueId())).findFirst()
|
||||
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
|
||||
viewer.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
|
||||
.ifPresentOrElse(
|
||||
entry -> entry.setDisplayName(displayName),
|
||||
() -> player.getPlayer().getTabList()
|
||||
.addEntry(createEntry(newPlayer, player.getPlayer().getTabList(), displayName))
|
||||
() -> viewer.getPlayer().getTabList()
|
||||
.addEntry(createEntry(player, viewer.getPlayer().getTabList(), displayName))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@NotNull
|
||||
public TabPlayer createTabPlayer(@NotNull Player player, @NotNull Group group) {
|
||||
return new TabPlayer(player,
|
||||
return new TabPlayer(plugin, player,
|
||||
plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE),
|
||||
group
|
||||
);
|
||||
@ -278,42 +425,98 @@ public class PlayerTabList {
|
||||
return;
|
||||
}
|
||||
|
||||
updateSorting(tabPlayer, force);
|
||||
}
|
||||
|
||||
private void updateSorting(@NotNull TabPlayer tabPlayer, boolean force) {
|
||||
tabPlayer.getTeamName(plugin).thenAccept(teamName -> {
|
||||
if (teamName.isBlank()) {
|
||||
return;
|
||||
}
|
||||
plugin.getScoreboardManager().ifPresent(manager -> manager.updateRole(
|
||||
tabPlayer, teamName, force
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) {
|
||||
final Component lastDisplayName = tabPlayer.getLastDisplayname();
|
||||
tabPlayer.getDisplayName(plugin).thenAccept(displayName -> {
|
||||
if (displayName == null || displayName.equals(lastDisplayName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername());
|
||||
|
||||
players.values().forEach(player -> {
|
||||
if (isVanished && !plugin.getVanishManager().canSee(player.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) {
|
||||
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;
|
||||
}
|
||||
|
||||
player.getPlayer().getTabList().getEntries().stream()
|
||||
.filter(e -> e.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId())).findFirst()
|
||||
.ifPresent(entry -> entry.setDisplayName(displayName));
|
||||
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());
|
||||
@ -330,90 +533,16 @@ public class PlayerTabList {
|
||||
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
|
||||
}
|
||||
|
||||
// Update the tab list periodically
|
||||
private void updatePeriodically(Group group) {
|
||||
cancelTasks(group);
|
||||
|
||||
if (group.headerFooterUpdateRate() > 0) {
|
||||
final ScheduledTask headerFooterTask = plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> updateGroupPlayers(group, false, true))
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(Math.max(200, group.headerFooterUpdateRate()), TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
headerFooterTasks.put(group, headerFooterTask);
|
||||
}
|
||||
|
||||
if (group.placeholderUpdateRate() > 0) {
|
||||
final ScheduledTask updateTask = plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> updateGroupPlayers(group, true, false))
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(Math.max(200, group.placeholderUpdateRate()), TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
placeholderTasks.put(group, updateTask);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
List<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
|
||||
if (groupPlayers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
groupPlayers.stream()
|
||||
.filter(player -> player.getPlayer().isActive())
|
||||
.forEach(player -> {
|
||||
if (incrementIndexes) {
|
||||
player.incrementIndexes();
|
||||
}
|
||||
if (all) {
|
||||
this.updatePlayer(player, false);
|
||||
}
|
||||
player.sendHeaderAndFooter(this);
|
||||
});
|
||||
if (all) {
|
||||
updateDisplayNames();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelTasks(Group group) {
|
||||
ScheduledTask task = placeholderTasks.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().equals(group))
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
placeholderTasks.remove(group);
|
||||
}
|
||||
|
||||
task = headerFooterTasks.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().equals(group))
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
headerFooterTasks.remove(group);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the TAB list for all players when a plugin or proxy reload is performed
|
||||
*/
|
||||
public void reloadUpdate() {
|
||||
placeholderTasks.values().forEach(ScheduledTask::cancel);
|
||||
placeholderTasks.clear();
|
||||
plugin.getTabGroups().getGroups().forEach(this::updatePeriodically);
|
||||
|
||||
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();
|
||||
@ -421,8 +550,12 @@ public class PlayerTabList {
|
||||
return;
|
||||
}
|
||||
final String serverName = server.get().getServerInfo().getName();
|
||||
final Group group = getGroup(serverName);
|
||||
player.setGroup(group);
|
||||
final Optional<Group> group = getGroup(serverName);
|
||||
if (group.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
player.setGroup(group.get());
|
||||
this.sendPlayerServerLinks(player);
|
||||
this.updatePlayer(player, true);
|
||||
player.sendHeaderAndFooter(this);
|
||||
});
|
||||
@ -430,10 +563,14 @@ public class PlayerTabList {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Group getGroup(@NotNull String serverName) {
|
||||
return plugin.getTabGroups().getGroupFromServer(serverName);
|
||||
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
|
||||
@ -444,4 +581,44 @@ public class PlayerTabList {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
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;
|
||||
@ -34,6 +35,8 @@ 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;
|
||||
|
||||
@ -46,70 +49,140 @@ 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(KickedFromServerEvent event) {
|
||||
event.getPlayer().getTabList().clearAll();
|
||||
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 Group group = tabList.getGroup(serverName);
|
||||
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group));
|
||||
final boolean isDefault = !group.servers().contains(serverName);
|
||||
|
||||
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()) {
|
||||
event.getPlayer().sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
|
||||
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;
|
||||
}
|
||||
|
||||
tabList.joinPlayer(joined, group);
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void onPlayerQuit(@NotNull DisconnectEvent event) {
|
||||
if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) {
|
||||
if (groupOptional.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the player from the tracking list, Print warning if player was not removed
|
||||
final UUID uuid = event.getPlayer().getUniqueId();
|
||||
final TabPlayer tabPlayer = tabList.getPlayers().get(uuid);
|
||||
if (tabPlayer == null) {
|
||||
plugin.log(String.format("Failed to remove disconnecting player %s (UUID: %s)",
|
||||
event.getPlayer().getUsername(), uuid));
|
||||
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
|
||||
plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(uuid));
|
||||
tabList.removePlayer(event.getPlayer());
|
||||
}
|
||||
|
||||
// Update the tab list of all players
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> tabList.getPlayers().values().forEach(player -> {
|
||||
player.getPlayer().getTabList().removeEntry(uuid);
|
||||
player.sendHeaderAndFooter(tabList);
|
||||
}))
|
||||
.delay(500, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
// Delete player team
|
||||
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(event.getPlayer()));
|
||||
//remove player from tab list cache
|
||||
tabList.getPlayers().remove(uuid);
|
||||
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
|
||||
|
121
src/main/java/net/william278/velocitab/tab/TaskManager.java
Normal file
121
src/main/java/net/william278/velocitab/tab/TaskManager.java
Normal 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))));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -20,27 +20,23 @@
|
||||
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.List;
|
||||
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 VanishTabList(Velocitab plugin, PlayerTabList tabList) {
|
||||
this.plugin = plugin;
|
||||
this.tabList = tabList;
|
||||
}
|
||||
|
||||
|
||||
public void vanishPlayer(@NotNull TabPlayer tabPlayer) {
|
||||
tabList.getPlayers().values().forEach(p -> {
|
||||
@ -57,17 +53,17 @@ public class VanishTabList {
|
||||
public void unVanishPlayer(@NotNull TabPlayer tabPlayer) {
|
||||
final UUID uuid = tabPlayer.getPlayer().getUniqueId();
|
||||
|
||||
tabPlayer.getDisplayName(plugin).thenAccept(c -> tabList.getPlayers().values().forEach(p -> {
|
||||
tabList.getPlayers().values().forEach(p -> {
|
||||
if (p.getPlayer().equals(tabPlayer.getPlayer())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!p.getPlayer().getTabList().containsEntry(uuid)) {
|
||||
p.getPlayer().getTabList().addEntry(tabList.createEntry(tabPlayer, p.getPlayer().getTabList(), c));
|
||||
tabList.createEntry(tabPlayer, p.getPlayer().getTabList(), p);
|
||||
} else {
|
||||
p.getPlayer().getTabList().getEntry(uuid).ifPresent(entry -> entry.setDisplayName(c));
|
||||
tabList.updateDisplayName(tabPlayer, p);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@ -79,8 +75,6 @@ public class VanishTabList {
|
||||
*/
|
||||
public void recalculateVanishForPlayer(@NotNull TabPlayer tabPlayer) {
|
||||
final Player player = tabPlayer.getPlayer();
|
||||
final List<String> serversInGroup = tabPlayer.getGroup().servers();
|
||||
|
||||
plugin.getServer().getAllPlayers().forEach(p -> {
|
||||
if (p.equals(player)) {
|
||||
return;
|
||||
@ -94,26 +88,24 @@ public class VanishTabList {
|
||||
final TabPlayer target = targetOptional.get();
|
||||
final String serverName = target.getServerName();
|
||||
|
||||
if (plugin.getSettings().isOnlyListPlayersInSameGroup()
|
||||
&& !serversInGroup.contains(serverName)) {
|
||||
if (tabPlayer.getGroup().onlyListPlayersInSameServer()
|
||||
&& !tabPlayer.getServerName().equals(serverName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) ||
|
||||
plugin.getVanishManager().canSee(player.getUsername(), p.getUsername());
|
||||
plugin.getVanishManager().canSee(player.getUsername(), p.getUsername());
|
||||
|
||||
if (!canSee) {
|
||||
player.getTabList().removeEntry(p.getUniqueId());
|
||||
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, false));
|
||||
plugin.getScoreboardManager().recalculateVanishForPlayer(tabPlayer, target, false);
|
||||
} else {
|
||||
if (!player.getTabList().containsEntry(p.getUniqueId())) {
|
||||
tabList.createEntry(target, player.getTabList()).thenAccept(e -> {
|
||||
player.getTabList().addEntry(e);
|
||||
plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, true));
|
||||
});
|
||||
final TabListEntry tabListEntry = tabList.createEntry(target, player.getTabList(), tabPlayer);
|
||||
player.getTabList().addEntry(tabListEntry);
|
||||
plugin.getScoreboardManager().recalculateVanishForPlayer(tabPlayer, target, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
@ -61,7 +61,7 @@ public class VanishManager {
|
||||
}
|
||||
|
||||
plugin.getTabList().getVanishTabList().vanishPlayer(tabPlayer.get());
|
||||
plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.vanishPlayer(tabPlayer.get()));
|
||||
plugin.getScoreboardManager().vanishPlayer(tabPlayer.get());
|
||||
}
|
||||
|
||||
public void unVanishPlayer(@NotNull Player player) {
|
||||
@ -72,6 +72,6 @@ public class VanishManager {
|
||||
}
|
||||
|
||||
plugin.getTabList().getVanishTabList().unVanishPlayer(tabPlayer.get());
|
||||
plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.unVanishPlayer(tabPlayer.get()));
|
||||
plugin.getScoreboardManager().unVanishPlayer(tabPlayer.get());
|
||||
}
|
||||
}
|
||||
|
3
src/main/resources/metadata.yml
Normal file
3
src/main/resources/metadata.yml
Normal 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}'
|
@ -3,9 +3,10 @@
|
||||
"name": "Velocitab",
|
||||
"version": "${version}",
|
||||
"description": "${description}",
|
||||
"url": "https://william278.net",
|
||||
"url": "https://william278.net/project/velocitab",
|
||||
"authors": [
|
||||
"William278"
|
||||
"William278",
|
||||
"AlexDev03"
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user