47 Commits
Author SHA1 Message Date
NotAlexNoyle 6b84a27f76 Exclude CODEOWNERS from language statistics 2026-10-26 12:28:35 -04:00
NotAlexNoyle a5a0be50c2 Add CODEOWNERS (one staff approval) 2026-10-26 06:17:55 -04:00
AlexDev_ 3e1aa9d39e feat: Add advanced helmet height rules configuration and logic for dynamic nametag positioning 2026-03-20 16:36:50 +01:00
AlexDev_ 385f505c99 shift fix 2026-03-11 20:35:41 +01:00
AlexDev_ fcfcd3c940 refactor: Improve code formatting and readability in various classes, changed libby repo 2026-02-12 21:26:43 +01:00
AlexDev_ 53f66e0cfb feat: Update entityLib dependency to use lowercase group ID and add new Maven repository 2026-01-09 22:47:44 +01:00
AlexDev_ 8781ff2276 feat: Refactor nametag visibility logic and improve player tracking management 2026-01-06 20:41:55 +01:00
Johannes / EinJojo ab319e4baa Chore/build fix (#59)
* chore(build): update entityLib module reference and add maven repositories

* Change entityLib module reference to use the correct namespace because new repository.
* Add additional maven repositories for dependency resolution.

* chore(build): update entityLib version to 3.0.3-SNAPSHOT
2026-01-03 10:44:08 +01:00
AlexDev_ 0006cf83a0 feat: Implement forced nametag functionality for players 2025-12-27 22:58:47 +01:00
AlexDev_ 7fd168d6c2 Confirmed support for 1.21.11 2025-12-10 12:38:50 +01:00
AlexDev_ cb5bf94e76 feat: Add support for nametag overrides and shift system blocking 2025-12-03 16:30:04 +01:00
coolomet 887cfdad8d fix: prevent WrapperEntity memory leak (#54) 2025-12-03 16:24:13 +01:00
AlexDev_ 24adfe121f Fixes https://github.com/alexdev03/UnlimitedNametags/issues/48 2025-11-04 14:49:46 +01:00
AlexDev_ b495a87a57 Fixes https://github.com/alexdev03/UnlimitedNametags/issues/50 2025-11-04 14:44:13 +01:00
AlexDev_ 12ede5bc1e refactor: Remove unused MineDown dependency and update Minecraft task version to 1.21.10 2025-11-04 14:41:57 +01:00
AlexDev_ db855188a8 Fixed entitylib dependency 2025-10-22 10:52:49 +02:00
Justin ee6c978b4d fix: Allow null permissions in nametag settings. (#45) 2025-10-11 18:18:29 +02:00
AlexDev_ 846b525e3c Improved HMCCosmeticsHook while using Nexo 2025-10-11 18:16:10 +02:00
AlexDev_ e9d6544263 Possible fix for HMCCosmetics's backpacks problem 2025-10-11 17:56:14 +02:00
AlexDev_ 9d3c5f5256 Performance tweaks
Bumped version to 1.6.10
2025-10-11 16:55:25 +02:00
AlexDev_ 5a22d45e39 General improvements
Fixed sneak problem for 1.21.10
Updated MiniPlaceholdersHook
2025-10-11 16:21:44 +02:00
AlexDev_ e1ddc32020 Temp fix for https://github.com/alexdev03/UnlimitedNametags/issues/43 2025-07-27 10:09:01 +02:00
archroot 7b1faa884b Update PlaceholderManager.java (#40)
Fix possible NullPointerException in PlaceholderManager#close when PAPI expansion is null
2025-07-22 17:46:23 +02:00
AlexDev_ 021a198aeb Fixes https://github.com/alexdev03/UnlimitedNametags/issues/36 2025-07-03 09:43:36 +02:00
Optima__ 7154210ee4 Avoid nametag errors when players are in different worlds (#35) 2025-06-25 12:52:45 +02:00
AlexDev_ 12098133b2 Updated to 1.6.9 2025-06-25 12:52:13 +02:00
AlexDev_ 0057bb464b Correctly remove display from map
Closes https://github.com/alexdev03/UnlimitedNametags/issues/33
2025-06-14 09:53:40 +02:00
AlexDev_ 8295644feb Updated some dependencies
Fixed problem in CreativeHook
Fixed gradient problem in some world areas
Improved support for ItemsAdder
2025-06-09 10:33:42 +02:00
M310N 751dc33c02 Improve compatibility with custom deaths, fix PAPI error (#30)
* Improve compatibility with custom deaths

* Fix error when PAPI not installed
2025-06-04 22:20:42 +02:00
AlexDev_ 94ee975b7f Fixed problem with ItemsAdder 2025-05-17 11:49:39 +02:00
AlexDev_ a61a058885 Fixed nametag problem on death
Added initial support for ItemsAdder and HMCCosmetics(They need some testing before marking them as completely supported).
Improved Nexo Support
2025-05-04 16:32:52 +02:00
AlexDev_ e918b8ad73 Added more methods 2025-04-30 22:19:09 +02:00
AlexDev_ 31062e6d02 Reduced method size 2025-04-30 22:09:28 +02:00
AlexDev_ 2cf0b225b2 Improved performance
Added option to disable relational placeholders
Fixed component join problem
2025-04-25 14:27:10 +02:00
AlexDev_ 1e6f00cd0c I added new placeholders %unt_phase_mm%, %unt_phase_md%, %unt_phase_mm-g%, %unt_-phase_mm%, %unt_-phase_md%
Recoded disableDefaultNameTag, now it will cache teams so with this option npcs with the default nametag should have their nametag visible again. This only works if there isn't a player with the same name of the npc online
Added forceDisableDefaultNameTag that forcibly hides all default nametags (basically the previous behavior of disableDefaultNameTag)
Added componentCaching & placeholderCacheTime to reduce cpu usage
Improved overall performance
2025-04-22 15:34:54 +02:00
AlexDev_ c9309937e5 Fixed HMCCosmetics Hook 2025-04-18 23:19:33 +02:00
AlexDev_ ca5969e25e Fixed some problems 2025-04-06 16:27:31 +02:00
AlexDev_ eb546a54d5 Added support for libs disguises
Fixed https://github.com/alexdev03/UnlimitedNametags/issues/21
Fixed gradient problem
Fixed condition problem
2025-03-16 13:02:44 +01:00
darksaid98 64a3ea699d fix: add gradle wrapper jar (#18) 2025-02-27 17:59:03 +01:00
AlexDev_ 1c9ab88f78 Fixed spectator problem 2025-02-26 18:16:34 +01:00
AlexDev_ 0136949d17 Fixed problems 2025-02-26 18:07:31 +01:00
AlexDev_ 50a4c7e2b5 Fix for relational placeholders 2025-02-15 23:23:33 +01:00
AlexDev_ 9cbe8e9ca2 Added support for placeholder replacements with relational placeholders 2025-02-15 23:16:07 +01:00
AlexDev_ 2f505bd7b1 Fixed nexo hook 2025-02-15 11:04:52 +01:00
AlexDev_ 3ecd52b928 Fixed problem with registry 2025-02-13 23:55:23 +01:00
AlexDev_ 9eb8b04912 Bug fixes
Backported to java 17
2025-02-13 21:51:55 +01:00
AlexDev_ 1a1f9688cc Bug fixes
Improved Nexo's hook
2025-02-11 14:19:29 +01:00
52 changed files with 3615 additions and 848 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
+3 -2
View File
@@ -73,6 +73,9 @@ Enhance your server's customization by tailoring player name tags, integrating d
5. **Configure**
Customize the `settings.yml` file located in the plugin's folder to suit your server’s needs.
6. **Optional `advanced.yml`**
For manual helmet → nametag height rules (e.g. ItemsAdder / custom CMD without relying on resource-pack parsing), copy [`advanced.example.yml`](advanced.example.yml) to `plugins/UnlimitedNameTags/advanced.yml` and adjust. The file is never created automatically; `/unt reload` reloads it when present.
---
## 🛠️ **Commands**
@@ -116,8 +119,6 @@ Unlimited Name Tags works seamlessly with:
- **MiniPlaceholders**: Works when using MiniMessage for advanced formatting.
- **Custom Plugins**: Easily hook into your custom plugins to extend functionality.
---
## 📜 **Supported Versions**
- **Paper**: Fully supported from **1.20.1+** *(highly recommended)*.
- **Spigot**: Supported from **1.20.2+**, but Paper is preferred for enhanced performance. (Not all spigot versions can be supported)
+26
View File
@@ -0,0 +1,26 @@
# Copy to plugins/UnlimitedNameTags/advanced.yml and edit.
# Optional: if this file is missing, the plugin behaves as before.
# Keys use camelCase (ConfigLib default).
helmetHeightRules:
# Example: custom head with a specific CustomModelData
- priority: 10
height: 28
material: PLAYER_HEAD
customModelData: 12345
# Example: any leather helmet with CMD in a range (inclusive)
- priority: 5
height: 20
material: LEATHER_HELMET
customModelDataMin: 1000
customModelDataMax: 1999
# Example: equippable item model (1.21.3+)
- priority: 8
height: 32
equippableModel: "mypack:item/tall_hat"
# Optional filters on any rule:
# worlds: [world, world_nether]
# permission: "myserver.bighelmet"
+59 -19
View File
@@ -8,33 +8,49 @@ plugins {
}
group = "org.alexdev"
version = "1.6.6"
version = property("version") as String
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.nexomc.com/snapshots/")
maven("https://oss.sonatype.org/content/groups/public/")
maven("https://repo.extendedclip.com/releases")
maven("https://repo.minebench.de/")
maven("https://oss.sonatype.org/content/repositories/snapshots")
maven("https://libraries.minecraft.net/")
maven("https://repo.codemc.io/repository/maven-public/")
maven("https://repo.codemc.io/repository/maven-snapshots/")
maven("https://repo.codemc.io/repository/maven-releases/")
maven("https://repo.viaversion.com/")
maven("https://maven.pvphub.me/#/tofaa/io/github/tofaa2/")
maven("https://repo.opencollab.dev/main/")
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
maven("https://repo.oraxen.com/releases")
maven("https://repo.oraxen.com/snapshots")
maven("https://jitpack.io")
maven("https://repo.viaversion.com")
maven("https://maven.evokegames.gg/snapshots")
maven("https://repo.alessiodp.com/releases")
//maven("https://maven.evokegames.gg/snapshots")
maven("https://repo.alessiodp.com/snapshots/")
maven("https://maven.typewritermc.com/beta")
maven("https://repo.nexomc.com/snapshots/")
maven("https://repo.nexomc.com/releases")
maven("https://repo.md-5.net/content/groups/public/")
maven("https://mvn.lib.co.nz/public")
maven {
name = "feather-repo"
url = uri("https://repo.feathermc.net/artifactory/maven-releases")
}
maven {
name = "labymod"
url = uri("https://dist.labymod.net/api/v1/maven/release/")
}
maven("https://repo.hibiscusmc.com/releases")
maven("https://maven.pvphub.me/tofaa")
}
dependencies {
compileOnly(libs.paperApi)
compileOnly(libs.adventureApi)
implementation(libs.entityLib)
@@ -49,7 +65,7 @@ dependencies {
compileOnly(libs.geyserApi)
compileOnly(libs.commonsLang)
compileOnly(libs.configlib)
compileOnly(libs.configlibPaper)
implementation(libs.configlibPaper)
compileOnly(libs.packeteventsSpigot)
compileOnly(libs.viaVersionApi)
compileOnly(libs.bstatsBukkit)
@@ -58,14 +74,18 @@ dependencies {
compileOnly(libs.nexo)
compileOnly(libs.oraxen)
compileOnly(libs.itemsAdder)
compileOnly(libs.hmccosmetics)
compileOnly(libs.creative.rp)
compileOnly(libs.creative.serializer)
compileOnly(libs.libs.disguises)
compileOnly(libs.hmcCosmetics)
implementation(libs.minedownAdventure)
implementation(libs.drink)
implementation(libs.universalScheduler)
implementation(libs.libbyBukkit)
compileOnly(libs.gson)
compileOnly(libs.feather)
compileOnly(libs.labymod)
compileOnly(libs.lombok)
annotationProcessor(libs.lombok)
@@ -84,6 +104,8 @@ tasks.named<ShadowJar>("shadowJar") {
relocate("net.kyori.adventure.text.serializer", "io.github.retrooper.packetevents.adventure.serializer")
relocate("net.byteflux.libby", relocation + "libby.bukkit")
relocate("de.exlll.configlib", "org.alexdev.unlimitednametags.configlib")
dependencies {
exclude(dependency(":kotlin-stdlib"))
exclude(dependency(":slf4j-api"))
@@ -113,7 +135,7 @@ tasks.named<ShadowJar>("shadowJar") {
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.release.set(21)
options.release.set(17)
}
tasks.withType<Javadoc> {
@@ -122,6 +144,12 @@ tasks.withType<Javadoc> {
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
disableAutoTargetJvm()
}
kotlin {
jvmToolchain(21)
}
tasks.named<Jar>("jar").configure {
@@ -131,32 +159,37 @@ tasks.named<Delete>("clean").configure {
delete(tasks.named<ShadowJar>("shadowJar").get().archiveFile)
}
tasks.jar {
manifest {
attributes["paperweight-mappings-namespace"] = "mojang"
}
}
tasks {
runServer {
minecraftVersion("1.21.3")
minecraftVersion("1.21.11")
downloadPlugins {
hangar("PlaceholderAPI", "2.11.6")
modrinth("luckperms", "v5.4.145-bukkit")
modrinth("multiverse-core", "4.3.14")
url("https://github.com/retrooper/packetevents/releases/download/v2.6.0/packetevents-spigot-2.6.0.jar")
github("ViaVersion", "ViaVersion", "5.1.1", "ViaVersion-5.1.1.jar")
github("ViaVersion", "ViaBackwards", "5.1.1", "ViaBackwards-5.1.1.jar")
github("MiniPlaceholders", "MiniPlaceholders", "3.0.1", "MiniPlaceholders-Paper-3.0.1.jar")
// github("retrooper", "packetevents", "v2.9.4", "packetevents-spigot-2.9.4.jar")
url("https://ci.codemc.io/job/retrooper/job/packetevents/796/artifact/build/libs/packetevents-spigot-2.11.1-SNAPSHOT.jar")
github("MilkBowl", "Vault", "1.7.3", "Vault.jar")
github("gecolay", "GSit", "1.11.2", "GSit-1.11.2.jar")
// url("https://github.com/EssentialsX/Essentials/releases/download/2.20.1/EssentialsX-2.20.1.jar")
github("FeatherMC", "feather-server-api", "v0.0.5", "feather-server-api-0.0.5-bukkit.jar")
github("LabyMod", "labymod4-server-api", "1.0.6", "labymod-server-api-bukkit-1.0.6.jar")
}
}
runPaper.folia.registerTask {
minecraftVersion("1.21.1")
minecraftVersion("1.21.11")
downloadPlugins {
github("Anon8281", "PlaceholderAPI", "2.11.7", "PlaceholderAPI-2.11.7-DEV-Folia.jar")
url("https://cdn.modrinth.com/data/HQyibRsN/versions/J2guR3GH/MiniPlaceholders-Paper-2.2.4.jar")
url("https://ci.lucko.me/job/LuckPerms-Folia/lastSuccessfulBuild/artifact/bukkit/loader/build/libs/LuckPerms-Bukkit-5.4.141.jar")
url("https://github.com/retrooper/packetevents/releases/download/v2.4.0/packetevents-spigot-2.4.0.jar")
github("ViaVersion", "ViaVersion", "5.0.1", "ViaVersion-5.0.1.jar")
url("https://ci.codemc.io/job/retrooper/job/packetevents/796/artifact/build/libs/packetevents-spigot-2.11.1-SNAPSHOT.jar")
github("ViaVersion", "ViaVersion", "5.4.1", "ViaVersion-5.4.1.jar")
}
}
}
@@ -167,6 +200,12 @@ tasks.processResources {
compiled = false
}
filesMatching("**/UnlimitedNameTags.java") {
expand(
"configlibVersion" to libs.versions.configlibVersion.get(),
)
}
from("src/main/resources") {
include("plugin.yml")
duplicatesStrategy = DuplicatesStrategy.INCLUDE
@@ -174,6 +213,7 @@ tasks.processResources {
"configlibVersion" to libs.versions.configlibVersion.get(),
"expiringMapVersion" to libs.versions.expiringMapVersion.get(),
"commonsJexl3Version" to libs.versions.commonsJexl3Version.get(),
"version" to version,
"compiled" to compiled
)
+1
View File
@@ -0,0 +1 @@
version = 1.6.11
+23 -16
View File
@@ -1,32 +1,34 @@
[versions]
paperApiVersion = "1.21.1-R0.1-SNAPSHOT"
paperApiVersion = "1.21.4-R0.1-SNAPSHOT"
adventureApiVersion = "4.16.0"
adventureTextMinimessageVersion = "4.17.0"
adventureTextSerializerLegacyVersion = "4.17.0"
adventurePlatformBukkitVersion = "4.3.3"
typeWriterVersion = "0.7.0"
typeWriterVersion = "0.9.0"
lombokVersion = "1.18.30"
placeholderapiVersion = "2.11.6"
miniplaceholdersApiVersion = "2.2.3"
miniplaceholdersApiVersion = "3.0.1"
floodgateApiVersion = "2.2.3-SNAPSHOT"
geyserApiVersion = "2.4.2-SNAPSHOT"
commonsLangVersion = "3.14.0"
configlibVersion = "4.5.0"
configlibVersion = "4.6.1"
minedownAdventureVersion = "1.7.2-SNAPSHOT"
drinkVersion = "1.5.3"
entityLibVersion = "+39cce79-SNAPSHOT"
packeteventsSpigotVersion = "2.6.0"
entityLibVersion = "3.0.3-SNAPSHOT"
packeteventsSpigotVersion = "2.10.0-SNAPSHOT"
universalSchedulerVersion = "0.1.6"
viaVersionApiVersion = "5.0.1"
libbyBukkitVersion = "1.3.1"
libbyBukkitVersion = "2.0.0-SNAPSHOT"
bstatsBukkitVersion = "3.0.2"
expiringMapVersion = "0.5.11"
commonsJexl3Version = "3.4.0"
nexoVersion = "0.9.0-dev.19"
oraxenVersion = "1.174.0"
nexoVersion = "1.2.0"
oraxenVersion = "1.195.0"
itemsAdderVersion = "3.6.1"
gsonVersion = "2.11.0"
hmccosmeticsVersion = "2.7.4"
creative-rp = "1.9.2-SNAPSHOT"
creative-serializer = "1.9.2-SNAPSHOT"
libs-disguises = "11.0.0"
featherVersion = "0.0.5"
labymodVersion = "1.0.6"
hmcCosmetics = "2.8.2-fed13b8e"
[libraries]
paperApi = { module = "io.papermc.paper:paper-api", version.ref = "paperApiVersion" }
@@ -42,11 +44,11 @@ configlib = { module = "de.exlll:configlib-yaml", version.ref = "configlibVersio
configlibPaper = { module = "de.exlll:configlib-paper", version.ref = "configlibVersion" }
minedownAdventure = { module = "de.themoep:minedown-adventure", version.ref = "minedownAdventureVersion" }
drink = { module = "com.github.alexdev03:drink", version.ref = "drinkVersion" }
entityLib = { module = "me.tofaa.entitylib:spigot", version.ref = "entityLibVersion" }
entityLib = { module = "io.github.tofaa2:spigot", version.ref = "entityLibVersion" }
packeteventsSpigot = { module = "com.github.retrooper:packetevents-spigot", version.ref = "packeteventsSpigotVersion" }
universalScheduler = { module = "com.github.Anon8281:UniversalScheduler", version.ref = "universalSchedulerVersion" }
viaVersionApi = { module = "com.viaversion:viaversion-api", version.ref = "viaVersionApiVersion" }
libbyBukkit = { module = "net.byteflux:libby-bukkit", version.ref = "libbyBukkitVersion" }
libbyBukkit = { module = "com.alessiodp.libby:libby-bukkit", version.ref = "libbyBukkitVersion" }
bstatsBukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstatsBukkitVersion" }
expiringMap = { module = "net.jodah:expiringmap", version.ref = "expiringMapVersion" }
commonsJexl3 = { module = "org.apache.commons:commons-jexl3", version.ref = "commonsJexl3Version" }
@@ -54,4 +56,9 @@ nexo = { module = "com.nexomc:nexo", version.ref = "nexoVersion" }
oraxen = { module = "io.th0rgal:oraxen", version.ref = "oraxenVersion" }
itemsAdder = { module = "com.github.LoneDev6:API-ItemsAdder", version.ref = "itemsAdderVersion" }
gson = { module = "com.google.code.gson:gson", version.ref = "gsonVersion" }
hmccosmetics = { module = "com.hibiscusmc:HMCCosmetics", version.ref = "hmccosmeticsVersion" }
creative-rp = { module = "team.unnamed:creative-api", version.ref = "creative-rp" }
creative-serializer = { module = "team.unnamed:creative-serializer-minecraft", version.ref = "creative-serializer" }
libs-disguises = { module = "me.libraryaddict.disguises:libsdisguises", version.ref = "libs-disguises" }
feather = { module = "net.digitalingot.feather-server-api:api", version.ref = "featherVersion" }
labymod = { module = "net.labymod.serverapi:server-bukkit", version.ref = "labymodVersion" }
hmcCosmetics = { module = "com.hibiscusmc:HMCCosmetics", version.ref = "hmcCosmetics" }
Binary file not shown.
+2 -1
View File
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+16 -10
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,7 +85,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -130,10 +133,13 @@ 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.
@@ -141,7 +147,7 @@ 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=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -149,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -198,11 +204,11 @@ fi
# 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, 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.
# 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" \
Vendored
+94 -92
View File
@@ -1,92 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
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.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
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.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
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!
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
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
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
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
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
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
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!
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
:omega
+4
View File
@@ -1 +1,5 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
}
rootProject.name = "UnlimitedNametags"
@@ -1,5 +1,7 @@
package org.alexdev.unlimitednametags;
import com.alessiodp.libby.BukkitLibraryManager;
import com.alessiodp.libby.Library;
import com.github.Anon8281.universalScheduler.UniversalScheduler;
import com.github.Anon8281.universalScheduler.scheduling.schedulers.TaskScheduler;
import com.google.common.collect.Lists;
@@ -7,14 +9,12 @@ import com.google.common.collect.Maps;
import com.jonahseguin.drink.CommandService;
import com.jonahseguin.drink.Drink;
import lombok.Getter;
import net.byteflux.libby.BukkitLibraryManager;
import net.byteflux.libby.Library;
import org.alexdev.unlimitednametags.api.UNTAPI;
import org.alexdev.unlimitednametags.commands.MainCommand;
import org.alexdev.unlimitednametags.config.ConfigManager;
import org.alexdev.unlimitednametags.events.*;
import org.alexdev.unlimitednametags.hook.*;
import org.alexdev.unlimitednametags.hook.hat.HatHook;
import org.alexdev.unlimitednametags.listeners.*;
import org.alexdev.unlimitednametags.metrics.Metrics;
import org.alexdev.unlimitednametags.nametags.ConditionalManager;
import org.alexdev.unlimitednametags.nametags.NameTagManager;
@@ -51,7 +51,7 @@ public final class UnlimitedNameTags extends JavaPlugin {
@Override
public void onLoad() {
hooks = Maps.newConcurrentMap();
hooks = Maps.newHashMap();
hatHooks = Lists.newCopyOnWriteArrayList();
}
@@ -68,6 +68,8 @@ public final class UnlimitedNameTags extends JavaPlugin {
return;
}
loadListeners();
trackerManager = new TrackerManager(this);
nametagManager = new NameTagManager(this);
placeholderManager = new PlaceholderManager(this);
@@ -77,7 +79,6 @@ public final class UnlimitedNameTags extends JavaPlugin {
loadCommands();
loadListeners();
loadHooks();
loadStats();
@@ -96,29 +97,50 @@ public final class UnlimitedNameTags extends JavaPlugin {
}
private CompletableFuture<Void> loadLibraries() {
if (isPaper) {
return CompletableFuture.completedFuture(null);
}
final BukkitLibraryManager bukkitLibraryManager = new BukkitLibraryManager(this);
bukkitLibraryManager.addRepository("https://maven-central.storage-download.googleapis.com/maven2");
bukkitLibraryManager.addMavenCentral();
final List<Library> libraries = Lists.newArrayList(
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-text-minimessage")
.version("4.17.0")
.relocate("net{}]kyori{}adventure{}text{}serializer", "io{}github{}retrooper{}packetevents{}adventure{}serializer")
.build()
);
return CompletableFuture.runAsync(() -> {
libraries.forEach(bukkitLibraryManager::loadLibrary);
});
if (!isPaper) {
libraries.add(Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-text-minimessage")
.version("4.17.0")
.relocate("net{}]kyori{}adventure{}text{}serializer", "io{}github{}retrooper{}packetevents{}adventure{}serializer")
.build());
}
if (false && Bukkit.getPluginManager().isPluginEnabled("ItemsAdder")) {
libraries.add(Library.builder()
.groupId("team.unnamed")
.artifactId("creative-server")
.version("1.8.3-SNAPSHOT")
.build());
libraries.add(Library.builder()
.groupId("team.unnamed")
.artifactId("creative-serializer-minecraft")
.version("1.8.3-SNAPSHOT")
.build());
libraries.add(Library.builder()
.groupId("team.unnamed")
.artifactId("creative-api")
.version("1.8.3-SNAPSHOT")
.build());
}
return CompletableFuture.runAsync(() -> libraries.forEach(bukkitLibraryManager::loadLibrary));
}
private void loadListeners() {
playerListener = new PlayerListener(this);
Bukkit.getPluginManager().registerEvents(playerListener, this);
Bukkit.getPluginManager().registerEvents(new OtherListener(this), this);
if (isPaper) {
getLogger().info("Paper found, using Paper's tracker");
@@ -129,6 +151,7 @@ public final class UnlimitedNameTags extends JavaPlugin {
getServer().getPluginManager().disablePlugin(this);
return;
}
getLogger().info("Paper not found, using Spigot's tracker");
Bukkit.getPluginManager().registerEvents(new SpigotTrackerListener(this), this);
}
@@ -163,6 +186,8 @@ public final class UnlimitedNameTags extends JavaPlugin {
}
private void loadHooks() {
hatHooks.add(new AdvancedHatHook(this));
if (Bukkit.getPluginManager().isPluginEnabled("TypeWriter")) {
hooks.put(TypeWriterListener.class, new TypeWriterListener(this));
getLogger().info("TypeWriter found, hooking into it");
@@ -174,24 +199,24 @@ public final class UnlimitedNameTags extends JavaPlugin {
}
if (Bukkit.getPluginManager().isPluginEnabled("Nexo")) {
getLogger().info("Nexo found, hooking into it");
final NexoHook hook = new NexoHook(this);
hatHooks.add(hook);
hooks.put(NexoHook.class, hook);
getLogger().info("Nexo found, hooking into it");
}
if (Bukkit.getPluginManager().isPluginEnabled("Oraxen")) {
getLogger().info("Oraxen found, hooking into it");
final OraxenHook hook = new OraxenHook(this);
hatHooks.add(hook);
hooks.put(OraxenHook.class, hook);
getLogger().info("Oraxen found, hooking into it");
}
if (Bukkit.getPluginManager().isPluginEnabled("ItemsAdder")) {
if (false && Bukkit.getPluginManager().isPluginEnabled("ItemsAdder")) {
getLogger().info("ItemsAdder found, hooking into it");
final ItemsAdderHook hook = new ItemsAdderHook(this);
hatHooks.add(hook);
hooks.put(ItemsAdderHook.class, hook);
getLogger().info("ItemsAdder found, hooking into it");
}
@@ -213,6 +238,31 @@ public final class UnlimitedNameTags extends JavaPlugin {
getLogger().info("Floodgate found, hooking into it");
}
if (Bukkit.getPluginManager().isPluginEnabled("LibsDisguises")) {
final LibsDisguisesHook hook = new LibsDisguisesHook(this);
hooks.put(LibsDisguisesHook.class, hook);
getLogger().info("LibsDisguises found, hooking into it");
}
if (Bukkit.getPluginManager().isPluginEnabled("FeatherServerAPI")) {
final FeatherClientHook hook = new FeatherClientHook(this);
hooks.put(FeatherClientHook.class, hook);
getLogger().info("FeatherServerAPI found, hooking into it");
}
if (Bukkit.getPluginManager().isPluginEnabled("HMCCosmetics")) {
final HMCCosmeticsHook hook = new HMCCosmeticsHook(this);
hatHooks.add(hook);
hooks.put(HMCCosmeticsHook.class, hook);
getLogger().info("HMCCosmetics found, hooking into it");
}
// if (Bukkit.getPluginManager().isPluginEnabled("LabyModServerAPI")) {
// final LabyModHook hook = new LabyModHook(this);
// hooks.put(LabyModHook.class, hook);
// getLogger().info("LabyModServerAPI found, hooking into it");
// }
hooks.values().forEach(Hook::onEnable);
}
@@ -262,6 +312,7 @@ public final class UnlimitedNameTags extends JavaPlugin {
placeholderManager.close();
packetManager.close();
taskScheduler.cancelTasks();
playerListener.close();
}
}
@@ -1,7 +1,10 @@
package org.alexdev.unlimitednametags.api;
import me.tofaa.entitylib.meta.display.AbstractDisplayMeta;
import net.kyori.adventure.text.Component;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.config.Settings;
import org.alexdev.unlimitednametags.hook.hat.HatHook;
import org.alexdev.unlimitednametags.packet.PacketNameTag;
import org.alexdev.unlimitednametags.vanish.VanishIntegration;
@@ -9,7 +12,9 @@ import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
/**
* The UnlimitedNameTags API class.
@@ -115,7 +120,7 @@ public class UNTAPI {
* @since 1.5
*/
public void showNametag(@NotNull Player player) {
plugin.getNametagManager().showToTrackedPlayers(player, plugin.getTrackerManager().getTrackedPlayers(player.getUniqueId()));
plugin.getNametagManager().showToTrackedPlayers(player);
}
/**
@@ -147,6 +152,268 @@ public class UNTAPI {
plugin.getHatHooks().remove(hook);
}
/**
* Sets a complete nametag override for the specified player.
* If the player already has a nametag, it will be swapped immediately.
* If the player doesn't have a nametag yet, it will be cached and applied when the nametag is created.
*
* @param player the player to set the override for
* @param nameTag the nametag override to set
*/
public void setNametagOverride(@NotNull Player player, @NotNull Settings.NameTag nameTag) {
plugin.getNametagManager().setNametagOverride(player, nameTag);
}
/**
* Modifies a single property of the player's nametag by cloning the current nametag (override or config)
* and applying the modification, then saving it as an override.
*
* @param player the player whose nametag should be modified
* @param modifier a function that takes the current nametag and returns a modified nametag
*/
public void modifyNametagProperty(@NotNull Player player, @NotNull Function<Settings.NameTag, Settings.NameTag> modifier) {
final Settings.NameTag current = plugin.getNametagManager().getEffectiveNametag(player);
final Settings.NameTag modified = modifier.apply(current);
plugin.getNametagManager().setNametagOverride(player, modified);
}
/**
* Removes the nametag override for the specified player, reverting to the config nametag.
*
* @param player the player to remove the override for
*/
public void removeNametagOverride(@NotNull Player player) {
plugin.getNametagManager().removeNametagOverride(player);
}
/**
* Checks if the specified player has an active nametag override.
*
* @param player the player to check
* @return true if the player has an override, false otherwise
*/
public boolean hasNametagOverride(@NotNull Player player) {
return plugin.getNametagManager().hasNametagOverride(player);
}
/**
* Gets the nametag override for the specified player, if present.
*
* @param player the player to get the override for
* @return an Optional containing the override if present, empty otherwise
*/
@NotNull
public Optional<Settings.NameTag> getNametagOverride(@NotNull Player player) {
return plugin.getNametagManager().getNametagOverride(player);
}
/**
* Gets the effective nametag for the specified player (override if present, otherwise config).
*
* @param player the player to get the effective nametag for
* @return the effective nametag
*/
@NotNull
public Settings.NameTag getEffectiveNametag(@NotNull Player player) {
return plugin.getNametagManager().getEffectiveNametag(player);
}
/**
* Gets the config nametag for the specified player (without any override).
*
* @param player the player to get the config nametag for
* @return the config nametag
*/
@NotNull
public Settings.NameTag getConfigNametag(@NotNull Player player) {
return plugin.getNametagManager().getConfigNametag(player);
}
/**
* Sets whether the shift system (sneak opacity) is blocked for the specified player.
*
* @param player the player to set the block for
* @param blocked true to block the shift system, false to allow it
*/
public void setShiftSystemBlocked(@NotNull Player player, boolean blocked) {
plugin.getNametagManager().setShiftSystemBlocked(player, blocked);
}
/**
* Checks if the shift system is blocked for the specified player.
*
* @param player the player to check
* @return true if the shift system is blocked, false otherwise
*/
public boolean isShiftSystemBlocked(@NotNull Player player) {
return plugin.getNametagManager().isShiftSystemBlocked(player);
}
/**
* Forces an immediate refresh of the player's nametag.
*
* @param player the player to refresh
*/
public void forceRefresh(@NotNull Player player) {
plugin.getNametagManager().refresh(player, true);
}
/**
* Forces an immediate refresh of the player's nametag with the specified force option.
*
* @param player the player to refresh
* @param force whether to force the refresh
*/
public void forceRefresh(@NotNull Player player, boolean force) {
plugin.getNametagManager().refresh(player, force);
}
/**
* Sets the scale of the player's nametag by cloning the current nametag and applying the new scale.
*
* @param player the player whose nametag scale should be modified
* @param scale the new scale value
*/
public void setNametagScale(@NotNull Player player, float scale) {
final Settings.NameTag current = plugin.getNametagManager().getEffectiveNametag(player);
final Settings.NameTag modified = current.withScale(scale);
plugin.getNametagManager().setNametagOverride(player, modified);
}
/**
* Sets the background of the player's nametag by cloning the current nametag and applying the new background.
*
* @param player the player whose nametag background should be modified
* @param background the new background
*/
public void setNametagBackground(@NotNull Player player, @NotNull Settings.Background background) {
final Settings.NameTag current = plugin.getNametagManager().getEffectiveNametag(player);
final Settings.NameTag modified = current.withBackground(background);
plugin.getNametagManager().setNametagOverride(player, modified);
}
/**
* Sets the lines groups of the player's nametag by cloning the current nametag and applying the new lines groups.
*
* @param player the player whose nametag lines should be modified
* @param linesGroups the new lines groups
*/
public void setNametagLines(@NotNull Player player, @NotNull List<Settings.LinesGroup> linesGroups) {
final Settings.NameTag current = plugin.getNametagManager().getEffectiveNametag(player);
final Settings.NameTag modified = current.withLinesGroups(linesGroups);
plugin.getNametagManager().setNametagOverride(player, modified);
}
/**
* Sets the billboard constraints of the player's nametag.
* Note: This modifies the display directly and doesn't require cloning the nametag.
*
* @param player the player whose nametag billboard should be modified
* @param billboard the new billboard constraints
*/
public void setNametagBillboard(@NotNull Player player, @NotNull AbstractDisplayMeta.BillboardConstraints billboard) {
plugin.getNametagManager().getPacketDisplayText(player).ifPresent(packetNameTag -> {
packetNameTag.setBillboard(billboard);
packetNameTag.refresh();
});
}
/**
* Sets the shadowed property of the player's nametag background by cloning the current nametag and applying the modification.
*
* @param player the player whose nametag shadowed property should be modified
* @param shadowed the new shadowed value
*/
public void setNametagShadowed(@NotNull Player player, boolean shadowed) {
final Settings.NameTag current = plugin.getNametagManager().getEffectiveNametag(player);
final Settings.Background bg = current.background();
Settings.Background newBg;
if (bg instanceof Settings.IntegerBackground intBg) {
newBg = new Settings.IntegerBackground(bg.enabled(), intBg.getRed(), intBg.getGreen(), intBg.getBlue(), bg.opacity(), shadowed, bg.seeThrough());
} else if (bg instanceof Settings.HexBackground hexBg) {
newBg = new Settings.HexBackground(bg.enabled(), hexBg.getHex(), bg.opacity(), shadowed, bg.seeThrough());
} else {
return;
}
final Settings.NameTag modified = current.withBackground(newBg);
plugin.getNametagManager().setNametagOverride(player, modified);
}
/**
* Sets the seeThrough property of the player's nametag background by cloning the current nametag and applying the modification.
*
* @param player the player whose nametag seeThrough property should be modified
* @param seeThrough the new seeThrough value
*/
public void setNametagSeeThrough(@NotNull Player player, boolean seeThrough) {
final Settings.NameTag current = plugin.getNametagManager().getEffectiveNametag(player);
final Settings.Background bg = current.background();
Settings.Background newBg;
if (bg instanceof Settings.IntegerBackground intBg) {
newBg = new Settings.IntegerBackground(bg.enabled(), intBg.getRed(), intBg.getGreen(), intBg.getBlue(), bg.opacity(), bg.shadowed(), seeThrough);
} else if (bg instanceof Settings.HexBackground hexBg) {
newBg = new Settings.HexBackground(bg.enabled(), hexBg.getHex(), bg.opacity(), bg.shadowed(), seeThrough);
} else {
return;
}
final Settings.NameTag modified = current.withBackground(newBg);
plugin.getNametagManager().setNametagOverride(player, modified);
}
/**
* Forces a specific component as the nametag for a player.
* This overrides any existing nametag configuration or overrides.
*
* @param player the player whose nametag should be forced
* @param component the component to display as the nametag
*/
public void setForcedNametag(@NotNull Player player, @NotNull Component component) {
plugin.getNametagManager().getPacketDisplayText(player).ifPresent(packetNameTag -> {
packetNameTag.setForcedNameTag(component);
packetNameTag.refresh();
});
}
/**
* Forces a specific component as the nametag for a player, visible only to a specific viewer.
* This overrides any existing nametag configuration or overrides for that specific viewer.
*
* @param player the player whose nametag should be forced
* @param viewer the player who will see the forced nametag
* @param component the component to display as the nametag
*/
public void setForcedNametag(@NotNull Player player, @NotNull Player viewer, @NotNull Component component) {
plugin.getNametagManager().getPacketDisplayText(player).ifPresent(packetNameTag -> {
packetNameTag.setForcedNameTag(viewer.getUniqueId(), component);
packetNameTag.refreshForPlayer(viewer);
});
}
/**
* Clears the forced nametag for a player.
*
* @param player the player whose forced nametag should be cleared
*/
public void clearForcedNametag(@NotNull Player player) {
plugin.getNametagManager().getPacketDisplayText(player).ifPresent(packetNameTag -> {
packetNameTag.clearForcedNameTag();
packetNameTag.refresh();
});
}
/**
* Clears the forced nametag for a player, for a specific viewer.
*
* @param player the player whose forced nametag should be cleared
* @param viewer the player who was seeing the forced nametag
*/
public void clearForcedNametag(@NotNull Player player, @NotNull Player viewer) {
plugin.getNametagManager().getPacketDisplayText(player).ifPresent(packetNameTag -> {
packetNameTag.clearForcedNameTag(viewer.getUniqueId());
packetNameTag.refreshForPlayer(viewer);
});
}
static final class NotRegisteredException extends IllegalStateException {
private static final String MESSAGE = """
@@ -30,6 +30,7 @@ public class MainCommand {
public void onReload(@Sender CommandSender sender) {
plugin.getConfigManager().reload();
plugin.getNametagManager().reload();
plugin.getPlaceholderManager().reload();
plugin.getKyoriManager().sendMessage(sender, Formatter.LEGACY.format(plugin, sender, "&aUnlimitedNameTags has been reloaded!"));
}
@@ -55,7 +56,7 @@ public class MainCommand {
@Command(name = "show", desc = "Shows the nametag", usage = "show")
@Require(value = "unt.show", message = "&cYou do not have permission to show the nametag")
public void onShow(@Sender CommandSender sender, Player target) {
plugin.getNametagManager().showToTrackedPlayers(target, plugin.getTrackerManager().getTrackedPlayers(target.getUniqueId()));
plugin.getNametagManager().showToTrackedPlayers(target);
}
@Command(name = "refresh", desc = "Refreshes the nametag of a player for you", usage = "refresh")
@@ -105,5 +106,4 @@ public class MainCommand {
}
}
}
}
@@ -0,0 +1,62 @@
package org.alexdev.unlimitednametags.config;
import de.exlll.configlib.Comment;
import de.exlll.configlib.Configuration;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Configuration
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@Getter
public class Advanced {
@Comment({
"Optional rules to raise the nametag when the worn helmet matches.",
"Higher priority is evaluated first. The first matching rule wins (same behavior as other hat hooks).",
"Create advanced.yml in the plugin folder manually; it is never auto-generated."
})
private List<HelmetHeightRule> helmetHeightRules = new ArrayList<>();
@Configuration
@Getter
@NoArgsConstructor
public static class HelmetHeightRule {
@Comment("Higher value = checked before lower priority rules.")
private int priority;
@Comment("Vertical offset (same unit as Nexo/Oraxen hat hooks). Must be > 0.")
private double height;
@Comment("Bukkit material name, e.g. LEATHER_HELMET. Omit to ignore material.")
private String material;
@Comment("Exact CustomModelData. Ignored if custom-model-data range below is set.")
private Integer customModelData;
@Comment("Inclusive CMD range; both must be set to use a range.")
private Integer customModelDataMin;
private Integer customModelDataMax;
@Comment("Equippable model key (1.21.3+), e.g. mypack:item/my_hat")
private String equippableModel;
@Comment("If non-empty, the player must be in one of these world names.")
private List<String> worlds = new ArrayList<>();
@Comment("If set, the player must have this permission.")
private String permission;
public boolean definesItemMatch() {
final boolean mat = material != null && !material.isEmpty();
final boolean eq = equippableModel != null && !equippableModel.isEmpty();
final boolean cmdExact = customModelData != null;
final boolean cmdRange = customModelDataMin != null && customModelDataMax != null;
return mat || eq || cmdExact || cmdRange;
}
}
}
@@ -7,13 +7,16 @@ import de.exlll.configlib.YamlConfigurations;
import lombok.Getter;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
@Getter
public class ConfigManager {
@@ -27,6 +30,7 @@ public class ConfigManager {
private final UnlimitedNameTags plugin;
private Settings settings;
private Advanced advanced = new Advanced();
private boolean compiled;
public ConfigManager(@NotNull UnlimitedNameTags plugin) {
@@ -55,6 +59,7 @@ public class ConfigManager {
PROPERTIES
);
checkData();
loadAdvancedConfig(false);
return Optional.empty();
} catch (Exception e) {
return Optional.of(e);
@@ -64,6 +69,58 @@ public class ConfigManager {
public void reload() {
settings = YamlConfigurations.load(new File(plugin.getDataFolder(), "settings.yml").toPath(), Settings.class, PROPERTIES);
checkData();
loadAdvancedConfig(true);
}
@NotNull
public Advanced getAdvanced() {
return advanced;
}
private void loadAdvancedConfig(boolean keepPreviousOnFailure) {
final File file = new File(plugin.getDataFolder(), "advanced.yml");
if (!file.exists()) {
advanced = new Advanced();
return;
}
try {
advanced = YamlConfigurations.load(file.toPath(), Advanced.class, PROPERTIES);
validateAdvancedRules();
plugin.getLogger().info("Loaded optional advanced.yml");
} catch (Exception e) {
plugin.getLogger().log(Level.WARNING, "Failed to load advanced.yml", e);
if (!keepPreviousOnFailure) {
advanced = new Advanced();
}
}
}
private void validateAdvancedRules() {
final List<Advanced.HelmetHeightRule> rules = advanced.getHelmetHeightRules();
if (rules == null) {
return;
}
for (int i = 0; i < rules.size(); i++) {
final Advanced.HelmetHeightRule rule = rules.get(i);
if (!rule.definesItemMatch()) {
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] has no item matcher; it will never apply.");
}
if (rule.getHeight() <= 0) {
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] height must be > 0; it will never apply.");
}
final boolean minSet = rule.getCustomModelDataMin() != null;
final boolean maxSet = rule.getCustomModelDataMax() != null;
if (minSet != maxSet) {
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] must set both customModelDataMin and customModelDataMax for a CMD range.");
}
if (minSet && maxSet && rule.getCustomModelData() != null) {
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] customModelData is ignored when customModelDataMin/Max range is set.");
}
if (rule.getMaterial() != null && !rule.getMaterial().isEmpty()
&& Material.matchMaterial(rule.getMaterial(), false) == null) {
plugin.getLogger().warning("advanced.yml helmetHeightRules[" + i + "] unknown material: " + rule.getMaterial());
}
}
}
private void checkData() {
@@ -1,6 +1,5 @@
package org.alexdev.unlimitednametags.config;
import de.themoep.minedown.adventure.MineDown;
import lombok.AccessLevel;
import lombok.Getter;
import net.kyori.adventure.text.Component;
@@ -19,10 +18,6 @@ import java.util.regex.Pattern;
@SuppressWarnings("unused")
public enum Formatter {
MINEDOWN(
(plugin, player, text) -> new MineDown(text).toComponent(),
"MineDown"
),
MINIMESSAGE(
(plugin, player, text) -> plugin.getHook(MiniPlaceholdersHook.class)
.map(hook -> hook.format(text, player))
@@ -14,6 +14,7 @@ import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.bukkit.Color;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@@ -24,7 +25,7 @@ public class Settings {
private Map<String, NameTag> nameTags = new LinkedHashMap<>() {{
put("staffer", new NameTag("nametag.staffer", List.of(new LinesGroup(List.of("%luckperms_prefix% %player_name% %luckperms_suffix%"), List.of(new GlobalModifier(true)))),
new IntegerBackground(true, 255, 0, 0, 255, true, false), 1f));
new IntegerBackground(false, 255, 0, 0, 255, true, false), 1f));
put("default", new NameTag("nametag.default", List.of(new LinesGroup(List.of("%luckperms_prefix% %player_name% %luckperms_suffix%"), List.of(new GlobalModifier(true))),
new LinesGroup(List.of("Rich Player"), List.of(new ConditionalModifier("%vault_eco_balance%", ">", "1000")))),
new HexBackground(false, "#ffffff", 255, false, false), 1f));
@@ -36,7 +37,7 @@ public class Settings {
public NameTag getNametag(Player player) {
return nameTags.entrySet().stream()
.filter(entry -> player.hasPermission(entry.getValue().permission))
.filter(entry -> entry.getValue().permission == null || player.hasPermission(entry.getValue().permission))
.findFirst()
.map(Map.Entry::getValue)
.orElse(nameTags.get("default"));
@@ -57,7 +58,7 @@ public class Settings {
@Comment("""
Which text formatter to use (MINIMESSAGE, MINEDOWN, LEGACY or UNIVERSAL)\s
Take note that UNIVERSAL is the most resource intensive but it supports all formatting options (except for MINEDOWN)\s
(&x&0&8&4&c&f&bc LEGACY OF LEGACY - &#084cfbc LEGACY - &#084cfbc& MINEDOWN - <color:#084cfbc> MINIMESSAGE)""")
@Setter
private Formatter format = Formatter.MINIMESSAGE;
@@ -65,6 +66,10 @@ public class Settings {
@Comment("Whether to disable the default name tag or not.")
private boolean disableDefaultNameTag = true;
@Comment("This option works if disableDefaultNameTag is enabled. This will skip the team internal cache and will make all default nametags invisible. " +
"The use case is when there is a npc from a plugin that doesn't use display entities / armorstands for nametags, and there is an online player with the name of the npc.")
private boolean forceDisableDefaultNameTag = false;
private boolean removeEmptyLines = true;
@Comment("Whether to see the NameTag of a user only while pointing at them")
@@ -73,6 +78,17 @@ public class Settings {
@Comment("Whether to see your own NameTag (Similar to nametag mod of Lunar Client)")
private boolean showCurrentNameTag = false;
@Comment("""
Whether to cache components for some time and reuse them or not.
This is useful for performance improvements where a lot of gradients are used, but it uses a bit more memory.
It could create problems with MiniPlaceholders, so it is disabled by default.""")
private boolean componentCaching = false;
@Comment("How long to cache placeholders for (in ticks)")
private int placeholderCacheTime = 1;
private boolean enableRelationalPlaceholders = false;
public float getViewDistance() {
return viewDistance / 160;
}
@@ -85,10 +101,35 @@ public class Settings {
public record PlaceholderReplacement(String placeholder, String replacement) {
}
public record NameTag(String permission, List<LinesGroup> linesGroups, Background background, float scale) {
public record NameTag(@Nullable String permission, @NotNull List<LinesGroup> linesGroups, @NotNull Background background, float scale) {
public NameTag(@NotNull List<LinesGroup> linesGroups, @NotNull Background background, float scale) {
this(null, linesGroups, background, scale);
}
@NotNull
public NameTag withPermission(@Nullable String permission) {
return new NameTag(permission, linesGroups, background, scale);
}
@NotNull
public NameTag withLinesGroups(@NotNull List<LinesGroup> linesGroups) {
return new NameTag(permission, linesGroups, background, scale);
}
@NotNull
public NameTag withBackground(@NotNull Background background) {
return new NameTag(permission, linesGroups, background, scale);
}
@NotNull
public NameTag withScale(float scale) {
return new NameTag(permission, linesGroups, background, scale);
}
}
public record LinesGroup(List<String> lines, List<Modifier> modifiers) {
public record LinesGroup(@NotNull List<String> lines, @Nullable List<Modifier> modifiers) {
}
@@ -179,7 +220,12 @@ public class Settings {
@Override
public Color getColor() {
return !enabled ? Color.BLACK.setAlpha(0) : Color.fromRGB(red, green, blue).setAlpha(opacity);
return !enabled() ? Color.BLACK.setAlpha(0) : Color.fromRGB(red, green, blue).setAlpha(opacity());
}
@NotNull
public IntegerBackground clone() {
return new IntegerBackground(enabled(), getRed(), getGreen(), getBlue(), opacity(), shadowed(), seeThrough());
}
}
@@ -198,7 +244,12 @@ public class Settings {
@Override
public Color getColor() {
int hex = Integer.parseInt(this.hex.substring(1), 16);
return !enabled ? Color.BLACK.setAlpha(0) : Color.fromRGB(hex).setAlpha(opacity);
return !enabled() ? Color.BLACK.setAlpha(0) : Color.fromRGB(hex).setAlpha(opacity());
}
@NotNull
public HexBackground clone() {
return new HexBackground(enabled(), getHex(), opacity(), shadowed(), seeThrough());
}
}
}
@@ -0,0 +1,336 @@
package org.alexdev.unlimitednametags.data;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;
/**
* High-performance thread-safe Multimap implementation optimized for concurrent access.
* Uses ConcurrentHashMap for both the main map and value collections.
*
* @param <K> the type of keys
* @param <V> the type of values
*/
@SuppressWarnings("unused")
public class ConcurrentMultimap<K, V> {
private final ConcurrentMap<K, Set<V>> map;
private final Supplier<Set<V>> setSupplier;
/**
* Creates a new ConcurrentMultimap with ConcurrentHashMap.newKeySet() as the set supplier.
*/
public ConcurrentMultimap() {
this(ConcurrentHashMap::newKeySet);
}
/**
* Creates a new ConcurrentMultimap with a custom set supplier.
*
* @param setSupplier supplier for creating new value sets (must be thread-safe)
*/
public ConcurrentMultimap(@NotNull Supplier<Set<V>> setSupplier) {
this.map = new ConcurrentHashMap<>();
this.setSupplier = setSupplier;
}
/**
* Creates a new ConcurrentMultimap with an expected size hint.
*
* @param expectedSize the expected number of keys
*/
public ConcurrentMultimap(int expectedSize) {
this(expectedSize, ConcurrentHashMap::newKeySet);
}
/**
* Creates a new ConcurrentMultimap with an expected size hint and custom set supplier.
*
* @param expectedSize the expected number of keys
* @param setSupplier supplier for creating new value sets (must be thread-safe)
*/
public ConcurrentMultimap(int expectedSize, @NotNull Supplier<Set<V>> setSupplier) {
this.map = new ConcurrentHashMap<>(expectedSize);
this.setSupplier = setSupplier;
}
/**
* Associates the specified value with the specified key.
*
* @param key the key
* @param value the value to add
* @return true if the value was added, false if it was already present
*/
public boolean put(@NotNull K key, @NotNull V value) {
return map.computeIfAbsent(key, k -> setSupplier.get()).add(value);
}
/**
* Adds all values to the specified key.
*
* @param key the key
* @param values the values to add
* @return true if any value was added
*/
public boolean putAll(@NotNull K key, @NotNull Collection<? extends V> values) {
if (values.isEmpty()) {
return false;
}
return map.computeIfAbsent(key, k -> setSupplier.get()).addAll(values);
}
/**
* Returns all values associated with the specified key.
* The returned collection is a live view - modifications will affect this multimap.
*
* @param key the key
* @return the collection of values, never null (may be empty)
*/
@NotNull
public Collection<V> get(@NotNull K key) {
Set<V> values = map.get(key);
return values != null ? values : Collections.emptySet();
}
/**
* Returns all values associated with the specified key as a defensive copy.
*
* @param key the key
* @return a new collection containing all values
*/
@NotNull
public Collection<V> getCopy(@NotNull K key) {
Set<V> values = map.get(key);
if (values == null || values.isEmpty()) {
return Collections.emptyList();
}
return new ArrayList<>(values);
}
/**
* Removes a single key-value pair.
*
* @param key the key
* @param value the value to remove
* @return true if the value was removed
*/
public boolean remove(@NotNull K key, @NotNull V value) {
Set<V> values = map.get(key);
if (values == null) {
return false;
}
boolean removed = values.remove(value);
// Clean up empty sets to prevent memory leaks
if (removed && values.isEmpty()) {
map.remove(key, values);
}
return removed;
}
/**
* Removes all values associated with the specified key.
*
* @param key the key
* @return the collection of values that were removed, never null
*/
@NotNull
public Collection<V> removeAll(@NotNull K key) {
Set<V> removed = map.remove(key);
return removed != null ? removed : Collections.emptySet();
}
/**
* Replaces all values for a key with the specified values.
*
* @param key the key
* @param values the new values
* @return the previous values associated with the key
*/
@NotNull
public Collection<V> replaceValues(@NotNull K key, @NotNull Collection<? extends V> values) {
if (values.isEmpty()) {
return removeAll(key);
}
Set<V> newSet = setSupplier.get();
newSet.addAll(values);
Set<V> previous = map.put(key, newSet);
return previous != null ? previous : Collections.emptySet();
}
/**
* Returns true if this multimap contains the specified key.
*
* @param key the key
* @return true if the key exists
*/
public boolean containsKey(@NotNull K key) {
return map.containsKey(key);
}
/**
* Returns true if this multimap contains the specified key-value pair.
*
* @param key the key
* @param value the value
* @return true if the pair exists
*/
public boolean containsEntry(@NotNull K key, @NotNull V value) {
Set<V> values = map.get(key);
return values != null && values.contains(value);
}
/**
* Returns the number of key-value pairs in this multimap.
*
* @return the total number of values across all keys
*/
public int size() {
return map.values().stream().mapToInt(Set::size).sum();
}
/**
* Returns the number of values associated with the specified key.
*
* @param key the key
* @return the number of values for the key
*/
public int size(@NotNull K key) {
Set<V> values = map.get(key);
return values != null ? values.size() : 0;
}
/**
* Returns true if this multimap contains no key-value pairs.
*
* @return true if empty
*/
public boolean isEmpty() {
return map.isEmpty();
}
/**
* Removes all key-value pairs from this multimap.
*/
public void clear() {
map.clear();
}
/**
* Returns a set view of all keys in this multimap.
*
* @return the set of keys
*/
@NotNull
public Set<K> keySet() {
return map.keySet();
}
/**
* Returns a collection view of all values in this multimap.
* This creates a new collection on each call.
*
* @return all values
*/
@NotNull
public Collection<V> values() {
List<V> allValues = new ArrayList<>();
for (Set<V> values : map.values()) {
allValues.addAll(values);
}
return allValues;
}
/**
* Returns a collection view of all entries in this multimap.
*
* @return all key-value pairs as Map.Entry objects
*/
@NotNull
public Collection<Map.Entry<K, V>> entries() {
List<Map.Entry<K, V>> entries = new ArrayList<>();
for (Map.Entry<K, Set<V>> mapEntry : map.entrySet()) {
K key = mapEntry.getKey();
for (V value : mapEntry.getValue()) {
entries.add(new AbstractMap.SimpleImmutableEntry<>(key, value));
}
}
return entries;
}
/**
* Returns a map view where each key is associated with a collection of values.
* The returned map is a snapshot - modifications will not affect this multimap.
*
* @return a map representation
*/
@NotNull
public Map<K, Collection<V>> asMap() {
Map<K, Collection<V>> result = new HashMap<>();
for (Map.Entry<K, Set<V>> entry : map.entrySet()) {
result.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
return result;
}
/**
* Executes the given action for each key-value pair.
*
* @param action the action to perform
*/
public void forEach(@NotNull java.util.function.BiConsumer<? super K, ? super V> action) {
for (Map.Entry<K, Set<V>> entry : map.entrySet()) {
K key = entry.getKey();
for (V value : entry.getValue()) {
action.accept(key, value);
}
}
}
/**
* Removes a specific value from all keys.
* Optimized for performance - avoids iteration over all values.
*
* @param value the value to remove from all keys
* @return the number of keys from which the value was removed
*/
public int removeValueFromAll(@NotNull V value) {
int removedCount = 0;
for (Map.Entry<K, Set<V>> entry : map.entrySet()) {
Set<V> values = entry.getValue();
if (values.remove(value)) {
removedCount++;
// Clean up empty sets
if (values.isEmpty()) {
map.remove(entry.getKey(), values);
}
}
}
return removedCount;
}
@Override
public String toString() {
return map.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ConcurrentMultimap)) return false;
ConcurrentMultimap<?, ?> that = (ConcurrentMultimap<?, ?>) o;
return map.equals(that.map);
}
@Override
public int hashCode() {
return map.hashCode();
}
}
@@ -0,0 +1,175 @@
package org.alexdev.unlimitednametags.data;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ConcurrentSetMultimap<K, V> {
private final ConcurrentHashMap<K, ConcurrentLinkedQueue<V>> map = new ConcurrentHashMap<>();
/**
* Adds the value to the specified key.
* If the value is already present for the key, it will not be added.
*
* @param key the key
* @param value the value to add
* @return true if the value was added, false otherwise
*/
public boolean put(K key, V value) {
// Create a new queue if one does not already exist for the key
ConcurrentLinkedQueue<V> queue = map.computeIfAbsent(key, k -> new ConcurrentLinkedQueue<>());
// Ensure uniqueness by synchronizing on the queue for this key
synchronized (queue) {
if (!queue.contains(value)) {
return queue.add(value);
}
}
return false;
}
/**
* Adds all values from the provided collection to the specified key.
* Only adds values that are not already associated with the key.
*
* @param key the key
* @param values the collection of values to add
* @return true if at least one value was added, false otherwise
*/
public boolean putAll(K key, Collection<? extends V> values) {
// Create a new queue if one does not already exist for the key
ConcurrentLinkedQueue<V> queue = map.computeIfAbsent(key, k -> new ConcurrentLinkedQueue<>());
boolean changed = false;
synchronized (queue) {
for (V value : values) {
if (!queue.contains(value)) {
queue.add(value);
changed = true;
}
}
}
return changed;
}
/**
* Returns a set of values associated with the specified key.
*
* @param key the key
* @return a Set containing the associated values, or an empty Set if the key does not exist
*/
public Set<V> get(K key) {
ConcurrentLinkedQueue<V> queue = map.get(key);
if (queue == null) {
return new HashSet<>();
}
// Create a HashSet of the values; synchronize to avoid concurrent modification issues
synchronized (queue) {
return new HashSet<>(queue);
}
}
/**
* Removes the specified value associated with the key.
* If the queue becomes empty after removal, the key is removed from the map.
*
* @param key the key
* @param value the value to remove
* @return true if the value was removed, false otherwise
*/
public boolean remove(K key, V value) {
ConcurrentLinkedQueue<V> queue = map.get(key);
if (queue == null) {
return false;
}
synchronized (queue) {
boolean removed = queue.remove(value);
if (queue.isEmpty()) {
map.remove(key, queue);
}
return removed;
}
}
/**
* Removes all values associated with the specified key.
*
* @param key the key
* @return a Set containing the removed values, or an empty Set if the key did not exist
*/
public Set<V> removeAll(K key) {
ConcurrentLinkedQueue<V> queue = map.remove(key);
if (queue == null) {
return new HashSet<>();
}
synchronized (queue) {
return new HashSet<>(queue);
}
}
/**
* Removes all key-value associations in the multimap.
*/
public void clear() {
map.clear();
}
/**
* Returns a collection view of all values present in the multimap.
* Note: This is a snapshot of the values at the time of invocation.
*
* @return a Collection containing all values from all keys
*/
public Collection<V> values() {
Collection<V> allValues = new ArrayList<>();
for (ConcurrentLinkedQueue<V> queue : map.values()) {
synchronized (queue) {
allValues.addAll(queue);
}
}
return allValues;
}
/**
* Returns the set of keys present in the multimap.
*
* @return a Set containing all keys
*/
public Set<K> keySet() {
return map.keySet();
}
/**
* Returns a set of key-value pairs in the multimap.
* Each entry consists of a key and the corresponding set of values.
*
* @return a Set of Map.Entry containing keys and their associated value sets
*/
public Set<java.util.Map.Entry<K, Set<V>>> entrySet() {
Set<java.util.Map.Entry<K, Set<V>>> entries = new HashSet<>();
for (K key : map.keySet()) {
entries.add(new java.util.AbstractMap.SimpleEntry<>(key, get(key)));
}
return entries;
}
//forEach <key, Set<value>>
public void forEach(java.util.function.BiConsumer<K, Set<V>> action) {
for (K key : map.keySet()) {
action.accept(key, get(key));
}
}
public boolean containsEntry(K key, V value) {
ConcurrentLinkedQueue<V> queue = map.get(key);
if (queue == null) {
return false;
}
synchronized (queue) {
return queue.contains(value);
}
}
}
@@ -0,0 +1,32 @@
package org.alexdev.unlimitednametags.data;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams;
import com.google.common.collect.Sets;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
@Getter
@Setter
public class TeamData {
private final String teamName;
private WrapperPlayServerTeams.ScoreBoardTeamInfo teamInfo;
private final Set<String> members;
private boolean changedVisibility = false;
public TeamData(@NotNull String teamName, @NotNull WrapperPlayServerTeams.ScoreBoardTeamInfo teamInfo) {
this.teamName = teamName;
this.teamInfo = teamInfo;
this.members = Sets.newConcurrentHashSet();
}
public TeamData(@NotNull String teamName, @NotNull WrapperPlayServerTeams.ScoreBoardTeamInfo teamInfo, @NotNull Set<String> members) {
this.teamName = teamName;
this.teamInfo = teamInfo;
this.members = Sets.newConcurrentHashSet(members);
}
}
@@ -1,142 +0,0 @@
package org.alexdev.unlimitednametags.events;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.util.Vector3f;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetPassengers;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.hook.ViaVersionHook;
import org.alexdev.unlimitednametags.packet.PacketNameTag;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.stream.Collectors;
public class PacketEventsListener extends PacketListenerAbstract {
private final UnlimitedNameTags plugin;
public PacketEventsListener(UnlimitedNameTags plugin) {
this.plugin = plugin;
}
public void onEnable() {
PacketEvents.getAPI().getEventManager().registerListener(this);
}
public void onPacketSend(@NotNull PacketSendEvent event) {
if (event.getPacketType() == PacketType.Play.Server.TEAMS) {
handleTeams(event);
} else if (event.getPacketType() == PacketType.Play.Server.SET_PASSENGERS) {
handlePassengers(event);
} else if (event.getPacketType() == PacketType.Play.Server.ENTITY_METADATA) {
handleMetaData(event);
}
}
@Override
public void onPacketReceive(@NotNull PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.ENTITY_ACTION) {
handleUseEntity(event);
}
}
private void handleUseEntity(PacketReceiveEvent event) {
if (!(event.getPlayer() instanceof Player)) {
return;
}
final WrapperPlayClientEntityAction packet = new WrapperPlayClientEntityAction(event);
final Optional<? extends Player> player = plugin.getPlayerListener().getPlayerFromEntityId(packet.getEntityId());
if (player.isEmpty()) {
return;
}
switch (packet.getAction()) {
case START_SNEAKING -> plugin.getNametagManager().updateSneaking(player.get(), true);
case STOP_SNEAKING -> plugin.getNametagManager().updateSneaking(player.get(), false);
case START_FLYING_WITH_ELYTRA -> plugin.getPlayerListener().logicElytra(player.get());
}
}
private void handlePassengers(@NotNull PacketSendEvent event) {
final WrapperPlayServerSetPassengers packet = new WrapperPlayServerSetPassengers(event);
final Optional<? extends Player> player = plugin.getPlayerListener().getPlayerFromEntityId(packet.getEntityId());
if (player.isEmpty()) {
return;
}
final Optional<PacketNameTag> optionalPacketDisplayText = plugin.getNametagManager().getPacketDisplayText(player.get());
if (optionalPacketDisplayText.isEmpty()) {
return;
}
final List<Integer> passengers = Arrays.stream(packet.getPassengers()).boxed().collect(Collectors.toList());
plugin.getPacketManager().setPassengers(player.get(), passengers);
if(!passengers.contains(optionalPacketDisplayText.get().getEntityId())) {
passengers.add(optionalPacketDisplayText.get().getEntityId());
packet.setPassengers(passengers.stream().mapToInt(i -> i).toArray());
}
}
private void handleTeams(@NotNull PacketSendEvent event) {
if (!plugin.getConfigManager().getSettings().isDisableDefaultNameTag()) {
return;
}
final Player player = Bukkit.getPlayer(event.getUser().getUUID());
if(player == null) {
return;
}
if(plugin.getHook(ViaVersionHook.class).map(h -> h.hasNotTextDisplays(player)).orElse(false)) {
return;
}
final WrapperPlayServerTeams packet = new WrapperPlayServerTeams(event);
if (packet.getTeamMode() == WrapperPlayServerTeams.TeamMode.CREATE || packet.getTeamMode() == WrapperPlayServerTeams.TeamMode.UPDATE) {
packet.getTeamInfo().ifPresent(t -> t.setTagVisibility(WrapperPlayServerTeams.NameTagVisibility.NEVER));
event.markForReEncode(true);
}
}
private void handleMetaData(@NotNull PacketSendEvent event) {
if (!(event.getPlayer() instanceof Player)) {
return;
}
int protocol = event.getUser().getClientVersion().getProtocolVersion();
//handle metadata for : bedrock players && client with version 1.20.1 or lower
if (protocol >= 764) {
return;
}
final WrapperPlayServerEntityMetadata packet = new WrapperPlayServerEntityMetadata(event);
final Optional<PacketNameTag> textDisplay = plugin.getNametagManager().getPacketDisplayText(packet.getEntityId());
if (textDisplay.isEmpty()) {
return;
}
for (final EntityData eData : packet.getEntityMetadata()) {
if (eData.getIndex() == 11) {
final Vector3f old = (Vector3f) eData.getValue();
final Vector3f newV = new Vector3f(old.getX(), old.getY() + 0.45f, old.getZ());
eData.setValue(newV);
event.markForReEncode(true);
return;
}
}
}
public void onDisable() {
PacketEvents.getAPI().getEventManager().unregisterListener(this);
}
}
@@ -1,119 +0,0 @@
package org.alexdev.unlimitednametags.events;
import com.github.retrooper.packetevents.PacketEvents;
import com.google.common.collect.*;
import lombok.Getter;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.packet.PacketNameTag;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public class TrackerManager {
private final UnlimitedNameTags plugin;
@Getter
private final SetMultimap<UUID, UUID> trackedPlayers;
public TrackerManager(UnlimitedNameTags plugin) {
this.plugin = plugin;
this.trackedPlayers = Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet);
loadTracker();
}
@SuppressWarnings({"deprecation"})
private void loadTracker() {
final boolean isPaper = plugin.isPaper();
Bukkit.getOnlinePlayers().forEach(player -> {
final Set<Player> currentTracked = (isPaper ? player.getTrackedPlayers() : player.getTrackedBy()).stream()
.filter(p -> Bukkit.getPlayer(p.getUniqueId()) != null)
.collect(ImmutableSet.toImmutableSet());
trackedPlayers.putAll(player.getUniqueId(), currentTracked
.stream()
.map(Player::getUniqueId)
.collect(ImmutableSet.toImmutableSet()));
});
}
public void onDisable() {
trackedPlayers.clear();
}
@NotNull
public Set<UUID> getTrackedPlayers(@NotNull UUID player) {
return trackedPlayers.get(player);
}
public void handleAdd(@NotNull Player player, @NotNull Player target) {
if (!target.isConnected()) {
return;
}
// Check if it's a real player
if (Bukkit.getPlayer(target.getUniqueId()) == null || Bukkit.getPlayer(player.getUniqueId()) == null) {
return;
}
if (PacketEvents.getAPI().getPlayerManager().getUser(target) == null || PacketEvents.getAPI().getPlayerManager().getUser(player) == null) {
return;
}
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> {
trackedPlayers.put(player.getUniqueId(), target.getUniqueId());
final boolean isVanished = plugin.getVanishManager().isVanished(target);
if (isVanished && !plugin.getVanishManager().canSee(player, target)) {
return;
}
if (target.hasPotionEffect(PotionEffectType.INVISIBILITY)) {
return;
}
final Optional<PacketNameTag> display = plugin.getNametagManager().getPacketDisplayText(target);
if (display.isEmpty()) {
// plugin.getLogger().warning("Display is empty for " + target.getName());
return;
}
plugin.getNametagManager().updateDisplay(player, target);
}, 3);
}
public void handleRemove(@NotNull Player player, @NotNull Player target) {
plugin.getTaskScheduler().runTaskAsynchronously(() -> {
trackedPlayers.remove(player.getUniqueId(), target.getUniqueId());
plugin.getNametagManager().removeDisplay(player, target);
});
}
public void handleQuit(@NotNull Player player) {
plugin.getTaskScheduler().runTaskAsynchronously(() -> {
trackedPlayers.removeAll(player.getUniqueId());
trackedPlayers.values().remove(player.getUniqueId());
});
}
public void forceUntrack(@NotNull Player player) {
plugin.getTaskScheduler().runTaskAsynchronously(() -> {
trackedPlayers.get(player.getUniqueId())
.stream()
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.forEach(p -> handleRemove(p, player));
Bukkit.getOnlinePlayers().stream()
.filter(p -> trackedPlayers.get(player.getUniqueId()).contains(p.getUniqueId()))
.forEach(p -> handleRemove(player, p));
});
}
}
@@ -0,0 +1,98 @@
package org.alexdev.unlimitednametags.hook;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.config.Advanced;
import org.alexdev.unlimitednametags.hook.hat.HatHook;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.List;
public final class AdvancedHatHook implements HatHook {
private final UnlimitedNameTags plugin;
public AdvancedHatHook(@NotNull UnlimitedNameTags plugin) {
this.plugin = plugin;
}
@Override
public double getHigh(@NotNull Player player) {
final List<Advanced.HelmetHeightRule> rules = plugin.getConfigManager().getAdvanced().getHelmetHeightRules();
if (rules == null || rules.isEmpty()) {
return 0;
}
final ItemStack helmet = player.getInventory().getHelmet();
if (helmet == null || helmet.getType().isAir()) {
return 0;
}
return rules.stream()
.sorted(Comparator.comparingInt(Advanced.HelmetHeightRule::getPriority).reversed())
.filter(rule -> rule.getHeight() > 0)
.filter(Advanced.HelmetHeightRule::definesItemMatch)
.filter(rule -> matches(rule, player, helmet))
.mapToDouble(Advanced.HelmetHeightRule::getHeight)
.findFirst()
.orElse(0);
}
private boolean matches(@NotNull Advanced.HelmetHeightRule rule, @NotNull Player player, @NotNull ItemStack helmet) {
if (rule.getPermission() != null && !rule.getPermission().isEmpty() && !player.hasPermission(rule.getPermission())) {
return false;
}
if (rule.getWorlds() != null && !rule.getWorlds().isEmpty()
&& !rule.getWorlds().contains(player.getWorld().getName())) {
return false;
}
if (rule.getMaterial() != null && !rule.getMaterial().isEmpty()) {
final Material mat = Material.matchMaterial(rule.getMaterial(), false);
if (mat == null || helmet.getType() != mat) {
return false;
}
}
if (rule.getEquippableModel() != null && !rule.getEquippableModel().isEmpty()) {
final ItemMeta meta = helmet.getItemMeta();
if (meta == null || !meta.hasEquippable()) {
return false;
}
final NamespacedKey modelKey = meta.getEquippable().getModel();
if (modelKey == null) {
return false;
}
final NamespacedKey wanted = NamespacedKey.fromString(rule.getEquippableModel());
if (wanted == null || !wanted.equals(modelKey)) {
return false;
}
}
final boolean wantsRange = rule.getCustomModelDataMin() != null && rule.getCustomModelDataMax() != null;
if (wantsRange) {
final ItemMeta meta = helmet.getItemMeta();
if (meta == null || !meta.hasCustomModelData()) {
return false;
}
final int cmd = meta.getCustomModelData();
return cmd >= rule.getCustomModelDataMin() && cmd <= rule.getCustomModelDataMax();
} else if (rule.getCustomModelData() != null) {
final ItemMeta meta = helmet.getItemMeta();
if (meta == null || !meta.hasCustomModelData()) {
return false;
}
return meta.getCustomModelData() == rule.getCustomModelData();
}
return true;
}
}
@@ -0,0 +1,45 @@
package org.alexdev.unlimitednametags.hook;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.User;
import net.digitalingot.feather.serverapi.api.FeatherAPI;
import net.digitalingot.feather.serverapi.api.event.player.PlayerHelloEvent;
import net.digitalingot.feather.serverapi.api.model.FeatherMod;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.List;
public class FeatherClientHook extends Hook {
private final List<FeatherMod> modsToBlock = Collections.singletonList(new FeatherMod("nametags"));
public FeatherClientHook(UnlimitedNameTags plugin) {
super(plugin);
}
@Override
public void onEnable() {
FeatherAPI.getEventService().subscribe(PlayerHelloEvent.class, event -> {
final Player player = plugin.getPlayerListener().getPlayer(event.getPlayer().getUniqueId());
if (player == null) {
return;
}
final User user = PacketEvents.getAPI().getPlayerManager().getUser(player);
if (user.getClientVersion().isOlderThan(ClientVersion.V_1_19_3)) {
return;
}
event.getPlayer().blockMods(modsToBlock);
});
}
@Override
public void onDisable() {
}
}
@@ -5,12 +5,15 @@ import com.hibiscusmc.hmccosmetics.cosmetic.Cosmetic;
import com.hibiscusmc.hmccosmetics.cosmetic.CosmeticSlot;
import com.hibiscusmc.hmccosmetics.user.CosmeticUser;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.hook.creative.CreativeHook;
import org.alexdev.unlimitednametags.hook.hat.HatHook;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
public class HMCCosmeticsHook extends Hook implements HatHook {
private CreativeHook creativeHook;
public HMCCosmeticsHook(UnlimitedNameTags plugin) {
super(plugin);
@@ -18,12 +21,24 @@ public class HMCCosmeticsHook extends Hook implements HatHook {
@Override
public void onEnable() {
plugin.getTaskScheduler().runTaskLater(() -> {
creativeHook = plugin.getHooks().values()
.stream()
.filter(hook -> hook instanceof CreativeHook)
.map(hook -> (CreativeHook) hook)
.findFirst()
.orElse(null);
}, 1);
}
@Override
public void onDisable() {
}
public boolean hasBackpack(@NotNull Player player) {
final CosmeticUser user = HMCCosmeticsAPI.getUser(player.getUniqueId());
return user != null && user.hasCosmeticInSlot(CosmeticSlot.BACKPACK);
}
@Override
public double getHigh(@NotNull Player player) {
@@ -37,6 +52,15 @@ public class HMCCosmeticsHook extends Hook implements HatHook {
return 0;
}
final ItemStack item = cosmetic.getItem();
if (item == null) {
return 0;
}
if (creativeHook != null) {
return creativeHook.getHigh(item);
}
return 0;
}
}
@@ -1,33 +1,47 @@
package org.alexdev.unlimitednametags.hook;
import com.google.common.collect.Maps;
import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent;
import dev.lone.itemsadder.api.Events.ItemsAdderPackCompressedEvent;
import lombok.Getter;
import net.kyori.adventure.key.Key;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.hook.creative.CreativeHook;
import org.alexdev.unlimitednametags.hook.creative.CustomMinecraftResourcePackReaderImpl;
import org.alexdev.unlimitednametags.hook.hat.HatHook;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import team.unnamed.creative.ResourcePack;
import team.unnamed.creative.model.Model;
import java.io.File;
import java.nio.file.Path;
import java.util.Map;
@SuppressWarnings("DuplicatedCode")
public class ItemsAdderHook extends Hook implements Listener, HatHook {
@Getter
public class ItemsAdderHook extends Hook implements Listener, HatHook, CreativeHook {
private final Map<String, Double> height;
private static final Path generatedPath = new File(Bukkit.getPluginsFolder(),"ItemsAdder" + File.separator + "output" + File.separator + "generated.zip").toPath();
private final Map<Key, Map<Integer, Model>> cmdCache;
private ResourcePack resourcePack;
public ItemsAdderHook(@NotNull UnlimitedNameTags plugin) {
super(plugin);
this.height = Maps.newConcurrentMap();
this.cmdCache = Maps.newConcurrentMap();
loadTexture();
}
public double getHigh(@NotNull Player player) {
return 0;
return CreativeHook.super.getHigh(player);
}
@EventHandler
public void onEnable(ItemsAdderLoadDataEvent event) {
height.clear();
public void onLoad(ItemsAdderPackCompressedEvent event) {
cmdCache.clear();
loadTexture();
plugin.getLogger().info("ItemsAdder items loaded, clearing cache");
}
@@ -38,6 +52,24 @@ public class ItemsAdderHook extends Hook implements Listener, HatHook {
@Override
public void onDisable() {
cmdCache.clear();
}
@Override
public void loadTexture() {
final File generated = generatedPath.toFile();
if (!generated.exists()) {
plugin.getLogger().warning("ItemsAdder generated.zip not found, skipping");
return;
}
try {
// resourcePack = MinecraftResourcePackReader.builder().lenient(true).build().readFromZipFile(generated);
resourcePack = CustomMinecraftResourcePackReaderImpl.INSTANCE.readFromZipFile(generated);
plugin.getLogger().info("ItemsAdder's resource pack loaded from " + generated.getAbsolutePath());
} catch (Throwable e) {
resourcePack = null;
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Failed to load ItemsAdder resource pack for file at " + generated.getAbsolutePath(), e);
}
}
}
@@ -0,0 +1,33 @@
package org.alexdev.unlimitednametags.hook;
import net.labymod.serverapi.server.bukkit.event.LabyModPlayerJoinEvent;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public class LabyModHook extends Hook implements Listener {
// private final List<String> modsToBlock = List.of("Nametag", "nametag");
public LabyModHook(UnlimitedNameTags plugin) {
super(plugin);
}
@Override
public void onEnable() {
Bukkit.getPluginManager().registerEvents(this, plugin);
}
@Override
public void onDisable() {
}
@EventHandler
public void onJoin(LabyModPlayerJoinEvent event) {
// event.labyModPlayer().disableAddons(modsToBlock);
}
}
@@ -0,0 +1,45 @@
package org.alexdev.unlimitednametags.hook;
import me.libraryaddict.disguise.events.DisguiseEvent;
import me.libraryaddict.disguise.events.UndisguiseEvent;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
public class LibsDisguisesHook extends Hook implements Listener {
public LibsDisguisesHook(@NotNull UnlimitedNameTags plugin) {
super(plugin);
}
@Override
public void onEnable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void onDisable() {
}
@EventHandler
public void onDisguise(DisguiseEvent event) {
if (event.getDisguise().getEntity() instanceof Player player) {
plugin.getNametagManager().removeAllViewers(player);
plugin.getNametagManager().blockPlayer(player);
}
}
@EventHandler
public void onUnDisguise(UndisguiseEvent event) {
if (event.isBeingReplaced()) {
return;
}
if (event.getDisguise().getEntity() instanceof Player player) {
plugin.getNametagManager().unblockPlayer(player);
plugin.getNametagManager().showToTrackedPlayers(player);
}
}
}
@@ -15,12 +15,7 @@ public class MiniPlaceholdersHook extends Hook {
@NotNull
public Component format(@NotNull String text, @NotNull Audience player) {
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getAudienceGlobalPlaceholders(player));
}
@NotNull
public Component format(@NotNull Component component, @NotNull Audience player) {
return format(MiniMessage.miniMessage().serialize(component), player);
return MiniMessage.miniMessage().deserialize(text, player, MiniPlaceholders.audienceGlobalPlaceholders());
}
@Override
@@ -1,114 +1,66 @@
package org.alexdev.unlimitednametags.hook;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.nexomc.nexo.api.NexoItems;
import com.nexomc.nexo.api.NexoPack;
import com.nexomc.nexo.api.events.NexoItemsLoadedEvent;
import com.nexomc.nexo.items.ItemBuilder;
import com.nexomc.nexo.items.NexoMeta;
import lombok.Getter;
import net.kyori.adventure.key.Key;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.hook.creative.CreativeHook;
import org.alexdev.unlimitednametags.hook.hat.HatHook;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import team.unnamed.creative.ResourcePack;
import team.unnamed.creative.model.Model;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
@SuppressWarnings("DuplicatedCode")
public class NexoHook extends Hook implements Listener, HatHook {
@Getter
public class NexoHook extends Hook implements Listener, CreativeHook, HatHook {
private static final File NEXO_FOLDER = new File("plugins/Nexo/pack/assets/minecraft/models");
private final Map<String, Double> height;
private final Gson jsonParser;
private final Map<Key, Map<Integer, Model>> cmdCache;
private ResourcePack resourcePack;
public NexoHook(@NotNull UnlimitedNameTags plugin) {
super(plugin);
this.height = Maps.newConcurrentMap();
this.jsonParser = new Gson();
this.cmdCache = Maps.newConcurrentMap();
loadTexture();
}
public void loadTexture() {
resourcePack = NexoPack.resourcePack();
}
@Override
public double getHigh(@NotNull Player player) {
final ItemStack helmet = player.getInventory().getHelmet();
if (helmet == null) {
return 0;
}
if (!helmet.hasItemMeta() || !helmet.getItemMeta().hasCustomModelData()) {
return 0;
}
final Optional<ItemBuilder> optionalItemBuilder = findItem(helmet);
return optionalItemBuilder.map(itemBuilder -> getHigh(getModelName(itemBuilder.getNexoMeta()) + ".json")).orElse(0.0);
return CreativeHook.super.getHigh(player);
}
private Optional<ItemBuilder> findItem(@NotNull ItemStack item) {
return Optional.ofNullable(NexoItems.builderFromItem(item));
}
private String getModelName(@NotNull NexoMeta meta) {
return meta.getModelKey().value();
}
private double getHigh(@NotNull String model) {
if (height.containsKey(model)) {
return height.get(model);
}
final File file = new File(NEXO_FOLDER, model);
if (!file.exists()) {
return -1;
}
final JsonObject jsonObject = parseFile(file);
final JsonArray elements = jsonObject.getAsJsonArray("elements");
if (elements.size() == 0) {
return -1;
}
double highest = 0;
for (int i = 0; i < elements.size(); i++) {
final JsonObject element = elements.get(i).getAsJsonObject();
final double to = element.getAsJsonArray("to").get(1).getAsDouble();
if (to > highest) {
highest = to;
public Optional<Model> findModel(@NotNull ItemStack item) {
final Optional<ItemBuilder> optionalItemBuilder = Optional.ofNullable(NexoItems.builderFromItem(item));
if (optionalItemBuilder.isPresent()) {
final Optional<Model> optionalModel = optionalItemBuilder.map(ItemBuilder::getNexoMeta)
.map(NexoMeta::getModel)
.map(key -> resourcePack.model(key));
if (optionalModel.isPresent()) {
return optionalModel;
}
}
final JsonObject display = jsonObject.getAsJsonObject("display");
if (!display.has("head")) {
return -1;
}
final JsonObject head = display.getAsJsonObject("head");
final double scale = head.has("scale") ? head.getAsJsonArray("scale").get(1).getAsDouble() : 1;
highest *= scale;
final double translation = head.has("translation") ? head.getAsJsonArray("translation").get(1).getAsDouble() : 0;
final double value = highest + translation;
height.put(model, value);
return value;
}
@NotNull
private JsonObject parseFile(@NotNull File file) {
try (final FileReader reader = new FileReader(file)) {
return jsonParser.fromJson(reader, JsonObject.class);
} catch (IOException e) {
throw new RuntimeException("Failed to parse file: " + file, e);
}
return CreativeHook.super.findModel(item);
}
@EventHandler
public void onEnable(NexoItemsLoadedEvent event) {
height.clear();
public void onLoad(NexoItemsLoadedEvent event) {
cmdCache.clear();
loadTexture();
plugin.getLogger().info("Nexo items loaded, clearing cache");
}
@@ -119,6 +71,6 @@ public class NexoHook extends Hook implements Listener, HatHook {
@Override
public void onDisable() {
cmdCache.clear();
}
}
@@ -59,7 +59,16 @@ public class OraxenHook extends Hook implements Listener, HatHook {
}
private int getCustomModelData(@NotNull OraxenMeta meta) {
return meta.getCustomModelData();
try {
if (meta.getCustomModelData() == null) {
return 0;
}
return meta.getCustomModelData();
} catch (NoSuchMethodError | NullPointerException e) {
// Compatibility with older Oraxen versions
return 0;
}
}
private String getModelName(OraxenMeta meta) {
@@ -35,6 +35,7 @@ public class TypeWriterListener extends Hook implements Listener {
.map(Bukkit::getPlayer)
.filter(p -> p != null && p != event.getPlayer())
.collect(Collectors.toSet());
plugin.getNametagManager().getPacketDisplayText(event.getPlayer()).ifPresent(display -> display.showToPlayers(viewers));
},
1);
@@ -0,0 +1,142 @@
package org.alexdev.unlimitednametags.hook.creative;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.google.common.collect.Maps;
import net.kyori.adventure.key.Key;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import team.unnamed.creative.ResourcePack;
import team.unnamed.creative.base.Vector3Float;
import team.unnamed.creative.model.ItemOverride;
import team.unnamed.creative.model.ItemTransform;
import team.unnamed.creative.model.Model;
import java.util.Map;
import java.util.Optional;
public interface CreativeHook {
double MULTIPLIER = 1.1;
void loadTexture();
@Nullable
ResourcePack getResourcePack();
@NotNull
Map<Key, Map<Integer, Model>> getCmdCache();
default double getHigh(@NotNull ItemStack helmet) {
if (!helmet.hasItemMeta() || !helmet.getItemMeta().hasCustomModelData()) {
return 0;
}
final Optional<Model> optionalModel = findModel(helmet);
return optionalModel.map(this::getHighFromModel).orElse(0.0);
}
default double getHigh(@NotNull Player player) {
final ItemStack helmet = player.getInventory().getHelmet();
if (helmet == null) {
return 0;
}
return getHigh(helmet);
}
@SuppressWarnings("UnstableApiUsage")
default Optional<Model> findModel(@NotNull ItemStack item) {
final ResourcePack pack = getResourcePack();
if (pack == null) {
return Optional.empty();
}
final Map<Integer, Model> cmdCache = getCmdCache().computeIfAbsent(item.getType().getKey(), k -> Maps.newConcurrentMap());
final ItemMeta itemMeta = item.getItemMeta();
if (itemMeta.hasCustomModelData()) {
final int customModelData = itemMeta.getCustomModelData();
final String asString = Integer.toString(customModelData);
if (cmdCache.containsKey(customModelData)) {
return Optional.of(cmdCache.get(customModelData));
}
var key = new NamespacedKey(item.getType().getKey().namespace(), "item/" + item.getType().getKey().value());
var modelIn = pack.model(key);
if (modelIn != null) {
final Optional<ItemOverride> optionalOverride = findItemOverride(modelIn, customModelData, asString);
if (optionalOverride.isPresent()) {
final Model model = pack.model(optionalOverride.get().model());
if (model != null) {
cmdCache.put(customModelData, model);
return Optional.of(model);
}
}
}
//Fallback
Optional<ItemOverride> optionalOverride = Optional.empty();
for (Model model : pack.models()) {
optionalOverride = findItemOverride(model, customModelData, asString);
if (optionalOverride.isPresent()) {
break;
}
}
return optionalOverride.map(override -> {
final Model model = pack.model(override.model());
cmdCache.put(customModelData, model);
return model;
});
}
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_21_3)) {
return Optional.empty();
}
if (itemMeta.hasEquippable()) {
final NamespacedKey model = itemMeta.getEquippable().getModel();
if (model == null) {
return Optional.empty();
}
return Optional.ofNullable(pack.model(model));
}
return Optional.empty();
}
default Optional<ItemOverride> findItemOverride(@NotNull Model model, int customModelData, @NotNull String asString) {
return model.overrides().stream()
.filter(e -> e.predicate().stream().anyMatch(p ->
p.name().equalsIgnoreCase("custom_model_data") &&
(p.value().equals(customModelData) || p.value().toString().equals(asString))))
.findFirst();
}
default double getHighFromModel(@NotNull Model model) {
double highest = 0;
for (int i = 0; i < model.elements().size(); i++) {
final double to = model.elements().get(i).to().y();
if (to > highest) {
highest = to;
}
}
if (!model.display().containsKey(ItemTransform.Type.HEAD)) {
return -1;
}
final ItemTransform itemTransform = model.display().get(ItemTransform.Type.HEAD);
final Vector3Float scale = itemTransform.scale();
highest *= scale.y();
highest *= MULTIPLIER;
final double translation = itemTransform.translation().y();
return highest + translation;
}
}
@@ -0,0 +1,357 @@
/*
* This file is part of creative, licensed under the MIT license
*
* Copyright (c) 2021-2025 Unnamed Team
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.alexdev.unlimitednametags.hook.creative;
import com.google.gson.JsonElement;
import com.google.gson.stream.JsonReader;
import net.kyori.adventure.key.Key;
import org.intellij.lang.annotations.Subst;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import team.unnamed.creative.ResourcePack;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.metadata.Metadata;
import team.unnamed.creative.metadata.overlays.OverlayEntry;
import team.unnamed.creative.metadata.overlays.OverlaysMeta;
import team.unnamed.creative.metadata.pack.PackMeta;
import team.unnamed.creative.overlay.Overlay;
import team.unnamed.creative.overlay.ResourceContainer;
import team.unnamed.creative.part.ResourcePackPart;
import team.unnamed.creative.serialize.minecraft.*;
import team.unnamed.creative.serialize.minecraft.fs.FileTreeReader;
import team.unnamed.creative.serialize.minecraft.io.BinaryResourceDeserializer;
import team.unnamed.creative.serialize.minecraft.io.JsonResourceDeserializer;
import team.unnamed.creative.serialize.minecraft.io.ResourceDeserializer;
import team.unnamed.creative.serialize.minecraft.metadata.MetadataSerializer;
import team.unnamed.creative.serialize.minecraft.sound.SoundRegistrySerializer;
import team.unnamed.creative.texture.Texture;
import team.unnamed.creative.util.Keys;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Queue;
import static java.util.Objects.requireNonNull;
import static team.unnamed.creative.serialize.minecraft.MinecraftResourcePackStructure.*;
public final class CustomMinecraftResourcePackReaderImpl implements MinecraftResourcePackReader {
public static final CustomMinecraftResourcePackReaderImpl INSTANCE = new CustomMinecraftResourcePackReaderImpl(false);
public static final String OVERLAYS_FOLDER = "overlays";
private final boolean lenient;
private CustomMinecraftResourcePackReaderImpl(
final boolean lenient
) {
this.lenient = lenient;
}
@Override
@SuppressWarnings("PatternValidation")
public @NotNull ResourcePack read(final @NotNull FileTreeReader reader) {
ResourcePack resourcePack = ResourcePack.resourcePack();
// textures that are waiting for metadata, or metadata
// waiting for textures (because we can't know the order
// they come in)
// (null key means it is root resource pack)
Map<@Nullable String, Map<Key, Texture>> incompleteTextures = new LinkedHashMap<>();
// fill in with the default ones first (pack format is unknown at the start)
Map<String, ResourceCategory<?>> categoriesByFolderThisPackFormat = ResourceCategories.buildCategoryMapByFolder(-1);
Map<String, Integer> packFormatsByOverlayDir = new HashMap<>();
int packFormat = -1;
while (reader.hasNext()) {
String path = reader.next();
// tokenize path in sections, e.g.: [ assets, minecraft, textures, ... ]
Queue<String> tokens = tokenize(path);
if (tokens.isEmpty()) {
// this should never happen
throw new IllegalStateException("Token collection is empty!");
}
// single token means the file is on the
// root level (top level files) so it may be:
// - pack.mcmeta
// - pack.png
if (tokens.size() == 1) {
switch (tokens.poll()) {
case PACK_METADATA_FILE: {
// found pack.mcmeta file, deserialize and add
Metadata metadata = MetadataSerializer.INSTANCE.readFromTree(parseJson(reader.stream()));
resourcePack.metadata(metadata);
// get the pack format from the metadata
PackMeta packMeta = metadata.meta(PackMeta.class);
if (packMeta == null) {
// TODO: better warning system
System.err.println("Reading a resource-pack with no pack meta in its pack.mcmeta file! Unknown pack format version :(");
} else {
// update the pack format and categories
packFormat = packMeta.formats().min();
categoriesByFolderThisPackFormat = ResourceCategories.buildCategoryMapByFolder(packFormat);
}
// overlays info
OverlaysMeta overlaysMeta = metadata.meta(OverlaysMeta.class);
if (overlaysMeta != null) for (OverlayEntry entry : overlaysMeta.entries()) {
packFormatsByOverlayDir.put(entry.directory(), entry.formats().min());
}
continue;
}
case PACK_ICON_FILE: {
// found pack.png file, add
resourcePack.icon(reader.content().asWritable());
continue;
}
default: {
// unknown top level file
resourcePack.unknownFile(path, reader.content().asWritable());
continue;
}
}
}
// the container to use, it is initially the default resource-pack,
// but it may change if the file is inside an overlay folder
@Subst("dir")
@Nullable String overlayDir = null;
int localPackFormat = packFormat;
// the file path, relative to the container
String containerPath = path;
ResourceContainer container = resourcePack;
// if there are two or more tokens, it means the
// file is inside a folder, in a Minecraft resource
// pack, the first folder is always "assets"
String folder = tokens.poll();
if (folder.equals(OVERLAYS_FOLDER)) {
// gets the overlay name, set after the
// "overlays" folder, e.g. "overlays/foo",
// or "overlays/bar"
overlayDir = tokens.poll();
if (tokens.isEmpty()) {
// this means that there is a file directly
// inside the "overlays" folder, this is illegal
resourcePack.unknownFile(containerPath, reader.content().asWritable());
continue;
}
Overlay overlay = resourcePack.overlay(overlayDir);
if (overlay == null) {
// first occurrence, register overlay
overlay = Overlay.overlay(overlayDir);
resourcePack.overlay(overlay);
}
container = overlay;
folder = tokens.poll();
containerPath = path.substring((OVERLAYS_FOLDER + '/' + overlayDir + '/').length());
localPackFormat = packFormatsByOverlayDir.getOrDefault(overlayDir, -1);
}
// null check to make ide happy
if (folder == null || !folder.equals(ASSETS_FOLDER) || tokens.isEmpty()) {
// not assets! this is an unknown file
container.unknownFile(containerPath, reader.content().asWritable());
continue;
}
// inside "assets", we should always have a folder
// with any name, which is a namespace, e.g. "minecraft"
String namespace = tokens.poll();
if (!Keys.isValidNamespace(namespace)) {
// invalid namespace found
container.unknownFile(containerPath, reader.content().asWritable());
continue;
}
if (tokens.isEmpty()) {
// found a file directly inside "assets", like
// assets/<file>, it is not allowed
container.unknownFile(containerPath, reader.content().asWritable());
continue;
}
// so we already have "assets/<namespace>/", most files inside
// the namespace folder always have a "category", e.g. textures,
// lang, font, etc. But not always! There is sounds.json file and
// gpu_warnlist.json file
String categoryName = tokens.poll();
if (tokens.isEmpty()) {
// this means "category" is a file
// (remember: last tokens are always files)
if (categoryName.equals(SOUNDS_FILE)) {
// found a sound registry!
container.soundRegistry(SoundRegistrySerializer.INSTANCE.readFromTree(
parseJson(reader.stream()),
namespace
));
continue;
} else {
// TODO: gpu_warnlist.json?
container.unknownFile(containerPath, reader.content().asWritable());
continue;
}
}
// so "category" is actually a category like "textures",
// "lang", "font", etc. next we can compute the relative
// path inside the category
String categoryPath = path(tokens);
if (categoryName.equals(TEXTURES_FOLDER)) {
String keyOfMetadata = withoutExtension(categoryPath, METADATA_EXTENSION);
if (keyOfMetadata != null) {
// found metadata for texture
Key key = Key.key(namespace, keyOfMetadata);
Metadata metadata = MetadataSerializer.INSTANCE.readFromTree(parseJson(reader.stream()));
Map<Key, Texture> incompleteTexturesThisContainer = incompleteTextures.computeIfAbsent(overlayDir, k -> new LinkedHashMap<>());
Texture texture = incompleteTexturesThisContainer.remove(key);
if (texture == null) {
// metadata was found first, put
incompleteTexturesThisContainer.put(key, Texture.texture(key, Writable.EMPTY, metadata));
} else {
// texture was found before the metadata, nice!
container.texture(texture.meta(metadata));
}
} else {
Key key = Key.key(namespace, categoryPath);
Writable data = reader.content().asWritable();
Map<Key, Texture> incompleteTexturesThisContainer = incompleteTextures.computeIfAbsent(overlayDir, k -> new LinkedHashMap<>());
Texture waiting = incompleteTexturesThisContainer.remove(key);
if (waiting == null) {
// found texture before metadata
incompleteTexturesThisContainer.put(key, Texture.texture(key, data));
} else {
// metadata was found first
container.texture(Texture.texture(
key,
data,
waiting.meta()
));
}
}
} else {
// get the resource category, if the local pack format (overlay or root) is the same as the
// root pack format, we can use the previously computed map, otherwise we need to compute it
// (we could save some time by caching the computed map, but, is it worth it?)
ResourceCategory<?> category = (localPackFormat == packFormat
? categoriesByFolderThisPackFormat
: ResourceCategories.buildCategoryMapByFolder(localPackFormat)).get(categoryName);
if (category == null) {
// unknown category
container.unknownFile(containerPath, reader.content().asWritable());
continue;
}
String keyValue = withoutExtension(categoryPath, category.extension(-1));
if (keyValue == null) {
// wrong extension
container.unknownFile(containerPath, reader.content().asWritable());
continue;
}
if (keyValue.startsWith("equipment/")) {
// skip trying to load "namespace:equipment/X.json" as a normal model
container.unknownFile(containerPath, reader.content().asWritable());
continue;
}
Key key = Key.key(namespace, keyValue);
try {
ResourceDeserializer<? extends ResourcePackPart> deserializer = category.deserializer();
ResourcePackPart resource;
if (deserializer instanceof BinaryResourceDeserializer) {
resource = ((BinaryResourceDeserializer<? extends ResourcePackPart>) deserializer)
.deserializeBinary(reader.content().asWritable(), key);
} else if (deserializer instanceof JsonResourceDeserializer) {
resource = ((JsonResourceDeserializer<? extends ResourcePackPart>) deserializer)
.deserializeFromJson(parseJson(reader.stream()), key);
} else {
resource = deserializer.deserialize(reader.stream(), key);
}
resource.addTo(container);
} catch (Throwable e) {
// throw new UncheckedIOException("Failed to deserialize resource at: '" + path + "'", e);
}
}
}
for (Map.Entry<String, Map<Key, Texture>> entry : incompleteTextures.entrySet()) {
@Subst("dir")
@Nullable String overlayDir = entry.getKey();
Map<Key, Texture> incompleteTexturesThisContainer = entry.getValue();
ResourceContainer container;
if (overlayDir == null) {
// root
container = resourcePack;
} else {
// from an overlay
container = resourcePack.overlay(overlayDir);
requireNonNull(container, "container"); // should never happen, but make ide happy
}
for (Texture texture : incompleteTexturesThisContainer.values()) {
if (texture.data() != Writable.EMPTY) {
container.texture(texture);
}
}
}
return resourcePack;
}
private static @Nullable String withoutExtension(String string, String extension) {
if (string.endsWith(extension)) {
return string.substring(0, string.length() - extension.length());
} else {
// string doesn't end with extension
return null;
}
}
private @NotNull JsonElement parseJson(final @NotNull InputStream input) {
try (final JsonReader jsonReader = new JsonReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
jsonReader.setLenient(lenient);
return GsonUtil.parseReader(jsonReader);
} catch (final IOException e) {
throw new UncheckedIOException("Failed to close JSON reader", e);
}
}
}
@@ -0,0 +1,19 @@
package org.alexdev.unlimitednametags.listeners;
import lombok.RequiredArgsConstructor;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@RequiredArgsConstructor
public class OtherListener implements Listener {
private final UnlimitedNameTags plugin;
@EventHandler
private void onPluginEnable(org.bukkit.event.server.PluginEnableEvent event) {
if (event.getPlugin().getName().equals("PlaceholderAPI")) {
plugin.getPlaceholderManager().getPapiManager().checkPapiEnabled();
}
}
}
@@ -0,0 +1,296 @@
package org.alexdev.unlimitednametags.listeners;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.entity.data.EntityData;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.util.Vector3f;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerInput;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerCamera;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetPassengers;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams;
import com.google.common.collect.Maps;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.data.TeamData;
import org.alexdev.unlimitednametags.packet.PacketNameTag;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
public class PacketEventsListener extends PacketListenerAbstract {
private final UnlimitedNameTags plugin;
private final Map<UUID, Map<String, TeamData>> teams;
public PacketEventsListener(UnlimitedNameTags plugin) {
this.plugin = plugin;
this.teams = Maps.newConcurrentMap();
}
public void onEnable() {
PacketEvents.getAPI().getEventManager().registerListener(this);
}
@NotNull
public Map<String, TeamData> getTeams(@NotNull UUID player) {
return teams.computeIfAbsent(player, p -> Maps.newConcurrentMap());
}
public void onPacketSend(@NotNull PacketSendEvent event) {
if (event.getPacketType() == PacketType.Play.Server.TEAMS) {
handleTeams(event);
} else if (event.getPacketType() == PacketType.Play.Server.SET_PASSENGERS) {
handlePassengers(event);
} else if (event.getPacketType() == PacketType.Play.Server.ENTITY_METADATA) {
handleMetaData(event);
} else if (event.getPacketType() == PacketType.Play.Server.CAMERA) {
handleCamera(event);
}
}
private void handleCamera(@NotNull PacketSendEvent event) {
if (!(event.getPlayer() instanceof Player player)) {
return;
}
if (!plugin.getConfigManager().getSettings().isShowCurrentNameTag()) {
return;
}
final WrapperPlayServerCamera camera = new WrapperPlayServerCamera(event);
if (camera.getCameraId() == player.getEntityId()) {
plugin.getNametagManager().getPacketDisplayText(player).ifPresent(PacketNameTag::showForOwner);
} else {
plugin.getNametagManager().getPacketDisplayText(player).ifPresent(PacketNameTag::hideForOwner);
}
}
@Override
public void onPacketReceive(@NotNull PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.ENTITY_ACTION) {
handleUseEntity(event);
} else if (event.getPacketType() == PacketType.Play.Client.PLAYER_INPUT) {
handlePlayerInput(event);
}
}
private void handleUseEntity(PacketReceiveEvent event) {
if (!(event.getPlayer() instanceof Player)) {
return;
}
final WrapperPlayClientEntityAction packet = new WrapperPlayClientEntityAction(event);
final Optional<? extends Player> player = plugin.getPlayerListener().getPlayerFromEntityId(packet.getEntityId());
if (player.isEmpty()) {
return;
}
switch (packet.getAction()) {
case START_SNEAKING -> plugin.getNametagManager().updateSneaking(player.get(), true);
case STOP_SNEAKING -> plugin.getNametagManager().updateSneaking(player.get(), false);
case START_FLYING_WITH_ELYTRA -> plugin.getPlayerListener().logicElytra(player.get());
}
}
private void handlePlayerInput(PacketReceiveEvent event) {
if (!(event.getPlayer() instanceof Player player)) {
return;
}
final WrapperPlayClientPlayerInput packet = new WrapperPlayClientPlayerInput(event);
final Optional<PacketNameTag> optionalPacketDisplayText = plugin.getNametagManager().getPacketDisplayText(player);
if (optionalPacketDisplayText.isEmpty()) {
return;
}
if (packet.isShift() != optionalPacketDisplayText.get().isSneaking()) {
plugin.getNametagManager().updateSneaking(player, packet.isShift());
}
}
private void handlePassengers(@NotNull PacketSendEvent event) {
final WrapperPlayServerSetPassengers packet = new WrapperPlayServerSetPassengers(event);
final Optional<? extends Player> player = plugin.getPlayerListener().getPlayerFromEntityId(packet.getEntityId());
if (player.isEmpty()) {
return;
}
final List<Integer> passengers = collectPassengers(packet.getPassengers());
final Optional<PacketNameTag> optionalPacketDisplayText = plugin.getNametagManager().getPacketDisplayText(player.get());
if (optionalPacketDisplayText.isEmpty()) {
plugin.getPacketManager().setPassengers(player.get(), passengers);
return;
}
if(!passengers.contains(optionalPacketDisplayText.get().getEntityId())) {
passengers.add(optionalPacketDisplayText.get().getEntityId());
passengers.sort(Comparator.naturalOrder());
packet.setPassengers(passengers.stream().mapToInt(i -> i).toArray());
event.markForReEncode(true);
}
plugin.getPacketManager().setPassengers(player.get(), passengers);
}
@NotNull
private List<Integer> collectPassengers(int[] passengers) {
final List<Integer> passengerList = new ArrayList<>(passengers.length);
for (int passenger : passengers) {
passengerList.add(passenger);
}
return passengerList;
}
private boolean preTeamsChecks(@NotNull PacketSendEvent event) {
if (!plugin.getConfigManager().getSettings().isDisableDefaultNameTag()) {
return false;
}
return event.getUser().getClientVersion().isNewerThan(ClientVersion.V_1_19_3);
}
private void handleTeams(@NotNull PacketSendEvent event) {
if (!preTeamsChecks(event)) {
return;
}
final WrapperPlayServerTeams packet = new WrapperPlayServerTeams(event);
if (handleForceDisableDefaultNameTag(event, packet)) {
return;
}
final Map<String, TeamData> teams = getTeams(event.getUser().getUUID());
final String teamName = packet.getTeamName();
switch (packet.getTeamMode()) {
case ADD_ENTITIES -> handleAddEntities(event, packet, teams, teamName);
case REMOVE_ENTITIES -> handleRemoveEntities(packet, teams, teamName);
case CREATE -> handleCreateTeam(event, packet, teams, teamName);
case UPDATE -> handleUpdateTeam(event, packet, teams, teamName);
case REMOVE -> teams.remove(teamName);
}
}
private boolean handleForceDisableDefaultNameTag(@NotNull PacketSendEvent event, @NotNull WrapperPlayServerTeams packet) {
if (plugin.getConfigManager().getSettings().isForceDisableDefaultNameTag()) {
if (packet.getTeamMode() == WrapperPlayServerTeams.TeamMode.CREATE || packet.getTeamMode() == WrapperPlayServerTeams.TeamMode.UPDATE) {
packet.getTeamInfo().ifPresent(t -> t.setTagVisibility(WrapperPlayServerTeams.NameTagVisibility.NEVER));
event.markForReEncode(true);
}
return true;
}
return false;
}
private void handleAddEntities(@NotNull PacketSendEvent event, @NotNull WrapperPlayServerTeams packet,
@NotNull Map<String, TeamData> teams, @NotNull String teamName) {
final Optional<TeamData> teamDataOpt = Optional.ofNullable(teams.get(teamName));
if (teamDataOpt.isEmpty()) {
return;
}
final TeamData teamData = teamDataOpt.get();
teamData.getMembers().addAll(packet.getPlayers());
if (!teamData.isChangedVisibility() && packet.getPlayers().stream().anyMatch(this::existsPlayer)) {
teamData.setChangedVisibility(true);
final WrapperPlayServerTeams.ScoreBoardTeamInfo teamInfo = teamData.getTeamInfo();
teamInfo.setTagVisibility(WrapperPlayServerTeams.NameTagVisibility.NEVER);
if (teamData.getTeamInfo() != null) {
event.getUser().sendPacket(new WrapperPlayServerTeams(teamName, WrapperPlayServerTeams.TeamMode.UPDATE, teamData.getTeamInfo(), teamData.getMembers()));
}
}
}
private void handleRemoveEntities(@NotNull WrapperPlayServerTeams packet, @NotNull Map<String, TeamData> teams, @NotNull String teamName) {
final Optional<TeamData> teamDataOpt = Optional.ofNullable(teams.get(teamName));
teamDataOpt.ifPresent(teamData -> teamData.getMembers().removeAll(packet.getPlayers()));
}
private void handleCreateTeam(@NotNull PacketSendEvent event, @NotNull WrapperPlayServerTeams packet,
@NotNull Map<String, TeamData> teams, @NotNull String teamName) {
if (teams.containsKey(teamName)) {
return;
}
packet.getTeamInfo().ifPresent(teamInfo -> {
final TeamData teamData = new TeamData(teamName, teamInfo, Set.copyOf(packet.getPlayers()));
teams.put(teamName, teamData);
if (teamData.getMembers().stream().anyMatch(this::existsPlayer)) {
teamData.setChangedVisibility(true);
// Ensure teamInfo is not null before setting visibility
if (teamData.getTeamInfo() != null) {
teamData.getTeamInfo().setTagVisibility(WrapperPlayServerTeams.NameTagVisibility.NEVER);
event.markForReEncode(true);
}
}
});
}
private void handleUpdateTeam(@NotNull PacketSendEvent event, @NotNull WrapperPlayServerTeams packet, @NotNull Map<String, TeamData> teams, @NotNull String teamName) {
final Optional<TeamData> teamDataOpt = Optional.ofNullable(teams.get(teamName));
if (teamDataOpt.isEmpty()) {
return;
}
final TeamData teamData = teamDataOpt.get();
packet.getTeamInfo().ifPresent(teamInfoFromPacket -> {
if (teamData.isChangedVisibility() && teamInfoFromPacket.getTagVisibility() != WrapperPlayServerTeams.NameTagVisibility.NEVER) {
teamInfoFromPacket.setTagVisibility(WrapperPlayServerTeams.NameTagVisibility.NEVER);
event.markForReEncode(true);
}
teamData.setTeamInfo(teamInfoFromPacket);
});
}
public void removePlayerData(@NotNull Player player) {
teams.remove(player.getUniqueId());
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void handleMetaData(@NotNull PacketSendEvent event) {
if (!(event.getPlayer() instanceof Player)) {
return;
}
int protocol = event.getUser().getClientVersion().getProtocolVersion();
//handle metadata for : bedrock players && client with version 1.20.1 or lower
if (protocol >= 764) {
return;
}
final WrapperPlayServerEntityMetadata packet = new WrapperPlayServerEntityMetadata(event);
final Optional<PacketNameTag> textDisplay = plugin.getNametagManager().getPacketDisplayText(packet.getEntityId());
if (textDisplay.isEmpty()) {
return;
}
for (final EntityData eData : packet.getEntityMetadata()) {
if (eData.getIndex() == 11) {
final Vector3f old = (Vector3f) eData.getValue();
final Vector3f newV = new Vector3f(old.getX(), old.getY() + 0.45f, old.getZ());
eData.setValue(newV);
event.markForReEncode(true);
return;
}
}
}
public boolean existsPlayer(@NotNull String name) {
// return plugin.getPlayerListener().getPlayerNameId().containsKey(name);
return Bukkit.getPlayer(name) != null;
}
public void onDisable() {
PacketEvents.getAPI().getEventManager().unregisterListener(this);
}
}
@@ -1,4 +1,4 @@
package org.alexdev.unlimitednametags.events;
package org.alexdev.unlimitednametags.listeners;
import io.papermc.paper.event.player.PlayerTrackEntityEvent;
import io.papermc.paper.event.player.PlayerUntrackEntityEvent;
@@ -1,8 +1,6 @@
package org.alexdev.unlimitednametags.events;
package org.alexdev.unlimitednametags.listeners;
import com.github.Anon8281.universalScheduler.foliaScheduler.FoliaScheduler;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.Getter;
@@ -18,11 +16,9 @@ import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.*;
import org.bukkit.potion.PotionEffectType;
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.*;
public class PlayerListener implements PackSendHandler {
@@ -30,17 +26,26 @@ public class PlayerListener implements PackSendHandler {
private final UnlimitedNameTags plugin;
private final Set<UUID> diedPlayers;
private final Map<Integer, UUID> playerEntityId;
@Getter
private final Map<String, UUID> playerNameId;
@Getter
private final Map<UUID, Player> onlinePlayers;
public PlayerListener(UnlimitedNameTags plugin) {
this.plugin = plugin;
this.diedPlayers = Sets.newConcurrentHashSet();
this.playerEntityId = Maps.newConcurrentMap();
this.playerNameId = Maps.newConcurrentMap();
this.onlinePlayers = Maps.newConcurrentMap();
this.loadFoliaRespawnTask();
this.loadEntityIds();
}
private void loadEntityIds() {
Bukkit.getOnlinePlayers().forEach(player -> playerEntityId.put(player.getEntityId(), player.getUniqueId()));
Bukkit.getOnlinePlayers().forEach(player -> {
playerEntityId.put(player.getEntityId(), player.getUniqueId());
onlinePlayers.put(player.getUniqueId(), player);
});
}
private void loadFoliaRespawnTask() {
@@ -48,13 +53,14 @@ public class PlayerListener implements PackSendHandler {
return;
}
plugin.getTaskScheduler().runTaskTimerAsynchronously(() -> diedPlayers.forEach(player -> {
final Player p = plugin.getServer().getPlayer(player);
if (p == null || p.isDead()) {
plugin.getTaskScheduler().runTaskTimerAsynchronously(() -> diedPlayers.forEach(died -> {
final Player player = plugin.getServer().getPlayer(died);
if (player == null || player.isDead()) {
return;
}
plugin.getNametagManager().addPlayer(p);
diedPlayers.remove(player);
plugin.getNametagManager().showToTrackedPlayers(player);
diedPlayers.remove(died);
}), 1, 1);
}
@@ -63,45 +69,70 @@ public class PlayerListener implements PackSendHandler {
if (player == null) {
return Optional.empty();
}
return Optional.ofNullable(plugin.getServer().getPlayer(player));
}
@EventHandler(priority = EventPriority.LOWEST)
public void onJoin(@NotNull PlayerJoinEvent event) {
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> plugin.getNametagManager().addPlayer(event.getPlayer()), 2);
onlinePlayers.put(event.getPlayer().getUniqueId(), event.getPlayer());
plugin.getTaskScheduler()
.runTaskLaterAsynchronously(() -> plugin.getNametagManager().addPlayer(event.getPlayer(), true), 6);
playerEntityId.put(event.getPlayer().getEntityId(), event.getPlayer().getUniqueId());
}
@EventHandler(priority = EventPriority.LOWEST)
public void onQuit(@NotNull PlayerQuitEvent event) {
plugin.getPacketEventsListener().removePlayerData(event.getPlayer());
playerNameId.remove(event.getPlayer().getName());
diedPlayers.remove(event.getPlayer().getUniqueId());
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> plugin.getNametagManager().removePlayer(event.getPlayer(), true), 1);
playerEntityId.remove(event.getPlayer().getEntityId());
plugin.getTaskScheduler().runTaskAsynchronously(() -> {
plugin.getNametagManager().removePlayer(event.getPlayer());
plugin.getNametagManager().clearCache(event.getPlayer().getUniqueId());
plugin.getPlaceholderManager().removePlayer(event.getPlayer());
diedPlayers.remove(event.getPlayer().getUniqueId());
onlinePlayers.remove(event.getPlayer().getUniqueId());
playerEntityId.remove(event.getPlayer().getEntityId());
});
}
@EventHandler
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPotion(@NotNull EntityPotionEffectEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
if (player.getGameMode() == GameMode.SPECTATOR) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger().info("Player is in spectator mode, skipping potion event");
}
return;
}
if (event.getAction() == EntityPotionEffectEvent.Action.ADDED) {
if (event.getNewEffect() == null || event.getNewEffect().getType() != PotionEffectType.INVISIBILITY) {
if (event.getNewEffect() == null
|| (!event.getNewEffect().getType().equals(PotionEffectType.INVISIBILITY))) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger().info("Potion effect is not invisibility, skipping potion event");
}
return;
}
plugin.getNametagManager().removeAllViewers(player);
} else if (event.getAction() == EntityPotionEffectEvent.Action.REMOVED || event.getAction() == EntityPotionEffectEvent.Action.CLEARED) {
if (event.getOldEffect() == null || event.getOldEffect().getType() != PotionEffectType.INVISIBILITY) {
plugin.getNametagManager().blockPlayer(player);
} else if (event.getAction() == EntityPotionEffectEvent.Action.REMOVED
|| event.getAction() == EntityPotionEffectEvent.Action.CLEARED) {
if (event.getOldEffect() == null || !event.getOldEffect().getType().equals(PotionEffectType.INVISIBILITY)) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger().info("Potion effect is not invisibility, skipping potion event: "
+ (event.getOldEffect() != null ? event.getOldEffect().getType() : null));
}
return;
}
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> {
plugin.getNametagManager().unblockPlayer(player);
plugin.getNametagManager().showToTrackedPlayers(player, plugin.getTrackerManager().getTrackedPlayers().get(player.getUniqueId()));
}, 1);
plugin.getNametagManager().showToTrackedPlayers(player);
}, 3);
}
}
@@ -109,27 +140,30 @@ public class PlayerListener implements PackSendHandler {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onGameModeChange(@NotNull PlayerGameModeChangeEvent e) {
if (e.getPlayer().getGameMode() == GameMode.SPECTATOR) {
plugin.getNametagManager().showToTrackedPlayers(e.getPlayer(), plugin.getTrackerManager().getTrackedPlayers().get(e.getPlayer().getUniqueId()));
plugin.getNametagManager().unblockPlayer(e.getPlayer());
plugin.getNametagManager().showToTrackedPlayers(e.getPlayer());
} else if (e.getNewGameMode() == GameMode.SPECTATOR) {
plugin.getNametagManager().removeAllViewers(e.getPlayer());
}
}
@EventHandler
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerDeath(@NotNull PlayerDeathEvent event) {
diedPlayers.add(event.getEntity().getUniqueId());
plugin.getNametagManager().removeAllViewers(event.getEntity());
}
@EventHandler
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerRespawn(@NotNull PlayerRespawnEvent event) {
diedPlayers.remove(event.getPlayer().getUniqueId());
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> {
plugin.getNametagManager().getPacketDisplayText(event.getPlayer()).ifPresent(d -> d.setVisible(true));
}, 1);
if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) {
return;
}
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> plugin.getNametagManager().showToTrackedPlayers(event.getPlayer()), 5);
}
@EventHandler
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerVanishes(@NotNull PlayerShowEntityEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
@@ -144,7 +178,7 @@ public class PlayerListener implements PackSendHandler {
});
}
@EventHandler
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerUnvanishes(@NotNull PlayerHideEntityEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
@@ -155,14 +189,12 @@ public class PlayerListener implements PackSendHandler {
});
}
@EventHandler
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onTeleport(@NotNull PlayerTeleportEvent event) {
//1.20.1+ bug
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_21_1) &&
event.getFrom().getWorld() != event.getTo().getWorld()
) {
if (event.getFrom().getWorld() != event.getTo().getWorld()) {
plugin.getTrackerManager().forceUntrack(event.getPlayer());
}
if (!plugin.getConfigManager().getSettings().isShowCurrentNameTag()) {
return;
}
@@ -175,7 +207,8 @@ public class PlayerListener implements PackSendHandler {
return;
}
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> plugin.getNametagManager().showToOwner(event.getPlayer()), 5);
plugin.getTaskScheduler()
.runTaskLaterAsynchronously(() -> plugin.getNametagManager().showToOwner(event.getPlayer()), 5);
}
public void logicElytra(Player player) {
@@ -189,5 +222,17 @@ public class PlayerListener implements PackSendHandler {
plugin.getTaskScheduler().runTaskLaterAsynchronously(packetDisplayText::showForOwner, 5);
});
}
@Nullable
public Player getPlayer(@NotNull UUID uuid) {
return onlinePlayers.get(uuid);
}
public void close() {
diedPlayers.clear();
playerEntityId.clear();
playerNameId.clear();
onlinePlayers.clear();
}
}
@@ -1,4 +1,4 @@
package org.alexdev.unlimitednametags.events;
package org.alexdev.unlimitednametags.listeners;
import com.google.common.collect.Sets;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
@@ -0,0 +1,188 @@
package org.alexdev.unlimitednametags.listeners;
import lombok.Getter;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.data.ConcurrentSetMultimap;
import org.bukkit.Bukkit;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
public class TrackerManager {
private final UnlimitedNameTags plugin;
/**
* Map associating a player (key) with the players they are tracking (value).
* A -> [B, C] means A sees B and C.
*/
@Getter
private final ConcurrentSetMultimap<UUID, UUID> trackedPlayers;
/**
* Reverse map associating a player (key) with those who are tracking them
* (value).
* B -> [A] means B is seen by A.
* Used to optimize reverse lookups.
*/
private final ConcurrentSetMultimap<UUID, UUID> trackedBy;
public TrackerManager(UnlimitedNameTags plugin) {
this.plugin = plugin;
this.trackedPlayers = new ConcurrentSetMultimap<>();
this.trackedBy = new ConcurrentSetMultimap<>();
loadTracker();
}
private void loadTracker() {
Bukkit.getOnlinePlayers().forEach(player -> {
// Unsafe call, but run only on startup
List<Entity> nearbyEntities = player.getNearbyEntities(
Bukkit.getViewDistance() * 16,
256,
Bukkit.getViewDistance() * 16);
for (Entity entity : nearbyEntities) {
if (entity instanceof Player target) {
trackedPlayers.put(player.getUniqueId(), target.getUniqueId());
trackedBy.put(target.getUniqueId(), player.getUniqueId());
}
}
});
}
public void onDisable() {
trackedPlayers.clear();
trackedBy.clear();
}
/**
* Handles adding a target to a player's tracking list.
* Updates both direct and reverse maps asynchronously.
*
* @param player The observer player.
* @param target The target player being observed.
*/
public void handleAdd(@NotNull Player player, @NotNull Player target) {
if (player.hasMetadata("NPC") || target.hasMetadata("NPC"))
return;
if (target.hasPotionEffect(org.bukkit.potion.PotionEffectType.INVISIBILITY))
return;
if (trackedPlayers.containsEntry(player.getUniqueId(), target.getUniqueId())) {
return;
}
trackedPlayers.put(player.getUniqueId(), target.getUniqueId());
trackedBy.put(target.getUniqueId(), player.getUniqueId());
plugin.getNametagManager().updateDisplay(player, target);
}
/**
* Removes tracking between two players.
*
* @param player The observer player.
* @param target The target player being observed.
*/
public void handleRemove(@NotNull Player player, @NotNull Player target) {
plugin.getTaskScheduler().runTaskAsynchronously(() -> removePlayerInternal(player, target));
}
private void removePlayerInternal(@NotNull Player player, @NotNull Player target) {
trackedPlayers.remove(player.getUniqueId(), target.getUniqueId());
trackedBy.remove(target.getUniqueId(), player.getUniqueId());
plugin.getNametagManager().removeDisplay(player, target);
}
/**
* Handles a player quitting the server.
* Efficiently cleans up all references in both maps.
*
* @param player The player who is quitting.
*/
public void handleQuit(@NotNull Player player) {
plugin.getTaskScheduler().runTaskAsynchronously(() -> {
UUID quittingUuid = player.getUniqueId();
// 1. Remove the player from the lists of those who were watching them.
// trackedBy.removeAll returns who was watching the quitting player.
Set<UUID> watchers = trackedBy.removeAll(quittingUuid);
if (watchers != null) {
for (UUID watcherUuid : watchers) {
trackedPlayers.remove(watcherUuid, quittingUuid);
}
}
// 2. Remove the lists of players the quitting player was watching.
// trackedPlayers.removeAll returns who the quitting player was watching.
Set<UUID> targets = trackedPlayers.removeAll(quittingUuid);
if (targets != null) {
for (UUID targetUuid : targets) {
trackedBy.remove(targetUuid, quittingUuid);
}
}
});
}
/**
* Forces removal of all targets tracked by a specific player.
* Useful during world changes or distant teleports.
*
* @param player The player to reset.
*/
public void forceUntrack(@NotNull Player player) {
Set<UUID> tracked = trackedPlayers.get(player.getUniqueId());
if (tracked == null || tracked.isEmpty())
return;
// Copy to avoid ConcurrentModification during iteration if necessary,
// although removePlayerInternal handles concurrency.
Set<UUID> targetsSnapshot = new HashSet<>(tracked);
Map<UUID, Player> onlinePlayers = plugin.getPlayerListener().getOnlinePlayers();
for (UUID targetUuid : targetsSnapshot) {
Player target = onlinePlayers.get(targetUuid);
if (target != null) {
removePlayerInternal(player, target);
}
}
}
/**
* Retrieves the list of players currently tracking the specified target.
* Uses the reverse map for O(1) performance.
*
* @param target The target player.
* @return List of players seeing the target.
*/
@NotNull
public List<Player> getWhoTracks(@NotNull Player target) {
final List<Player> trackers = new ArrayList<>();
final Map<UUID, Player> onlinePlayers = plugin.getPlayerListener().getOnlinePlayers();
Set<UUID> trackerUuids = trackedBy.get(target.getUniqueId());
if (trackerUuids != null) {
for (UUID uuid : trackerUuids) {
Player p = onlinePlayers.get(uuid);
if (p != null) {
trackers.add(p);
}
}
}
return trackers;
}
/**
* Retrieves the UUIDs of players that the specified tracker is observing.
*
* @param uuid UUID of the tracker.
* @return Set of UUIDs of the targets.
*/
public Set<UUID> getTrackedPlayers(@NotNull UUID uuid) {
return trackedPlayers.get(uuid);
}
}
@@ -22,6 +22,7 @@ import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.zip.GZIPOutputStream;
@SuppressWarnings("all")
public class Metrics {
private final Plugin plugin;
@@ -44,12 +44,13 @@ public class ConditionalManager {
}
public boolean evaluateExpression(@NotNull Settings.ConditionalModifier modifier, @NotNull Player player) {
final String entireExpression = plugin.getPlaceholderManager().getPapiManager().isPAPIEnabled() ?
final String entireExpression = plugin.getPlaceholderManager().getPapiManager().isPapiEnabled() ?
PlaceholderAPI.setPlaceholders(player, modifier.getExpression()) :
modifier.getExpression();
if (cachedExpressions.containsKey(entireExpression)) {
return (boolean) cachedExpressions.get(entireExpression);
final Boolean cached = cachedExpressions.getOrDefault(entireExpression, null) instanceof Boolean b ? b : null;
if (cached != null) {
return cached;
}
JexlEngine jexlEngine = null;
@@ -14,11 +14,10 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.config.Settings;
import org.alexdev.unlimitednametags.hook.HMCCosmeticsHook;
import org.alexdev.unlimitednametags.hook.ViaVersionHook;
import org.alexdev.unlimitednametags.packet.PacketNameTag;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.*;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.command.CommandSender;
@@ -28,7 +27,6 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@@ -40,10 +38,13 @@ public class NameTagManager {
private final Map<Integer, PacketNameTag> entityIdToDisplay;
private final Set<UUID> creating;
private final Set<UUID> blocked;
private final Set<UUID> hiddenOtherNametags;
private final Set<UUID> hideNametags;
private final Map<UUID, Settings.NameTag> nameTagOverrides;
private final Map<UUID, Boolean> shiftSystemBlocked;
private final List<MyScheduledTask> tasks;
@Setter
private boolean debug = false;
private final Attribute scaleAttribute;
public NameTagManager(@NotNull UnlimitedNameTags plugin) {
this.plugin = plugin;
@@ -52,13 +53,16 @@ public class NameTagManager {
this.tasks = Lists.newCopyOnWriteArrayList();
this.creating = Sets.newConcurrentHashSet();
this.blocked = Sets.newConcurrentHashSet();
this.hiddenOtherNametags = Sets.newConcurrentHashSet();
this.hideNametags = Sets.newConcurrentHashSet();
this.nameTagOverrides = Maps.newConcurrentMap();
this.shiftSystemBlocked = Maps.newConcurrentMap();
this.loadAll();
this.scaleAttribute = loadScaleAttribute();
}
private void loadAll() {
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> {
Bukkit.getOnlinePlayers().forEach(this::addPlayer);
plugin.getPlayerListener().getOnlinePlayers().values().forEach(p -> addPlayer(p, true));
this.startTask();
}, 5);
}
@@ -66,55 +70,55 @@ public class NameTagManager {
private void startTask() {
tasks.forEach(MyScheduledTask::cancel);
final MyScheduledTask refresh = plugin.getTaskScheduler().runTaskTimerAsynchronously(
() -> Bukkit.getOnlinePlayers().forEach(p -> refresh(p, false)),
() -> {
if (plugin.isPaper() && plugin.getServer().isStopping()) {
return;
}
nameTags.values().forEach(tag -> refresh(tag.getOwner(), false));
},
10, plugin.getConfigManager().getSettings().getTaskInterval());
// Refresh passengers
final MyScheduledTask passengers = plugin.getTaskScheduler().runTaskTimerAsynchronously(() ->
Bukkit.getOnlinePlayers().forEach(player ->
getPacketDisplayText(player)
.ifPresent(PacketNameTag::sendPassengerPacketToViewers))
, 20, 20 * 5L);
final MyScheduledTask passengers = plugin.getTaskScheduler().runTaskTimerAsynchronously(() -> nameTags.values()
.stream()
.map(PacketNameTag::getOwner)
.filter(p -> plugin.getHook(HMCCosmeticsHook.class).map(h -> !h.hasBackpack(p)).orElse(true))
.forEach(player -> getPacketDisplayText(player)
.ifPresent(PacketNameTag::sendPassengerPacketToViewers)),
20, 20 * 5L);
// Scale task
if (isScalePresent()) {
final MyScheduledTask scale = plugin.getTaskScheduler().runTaskTimerAsynchronously(() ->
Bukkit.getOnlinePlayers().forEach(player ->
getPacketDisplayText(player)
.filter(PacketNameTag::checkScale)
.ifPresent(PacketNameTag::refresh))
, 20, 10);
final MyScheduledTask scale = plugin.getTaskScheduler()
.runTaskTimerAsynchronously(() -> nameTags.values().forEach(tag -> {
if (tag.checkScale()) {
tag.refresh();
}
}), 20, 10);
tasks.add(scale);
}
if (plugin.getConfigManager().getSettings().isShowWhileLooking()) {
final boolean current = plugin.getConfigManager().getSettings().isShowCurrentNameTag();
final MyScheduledTask point = plugin.getTaskScheduler().runTaskTimerAsynchronously(() ->
Bukkit.getOnlinePlayers().forEach(player1 -> {
final Optional<PacketNameTag> display = getPacketDisplayText(player1);
if (display.isEmpty()) {
return;
}
Bukkit.getOnlinePlayers().forEach(player2 -> {
if (plugin.getHook(ViaVersionHook.class).map(h -> h.hasNotTextDisplays(player2)).orElse(false)) {
return;
}
final MyScheduledTask point = plugin.getTaskScheduler().runTaskTimerAsynchronously(() -> {
nameTags.values().forEach(tag -> {
final Player targetOwner = tag.getOwner();
final List<Player> viewers = plugin.getTrackerManager().getWhoTracks(targetOwner);
if (player1.getWorld() != player2.getWorld()) {
return;
}
for (Player viewer : viewers) {
if (plugin.getHook(ViaVersionHook.class).map(h -> h.hasNotTextDisplays(viewer)).orElse(false)) {
continue;
}
final boolean isPointing = isPlayerPointingAt(player2, player1) || (current && player1 == player2);
if (display.get().canPlayerSee(player2) && !isPointing) {
display.get().hideFromPlayer(player2);
} else if (!display.get().canPlayerSee(player2) && isPointing) {
display.get().showToPlayer(player2);
}
final boolean isPointing = isPlayerPointingAt(viewer, targetOwner);
});
})
, 5, 5);
if (tag.canPlayerSee(viewer) && !isPointing) {
tag.hideFromPlayer(viewer);
} else if (!tag.canPlayerSee(viewer) && isPointing) {
tag.showToPlayer(viewer);
}
}
});
}, 5, 5);
tasks.add(point);
}
@@ -122,6 +126,17 @@ public class NameTagManager {
tasks.add(passengers);
}
private Attribute loadScaleAttribute() {
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_20_5)) {
return null;
}
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThan(ServerVersion.V_1_21_1)) {
return Attribute.SCALE;
} else {
return Registry.ATTRIBUTE.get(NamespacedKey.minecraft("generic.scale"));
}
}
public boolean isPlayerPointingAt(Player player1, Player player2) {
if (player1.getWorld() != player2.getWorld()) {
return false;
@@ -148,11 +163,11 @@ public class NameTagManager {
return 1;
}
if(PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_20_5)) {
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_20_5)) {
return 1;
}
final AttributeInstance attribute = player.getAttribute(Attribute.GENERIC_SCALE);
final AttributeInstance attribute = player.getAttribute(scaleAttribute);
if (attribute == null) {
return 1;
@@ -163,42 +178,110 @@ public class NameTagManager {
public void blockPlayer(@NotNull Player player) {
blocked.add(player.getUniqueId());
if (debug) {
plugin.getLogger().info("Blocked " + player.getName());
}
}
public void unblockPlayer(@NotNull Player player) {
blocked.remove(player.getUniqueId());
if (debug) {
plugin.getLogger().info("Unblocked " + player.getName());
}
}
public void addPlayer(@NotNull Player player) {
public void clearCache(@NotNull UUID uuid) {
blocked.remove(uuid);
creating.remove(uuid);
hideNametags.remove(uuid);
nameTagOverrides.remove(uuid);
shiftSystemBlocked.remove(uuid);
}
public boolean hasNametagOverride(@NotNull Player player) {
return nameTagOverrides.containsKey(player.getUniqueId());
}
@NotNull
public Optional<Settings.NameTag> getNametagOverride(@NotNull Player player) {
return Optional.ofNullable(nameTagOverrides.get(player.getUniqueId()));
}
@NotNull
public Settings.NameTag getEffectiveNametag(@NotNull Player player) {
return nameTagOverrides.getOrDefault(player.getUniqueId(),
plugin.getConfigManager().getSettings().getNametag(player));
}
@NotNull
public Settings.NameTag getConfigNametag(@NotNull Player player) {
return plugin.getConfigManager().getSettings().getNametag(player);
}
private boolean preAddChecks(@NotNull Player player, boolean canBlock) {
if (nameTags.containsKey(player.getUniqueId())) {
if (debug) {
plugin.getLogger().info("Player " + player.getName() + " already has a nametag");
}
return;
return false;
}
if (creating.contains(player.getUniqueId())) {
if (debug) {
plugin.getLogger().info("Player " + player.getName() + " is already creating a nametag");
}
return;
return false;
}
if (blocked.contains(player.getUniqueId())) {
if (debug) {
plugin.getLogger().info("Player " + player.getName() + " is blocked");
}
return;
return false;
} else {
if (debug) {
plugin.getLogger().info("Player " + player.getName() + " is not blocked");
}
}
if (PacketEvents.getAPI().getPlayerManager().getUser(player) == null) {
if (debug) {
plugin.getLogger().info("Player " + player.getName() + " is not loaded");
}
return false;
}
if (player.hasPotionEffect(PotionEffectType.INVISIBILITY)) {
if (debug) {
plugin.getLogger().info("Player " + player.getName() + " has invisibility potion effect, blocking");
}
blockPlayer(player);
if (canBlock) {
blockPlayer(player);
}
return false;
}
if (player.getGameMode() == GameMode.SPECTATOR) {
if (debug) {
plugin.getLogger().info("Player " + player.getName() + " is in spectator mode, skipping");
}
if (canBlock) {
blockPlayer(player);
}
return false;
}
return true;
}
public void addPlayer(@NotNull Player player, boolean canBlock) {
if (!preAddChecks(player, canBlock)) {
return;
}
final Settings.NameTag nametag = plugin.getConfigManager().getSettings().getNametag(player);
creating.add(player.getUniqueId());
final Settings.NameTag nametag = getEffectiveNametag(player);
final PacketNameTag display = new PacketNameTag(plugin, player, nametag);
display.text(player, Component.empty());
display.spawn(player);
@@ -210,23 +293,25 @@ public class NameTagManager {
handleVanish(player, display);
nameTags.put(player.getUniqueId(), display);
if (debug) {
plugin.getLogger().info("Added nametag for " + player.getName());
}
entityIdToDisplay.put(display.getEntityId(), display);
creating.add(player.getUniqueId());
plugin.getPlaceholderManager().applyPlaceholders(player, nametag.linesGroups(), List.of(player))
.thenAccept(lines -> loadDisplay(player, lines.get(player), nametag, display))
.exceptionally(throwable -> {
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Failed to create nametag for " + player.getName(), throwable);
plugin.getLogger().log(java.util.logging.Level.SEVERE,
"Failed to create nametag for " + player.getName(), throwable);
creating.remove(player.getUniqueId());
return null;
});
}
public void refresh(@NotNull Player player, boolean force) {
final Settings.NameTag nametag = plugin.getConfigManager().getSettings().getNametag(player);
final Settings.NameTag nametag = getEffectiveNametag(player);
if(PacketEvents.getAPI().getPlayerManager().getUser(player) == null) {
if (PacketEvents.getAPI().getPlayerManager().getUser(player) == null) {
return;
}
@@ -246,6 +331,14 @@ public class NameTagManager {
display.hideFromPlayer(player);
}
if (force) {
if (show) {
display.showToPlayer(display.getOwner());
} else {
display.hideFromPlayer(display.getOwner());
}
}
final List<Player> relationalPlayers = display.getViewers().stream()
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
@@ -254,13 +347,14 @@ public class NameTagManager {
plugin.getPlaceholderManager().applyPlaceholders(player, nametag.linesGroups(), relationalPlayers)
.thenAccept(lines -> editDisplay(player, lines, nametag, force))
.exceptionally(throwable -> {
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Failed to edit nametag for " + player.getName(), throwable);
plugin.getLogger().log(java.util.logging.Level.SEVERE,
"Failed to edit nametag for " + player.getName(), throwable);
return null;
});
}
private void editDisplay(@NotNull Player player, Map<Player, Component> components,
@NotNull Settings.NameTag nameTag, boolean force) {
@NotNull Settings.NameTag nameTag, boolean force) {
getPacketDisplayText(player).ifPresent(packetNameTag -> {
if (!packetNameTag.getNameTag().equals(nameTag)) {
packetNameTag.setNameTag(nameTag);
@@ -268,35 +362,37 @@ public class NameTagManager {
if (force && isScalePresent()) {
packetNameTag.checkScale();
}
final boolean shadowed = nameTag.background().shadowed();
final boolean seeThrough = nameTag.background().seeThrough() && !packetNameTag.isSneaking();
final int backgroundColor = nameTag.background().getColor().asARGB();
components.forEach((p, c) -> {
final AtomicBoolean update = new AtomicBoolean(false);
update.set(packetNameTag.text(p, c) || force);
final boolean[] updateRef = { packetNameTag.text(p, c) || force };
final User user = PacketEvents.getAPI().getPlayerManager().getUser(p);
if (user == null) {
return;
}
packetNameTag.modify(user, m -> {
if(force) {
m.setShadow(nameTag.background().shadowed());
m.setSeeThrough(nameTag.background().seeThrough());
m.setBackgroundColor(nameTag.background().getColor().asARGB());
update.set(true);
if (force) {
m.setShadow(shadowed);
m.setSeeThrough(seeThrough);
m.setBackgroundColor(backgroundColor);
} else {
if(m.isShadow() != nameTag.background().shadowed()) {
m.setShadow(nameTag.background().shadowed());
update.set(true);
if (m.isShadow() != shadowed) {
m.setShadow(shadowed);
updateRef[0] = true;
}
if(m.isSeeThrough() != nameTag.background().seeThrough()) {
m.setSeeThrough(nameTag.background().seeThrough());
update.set(true);
if (m.isSeeThrough() != seeThrough) {
m.setSeeThrough(seeThrough);
updateRef[0] = true;
}
}
});
if (update.get()) {
if (updateRef[0]) {
packetNameTag.refreshForPlayer(p);
}
});
@@ -304,11 +400,11 @@ public class NameTagManager {
}
private void loadDisplay(@NotNull Player player, @NotNull Component component,
@NotNull Settings.NameTag nameTag,
@NotNull PacketNameTag display) {
@NotNull Settings.NameTag nameTag,
@NotNull PacketNameTag display) {
try {
final Location location = player.getLocation().clone();
//add 1.80 to make a perfect tp animation
// add 1.80 to make a perfect tp animation
location.setY(location.getY() + 1.80);
creating.remove(player.getUniqueId());
@@ -316,11 +412,10 @@ public class NameTagManager {
display.text(player, component);
display.setBillboard(plugin.getConfigManager().getSettings().getDefaultBillboard());
display.setShadowed(nameTag.background().shadowed());
display.setSeeThrough(nameTag.background().seeThrough());
//background color, if disabled, set to transparent
display.setSeeThrough(nameTag.background().seeThrough() && !display.isSneaking());
// background color, if disabled, set to transparent
display.setBackgroundColor(nameTag.background().getColor());
// display.setTransformation(new Vector3f(0, plugin.getConfigManager().getSettings().getYOffset(), 0));
display.resetOffset(plugin.getConfigManager().getSettings().getYOffset());
display.setViewRange(plugin.getConfigManager().getSettings().getViewDistance());
@@ -332,15 +427,17 @@ public class NameTagManager {
handleVanish(player, display);
} catch (Throwable e) {
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Failed to create nametag for " + player.getName(), e);
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Failed to create nametag for " + player.getName(),
e);
}
}
private void handleVanish(@NotNull Player player, @NotNull PacketNameTag display) {
final boolean isVanished = plugin.getVanishManager().isVanished(player);
//if player is vanished, hide display for all players except for who can see the player
Bukkit.getOnlinePlayers().stream()
// if player is vanished, hide display for all players except for who can see
// the player
plugin.getPlayerListener().getOnlinePlayers().values().stream()
.filter(p -> p != player)
.filter(p -> p.getLocation().getWorld() == player.getLocation().getWorld())
.filter(p -> !isVanished || plugin.getVanishManager().canSee(p, player))
@@ -349,21 +446,15 @@ public class NameTagManager {
.forEach(display::showToPlayer);
}
public void removePlayer(@NotNull Player player, boolean quit) {
public void removePlayer(@NotNull Player player) {
final PacketNameTag packetNameTag = nameTags.remove(player.getUniqueId());
if (packetNameTag != null) {
packetNameTag.remove();
entityIdToDisplay.remove(packetNameTag.getEntityId());
}
entityIdToDisplay.remove(player.getEntityId());
nameTags.forEach((uuid, display) -> {
if (quit) {
display.handleQuit(player);
} else {
display.hideFromPlayerSilently(player);
}
display.handleQuit(player);
display.getBlocked().remove(player.getUniqueId());
});
}
@@ -376,18 +467,27 @@ public class NameTagManager {
}
}
public void showToTrackedPlayers(@NotNull Player player, @NotNull Collection<UUID> tracked) {
public void showToTrackedPlayers(@NotNull Player player) {
showToTrackedPlayers(player, plugin.getTrackerManager().getWhoTracks(player));
}
public void showToTrackedPlayers(@NotNull Player player, @NotNull Collection<Player> tracked) {
final PacketNameTag packetNameTag = nameTags.get(player.getUniqueId());
if (packetNameTag != null) {
packetNameTag.setVisible(true);
packetNameTag.showToPlayers(tracked.stream()
.map(Bukkit::getPlayer)
final Set<Player> players = tracked.stream()
.filter(Objects::nonNull)
.collect(Collectors.toSet()));
.collect(Collectors.toSet());
players.add(packetNameTag.getOwner());
packetNameTag.showToPlayers(players);
if (debug) {
plugin.getLogger().info("Showing nametag of " + player.getName() + " to tracked players: " +
players.stream().map(Player::getName).collect(Collectors.joining(", ")));
}
return;
}
addPlayer(player);
addPlayer(player, false);
}
public void hideAllDisplays(@NotNull Player player) {
@@ -406,11 +506,18 @@ public class NameTagManager {
}
public void updateSneaking(@NotNull Player player, boolean sneaking) {
if (shiftSystemBlocked.getOrDefault(player.getUniqueId(), false)) {
return;
}
getPacketDisplayText(player).ifPresent(packetNameTag -> {
if (packetNameTag.getNameTag().background().seeThrough()) {
packetNameTag.setSeeThrough(!sneaking);
}
packetNameTag.setTextOpacity((byte) (sneaking ? plugin.getConfigManager().getSettings().getSneakOpacity() : -1));
packetNameTag.setSneaking(sneaking);
packetNameTag
.setTextOpacity((byte) (sneaking ? plugin.getConfigManager().getSettings().getSneakOpacity() : -1));
packetNameTag.refresh();
});
}
@@ -418,22 +525,27 @@ public class NameTagManager {
public void reload() {
final float yOffset = plugin.getConfigManager().getSettings().getYOffset();
final float viewDistance = plugin.getConfigManager().getSettings().getViewDistance();
final AbstractDisplayMeta.BillboardConstraints billboard = plugin.getConfigManager().getSettings().getDefaultBillboard();
final AbstractDisplayMeta.BillboardConstraints billboard = plugin.getConfigManager().getSettings()
.getDefaultBillboard();
plugin.getTaskScheduler().runTaskAsynchronously(() -> Bukkit.getOnlinePlayers().forEach(p -> {
setYOffset(p, yOffset);
setViewDistance(p, viewDistance);
setBillBoard(p, billboard);
refresh(p, true);
}));
plugin.getTaskScheduler()
.runTaskAsynchronously(() -> plugin.getPlayerListener().getOnlinePlayers().values().forEach(p -> {
setYOffset(p, yOffset);
setViewDistance(p, viewDistance);
setBillBoard(p, billboard);
refresh(p, true);
}));
startTask();
}
@SuppressWarnings("UnstableApiUsage")
public void debug(@NotNull CommandSender audience) {
audience.sendRichMessage("<red>UnlimitedNameTags v" + plugin.getPluginMeta().getVersion() + " . Compiled: " + plugin.getConfigManager().isCompiled());
final AtomicReference<Component> component = new AtomicReference<>(Component.text("Nametags:").colorIfAbsent(TextColor.color(0xFF0000)));
audience.sendRichMessage("<red>UnlimitedNameTags v" + plugin.getPluginMeta().getVersion() + " . Compiled: "
+ plugin.getConfigManager().isCompiled());
final AtomicReference<Component> component = new AtomicReference<>(
Component.text("Nametags:").colorIfAbsent(TextColor.color(0xFF0000)));
nameTags.forEach((uuid, display) -> {
final Player player = Bukkit.getPlayer(uuid);
final Player player = plugin.getPlayerListener().getPlayer(uuid);
if (player == null) {
return;
@@ -443,7 +555,12 @@ public class NameTagManager {
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.map(Player::getName)
.toList();
.collect(Collectors.toList());
if (!plugin.getConfigManager().getSettings().isShowCurrentNameTag()) {
viewers.remove(player.getName());
}
final long lastUpdate = display.getLastUpdate();
final Component text = getComponent(display, viewers, player, lastUpdate);
@@ -455,7 +572,7 @@ public class NameTagManager {
@NotNull
private Component getComponent(@NotNull PacketNameTag display, @NotNull List<String> viewers,
@NotNull Player player, long lastUpdate) {
@NotNull Player player, long lastUpdate) {
final int seconds = (int) ((System.currentTimeMillis() - lastUpdate) / 1000);
final Map<String, String> properties = display.properties();
Component hover = Component.text("Viewers: " + viewers).appendNewline()
@@ -487,13 +604,12 @@ public class NameTagManager {
getPacketDisplayText(player).ifPresent(packetNameTag -> packetNameTag.setViewRange(viewDistance));
}
public void vanishPlayer(@NotNull Player player) {
getPacketDisplayText(player).ifPresent(packetNameTag -> {
final Set<UUID> viewers = new HashSet<>(packetNameTag.getViewers());
final boolean isVanished = plugin.getVanishManager().isVanished(player);
viewers.forEach(uuid -> {
final Player viewer = Bukkit.getPlayer(uuid);
final Player viewer = plugin.getPlayerListener().getPlayer(uuid);
if (viewer == null || viewer == player) {
return;
}
@@ -509,7 +625,7 @@ public class NameTagManager {
getPacketDisplayText(player).ifPresent(packetNameTag -> {
final Set<UUID> viewers = new HashSet<>(packetNameTag.getViewers());
viewers.forEach(uuid -> {
final Player viewer = Bukkit.getPlayer(uuid);
final Player viewer = plugin.getPlayerListener().getPlayer(uuid);
if (viewer == null || viewer == player) {
return;
}
@@ -518,7 +634,6 @@ public class NameTagManager {
});
}
@NotNull
public Optional<PacketNameTag> getPacketDisplayText(@NotNull Player player) {
return Optional.ofNullable(nameTags.get(player.getUniqueId()));
@@ -596,7 +711,7 @@ public class NameTagManager {
}
public void hideOtherNametags(@NotNull Player player) {
hiddenOtherNametags.add(player.getUniqueId());
hideNametags.add(player.getUniqueId());
nameTags.forEach((uuid, display) -> {
if (display.canPlayerSee(player)) {
display.hideFromPlayer(player);
@@ -605,17 +720,91 @@ public class NameTagManager {
}
public void showOtherNametags(@NotNull Player player) {
hiddenOtherNametags.remove(player.getUniqueId());
hideNametags.remove(player.getUniqueId());
plugin.getTrackerManager().getTrackedPlayers(player.getUniqueId()).forEach(uuid -> {
final Player tracked = Bukkit.getPlayer(uuid);
final Player tracked = plugin.getPlayerListener().getPlayer(uuid);
if (tracked == null) {
return;
}
getPacketDisplayText(tracked).ifPresent(display -> display.showToPlayer(player));
});
}
public boolean isHiddenOtherNametags(@NotNull Player player) {
return hiddenOtherNametags.contains(player.getUniqueId());
return hideNametags.contains(player.getUniqueId());
}
public void swapNametag(@NotNull Player player, @NotNull Settings.NameTag nameTag) {
final PacketNameTag display = nameTags.get(player.getUniqueId());
if (display == null) {
return;
}
display.setNameTag(nameTag);
final List<Player> relationalPlayers = display.getViewers().stream()
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.toList();
plugin.getPlaceholderManager().applyPlaceholders(player, nameTag.linesGroups(), relationalPlayers)
.thenAccept(lines -> {
final Component component = lines.get(player);
display.text(player, component);
display.setBillboard(plugin.getConfigManager().getSettings().getDefaultBillboard());
display.setShadowed(nameTag.background().shadowed());
display.setSeeThrough(nameTag.background().seeThrough() && !display.isSneaking());
display.setBackgroundColor(nameTag.background().getColor());
display.resetOffset(plugin.getConfigManager().getSettings().getYOffset());
display.setViewRange(plugin.getConfigManager().getSettings().getViewDistance());
plugin.getTaskScheduler().runTaskLater(() -> display.modifyOwner(meta -> meta.setText(component)),
1);
lines.forEach((p, c) -> {
if (!p.equals(player)) {
display.text(p, c);
}
});
display.refresh();
})
.exceptionally(throwable -> {
plugin.getLogger().log(java.util.logging.Level.SEVERE,
"Failed to swap nametag for " + player.getName(), throwable);
return null;
});
}
public void setNametagOverride(@NotNull Player player, @NotNull Settings.NameTag nameTag) {
nameTagOverrides.put(player.getUniqueId(), nameTag);
final PacketNameTag display = nameTags.get(player.getUniqueId());
if (display != null) {
swapNametag(player, nameTag);
}
}
public void removeNametagOverride(@NotNull Player player) {
nameTagOverrides.remove(player.getUniqueId());
final PacketNameTag display = nameTags.get(player.getUniqueId());
if (display != null) {
final Settings.NameTag configNametag = getConfigNametag(player);
swapNametag(player, configNametag);
}
}
public void setShiftSystemBlocked(@NotNull Player player, boolean blocked) {
if (blocked) {
shiftSystemBlocked.put(player.getUniqueId(), true);
} else {
shiftSystemBlocked.remove(player.getUniqueId());
}
}
public boolean isShiftSystemBlocked(@NotNull Player player) {
return shiftSystemBlocked.getOrDefault(player.getUniqueId(), false);
}
}
@@ -1,9 +1,9 @@
package org.alexdev.unlimitednametags.packet;
import com.github.retrooper.packetevents.PacketEvents;
import io.github.retrooper.packetevents.adventure.serializer.legacy.LegacyComponentSerializer;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -1,40 +1,41 @@
package org.alexdev.unlimitednametags.packet;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.protocol.player.User;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetPassengers;
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.google.common.util.concurrent.ThreadFactoryBuilder;
import io.github.retrooper.packetevents.util.SpigotReflectionUtil;
import me.tofaa.entitylib.APIConfig;
import me.tofaa.entitylib.EntityLib;
import me.tofaa.entitylib.spigot.SpigotEntityLibPlatform;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.data.ConcurrentMultimap;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class PacketManager {
private final UnlimitedNameTags plugin;
private final Multimap<UUID, Integer> passengers;
private final ConcurrentMultimap<UUID, Integer> passengers;
private final ExecutorService executorService;
public PacketManager(@NotNull UnlimitedNameTags plugin) {
this.plugin = plugin;
this.initialize();
this.passengers = (Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet)); //Multimaps.synchronizedMultimap
this.executorService = Executors.newFixedThreadPool(2, r -> {
final Thread thread = new Thread(r);
thread.setName("UnlimitedNameTags-PacketManager");
return thread;
});
this.passengers = new ConcurrentMultimap<>();
final ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("UnlimitedNameTags-PacketManager-%d")
.build();
this.executorService = Executors.newFixedThreadPool(
Math.max(2, Runtime.getRuntime().availableProcessors() / 2),
namedThreadFactory
);
}
private void initialize() {
@@ -49,34 +50,37 @@ public class PacketManager {
}
public void setPassengers(@NotNull Player player, @NotNull List<Integer> passengers) {
final List<Integer> clone = List.copyOf(passengers);
plugin.getTaskScheduler().runTaskAsynchronously(() -> {
executorService.submit(() -> this.passengers.replaceValues(player.getUniqueId(), clone));
});
this.passengers.replaceValues(player.getUniqueId(), passengers);
}
public void sendPassengersPacket(@NotNull Player player, @NotNull PacketNameTag packetNameTag) {
public void sendPassengersPacket(@NotNull User player, @NotNull PacketNameTag packetNameTag) {
final int entityId = packetNameTag.getEntityId();
final int ownerId = packetNameTag.getOwner().getEntityId();
executorService.submit(() -> {
final Set<Integer> passengers = Sets.newHashSet(this.passengers.get(packetNameTag.getOwner().getUniqueId()));
if (player.getChannel() == null) {
return;
}
final Collection<Integer> ownerPassengers = this.passengers.get(packetNameTag.getOwner().getUniqueId());
final Set<Integer> passengers = Sets.newHashSetWithExpectedSize(ownerPassengers.size() + 1);
passengers.addAll(ownerPassengers);
passengers.add(entityId);
final int[] passengersArray = passengers.stream().mapToInt(i -> i).toArray();
final int[] passengersArray = passengers.stream().sorted().mapToInt(i -> i).toArray();
final WrapperPlayServerSetPassengers packet = new WrapperPlayServerSetPassengers(ownerId, passengersArray);
PacketEvents.getAPI().getPlayerManager().sendPacket(player, packet);
player.sendPacketSilently(packet);
});
}
public void removePassenger(@NotNull Player player, int passenger) {
executorService.submit(() -> this.passengers.remove(player.getUniqueId(), passenger));
this.passengers.remove(player.getUniqueId(), passenger);
}
public synchronized int getEntityIndex() {
public int getEntityIndex() {
return SpigotReflectionUtil.generateEntityId();
}
public void removePassenger(int passenger) {
this.passengers.values().remove(passenger);
this.passengers.removeValueFromAll(passenger);
}
}
@@ -11,7 +11,6 @@ import com.google.common.collect.Sets;
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import me.tofaa.entitylib.meta.Metadata;
import me.tofaa.entitylib.meta.display.AbstractDisplayMeta;
import me.tofaa.entitylib.meta.display.TextDisplayMeta;
@@ -22,14 +21,12 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.config.Settings;
import org.alexdev.unlimitednametags.hook.ViaVersionHook;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Display;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Consumer;
@@ -40,13 +37,15 @@ public class PacketNameTag {
private final UnlimitedNameTags plugin;
private final WrapperPerPlayerEntity perPlayerEntity;
private final Set<UUID> viewers;
private final int entityId;
private final UUID entityIdUuid;
private final Player owner;
private final Set<UUID> blocked;
@NotNull
private final Map<UUID, Component> relationalCache;
private final Map<UUID, Component> calculatedTextCache = Maps.newConcurrentMap();
private final Map<UUID, Component> forcedRelationalText = Maps.newConcurrentMap();
private volatile Component forcedGlobalText = null;
private long lastUpdate;
@Setter
private boolean visible;
@@ -55,6 +54,9 @@ public class PacketNameTag {
private float scale;
private float offset;
private float increasedOffset;
private volatile boolean removed;
@Setter
private boolean sneaking;
public PacketNameTag(@NotNull UnlimitedNameTags plugin, @NotNull Player owner, @NotNull Settings.NameTag nameTag) {
this.plugin = plugin;
@@ -63,7 +65,6 @@ public class PacketNameTag {
this.entityId = plugin.getPacketManager().getEntityIndex();
this.entityIdUuid = UUID.randomUUID();
this.perPlayerEntity = new WrapperPerPlayerEntity(this.getBaseSupplier());
this.viewers = Sets.newConcurrentHashSet();
this.blocked = Sets.newConcurrentHashSet();
this.lastUpdate = System.currentTimeMillis();
this.nameTag = nameTag;
@@ -82,6 +83,20 @@ public class PacketNameTag {
}
public boolean text(@NotNull Player player, @NotNull Component text) {
if (removed) {
return false;
}
calculatedTextCache.put(player.getUniqueId(), text);
if (forcedRelationalText.containsKey(player.getUniqueId()) || forcedGlobalText != null) {
return false;
}
return applyText(player, text);
}
private boolean applyText(@NotNull Player player, @NotNull Component text) {
if (text.equals(relationalCache.get(player.getUniqueId()))) {
return false;
}
@@ -97,44 +112,74 @@ public class PacketNameTag {
return true;
}
public void modify(User user, Consumer<TextDisplayMeta> consumer) {
public void modify(@Nullable User user, @NotNull Consumer<TextDisplayMeta> consumer) {
if (removed) {
return;
}
if (user == null) {
return;
}
perPlayerEntity.modify(user, e -> {
if (e == null) {
return; // Entity removed (e.g. player disconnected during async refresh)
}
final TextDisplayMeta meta = (TextDisplayMeta) e.getEntityMeta();
consumer.accept(meta);
});
}
public void modifyOwner(Consumer<TextDisplayMeta> consumer) {
public void modifyOwner(@NotNull Consumer<TextDisplayMeta> consumer) {
if (removed) {
return;
}
final User owner = PacketEvents.getAPI().getPlayerManager().getUser(this.owner);
if (owner == null) {
return;
}
modify(owner, consumer);
}
public void modifyOwnerEntity(Consumer<WrapperEntity> consumer) {
public void modifyOwnerEntity(@NotNull Consumer<WrapperEntity> consumer) {
if (removed) {
return;
}
final User owner = PacketEvents.getAPI().getPlayerManager().getUser(this.owner);
if (owner == null) {
return;
}
modifyEntity(owner, consumer);
}
public void modify(Consumer<TextDisplayMeta> consumer) {
public void modify(@NotNull Consumer<TextDisplayMeta> consumer) {
if (removed) {
return;
}
perPlayerEntity.execute(e -> {
final TextDisplayMeta meta = (TextDisplayMeta) e.getEntityMeta();
consumer.accept(meta);
});
}
public void modifyEntity(User user, Consumer<WrapperEntity> consumer) {
public void modifyEntity(@Nullable User user, @NotNull Consumer<WrapperEntity> consumer) {
if (removed) {
return;
}
perPlayerEntity.modify(user, consumer);
}
public void modifyEntity(Consumer<WrapperEntity> consumer) {
public void modifyEntity(@NotNull Consumer<WrapperEntity> consumer) {
if (removed) {
return;
}
perPlayerEntity.execute(consumer);
}
@@ -143,7 +188,7 @@ public class PacketNameTag {
}
public boolean checkScale() {
final AttributeInstance attribute = owner.getAttribute(Attribute.GENERIC_SCALE);
final AttributeInstance attribute = owner.getAttribute(plugin.getNametagManager().getScaleAttribute());
if (attribute == null) {
if (scale != getDefaultScale()) {
setScale(getDefaultScale());
@@ -168,11 +213,6 @@ public class PacketNameTag {
modify(meta -> meta.setScale(new Vector3f(scale, scale, scale)));
}
public void setBillboard(@NotNull Display.Billboard billboard) {
final AbstractDisplayMeta.BillboardConstraints billboardConstraints = AbstractDisplayMeta.BillboardConstraints.valueOf(billboard.name());
modify(meta -> meta.setBillboardConstraints(billboardConstraints));
}
public void setBillboard(@NotNull AbstractDisplayMeta.BillboardConstraints billboard) {
modify(meta -> meta.setBillboardConstraints(billboard));
}
@@ -208,11 +248,18 @@ public class PacketNameTag {
public void showToPlayer(@NotNull Player player) {
if (!isEligibleToShow(player)) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger()
.info("Player " + player.getName() + " is not eligible to show nametag for " + owner.getName());
}
return;
}
if (!player.getUniqueId().equals(owner.getUniqueId())) {
applyOwnerData(perPlayerEntity.getEntityOf(getUser(player)));
WrapperEntity entity = perPlayerEntity.getEntityOf(getUser(player));
if (entity != null) {
applyOwnerData(entity);
}
}
spawn(player);
@@ -223,47 +270,103 @@ public class PacketNameTag {
setPosition();
}
viewers.add(player.getUniqueId());
perPlayerEntity.addViewer(getUser(player));
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> sendPassengersPacket(player), 1);
updateViewer(player.getUniqueId());
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> {
final User user = getUser(player);
if (user == null) {
return;
}
sendPassengersPacket(user);
}, 1);
}
private boolean isEligibleToShow(@NotNull Player player) {
if (!visible) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger().info("Nametag is not visible for " + owner.getName());
}
return false;
}
if (plugin.getHook(ViaVersionHook.class).map(h -> h.hasNotTextDisplays(player)).orElse(false)) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger()
.info("Player " + player.getName() + " is on a version that does not support text displays.");
}
return false;
}
if (blocked.contains(player.getUniqueId())) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger()
.info("Player " + player.getName() + " is blocked from seeing nametag of " + owner.getName());
}
return false;
}
if (!player.canSee(owner)) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger().info("Player " + player.getName() + " cannot see owner " + owner.getName());
}
return false;
}
if (player.getWorld() != owner.getWorld()) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger()
.info("Player " + player.getName() + " is in a different world than owner " + owner.getName());
}
return false;
}
if (isPlayerChannelNotValid(player) || isPlayerChannelNotValid(owner)) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger().info("Player " + player.getName() + " or owner " + owner.getName()
+ " has an invalid channel/user.");
}
return false;
}
if (!hasRequiredPermissions(player)) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger().info("Player " + player.getName()
+ " does not have required permissions to see nametag of " + owner.getName());
}
return false;
}
if (plugin.getNametagManager().isHiddenOtherNametags(player)) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger().info("Player " + player.getName() + " has other nametags hidden.");
}
return false;
}
if (plugin.getConfigManager().getSettings().isShowWhileLooking() &&
!plugin.getNametagManager().isPlayerPointingAt(player, owner)) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger().info("Player " + player.getName() + " is not looking at owner " + owner.getName());
}
return false;
}
return !viewers.contains(player.getUniqueId());
if (plugin.getConfigManager().getSettings().isShowCurrentNameTag()
&& player.getUniqueId() == owner.getUniqueId()) {
if (plugin.getNametagManager().isDebug()) {
plugin.getLogger()
.info("Player " + player.getName() + " is the owner and show current nametag is enabled.");
}
return true;
}
if (plugin.getNametagManager().isDebug() && !getViewers().contains(player.getUniqueId())) {
plugin.getLogger().info("Player " + player.getName() + " is eligible to see nametag of " + owner.getName());
}
return !getViewers().contains(player.getUniqueId());
}
private boolean isPlayerChannelNotValid(@NotNull Player player) {
@@ -280,7 +383,7 @@ public class PacketNameTag {
}
private boolean hasRequiredPermissions(@NotNull Player player) {
boolean isOwner = isOwner(player);
final boolean isOwner = isOwner(player);
if (!isOwner && !player.hasPermission("unt.shownametags")) {
return false;
}
@@ -300,11 +403,18 @@ public class PacketNameTag {
});
plugin.getTaskScheduler().runTaskLaterAsynchronously(() -> {
sendPassengersPacket(owner);
final User ownerUser = getUser(owner);
if (ownerUser == null) {
return;
}
sendPassengersPacket(ownerUser);
}, 1);
}
public void sendPassengersPacket(@NotNull Player player) {
public void sendPassengersPacket(@NotNull User player) {
if (removed) {
return;
}
plugin.getPacketManager().sendPassengersPacket(player, this);
}
@@ -313,10 +423,14 @@ public class PacketNameTag {
return;
}
viewers.forEach(u -> {
final Player player = Bukkit.getPlayer(u);
if (player != null) {
sendPassengersPacket(player);
getViewers().forEach(u -> {
final Player player = plugin.getPlayerListener().getPlayer(u);
if (player == null) {
return;
}
final User user = PacketEvents.getAPI().getPlayerManager().getUser(player);
if (user != null) {
sendPassengersPacket(user);
}
});
}
@@ -327,14 +441,14 @@ public class PacketNameTag {
}
private void setOwnerPosition() {
final Location location = getOffsetLocation().add(0, 0.25, 0);
final Location location = getOffsetLocation(); // .add(0, 0.25, 0)
modifyOwnerEntity(meta -> meta.setLocation(SpigotConversionUtil.fromBukkitLocation(location)));
}
public Location getOffsetLocation() {
final Location location = owner.getLocation().clone();
final Location location = owner.getLocation();
location.setPitch(0);
location.setYaw(0);
location.setYaw(-180);
location.setY(location.getY() + (1.8) * scale);
return location;
}
@@ -343,9 +457,9 @@ public class PacketNameTag {
if (blocked.contains(player.getUniqueId())) {
return;
}
viewers.remove(player.getUniqueId());
final User user = PacketEvents.getAPI().getPlayerManager().getUser(player);
if (user == null) {
perPlayerEntity.getEntities().remove(player.getUniqueId());
return;
}
final WrapperEntity wrapperEntity = perPlayerEntity.getEntityOf(user);
@@ -354,19 +468,22 @@ public class PacketNameTag {
}
if (!player.getUniqueId().equals(owner.getUniqueId())) {
perPlayerEntity.getEntities().remove(user.getUUID());
}
relationalCache.remove(player.getUniqueId());
calculatedTextCache.remove(player.getUniqueId());
plugin.getPacketManager().removePassenger(player, entityId);
}
public void clearViewers() {
viewers.forEach(u -> {
final Player player = Bukkit.getPlayer(u);
getViewers().forEach(u -> {
final Player player = plugin.getPlayerListener().getPlayer(u);
if (player != null) {
hideFromPlayer(player);
}
});
}
public void showToPlayers(Set<Player> players) {
@@ -377,13 +494,13 @@ public class PacketNameTag {
if (blocked.contains(player.getUniqueId())) {
return;
}
viewers.remove(player.getUniqueId());
perPlayerEntity.getEntities().remove(player.getUniqueId());
relationalCache.remove(player.getUniqueId());
calculatedTextCache.remove(player.getUniqueId());
}
public boolean canPlayerSee(@NotNull Player player) {
return viewers.contains(player.getUniqueId());
return getViewers().contains(player.getUniqueId());
}
public void spawn(@NotNull Player player) {
@@ -392,12 +509,13 @@ public class PacketNameTag {
if (user == null) {
return;
}
modifyEntity(user, e -> e.spawn(SpigotConversionUtil.fromBukkitLocation(getOffsetLocation())));
}
public void refresh() {
perPlayerEntity.getEntities().forEach((u, e) -> {
if(blocked.contains(u)) {
if (blocked.contains(u)) {
return;
}
@@ -411,41 +529,44 @@ public class PacketNameTag {
return;
}
if(blocked.contains(player.getUniqueId())) {
if (blocked.contains(player.getUniqueId())) {
return;
}
perPlayerEntity.getEntityOf(user).refresh();
final WrapperEntity entity = perPlayerEntity.getEntityOf(user);
if (entity != null) {
entity.refresh();
}
}
public void remove() {
viewers.forEach(u -> {
final Player player = Bukkit.getPlayer(u);
removed = true;
perPlayerEntity.getEntities().keySet().forEach(u -> {
final Player player = plugin.getPlayerListener().getPlayer(u);
if (player == null) {
return;
}
final User user = PacketEvents.getAPI().getPlayerManager().getUser(player);
if (user != null) {
if (user != null && user.getChannel() != null) {
perPlayerEntity.removeViewer(user);
}
});
viewers.clear();
perPlayerEntity.getEntities().values().forEach(WrapperEntity::remove);
perPlayerEntity.getEntities().clear();
plugin.getPacketManager().removePassenger(entityId);
relationalCache.clear();
calculatedTextCache.clear();
forcedRelationalText.clear();
forcedGlobalText = null;
}
public void handleQuit(@NotNull Player player) {
viewers.remove(player.getUniqueId());
clearCache(player.getUniqueId());
perPlayerEntity.getEntities().remove(player.getUniqueId());
plugin.getPacketManager().removePassenger(player, entityId);
}
@SneakyThrows
private void clearCache(@NotNull UUID uuid) {
perPlayerEntity.getEntities().remove(uuid);
}
public void setTextOpacity(byte b) {
modify(meta -> meta.setTextOpacity(b));
}
@@ -463,20 +584,22 @@ public class PacketNameTag {
private void applyOwnerData(@NotNull WrapperEntity wrapper) {
final User ownerUser = PacketEvents.getAPI().getPlayerManager().getUser(owner);
final Metadata metadata = wrapper.getEntityMeta().getMetadata();
final Optional<EntityData> component = wrapper.getEntityMeta().entityData()
final Optional<EntityData<?>> component = wrapper.getEntityMeta().entityData()
.stream()
.filter(e -> e.getType() == EntityDataTypes.ADV_COMPONENT)
.findFirst();
final Metadata ownerMetadata = perPlayerEntity.getEntityOf(ownerUser).getEntityMeta().getMetadata();
metadata.copyFrom(ownerMetadata);
component.ifPresent(entityData -> ((TextDisplayMeta) wrapper.getEntityMeta()).setText((Component) entityData.getValue()));
component.ifPresent(
entityData -> ((TextDisplayMeta) wrapper.getEntityMeta()).setText((Component) entityData.getValue()));
metadata.setNotifyAboutChanges(false);
}
@NotNull
public Map<String, String> properties() {
final Map<String, String> properties = new LinkedHashMap<>();
final TextDisplayMeta meta = (TextDisplayMeta) this.perPlayerEntity.getEntityOf(PacketEvents.getAPI().getPlayerManager().getUser(owner)).getEntityMeta();
final TextDisplayMeta meta = (TextDisplayMeta) this.perPlayerEntity
.getEntityOf(PacketEvents.getAPI().getPlayerManager().getUser(owner)).getEntityMeta();
properties.put("text", MiniMessage.miniMessage().serialize(meta.getText()));
properties.put("billboard", meta.getBillboardConstraints().name());
properties.put("shadowed", String.valueOf(meta.isShadow()));
@@ -490,4 +613,58 @@ public class PacketNameTag {
return properties;
}
}
public void setForcedNameTag(@NotNull Component component) {
this.forcedGlobalText = component;
updateAllViewers();
}
public void setForcedNameTag(@NotNull UUID viewer, @NotNull Component component) {
this.forcedRelationalText.put(viewer, component);
updateViewer(viewer);
}
public void clearForcedNameTag() {
this.forcedGlobalText = null;
updateAllViewers();
}
public void clearForcedNameTag(@NotNull UUID viewer) {
this.forcedRelationalText.remove(viewer);
updateViewer(viewer);
}
private void updateAllViewers() {
for (final UUID uuid : getViewers()) {
updateViewer(uuid);
}
}
private void updateViewer(@NotNull UUID uuid) {
final Player player = plugin.getPlayerListener().getPlayer(uuid);
if (player == null)
return;
Component textToUse = calculatedTextCache.get(uuid);
if (forcedRelationalText.containsKey(uuid)) {
textToUse = forcedRelationalText.get(uuid);
} else if (forcedGlobalText != null) {
textToUse = forcedGlobalText;
}
if (textToUse != null) {
if (applyText(player, textToUse)) {
refreshForPlayer(player);
}
}
}
/**
* Returns an unmodifiable set containing the viewers of the nametag.
*
* @return the viewers of the nametag
*/
public Set<UUID> getViewers() {
return Collections.unmodifiableSet(perPlayerEntity.getEntities().keySet());
}
}
@@ -1,18 +1,39 @@
package org.alexdev.unlimitednametags.placeholders;
import lombok.RequiredArgsConstructor;
import lombok.Getter;
import me.clip.placeholderapi.PlaceholderAPI;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@RequiredArgsConstructor
public class PAPIManager {
private final UnlimitedNameTags plugin;
@Getter
private boolean papiEnabled;
private UntPapiExpansion untPapiExpansion;
public boolean isPAPIEnabled() {
return plugin.getServer().getPluginManager().isPluginEnabled("PlaceholderAPI");
public PAPIManager(@NotNull UnlimitedNameTags plugin) {
this.plugin = plugin;
this.checkPapiEnabled();
}
public void checkPapiEnabled() {
if (papiEnabled) {
return;
}
this.papiEnabled = plugin.getServer().getPluginManager().isPluginEnabled("PlaceholderAPI");
if (papiEnabled) {
try {
this.untPapiExpansion = new UntPapiExpansion(plugin);
this.untPapiExpansion.register();
} catch (Throwable e) {
plugin.getLogger().warning("Failed to register PlaceholderAPI expansion: " + e.getMessage());
}
} else {
plugin.getLogger().info("PlaceholderAPI is not enabled, some features may not work.");
}
}
@NotNull
@@ -20,8 +41,12 @@ public class PAPIManager {
if (text.isEmpty()) {
return text;
}
if (!papiEnabled) {
return text;
}
try {
return PlaceholderAPI.setPlaceholders(player, text);
final String firstReplacement = PlaceholderAPI.setPlaceholders(player, text);
return PlaceholderAPI.setPlaceholders(player, firstReplacement);
} catch (Throwable e) {
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Failed to set placeholders for text: " + text, e);
return text;
@@ -33,12 +58,22 @@ public class PAPIManager {
if (text.isEmpty()) {
return text;
}
if (!papiEnabled) {
return text;
}
try {
return PlaceholderAPI.setRelationalPlaceholders(whoSees, target, text);
final String firstReplacement = PlaceholderAPI.setRelationalPlaceholders(whoSees, target, text);
return PlaceholderAPI.setRelationalPlaceholders(whoSees, target, firstReplacement);
} catch (Throwable e) {
plugin.getLogger().log(java.util.logging.Level.SEVERE, "Failed to set relational placeholders for text: " + text, e);
return text;
}
}
public void close() {
if (papiEnabled && untPapiExpansion != null) {
untPapiExpansion.unregister();
}
}
}
@@ -1,54 +1,109 @@
package org.alexdev.unlimitednametags.placeholders;
import com.google.common.collect.Maps;
import lombok.Getter;
import net.jodah.expiringmap.ExpiringMap;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.alexdev.unlimitednametags.config.Settings;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Getter
public class PlaceholderManager {
public static final String PHASE_MD_KEY = "phase-md";
public static final String PHASE_MM_KEY = "phase-mm";
public static final String PHASE_MM_G_KEY = "phase-mm-g";
public static final String NEG_PHASE_MD_KEY = "-phase-md";
public static final String NEG_PHASE_MM_KEY = "-phase-mm";
public static final String NEG_PHASE_MM_G_KEY = "-phase-mm-g";
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%.*?%", Pattern.DOTALL);
private static final JoinConfiguration JOIN_CONFIGURATION = JoinConfiguration.separator(Component.newline());
private static final String ELSE_PLACEHOLDER = "ELSE";
private static final int maxIndex = 16777215;
private static final int maxMIndex = 10;
private static final double minMGIndex = -1.0;
private static final int MORE_LINES = 15;
private static final int MORE_LINES = 14;
private final UnlimitedNameTags plugin;
private final ExecutorService executorService;
private final PAPIManager papiManager;
private int index = maxIndex;
private int mmIndex = maxMIndex;
private double mGIndex = 1.0;
private DecimalFormat decimalFormat;
private BigDecimal miniGradientIndexBD = new BigDecimal("-1.0");
private final BigDecimal stepBD = new BigDecimal("0.1");
private final BigDecimal one = new BigDecimal("1.0");
private final BigDecimal minusOne = new BigDecimal("-1.0");
private final Map<String, Component> cachedComponents;
private Map<UUID, Map<String, String>> cachedPlaceholders;
private final Map<String, String> formattedPhaseValues;
private final Map<String, Map<String, String>> placeholdersReplacements;
public PlaceholderManager(@NotNull UnlimitedNameTags plugin) {
this.plugin = plugin;
this.executorService = Executors.newFixedThreadPool(5);
this.executorService = Executors.newWorkStealingPool(
Math.max(2, Runtime.getRuntime().availableProcessors() / 2)
);
this.papiManager = new PAPIManager(plugin);
startIndexTask();
this.cachedComponents = ExpiringMap.builder()
.expiration(2, TimeUnit.MINUTES)
.build();
this.formattedPhaseValues = Maps.newConcurrentMap();
this.placeholdersReplacements = Maps.newConcurrentMap();
reloadPlaceholdersReplacements();
createDecimalFormat();
reload();
updateFormattedPhaseValues(); // Populate immediately to avoid race: gradient with #-phase-mm-g# fails when map is empty
startIndexTask();
}
public void reload() {
cachedComponents.clear();
this.cachedPlaceholders = Maps.newConcurrentMap();
plugin.getPlayerListener().getOnlinePlayers().values().forEach(p -> cachedPlaceholders.put(p.getUniqueId(), ExpiringMap.builder()
.expiration(plugin.getConfigManager().getSettings().getPlaceholderCacheTime() * 50L, TimeUnit.MILLISECONDS) // Adjusted unit based on previous code
.build()));
reloadPlaceholdersReplacements();
}
private void reloadPlaceholdersReplacements() {
placeholdersReplacements.clear();
plugin.getConfigManager().getSettings().getPlaceholdersReplacements().forEach((key, value) -> {
final Map<String, String> replacements = Maps.newHashMapWithExpectedSize(value.size());
value.forEach((pr) -> replacements.put(pr.placeholder().toLowerCase(Locale.ROOT), pr.replacement()));
placeholdersReplacements.put(key.toLowerCase(Locale.ROOT), replacements);
});
}
public void removePlayer(@NotNull Player player) {
cachedPlaceholders.remove(player.getUniqueId());
}
private void createDecimalFormat() {
decimalFormat = new DecimalFormat("#.#");
decimalFormat = new DecimalFormat("#.#", DecimalFormatSymbols.getInstance(Locale.US));
decimalFormat.setRoundingMode(RoundingMode.DOWN);
final DecimalFormatSymbols symbols = new DecimalFormatSymbols();
final DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.US);
symbols.setDecimalSeparator('.');
decimalFormat.setDecimalFormatSymbols(symbols);
}
@@ -57,7 +112,7 @@ public class PlaceholderManager {
plugin.getTaskScheduler().runTaskTimerAsynchronously(() -> {
index -= 1;
if (index == 0) {
index = 16777215;
index = maxIndex;
}
}, 0, 1);
plugin.getTaskScheduler().runTaskTimerAsynchronously(() -> {
@@ -65,33 +120,56 @@ public class PlaceholderManager {
if (mmIndex == 1) {
mmIndex = maxMIndex;
}
}, 0, 2);
}, 0, 1);
plugin.getTaskScheduler().runTaskTimerAsynchronously(() -> {
mGIndex += 0.1;
if (mGIndex >= 1d) {
mGIndex = minMGIndex;
miniGradientIndexBD = miniGradientIndexBD.add(stepBD);
if (miniGradientIndexBD.compareTo(one) > 0) {
miniGradientIndexBD = minusOne;
}
}, 0, 2);
updateFormattedPhaseValues();
}, 0, 1);
}
private void updateFormattedPhaseValues() {
final int currentIndex = this.index;
final int currentMmIndex = this.mmIndex;
final BigDecimal currentMiniGradientIndexBD = this.miniGradientIndexBD;
final String indexStr = Integer.toString(currentIndex);
final String mmIndexStr = Integer.toString(currentMmIndex);
final String phaseMmGStr = decimalFormat.format(currentMiniGradientIndexBD);
final String negIndexStr = Integer.toString(maxIndex - currentIndex);
final String negMmIndexStr = Integer.toString(maxMIndex - currentMmIndex);
final String negPhaseMmGStr = decimalFormat.format(currentMiniGradientIndexBD.multiply(minusOne));
formattedPhaseValues.put(PHASE_MD_KEY, indexStr);
formattedPhaseValues.put(PHASE_MM_KEY, mmIndexStr);
formattedPhaseValues.put(PHASE_MM_G_KEY, phaseMmGStr);
formattedPhaseValues.put(NEG_PHASE_MD_KEY, negIndexStr);
formattedPhaseValues.put(NEG_PHASE_MM_KEY, negMmIndexStr);
formattedPhaseValues.put(NEG_PHASE_MM_G_KEY, negPhaseMmGStr);
}
public void close() {
this.executorService.shutdown();
papiManager.close();
}
@NotNull
public CompletableFuture<Map<Player, Component>> applyPlaceholders(@NotNull Player player, @NotNull List<Settings.LinesGroup> lines,
@NotNull List<Player> relationalPlayers) {
return getCheckedLines(player, lines).thenApply(strings -> createComponent(player, strings, relationalPlayers));
return getCheckedLines(player, lines).thenApplyAsync(strings -> createComponent(player, strings, relationalPlayers), executorService);
}
@NotNull
private CompletableFuture<List<String>> getCheckedLines(@NotNull Player player, @NotNull List<Settings.LinesGroup> lines) {
return CompletableFuture.supplyAsync(() -> lines.parallelStream()
.filter(l -> l.modifiers().stream().allMatch(m -> m.isVisible(player, plugin)))
.map(Settings.LinesGroup::lines)
.flatMap(List::stream)
.toList(), executorService);
return CompletableFuture.supplyAsync(() -> lines.stream()
.filter(l -> l.modifiers() == null || l.modifiers().isEmpty() || l.modifiers().stream().allMatch(m -> m.isVisible(player, plugin)))
.map(Settings.LinesGroup::lines)
.flatMap(List::stream)
.toList(), executorService);
}
@NotNull
@@ -102,76 +180,165 @@ public class PlaceholderManager {
.findFirst()
.orElse(0d);
if (moreLines > 0) {
strings = new ArrayList<>(strings);
int lines = (int) (moreLines / MORE_LINES);
for (int i = 0; i < lines; i++) {
strings.add(" ");
final int moreLinesInt = (int) Math.ceil(moreLines);
Component emptyLines = Component.empty();
if (moreLinesInt > 0) {
int linesCount = moreLinesInt / MORE_LINES;
for (int i = 0; i < linesCount; i++) {
emptyLines = emptyLines.append(Component.newline());
}
}
final List<String> stringsCopy = papiManager.isPAPIEnabled() ?
final Component hatLines = emptyLines;
final Settings settings = plugin.getConfigManager().getSettings();
final boolean removeEmptyLines = settings.isRemoveEmptyLines();
final boolean enableRelationalPlaceholders = settings.isEnableRelationalPlaceholders();
final List<String> baseStrings = papiManager.isPapiEnabled() ?
strings.stream()
.map(s -> replacePlaceholders(s, player))
.map(s -> replacePlaceholders(s, player, null))
.toList()
: strings;
return relationalPlayers.stream()
.map(r -> Map.entry(r, Component.join(JoinConfiguration.separator(Component.newline()), stringsCopy.parallelStream()
.map(t -> papiManager.isPAPIEnabled() ? papiManager.setRelationalPlaceholders(r, player, t) : t)
.filter(s -> !plugin.getConfigManager().getSettings().isRemoveEmptyLines() || !s.isEmpty())
.map(this::formatPhases)
.map(t -> format(t, player))
.filter(c -> !plugin.getConfigManager().getSettings().isRemoveEmptyLines() || !c.equals(Component.empty()))
.toList())
.compact()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (enableRelationalPlaceholders) {
final Map<Player, Component> result = Maps.newHashMapWithExpectedSize(relationalPlayers.size());
for (Player viewer : relationalPlayers) {
List<Component> processedLines = baseStrings.stream()
.map(line -> replacePlaceholders(line, player, viewer))
.filter(s -> !removeEmptyLines || !s.isEmpty())
.map(this::formatPhases)
.map(line -> format(line, player))
.filter(c -> !removeEmptyLines || !c.equals(Component.empty()))
.toList();
Component finalComponent = joinLines(processedLines).append(hatLines);
result.put(viewer, finalComponent);
}
return result;
} else {
List<Component> processedLines = baseStrings.stream()
.map(line -> replacePlaceholders(line, player, null))
.filter(s -> !removeEmptyLines || !s.isEmpty())
.map(this::formatPhases)
.map(line -> format(line, player))
.filter(c -> !removeEmptyLines || !c.equals(Component.empty()))
.toList();
Component finalComponent = joinLines(processedLines).append(hatLines);
final Map<Player, Component> result = Maps.newHashMapWithExpectedSize(relationalPlayers.size());
for (Player viewer : relationalPlayers) {
result.put(viewer, finalComponent);
}
return result;
}
}
private Component joinLines(List<Component> lines) {
return Component.join(JOIN_CONFIGURATION, lines);
}
@NotNull
private String formatPhases(@NotNull String value) {
return value.replace("#phase-md#", Integer.toString(index))
.replace("#phase-mm#", Integer.toString(mmIndex))
.replace("#phase-mm-g#", decimalFormat.format(mGIndex))
.replace("#-phase-md#", Integer.toString(maxIndex - index))
.replace("#-phase-mm#", Integer.toString(maxMIndex - mmIndex))
.replace("#-phase-mm-g#", decimalFormat.format(mGIndex * -1));
if (value.indexOf('#') == -1) {
return value;
}
String result = value.replace("#phase-mm-g#", formattedPhaseValues.getOrDefault(PHASE_MM_G_KEY, "0"));
if (result.indexOf('#') == -1) return result;
result = result.replace("#-phase-mm-g#", formattedPhaseValues.getOrDefault(NEG_PHASE_MM_G_KEY, "0"));
if (result.indexOf('#') == -1) return result;
result = result.replace("#phase-md#", formattedPhaseValues.getOrDefault(PHASE_MD_KEY, ""));
if (result.indexOf('#') == -1) return result;
result = result.replace("#phase-mm#", formattedPhaseValues.getOrDefault(PHASE_MM_KEY, ""));
if (result.indexOf('#') == -1) return result;
result = result.replace("#-phase-md#", formattedPhaseValues.getOrDefault(NEG_PHASE_MD_KEY, ""));
if (result.indexOf('#') == -1) return result;
return result.replace("#-phase-mm#", formattedPhaseValues.getOrDefault(NEG_PHASE_MM_KEY, ""));
}
@NotNull
public String getFormattedPhases(@NotNull String text) {
return formattedPhaseValues.getOrDefault(text, "");
}
private boolean containsAnyPlaceholders(String text) {
return text != null && text.indexOf('%') != -1;
}
@NotNull
private Component format(@NotNull String value, @NotNull Player player) {
if (plugin.getConfigManager().getSettings().isComponentCaching()) {
return cachedComponents.computeIfAbsent(value, v ->
plugin.getConfigManager().getSettings().getFormat().format(plugin, player, v)
);
}
return plugin.getConfigManager().getSettings().getFormat().format(plugin, player, value);
}
@NotNull
private String replacePlaceholders(@NotNull String string, @NotNull Player player) {
for (Map.Entry<String, List<Settings.PlaceholderReplacement>> entry : plugin.getConfigManager().getSettings().getPlaceholdersReplacements().entrySet()) {
if (!string.contains(entry.getKey())) {
continue;
}
final String replaced = papiManager.setPlaceholders(player, entry.getKey());
final Optional<Settings.PlaceholderReplacement> replacement = entry.getValue().stream()
.filter(r -> r.placeholder().equals(replaced))
.findFirst();
if (replacement.isPresent()) {
string = string.replace(entry.getKey(), replacement.get().replacement());
} else {
final Optional<Settings.PlaceholderReplacement> elseReplacement = entry.getValue().stream()
.filter(r -> r.placeholder().equalsIgnoreCase(ELSE_PLACEHOLDER))
.findFirst();
if (elseReplacement.isPresent()) {
string = string.replace(entry.getKey(), elseReplacement.get().replacement());
}
}
private String replacePlaceholders(@NotNull String string, @NotNull Player player, @Nullable Player viewer) {
if (!containsAnyPlaceholders(string)) {
return string;
}
if (papiManager.isPAPIEnabled()) {
return papiManager.setPlaceholders(player, string);
final StringBuilder builder = new StringBuilder(string.length() + 32); // Pre-allocate extra space
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(string);
int lastAppendPosition = 0;
while (matcher.find()) {
builder.append(string, lastAppendPosition, matcher.start());
final String placeholder = matcher.group();
final String value = viewer == null ? getCachedPlaceholder(player, placeholder)
: papiManager.setRelationalPlaceholders(viewer, player, placeholder);
final String customReplacement = getReplacement(placeholder, value);
builder.append(customReplacement != null ? customReplacement : value);
lastAppendPosition = matcher.end();
}
return string;
builder.append(string, lastAppendPosition, string.length());
final String intermediateResult = builder.toString();
if (papiManager.isPapiEnabled()) {
return viewer == null ? papiManager.setPlaceholders(player, intermediateResult)
: papiManager.setRelationalPlaceholders(viewer, player, intermediateResult);
}
return intermediateResult;
}
@Nullable
private String getReplacement(@NotNull String placeholder, @NotNull String value) {
var replacements = placeholdersReplacements.get(placeholder.toLowerCase(Locale.ROOT));
if (replacements == null) {
return null;
}
final String replacement = replacements.get(value.toLowerCase(Locale.ROOT));
if (replacement != null) {
return replacement;
}
return replacements.get(ELSE_PLACEHOLDER.toLowerCase(Locale.ROOT));
}
private Map<String, String> getCachedPlaceholders(@NotNull Player player) {
return cachedPlaceholders.computeIfAbsent(player.getUniqueId(), u -> ExpiringMap.builder()
.expiration(plugin.getConfigManager().getSettings().getPlaceholderCacheTime() * 50L, TimeUnit.MILLISECONDS)
.build());
}
@NotNull
public String getCachedPlaceholder(@NotNull Player player, @NotNull String placeholder) {
final Map<String, String> playerCache = getCachedPlaceholders(player);
return playerCache.computeIfAbsent(placeholder, p -> papiManager.setPlaceholders(player, p));
}
}
@@ -0,0 +1,51 @@
package org.alexdev.unlimitednametags.placeholders;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.alexdev.unlimitednametags.UnlimitedNameTags;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class UntPapiExpansion extends PlaceholderExpansion {
private final UnlimitedNameTags plugin;
public UntPapiExpansion(UnlimitedNameTags plugin) {
this.plugin = plugin;
}
@Override
public @NotNull String getIdentifier() {
return "unt";
}
@Override
public @NotNull String getAuthor() {
return "AlexDev";
}
@Override
@SuppressWarnings("UnstableApiUsage")
public @NotNull String getVersion() {
return plugin.getPluginMeta().getVersion();
}
@Override
public boolean persist() {
return true;
}
@Override
public @Nullable String onPlaceholderRequest(Player player, @NotNull String params) {
return switch (params) {
case "phase-mm" -> plugin.getPlaceholderManager().getFormattedPhases(PlaceholderManager.PHASE_MM_KEY);
case "phase-md" -> plugin.getPlaceholderManager().getFormattedPhases(PlaceholderManager.PHASE_MD_KEY);
case "phase-mm-g" -> plugin.getPlaceholderManager().getFormattedPhases(PlaceholderManager.PHASE_MM_G_KEY);
case "-phase-mm" -> plugin.getPlaceholderManager().getFormattedPhases(PlaceholderManager.NEG_PHASE_MM_KEY);
case "-phase-md" -> plugin.getPlaceholderManager().getFormattedPhases(PlaceholderManager.NEG_PHASE_MD_KEY);
default -> null;
};
}
//Placeholders:
}
+6 -2
View File
@@ -5,10 +5,11 @@ api-version: '1.19'
author: AlexDev_
folia-supported: true
libraries:
- de.exlll:configlib-yaml:${configlibVersion}
- de.exlll:configlib-paper:${configlibVersion}
# - de.exlll:configlib-yaml:${configlibVersion}
# - de.exlll:configlib-paper:${configlibVersion}
- net.jodah:expiringmap:${expiringMapVersion}
- org.apache.commons:commons-jexl3:${commonsJexl3Version}
depend:
- packetevents
softdepend:
@@ -19,6 +20,9 @@ softdepend:
- ViaBackwards
- ViaRewind
- Oraxen
- Nexo
- ItemsAdder
- HMCCosmetics
permissions:
unt.shownametags:
default: true