diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 1bec35e..804661f 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -3,6 +3,11 @@ + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 68e2cf5..6dfc449 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,6 +6,9 @@ + + + diff --git a/README.md b/README.md index 7537e5f..5a4f32c 100644 --- a/README.md +++ b/README.md @@ -126,10 +126,6 @@ fun onItemSpawn(event: ItemSpawnEvent) = event.apply { ## Roadmap -- `/feat/rel_placeholders` - Currently the plugin does not support PlaceholderAPI's - relational placeholders. - - `/feat/customization` Extension plugin to give players ability to customize their own name tags by using a command and customizable GUI interface. diff --git a/build.gradle.kts b/build.gradle.kts index 53d94ed..c5feac7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,47 +3,72 @@ import java.io.InputStreamReader plugins { id("java") - alias(libs.plugins.runPaper) - alias(libs.plugins.paperweight) apply false - alias(libs.plugins.shadow) apply true + alias(libs.plugins.run.paper) + alias(libs.plugins.shadow) `maven-publish` } -runPaper.folia.registerTask() - val id = findProperty("id").toString() val pluginName = findProperty("plugin_name") repositories { - mavenLocal() - mavenCentral() - maven("https://repo.papermc.io/repository/maven-public/") maven("https://maven.pvphub.me/releases") - maven("https://repo.dmulloy2.net/repository/public/") - maven("https://jitpack.io") - maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") - maven("https://repo.codemc.io/repository/maven-releases/") - maven("https://maven.evokegames.gg/snapshots") maven("https://repo.viaversion.com") maven("https://repo.codemc.org/repository/maven-public/") { name = "codemc" } + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.dmulloy2.net/repository/public/") + maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") + maven("https://repo.codemc.io/repository/maven-releases/") + maven("https://maven.evokegames.gg/snapshots") + + mavenLocal() + mavenCentral() + // Always make sure to put JitPack at the end of the list for performance reasons + maven("https://jitpack.io") } dependencies { -// paperweight.paperDevBundle(libs.versions.paperApi.get()) - compileOnly(libs.paper.api) - - compileOnly(libs.placeholder.api) - compileOnly(libs.tab.api) - compileOnly(libs.packet.events) - implementation(libs.entity.lib) - testImplementation("org.junit.jupiter:junit-jupiter:5.7.1") - compileOnly("net.skinsrestorer:skinsrestorer-api:15.5.1") + // Provided + compileOnly(libs.paper) + compileOnly(libs.placeholderapi) + compileOnly(libs.tab) + compileOnly(libs.packetevents) + compileOnly(libs.skinsrestorer) + + // Downloaded during runtime + compileOnly(libs.caffeine) + + // Shaded + implementation(libs.entitylib) + implementation(libs.bstats) + + testImplementation(libs.junit.jupiter) } tasks { + jar { + enabled = false + } + + shadowJar { + archiveFileName = "${rootProject.name}-${version}.jar" + archiveClassifier = null + + mergeServiceFiles() + manifest { + attributes["paperweight-mappings-namespace"] = "mojang" + } + + relocate("me.tofaa.entitylib", "com.mattmx.nametags.shaded.entitylib") + relocate("org.bstats", "com.mattmx.nametags.shaded.bstats") + } + + assemble { + dependsOn(shadowJar) + } withType { val props = mapOf( @@ -66,30 +91,35 @@ tasks { mergeServiceFiles() } + build { + dependsOn(shadowJar) + } + test { useJUnitPlatform() } -// assemble { -// dependsOn(reobfJar) -// } - runServer { - val mcVersion = libs.versions.paperApi.get().split("-")[0] + val mcVersion = libs.versions.paper.get().split("-")[0] minecraftVersion(mcVersion) downloadPlugins { hangar("ViaVersion", "5.3.2") hangar("ViaBackwards", "5.3.2") + modrinth("packetevents","2HJtPM2W") // For testing groups in config.yml modrinth("luckperms", "v5.4.145-bukkit") } + + jvmArgs("-Dcom.mojang.eula.agree=true") } + + runPaper.folia.registerTask() } java { -// withJavadocJar() + //withJavadocJar() withSourcesJar() toolchain { diff --git a/gradle.properties b/gradle.properties index 7c3a115..e13323d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ kotlin.code.style=official # Project configuration group_name = com.mattmx id = nametags -version = 1.5.2 +version = 1.5.6 plugin_name = NameTags plugin_main_class_name = NameTags diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ebe73a..4000973 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,35 +1,32 @@ [versions] +shadow = "8.3.6" +runPaper = "2.3.1" + +paper = "1.21.5-R0.1-SNAPSHOT" -paperweight = "1.7.1" -kotlin = "2.0.0" -updateVersions = "0.51.0" -shadow = "8.1.8" -paperApi = "1.21.5-R0.1-SNAPSHOT" placeholderapi = "2.11.6" -ktgui = "2.4.2-alpha" -runPaper = "2.2.4" +packetevents = "2.8.0" +tab = "5.2.0" +skinsrestorer = "15.6.4" +caffeine = "3.2.0" +entitylib = "+1f4aeef-SNAPSHOT" +bstats = "3.1.0" -packetEvents = "2.7.0" -entityLib = "3.0.3-SNAPSHOT" -tab = "4.1.8" -viaversion = "5.0.0" +junit-jupiter = "5.13.0" [libraries] +paper = { module = "io.papermc.paper:paper-api", version.ref = "paper" } -kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } -paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paperApi" } -placeholder-api = { module = "me.clip:placeholderapi", version.ref = "placeholderapi" } -ktgui = { module = "com.mattmx:ktgui", version.ref = "ktgui" } -packet-events = { module = "com.github.retrooper:packetevents-spigot", version.ref = "packetEvents" } -entity-lib = { module = "me.tofaa.entitylib:spigot", version.ref = "entityLib" } -tab-api = { module = "com.github.NEZNAMY:TAB-API", version.ref = "tab" } -via-version = { module = "com.viaversion:viaversion-api", version.ref = "viaversion" } +placeholderapi = { module = "me.clip:placeholderapi", version.ref = "placeholderapi" } +packetevents = { module = "com.github.retrooper:packetevents-spigot", version.ref = "packetevents" } +tab = { module = "com.github.NEZNAMY:TAB-API", version.ref = "tab" } +skinsrestorer = { module = "net.skinsrestorer:skinsrestorer-api", version.ref = "skinsrestorer" } +caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" } +entitylib = { module = "me.tofaa.entitylib:spigot", version.ref = "entitylib" } +bstats = { group = "org.bstats", name = "bstats-bukkit", version.ref = "bstats" } -[plugins] +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } -kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -updateDeps = { id = "com.github.ben-manes.versions", version.ref = "updateVersions" } -shadow = { id = "io.github.goooler.shadow", version.ref = "shadow" } -paperweight = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight" } -runPaper = { id = "xyz.jpenilla.run-paper", version.ref = "runPaper" } \ No newline at end of file +[plugins] +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } +run-paper = { id = "xyz.jpenilla.run-paper", version.ref = "runPaper" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e583..1b33c55 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e846b64..002b867 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..23d15a9 100644 --- a/gradlew +++ b/gradlew @@ -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/master/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/. @@ -80,13 +82,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -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 @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -133,22 +133,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,18 +200,28 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, 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" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..db3a6ac 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @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 +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,32 +59,34 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/java/com/mattmx/nametags/DependencyVersionChecker.java b/src/main/java/com/mattmx/nametags/DependencyVersionChecker.java index a6c1fdf..a9317d9 100644 --- a/src/main/java/com/mattmx/nametags/DependencyVersionChecker.java +++ b/src/main/java/com/mattmx/nametags/DependencyVersionChecker.java @@ -11,19 +11,21 @@ public class DependencyVersionChecker { public static void checkPacketEventsVersion() { final PacketEventsAPI api = PacketEvents.getAPI(); - boolean isOutdated = api.getVersion().isOlderThan(PEVersion.fromString("2.7.0")); - boolean isUnsupported = api.getServerManager().getVersion().isNewerThan(ServerVersion.V_1_21_4); + PEVersion currentPEVersion = api.getVersion(); + ServerVersion outdatedMCVersion = ServerVersion.V_1_21_4; + + boolean isOutdated = currentPEVersion.isOlderThan(new PEVersion(2, 8, 0)); + boolean isUnsupported = api.getServerManager().getVersion().isNewerThan(outdatedMCVersion); if (isOutdated && isUnsupported) { - NameTags.getInstance().getComponentLogger().warn(Component.text(""" - - ⚠ PacketEvents version 2.7.0 does not support versions newer than 1.21.4! - - Please update to a development 2.8.0 build that adds 1.21.5+ support. - https://ci.codemc.io/job/retrooper/job/packetevents/ - - """)); + NameTags.getInstance().getComponentLogger().warn(Component.text(String.format(""" + + ⚠ Detected PacketEvents version %s, which does not support Minecraft versions newer than %s! + + Please update to the latest PacketEvents release to ensure compatibility. + Download it here: https://modrinth.com/plugin/packetevents + + """, currentPEVersion.toStringWithoutSnapshot(), outdatedMCVersion.getReleaseName()))); } } - } diff --git a/src/main/java/com/mattmx/nametags/EventsListener.java b/src/main/java/com/mattmx/nametags/EventsListener.java index e34ac6f..6eac7ad 100644 --- a/src/main/java/com/mattmx/nametags/EventsListener.java +++ b/src/main/java/com/mattmx/nametags/EventsListener.java @@ -4,17 +4,14 @@ import com.mattmx.nametags.entity.trait.SneakTrait; import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.player.PlayerRespawnEvent; -import org.bukkit.event.player.PlayerToggleSneakEvent; +import org.bukkit.event.player.*; import org.jetbrains.annotations.NotNull; import org.spigotmc.event.player.PlayerSpawnLocationEvent; -import java.util.concurrent.TimeUnit; +import java.util.UUID; public class EventsListener implements Listener { @@ -24,18 +21,17 @@ public EventsListener(@NotNull NameTags plugin) { this.plugin = plugin; } - @EventHandler + @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onPlayerJoin(@NotNull PlayerJoinEvent event) { Bukkit.getAsyncScheduler().runNow(plugin, (task) -> { - if (!event.getPlayer().isOnline()) { + if (!event.getPlayer().isConnected()) { return; } plugin.getEntityManager() - .getOrCreateNameTagEntity(event.getPlayer()) - .updateVisibility(); + .getOrCreateNameTagEntity(event.getPlayer()) + .updateVisibility(); }); - } // @EventHandler @@ -50,9 +46,10 @@ public void onPlayerJoin(@NotNull PlayerJoinEvent event) { // } // } - @EventHandler + @EventHandler(priority = EventPriority.LOWEST) public void onPlayerQuit(@NotNull PlayerQuitEvent event) { plugin.getEntityManager().removeLastSentPassengersCache(event.getPlayer().getEntityId()); + // TODO(matt): might not be sending de-spawn packet to viewers all the time? // Remove as a viewer from all entities for (final NameTagEntity entity : plugin.getEntityManager().getAllEntities()) { @@ -68,8 +65,7 @@ public void onPlayerQuit(@NotNull PlayerQuitEvent event) { @EventHandler public void onPlayerChangeWorld(@NotNull PlayerChangedWorldEvent event) { - NameTagEntity nameTagEntity = plugin.getEntityManager() - .getNameTagEntity(event.getPlayer()); + NameTagEntity nameTagEntity = plugin.getEntityManager().getNameTagEntity(event.getPlayer()); if (nameTagEntity == null) return; @@ -86,7 +82,7 @@ public void onPlayerChangeWorld(@NotNull PlayerChangedWorldEvent event) { @EventHandler public void onPlayerDeath(@NotNull PlayerDeathEvent event) { NameTagEntity nameTagEntity = plugin.getEntityManager() - .getNameTagEntity(event.getPlayer()); + .getNameTagEntity(event.getPlayer()); if (nameTagEntity == null) return; @@ -99,7 +95,7 @@ public void onPlayerDeath(@NotNull PlayerDeathEvent event) { @EventHandler public void onPlayerRespawn(@NotNull PlayerRespawnEvent event) { NameTagEntity nameTagEntity = plugin.getEntityManager() - .getNameTagEntity(event.getPlayer()); + .getNameTagEntity(event.getPlayer()); if (nameTagEntity == null) return; @@ -130,12 +126,12 @@ public void onPlayerSneak(@NotNull PlayerToggleSneakEvent event) { if (event.getPlayer().isInsideVehicle()) return; NameTagEntity nameTagEntity = plugin.getEntityManager() - .getNameTagEntity(event.getPlayer()); + .getNameTagEntity(event.getPlayer()); if (nameTagEntity == null) return; nameTagEntity.getTraits() - .getOrAddTrait(SneakTrait.class, SneakTrait::new) - .updateSneak(event.isSneaking()); + .getOrAddTrait(SneakTrait.class, SneakTrait::new) + .updateSneak(event.isSneaking()); } } diff --git a/src/main/java/com/mattmx/nametags/NameTags.java b/src/main/java/com/mattmx/nametags/NameTags.java index da3a865..a9dc25d 100644 --- a/src/main/java/com/mattmx/nametags/NameTags.java +++ b/src/main/java/com/mattmx/nametags/NameTags.java @@ -8,10 +8,12 @@ import com.mattmx.nametags.entity.NameTagEntityManager; import com.mattmx.nametags.hook.NeznamyTABHook; import com.mattmx.nametags.hook.SkinRestorerHook; -import com.mattmx.nametags.utils.Metrics; +import com.mattmx.nametags.utils.test.TestPlaceholderExpansion; import me.tofaa.entitylib.APIConfig; import me.tofaa.entitylib.EntityLib; import me.tofaa.entitylib.spigot.SpigotEntityLibPlatform; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.DrilldownPie; import org.bukkit.Bukkit; import org.bukkit.Color; import org.bukkit.configuration.ConfigurationSection; @@ -31,8 +33,8 @@ public class NameTags extends JavaPlugin { public static final int TRANSPARENT = Color.fromARGB(0).asARGB(); public static final char LEGACY_CHAR = (char) 167; private static @Nullable NameTags instance; - private @Nullable Executor executor = null; private final HashMap groups = new HashMap<>(); + private @Nullable Executor executor = null; private @NotNull TextFormatter formatter = TextFormatter.MINI_MESSAGE; private NameTagEntityManager entityManager; private EventsListener eventsListener; @@ -40,6 +42,10 @@ public class NameTags extends JavaPlugin { private Metrics metrics; private @Nullable ConfigDefaultsListener defaultsListener = null; + public static @NotNull NameTags getInstance() { + return Objects.requireNonNull(instance, "NameTags plugin has not initialized yet! Did you forget to depend?"); + } + @Override public void onEnable() { instance = this; @@ -54,11 +60,11 @@ public void onEnable() { registerMetrics(); executor = Executors.newFixedThreadPool( - getConfig().getInt("options.threads", 2), - new ThreadFactoryBuilder() - .setPriority(Thread.NORM_PRIORITY + 1) - .setNameFormat("NameTags-Processor") - .build() + getConfig().getInt("options.threads", 2), + new ThreadFactoryBuilder() + .setPriority(Thread.NORM_PRIORITY + 1) + .setNameFormat("NameTags-Processor") + .build() ); SpigotEntityLibPlatform platform = new SpigotEntityLibPlatform(this); @@ -77,7 +83,11 @@ public void onEnable() { Bukkit.getPluginManager().registerEvents(eventsListener, this); Bukkit.getScheduler().runTaskLater(this, DependencyVersionChecker::checkPacketEventsVersion, 10L); - Objects.requireNonNull(Bukkit.getPluginCommand("nametags-reload")).setExecutor(new NameTagsCommand(this)); + Objects.requireNonNull(Bukkit.getPluginCommand("nametags")).setExecutor(new NameTagsCommand(this)); + + if (false) { + new TestPlaceholderExpansion().register(); + } } @Override @@ -98,7 +108,7 @@ public void reloadConfig() { String textFormatterIdentifier = getConfig().getString("formatter", "minimessage"); formatter = TextFormatter.getById(textFormatterIdentifier) - .orElse(TextFormatter.MINI_MESSAGE); + .orElse(TextFormatter.MINI_MESSAGE); getLogger().info("Using " + formatter.name() + " as text formatter."); @@ -124,7 +134,7 @@ public void reloadConfig() { } public void registerMetrics() { - metrics.addCustomChart(new Metrics.DrilldownPie("serverName", () -> Map.of(Bukkit.getName(), Map.of(Bukkit.getName(), 1)))); + metrics.addCustomChart(new DrilldownPie("serverName", () -> Map.of(Bukkit.getName(), Map.of(Bukkit.getName(), 1)))); } @Override @@ -134,8 +144,8 @@ public void onDisable() { HandlerList.unregisterAll(this.eventsListener); PacketEvents.getAPI() - .getEventManager() - .unregisterListener(this.packetListener); + .getEventManager() + .unregisterListener(this.packetListener); } public Executor getExecutor() { @@ -157,8 +167,4 @@ public HashMap getGroups() { public @NotNull TextFormatter getFormatter() { return this.formatter; } - - public static @NotNull NameTags getInstance() { - return Objects.requireNonNull(instance, "NameTags plugin has not initialized yet! Did you forget to depend?"); - } } diff --git a/src/main/java/com/mattmx/nametags/NameTagsCommand.java b/src/main/java/com/mattmx/nametags/NameTagsCommand.java index 6a16d6f..4ddfcf3 100644 --- a/src/main/java/com/mattmx/nametags/NameTagsCommand.java +++ b/src/main/java/com/mattmx/nametags/NameTagsCommand.java @@ -2,18 +2,22 @@ import com.mattmx.nametags.entity.NameTagEntity; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.Map; +import java.util.List; import java.util.UUID; +import java.util.stream.Stream; -public class NameTagsCommand implements CommandExecutor { +public class NameTagsCommand implements CommandExecutor, TabCompleter { private final @NotNull NameTags plugin; public NameTagsCommand(@NotNull NameTags plugin) { @@ -22,6 +26,59 @@ public NameTagsCommand(@NotNull NameTags plugin) { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + // SHUT UP EVA + + if (args.length == 0) { + return false; + } + + if (args[0].equalsIgnoreCase("reload")) { + reload(); + sender.sendMessage(Component.text("Reloaded!").color(NamedTextColor.GREEN)); + } else if (args[0].equalsIgnoreCase("debug")) { + sender.sendMessage( + Component.text("NameTags debug") + .appendNewline() + .append( + Component.text("Total NameTags: " + plugin.getEntityManager().getCacheSize()) + .hoverEvent(HoverEvent.showText( + Component.text("By Entity UUID: " + plugin.getEntityManager().getCacheSize()) + .appendNewline() + .append(Component.text("By Entity ID: " + plugin.getEntityManager().getEntityIdMapSize())) + .appendNewline() + .append(Component.text("By Passenger ID: " + plugin.getEntityManager().getPassengerIdMapSize())) + )) + .color(NamedTextColor.WHITE) + ) + .appendNewline() + .append( + Component.text("Cached last sent passengers: " + plugin.getEntityManager().getLastSentPassengersSize()) + .color(NamedTextColor.WHITE) + ) + .appendNewline() + .append( + Component.text("Viewers:") + .appendNewline() + .append( + Component.text( + String.join("\n", + plugin.getEntityManager() + .getAllEntities() + .stream() + .map((nameTag) -> " - " + nameTag.getBukkitEntity().getUniqueId() + ": " + nameTag.getPassenger().getViewers()) + .toList() + ) + ) + ) + ) + .color(NamedTextColor.GOLD) + ); + } + + return false; + } + + private void reload() { for (final Player player : Bukkit.getOnlinePlayers()) { final NameTagEntity tag = plugin.getEntityManager().getNameTagEntity(player); @@ -56,8 +113,14 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command newTag.updateVisibility(); } + } - sender.sendMessage(Component.text("Reloaded!").color(NamedTextColor.GREEN)); - return false; + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String @NotNull [] args) { + String lastArg = args.length >= 1 ? args[0].toLowerCase() : ""; + return Stream.of("reload", "debug") + .filter((arg) -> arg.toLowerCase().startsWith(lastArg)) + .toList(); } } diff --git a/src/main/java/com/mattmx/nametags/NameTagsLoader.java b/src/main/java/com/mattmx/nametags/NameTagsLoader.java index d0875fd..c9caa81 100644 --- a/src/main/java/com/mattmx/nametags/NameTagsLoader.java +++ b/src/main/java/com/mattmx/nametags/NameTagsLoader.java @@ -21,13 +21,13 @@ public void classloader(@NotNull PluginClasspathBuilder classpathBuilder) { // File to override version final File override = classpathBuilder.getContext() - .getDataDirectory() - .resolve(".override") - .toFile(); + .getDataDirectory() + .resolve(".override") + .toFile(); - String entityLibVersion = "3.0.3-SNAPSHOT"; + String entityLibVersion = "+1f4aeef-SNAPSHOT"; if (override.exists()) { - try(BufferedReader reader = new BufferedReader(new FileReader(override))) { + try (BufferedReader reader = new BufferedReader(new FileReader(override))) { entityLibVersion = reader.readLine(); } catch (Exception error) { error.printStackTrace(); @@ -36,17 +36,17 @@ public void classloader(@NotNull PluginClasspathBuilder classpathBuilder) { MavenLibraryResolver resolver = new MavenLibraryResolver(); resolver.addRepository( - new RemoteRepository.Builder( - "evoke-games", - "default", - "https://maven.evokegames.gg/snapshots" - ).build() + new RemoteRepository.Builder( + "evoke-games", + "default", + "https://maven.evokegames.gg/snapshots" + ).build() ); resolver.addDependency( - new Dependency( - new DefaultArtifact("me.tofaa.entitylib:spigot:" + entityLibVersion), - null - ).setOptional(false) + new Dependency( + new DefaultArtifact("me.tofaa.entitylib:spigot:" + entityLibVersion), + null + ).setOptional(false) ); classpathBuilder.addLibrary(resolver); diff --git a/src/main/java/com/mattmx/nametags/config/ConfigDefaultsListener.java b/src/main/java/com/mattmx/nametags/config/ConfigDefaultsListener.java index b9fd599..2ff96ee 100644 --- a/src/main/java/com/mattmx/nametags/config/ConfigDefaultsListener.java +++ b/src/main/java/com/mattmx/nametags/config/ConfigDefaultsListener.java @@ -6,15 +6,12 @@ import com.mattmx.nametags.entity.trait.SneakTrait; import com.mattmx.nametags.event.NameTagEntityCreateEvent; import me.tofaa.entitylib.meta.display.AbstractDisplayMeta; -import org.bukkit.Color; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; -import java.util.Comparator; import java.util.List; import java.util.Map; @@ -65,50 +62,48 @@ public void registerDefaultRefreshListener(@NotNull NameTagEntity tag, long refr plugin, refreshMillis, (entity) -> { - synchronized (entity) { - TextDisplayMetaConfiguration.applyMeta(defaultSection(), entity.getMeta()); - TextDisplayMetaConfiguration.applyTextMeta(defaultSection(), entity.getMeta(), player); - - // TODO we should cache this stuff - List> groups = plugin.getGroups() - .entrySet() - .stream() - .filter((e) -> player.hasPermission(e.getKey())) - .sorted(GroupPriorityComparator.get()) - .toList(); - - long recentRefreshEvery = plugin.getConfig().getLong("defaults.refresh-every", 50); - if (!groups.isEmpty()) { - Map.Entry highest = groups.getLast(); - - TextDisplayMetaConfiguration.applyMeta(highest.getValue(), entity.getMeta()); - TextDisplayMetaConfiguration.applyTextMeta(highest.getValue(), entity.getMeta(), player); - - long groupRefresh = highest.getValue().getLong("refresh-every", -1); - if (groupRefresh > 0) { - recentRefreshEvery = groupRefresh; - } + TextDisplayMetaConfiguration.applyMeta(defaultSection(), entity.getMeta()); + TextDisplayMetaConfiguration.applyTextMeta(defaultSection(), entity.getMeta(), player); + + // TODO we should cache this stuff + List> groups = plugin.getGroups() + .entrySet() + .stream() + .filter((e) -> player.hasPermission(e.getKey())) + .sorted(GroupPriorityComparator.get()) + .toList(); + + long recentRefreshEvery = plugin.getConfig().getLong("defaults.refresh-every", 50); + if (!groups.isEmpty()) { + Map.Entry highest = groups.getLast(); + + TextDisplayMetaConfiguration.applyMeta(highest.getValue(), entity.getMeta()); + TextDisplayMetaConfiguration.applyTextMeta(highest.getValue(), entity.getMeta(), player); + + long groupRefresh = highest.getValue().getLong("refresh-every", -1); + if (groupRefresh > 0) { + recentRefreshEvery = groupRefresh; } + } - if (recentRefreshEvery != refreshMillis) { - entity.getTraits().removeTrait(RefreshTrait.class); - registerDefaultRefreshListener(tag, recentRefreshEvery); - } + if (recentRefreshEvery != refreshMillis) { + entity.getTraits().removeTrait(RefreshTrait.class); + registerDefaultRefreshListener(tag, recentRefreshEvery); + } - if (entity.getMeta().getBillboardConstraints() == AbstractDisplayMeta.BillboardConstraints.CENTER) { - // Look passenger down to remove debug getting in the way - entity.getPassenger().rotateHead(0f, 90f); - } + if (entity.getMeta().getBillboardConstraints() == AbstractDisplayMeta.BillboardConstraints.CENTER) { + // Look passenger down to remove debug getting in the way + entity.getPassenger().rotateHead(0f, 90f); + } - // Preserve background color for sneaking - // Maybe we should introduce an `afterRefresh` callback? - entity.getTraits() - .getTrait(SneakTrait.class) - .ifPresent(SneakTrait::manuallyUpdateSneakingOpacity); + // Preserve background color for sneaking + // Maybe we should introduce an `afterRefresh` callback? + entity.getTraits() + .getTrait(SneakTrait.class) + .ifPresent(SneakTrait::manuallyUpdateSneakingOpacity); - entity.updateVisibility(); - entity.getPassenger().refresh(); - } + entity.updateVisibility(); + entity.getPassenger().refresh(); } ) ); diff --git a/src/main/java/com/mattmx/nametags/config/TextFormatter.java b/src/main/java/com/mattmx/nametags/config/TextFormatter.java index 3ea4902..802bc4f 100644 --- a/src/main/java/com/mattmx/nametags/config/TextFormatter.java +++ b/src/main/java/com/mattmx/nametags/config/TextFormatter.java @@ -55,12 +55,12 @@ public enum TextFormatter { return MINI_MESSAGE.format(mutableLine); } - ) - ; + ); // Converts legacy hex format &x&9&0&0&c&3&f -> ΄c3f modern hex format // https://github.com/Matt-MX/DisplayNameTags/issues/32#issuecomment-2509403581 private static final Pattern LEGACY_HEX_PATTERN = Pattern.compile("&x(&[0-9a-fA-F]){6}"); + public static String convertLegacyHex(String input) { Matcher matcher = LEGACY_HEX_PATTERN.matcher(input); diff --git a/src/main/java/com/mattmx/nametags/entity/NameTagEntity.java b/src/main/java/com/mattmx/nametags/entity/NameTagEntity.java index 9d4010a..c4d6c79 100644 --- a/src/main/java/com/mattmx/nametags/entity/NameTagEntity.java +++ b/src/main/java/com/mattmx/nametags/entity/NameTagEntity.java @@ -115,11 +115,10 @@ public PacketWrapper getPassengersPacket() { } public @NotNull Location updateLocation() { - Location location = SpigotConversionUtil.fromBukkitLocation( - bukkitEntity.getLocation() - .clone() - .add(0.0, bukkitEntity.getBoundingBox().getMaxY(), 0.0) - ); + org.bukkit.Location bukkitLocation = bukkitEntity.getLocation(); + bukkitLocation.setY(bukkitEntity.getBoundingBox().getMaxY()); + + Location location = SpigotConversionUtil.fromBukkitLocation(bukkitLocation); location.setYaw(0f); location.setPitch(0f); diff --git a/src/main/java/com/mattmx/nametags/entity/NameTagEntityManager.java b/src/main/java/com/mattmx/nametags/entity/NameTagEntityManager.java index 14bdf54..990b09e 100644 --- a/src/main/java/com/mattmx/nametags/entity/NameTagEntityManager.java +++ b/src/main/java/com/mattmx/nametags/entity/NameTagEntityManager.java @@ -1,26 +1,36 @@ package com.mattmx.nametags.entity; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; import com.github.retrooper.packetevents.util.Vector3f; +import com.mattmx.nametags.NameTags; import com.mattmx.nametags.event.NameTagEntityCreateEvent; import me.tofaa.entitylib.meta.display.AbstractDisplayMeta; import me.tofaa.entitylib.meta.display.TextDisplayMeta; import org.bukkit.Bukkit; import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.time.Duration; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; public class NameTagEntityManager { - private final @NotNull Map nameTagByEntityUUID = Collections.synchronizedMap(new HashMap<>()); - private final @NotNull Map nameTagEntityByEntityId = Collections.synchronizedMap(new HashMap<>()); - private final @NotNull Map nameTagEntityByPassengerEntityId = Collections.synchronizedMap(new HashMap<>()); - private final Map lastSentPassengers = new ConcurrentHashMap<>(); + private final Cache nameTagCache = Caffeine.newBuilder() + .expireAfterAccess(Duration.ofMinutes(1)) + .removalListener(this::handleRemoval) + .build(); + + private final ConcurrentHashMap nameTagEntityByEntityId = new ConcurrentHashMap<>(); + private final ConcurrentHashMap nameTagEntityByPassengerEntityId = new ConcurrentHashMap<>(); + private final ConcurrentHashMap lastSentPassengers = new ConcurrentHashMap<>(); + private @NotNull BiConsumer defaultProvider = (entity, meta) -> { - // Default minecraft name-tag appearance meta.setText(entity.name()); meta.setTranslation(new Vector3f(0f, 0.2f, 0f)); meta.setBillboardConstraints(AbstractDisplayMeta.BillboardConstraints.CENTER); @@ -28,48 +38,59 @@ public class NameTagEntityManager { }; public @NotNull NameTagEntity getOrCreateNameTagEntity(@NotNull Entity entity) { - return nameTagByEntityUUID.computeIfAbsent(entity.getUniqueId(), (k) -> { - NameTagEntity nameTagEntity = new NameTagEntity(entity); + NameTagEntity tagEntity = nameTagCache.get(entity.getUniqueId(), uuid -> { + NameTagEntity newlyCreated = new NameTagEntity(entity); - nameTagEntity.getPassenger().consumeEntityMeta(TextDisplayMeta.class, (meta) -> defaultProvider.accept(entity, meta)); + newlyCreated.getPassenger().consumeEntityMeta(TextDisplayMeta.class, meta -> + defaultProvider.accept(entity, meta) + ); - Bukkit.getPluginManager().callEvent(new NameTagEntityCreateEvent(nameTagEntity)); + Bukkit.getPluginManager().callEvent(new NameTagEntityCreateEvent(newlyCreated)); - nameTagEntityByEntityId.put(entity.getEntityId(), nameTagEntity); - nameTagEntityByPassengerEntityId.put(nameTagEntity.getPassenger().getEntityId(), nameTagEntity); + nameTagEntityByEntityId.put(entity.getEntityId(), newlyCreated); + nameTagEntityByPassengerEntityId.put(newlyCreated.getPassenger().getEntityId(), newlyCreated); - return nameTagEntity; + return newlyCreated; }); + return Objects.requireNonNull(tagEntity, "Cache.get(…) unexpectedly returned null for UUID " + entity.getUniqueId()); } public @Nullable NameTagEntity removeEntity(@NotNull Entity entity) { - final NameTagEntity nameTagEntity = nameTagEntityByPassengerEntityId.remove(entity.getEntityId()); - - if (nameTagEntity != null) { - nameTagEntityByPassengerEntityId.remove(nameTagEntity.getPassenger().getEntityId()); + lastSentPassengers.remove(entity.getEntityId()); + nameTagCache.invalidate(entity.getUniqueId()); + + final NameTagEntity removed = nameTagEntityByEntityId.remove(entity.getEntityId()); + if (removed != null) { + nameTagEntityByPassengerEntityId.remove(removed.getPassenger().getEntityId()); + } else { + throw new IllegalArgumentException("No cached NameTag by the passenger entity ID, this could be a memory leak."); } - return nameTagByEntityUUID.remove(entity.getUniqueId()); + return removed; } public @Nullable NameTagEntity getNameTagEntity(@NotNull Entity entity) { - return nameTagByEntityUUID.get(entity.getUniqueId()); + return nameTagCache.getIfPresent(entity.getUniqueId()); } public @Nullable NameTagEntity getNameTagEntityByUUID(UUID uuid) { - return nameTagByEntityUUID.get(uuid); + return nameTagCache.getIfPresent(uuid); } public @Nullable NameTagEntity getNameTagEntityById(int entityId) { return nameTagEntityByEntityId.get(entityId); } - public @Nullable NameTagEntity getNameTagEntityByTagEntityId(int entityId) { - return nameTagEntityByPassengerEntityId.get(entityId); + public @Nullable NameTagEntity getNameTagEntityByTagEntityId(int tagEntityId) { + return nameTagEntityByPassengerEntityId.get(tagEntityId); + } + + public @NotNull Map getMappedEntities() { + return nameTagCache.asMap(); } public @NotNull Collection getAllEntities() { - return this.nameTagByEntityUUID.values(); + return nameTagCache.asMap().values(); } public void setDefaultProvider(@NotNull BiConsumer consumer) { @@ -87,4 +108,44 @@ public void removeLastSentPassengersCache(int entityId) { public @NotNull Optional getLastSentPassengers(int entityId) { return Optional.ofNullable(this.lastSentPassengers.get(entityId)); } + + public int getCacheSize() { + return nameTagCache.asMap().size(); + } + + public int getEntityIdMapSize() { + return nameTagEntityByEntityId.size(); + } + + public int getPassengerIdMapSize() { + return nameTagEntityByPassengerEntityId.size(); + } + + public int getLastSentPassengersSize() { + return lastSentPassengers.size(); + } + + private void handleRemoval(UUID uuid, NameTagEntity tagEntity, RemovalCause cause) { + if (cause != RemovalCause.EXPIRED || tagEntity == null) return; + + Entity entity = tagEntity.getBukkitEntity(); + + if (entity instanceof Player player) { + if (!player.isOnline()) { + tagEntity.destroy(); + removeEntity(entity); + } else { + this.nameTagCache.put(uuid, tagEntity); + } + } else { + Bukkit.getScheduler().runTask(NameTags.getInstance(), () -> { + if (Bukkit.getEntity(uuid) == null) { + tagEntity.destroy(); + removeEntity(entity); + } else { + this.nameTagCache.put(uuid, tagEntity); + } + }); + } + } } diff --git a/src/main/java/com/mattmx/nametags/hook/NeznamyTABHook.java b/src/main/java/com/mattmx/nametags/hook/NeznamyTABHook.java index 50a8e2e..39ea19c 100644 --- a/src/main/java/com/mattmx/nametags/hook/NeznamyTABHook.java +++ b/src/main/java/com/mattmx/nametags/hook/NeznamyTABHook.java @@ -5,7 +5,6 @@ import me.neznamy.tab.api.TabPlayer; import me.neznamy.tab.api.event.player.PlayerLoadEvent; import me.neznamy.tab.api.nametag.NameTagManager; -import me.neznamy.tab.api.nametag.UnlimitedNameTagManager; import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; @@ -20,51 +19,16 @@ public static void inject(@NotNull NameTags plugin) { private static void start() { final boolean isTab = Bukkit.getPluginManager().isPluginEnabled("TAB"); - if (!isTab) return; - NameTags plugin = NameTags.getInstance(); - NameTagManager nameTagManager = TabAPI.getInstance().getNameTagManager(); - - boolean isUnlimitedNameTag = false; - - try { - Class.forName("me.neznamy.tab.api.nametag.UnlimitedNameTagManager"); - isUnlimitedNameTag = nameTagManager instanceof UnlimitedNameTagManager; - } catch (ClassNotFoundException ignored) { - } - - if (isUnlimitedNameTag) { - plugin.getLogger().warning(""" - ⚠ TAB UnlimitedNameTags Mode detected! ⚠ - - DisplayNameTags will attempt to disable this module however - we strongly recommend disabling it in TAB's config. - - This is because both TAB UNT mode and DisplayNameTags attempt - to use Passengers to sync positions of custom name tags. - Having both could cause some visual issues in-game. - - Furthermore, the UnlimitedNameTags module is deprecated and - will be removed in 5.0.0 - - Read more at https://gist.github.com/NEZNAMY/f4cabf2fd9251a836b5eb877720dee5c - - """); - } else { - plugin.getLogger().info("Attempting to override TAB's name tags"); - } - - Objects.requireNonNull(TabAPI.getInstance().getEventBus()) - .register(PlayerLoadEvent.class, (event) -> { - final TabPlayer tabPlayer = event.getPlayer(); - - NameTagManager manager = TabAPI.getInstance().getNameTagManager(); + Objects.requireNonNull(TabAPI.getInstance().getEventBus()).register(PlayerLoadEvent.class, (event) -> { + final TabPlayer tabPlayer = event.getPlayer(); + NameTagManager manager = TabAPI.getInstance().getNameTagManager(); - if (manager != null) { - manager.hideNameTag(tabPlayer); - } - }); + if (manager != null) { + manager.hideNameTag(tabPlayer); + } + }); } } diff --git a/src/main/java/com/mattmx/nametags/hook/PapiHook.java b/src/main/java/com/mattmx/nametags/hook/PapiHook.java index 0c04803..90bbc4c 100644 --- a/src/main/java/com/mattmx/nametags/hook/PapiHook.java +++ b/src/main/java/com/mattmx/nametags/hook/PapiHook.java @@ -1,5 +1,6 @@ package com.mattmx.nametags.hook; +import com.mattmx.nametags.NameTags; import me.clip.placeholderapi.PlaceholderAPI; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; @@ -31,13 +32,13 @@ public static Component setPlaceholders(Player one, Component text) { if (!isPapi()) return text; return text.replaceText(TextReplacementConfig.builder() - .match(PLACEHOLDER_REGEX) - .replacement((match, ctx) -> { - String matchedText = match.group(); - String parsed = PlaceholderAPI.setPlaceholders(one, matchedText); - return Component.text(parsed); - }) - .build() + .match(PLACEHOLDER_REGEX) + .replacement((match, ctx) -> { + String matchedText = match.group(); + String parsed = PlaceholderAPI.setPlaceholders(one, matchedText); + return Component.text(parsed); + }) + .build() ); } @@ -45,13 +46,13 @@ public static Component setRelationalPlaceholders(Player one, Player two, Compon if (!isPapi()) return text; return text.replaceText(TextReplacementConfig.builder() - .match(RELATIVE_PLACEHOLDER_REGEX) - .replacement((match, ctx) -> { - String matchedText = match.group(); - String parsed = PlaceholderAPI.setRelationalPlaceholders(one, two, matchedText); - return Component.text(parsed); - }) - .build() + .match(RELATIVE_PLACEHOLDER_REGEX) + .replacement((match, ctx) -> { + String matchedText = match.group(); + String parsed = PlaceholderAPI.setRelationalPlaceholders(one, two, matchedText); + return NameTags.getInstance().getFormatter().format(parsed); + }) + .build() ); } diff --git a/src/main/java/com/mattmx/nametags/packet/PlayServerEntityMetaDataHandler.java b/src/main/java/com/mattmx/nametags/packet/PlayServerEntityMetaDataHandler.java index ee80ce1..09eabb6 100644 --- a/src/main/java/com/mattmx/nametags/packet/PlayServerEntityMetaDataHandler.java +++ b/src/main/java/com/mattmx/nametags/packet/PlayServerEntityMetaDataHandler.java @@ -40,11 +40,11 @@ public class PlayServerEntityMetaDataHandler { private static final Vector3f PRE_1_20_2_TRANSLATION_OFFSET = new Vector3f(0f, 0.4f, 0f); private static final byte ENTITY_OFFSET_INDEX = PacketEvents.getAPI() - .getServerManager() - .getVersion() - .is(VersionComparison.OLDER_THAN, ServerVersion.V_1_20_2) - ? PRE_1_20_2_TRANSLATION_INDEX - : POST_1_20_2_TRANSLATION_INDEX; + .getServerManager() + .getVersion() + .is(VersionComparison.OLDER_THAN, ServerVersion.V_1_20_2) + ? PRE_1_20_2_TRANSLATION_INDEX + : POST_1_20_2_TRANSLATION_INDEX; private static final TextComponent RELATIVE_ARG_PREFIX = Component.text("%rel_"); @@ -67,8 +67,8 @@ public static void handlePacket(@NotNull PacketSendEvent event) { // This could prove a concurrency issue, maybe we should keep track of if there is a newer packet processing? plugin.getExecutor().execute(() -> { boolean isOldClient = eventClone.getUser() - .getClientVersion() - .isOlderThan(ClientVersion.V_1_20_2); + .getClientVersion() + .isOlderThan(ClientVersion.V_1_20_2); boolean containsEntityOffset = false; @Nullable EntityData textEntry = null; @@ -94,21 +94,21 @@ public static void handlePacket(@NotNull PacketSendEvent event) { if (isOldClient && !containsEntityOffset) { // If there was no offset found then add one ourselves for the offset. packet.getEntityMetadata().add(new EntityData( - ENTITY_OFFSET_INDEX, - EntityDataTypes.VECTOR3F, - PRE_1_20_2_TRANSLATION_OFFSET + ENTITY_OFFSET_INDEX, + EntityDataTypes.VECTOR3F, + PRE_1_20_2_TRANSLATION_OFFSET )); } // Apply relational placeholders to the text of an outgoing display entity if (plugin.getConfig().getBoolean("options.relative-placeholders-support") && - nameTagEntity.getBukkitEntity() instanceof Player from && - textEntry != null + nameTagEntity.getBukkitEntity() instanceof Player from && + textEntry != null ) { final TextComponent originalText = (TextComponent) textEntry.getValue(); final Player to = eventClone.getPlayer(); - boolean containsRelativePlaceholder = ComponentUtils.startsWith(originalText, RELATIVE_ARG_PREFIX); + boolean containsRelativePlaceholder = ComponentUtils.contains(originalText, RELATIVE_ARG_PREFIX); // If it doesn't have any placeholders in then stop if (!containsRelativePlaceholder) { diff --git a/src/main/java/com/mattmx/nametags/packet/PlayServerSetPassengersHandler.java b/src/main/java/com/mattmx/nametags/packet/PlayServerSetPassengersHandler.java index 25a5c8a..2181802 100644 --- a/src/main/java/com/mattmx/nametags/packet/PlayServerSetPassengersHandler.java +++ b/src/main/java/com/mattmx/nametags/packet/PlayServerSetPassengersHandler.java @@ -36,8 +36,8 @@ public static void handlePacket(@NotNull PacketSendEvent event) { packet.setPassengers(passengers); NameTags.getInstance() - .getEntityManager() - .setLastSentPassengers(packet.getEntityId(), passengers); + .getEntityManager() + .setLastSentPassengers(packet.getEntityId(), passengers); event.markForReEncode(true); } diff --git a/src/main/java/com/mattmx/nametags/packet/PlayServerSpawnEntityHandler.java b/src/main/java/com/mattmx/nametags/packet/PlayServerSpawnEntityHandler.java index 1a4ae9e..c53f1ec 100644 --- a/src/main/java/com/mattmx/nametags/packet/PlayServerSpawnEntityHandler.java +++ b/src/main/java/com/mattmx/nametags/packet/PlayServerSpawnEntityHandler.java @@ -1,11 +1,19 @@ package com.mattmx.nametags.packet; +import com.github.retrooper.packetevents.PacketEvents; import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes; +import com.github.retrooper.packetevents.protocol.player.User; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity; import com.mattmx.nametags.NameTags; import com.mattmx.nametags.entity.NameTagEntity; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + /** * Responsible for appending the name tag spawn packet and * passenger packet with the name tag entity when sending @@ -20,22 +28,41 @@ public static void handlePacket(@NotNull PacketSendEvent event) { if (packet.getUUID().isEmpty()) return; - final NameTagEntity nameTagEntity = plugin.getEntityManager().getNameTagEntityByUUID(packet.getUUID().get()); + final UUID packetUUID = packet.getUUID().get(); + final NameTagEntity nameTagEntity = plugin.getEntityManager().getNameTagEntityByUUID(packetUUID); + + final User user = event.getUser(); + if (nameTagEntity == null) { + + // If it's a player, and they don't have a name tag yet, retry after a delay. + if (packet.getEntityType() == EntityTypes.PLAYER) { + Bukkit.getAsyncScheduler().runDelayed(plugin, (task) -> { + final NameTagEntity nameTagEntity0 = plugin.getEntityManager().getNameTagEntityByUUID(packetUUID); + + if (nameTagEntity0 == null) { + return; + } - if (nameTagEntity == null) return; + attachPassengerToEntity(nameTagEntity0, user); + }, 1L, TimeUnit.SECONDS); + } + + return; + } // Add passenger and send to player after (off the netty thread) - final PacketSendEvent clone = event.clone(); - event.getTasksAfterSend().add(() -> plugin.getExecutor().execute(() -> { - // To avoid name tag moving when being added - nameTagEntity.updateLocation(); + event.getTasksAfterSend().add(() -> plugin.getExecutor().execute(() -> attachPassengerToEntity(nameTagEntity, user))); + } + + private static void attachPassengerToEntity(final NameTagEntity nameTagEntity, final User receiver) { + // To avoid name tag moving when being added + nameTagEntity.updateLocation(); - // Refreshes as viewer (crusty fix) - nameTagEntity.getPassenger().removeViewer(clone.getUser()); - nameTagEntity.getPassenger().addViewer(clone.getUser()); + // Refreshes as viewer (crusty fix) + nameTagEntity.getPassenger().removeViewer(receiver); + nameTagEntity.getPassenger().addViewer(receiver); - clone.getUser().sendPacket(nameTagEntity.getPassengersPacket()); - })); + receiver.sendPacket(nameTagEntity.getPassengersPacket()); } } diff --git a/src/main/java/com/mattmx/nametags/utils/ComponentUtils.java b/src/main/java/com/mattmx/nametags/utils/ComponentUtils.java index 497be6f..240cbac 100644 --- a/src/main/java/com/mattmx/nametags/utils/ComponentUtils.java +++ b/src/main/java/com/mattmx/nametags/utils/ComponentUtils.java @@ -5,13 +5,13 @@ public class ComponentUtils { - public static boolean startsWith(@NotNull TextComponent checking, @NotNull TextComponent test) { + public static boolean contains(@NotNull TextComponent checking, @NotNull TextComponent test) { return checking.contains(test, (a, b) -> { if (!(a instanceof TextComponent aText) || !(b instanceof TextComponent bText)) { return false; } - return (aText).content().startsWith((bText).content()); + return aText.content().contains(bText.content()); }); } diff --git a/src/main/java/com/mattmx/nametags/utils/Metrics.java b/src/main/java/com/mattmx/nametags/utils/Metrics.java deleted file mode 100644 index c8b8bb5..0000000 --- a/src/main/java/com/mattmx/nametags/utils/Metrics.java +++ /dev/null @@ -1,912 +0,0 @@ -/* - * This Metrics class was auto-generated and can be copied into your project if you are - * not using a build tool like Gradle or Maven for dependency management. - * - * IMPORTANT: You are not allowed to modify this class, except changing the package. - * - * Disallowed modifications include but are not limited to: - * - Remove the option for users to opt-out - * - Change the frequency for data submission - * - Obfuscate the code (every obfuscator should allow you to make an exception for specific files) - * - Reformat the code (if you use a linter, add an exception) - * - * Violations will result in a ban of your plugin and account from bStats. - */ -package com.mattmx.nametags.utils; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.reflect.Method; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.stream.Collectors; -import java.util.zip.GZIPOutputStream; -import javax.net.ssl.HttpsURLConnection; - -import org.bukkit.Bukkit; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; - -public class Metrics { - - private final Plugin plugin; - - private final MetricsBase metricsBase; - - /** - * Creates a new Metrics instance. - * - * @param plugin Your plugin instance. - * @param serviceId The id of the service. It can be found at What is my plugin id? - */ - public Metrics(Plugin plugin, int serviceId) { - this.plugin = plugin; - // Get the config file - File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); - File configFile = new File(bStatsFolder, "config.yml"); - YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); - if (!config.isSet("serverUuid")) { - config.addDefault("enabled", true); - config.addDefault("serverUuid", UUID.randomUUID().toString()); - config.addDefault("logFailedRequests", false); - config.addDefault("logSentData", false); - config.addDefault("logResponseStatusText", false); - // Inform the server owners about bStats - config - .options() - .header( - "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" - + "many people use their plugin and their total player count. It's recommended to keep bStats\n" - + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" - + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" - + "anonymous.") - .copyDefaults(true); - try { - config.save(configFile); - } catch (IOException ignored) { - } - } - // Load the data - boolean enabled = config.getBoolean("enabled", true); - String serverUUID = config.getString("serverUuid"); - boolean logErrors = config.getBoolean("logFailedRequests", false); - boolean logSentData = config.getBoolean("logSentData", false); - boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); - boolean isFolia = false; - try { - isFolia = Class.forName("io.papermc.paper.threadedregions.RegionizedServer") != null; - } catch (Exception e) { - } - metricsBase = - new // See https://github.com/Bastian/bstats-metrics/pull/126 - // See https://github.com/Bastian/bstats-metrics/pull/126 - // See https://github.com/Bastian/bstats-metrics/pull/126 - // See https://github.com/Bastian/bstats-metrics/pull/126 - // See https://github.com/Bastian/bstats-metrics/pull/126 - // See https://github.com/Bastian/bstats-metrics/pull/126 - // See https://github.com/Bastian/bstats-metrics/pull/126 - MetricsBase( - "bukkit", - serverUUID, - serviceId, - enabled, - this::appendPlatformData, - this::appendServiceData, - isFolia - ? null - : submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), - plugin::isEnabled, - (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), - (message) -> this.plugin.getLogger().log(Level.INFO, message), - logErrors, - logSentData, - logResponseStatusText, - false); - } - - /** - * Shuts down the underlying scheduler service. - */ - public void shutdown() { - metricsBase.shutdown(); - } - - /** - * Adds a custom chart. - * - * @param chart The chart to add. - */ - public void addCustomChart(CustomChart chart) { - metricsBase.addCustomChart(chart); - } - - private void appendPlatformData(JsonObjectBuilder builder) { - builder.appendField("playerAmount", getPlayerAmount()); - builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); - builder.appendField("bukkitVersion", Bukkit.getVersion()); - builder.appendField("bukkitName", Bukkit.getName()); - builder.appendField("javaVersion", System.getProperty("java.version")); - builder.appendField("osName", System.getProperty("os.name")); - builder.appendField("osArch", System.getProperty("os.arch")); - builder.appendField("osVersion", System.getProperty("os.version")); - builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); - } - - private void appendServiceData(JsonObjectBuilder builder) { - builder.appendField("pluginVersion", plugin.getDescription().getVersion()); - } - - private int getPlayerAmount() { - try { - // Around MC 1.8 the return type was changed from an array to a collection, - // This fixes java.lang.NoSuchMethodError: - // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; - Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); - return onlinePlayersMethod.getReturnType().equals(Collection.class) - ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() - : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; - } catch (Exception e) { - // Just use the new method if the reflection failed - return Bukkit.getOnlinePlayers().size(); - } - } - - public static class MetricsBase { - - /** - * The version of the Metrics class. - */ - public static final String METRICS_VERSION = "3.1.0"; - - private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; - - private final ScheduledExecutorService scheduler; - - private final String platform; - - private final String serverUuid; - - private final int serviceId; - - private final Consumer appendPlatformDataConsumer; - - private final Consumer appendServiceDataConsumer; - - private final Consumer submitTaskConsumer; - - private final Supplier checkServiceEnabledSupplier; - - private final BiConsumer errorLogger; - - private final Consumer infoLogger; - - private final boolean logErrors; - - private final boolean logSentData; - - private final boolean logResponseStatusText; - - private final Set customCharts = new HashSet<>(); - - private final boolean enabled; - - /** - * Creates a new MetricsBase class instance. - * - * @param platform The platform of the service. - * @param serviceId The id of the service. - * @param serverUuid The server uuid. - * @param enabled Whether or not data sending is enabled. - * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and - * appends all platform-specific data. - * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and - * appends all service-specific data. - * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be - * used to delegate the data collection to a another thread to prevent errors caused by - * concurrency. Can be {@code null}. - * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. - * @param errorLogger A consumer that accepts log message and an error. - * @param infoLogger A consumer that accepts info log messages. - * @param logErrors Whether or not errors should be logged. - * @param logSentData Whether or not the sent data should be logged. - * @param logResponseStatusText Whether or not the response status text should be logged. - * @param skipRelocateCheck Whether or not the relocate check should be skipped. - */ - public MetricsBase( - String platform, - String serverUuid, - int serviceId, - boolean enabled, - Consumer appendPlatformDataConsumer, - Consumer appendServiceDataConsumer, - Consumer submitTaskConsumer, - Supplier checkServiceEnabledSupplier, - BiConsumer errorLogger, - Consumer infoLogger, - boolean logErrors, - boolean logSentData, - boolean logResponseStatusText, - boolean skipRelocateCheck) { - ScheduledThreadPoolExecutor scheduler = - new ScheduledThreadPoolExecutor( - 1, - task -> { - Thread thread = new Thread(task, "bStats-Metrics"); - thread.setDaemon(true); - return thread; - }); - // We want delayed tasks (non-periodic) that will execute in the future to be - // cancelled when the scheduler is shutdown. - // Otherwise, we risk preventing the server from shutting down even when - // MetricsBase#shutdown() is called - scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); - this.scheduler = scheduler; - this.platform = platform; - this.serverUuid = serverUuid; - this.serviceId = serviceId; - this.enabled = enabled; - this.appendPlatformDataConsumer = appendPlatformDataConsumer; - this.appendServiceDataConsumer = appendServiceDataConsumer; - this.submitTaskConsumer = submitTaskConsumer; - this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; - this.errorLogger = errorLogger; - this.infoLogger = infoLogger; - this.logErrors = logErrors; - this.logSentData = logSentData; - this.logResponseStatusText = logResponseStatusText; - if (!skipRelocateCheck) { - checkRelocation(); - } - if (enabled) { - // WARNING: Removing the option to opt-out will get your plugin banned from - // bStats - startSubmitting(); - } - } - - public void addCustomChart(CustomChart chart) { - this.customCharts.add(chart); - } - - public void shutdown() { - scheduler.shutdown(); - } - - private void startSubmitting() { - final Runnable submitTask = - () -> { - if (!enabled || !checkServiceEnabledSupplier.get()) { - // Submitting data or service is disabled - scheduler.shutdown(); - return; - } - if (submitTaskConsumer != null) { - submitTaskConsumer.accept(this::submitData); - } else { - this.submitData(); - } - }; - // Many servers tend to restart at a fixed time at xx:00 which causes an uneven - // distribution of requests on the - // bStats backend. To circumvent this problem, we introduce some randomness into - // the initial and second delay. - // WARNING: You must not modify and part of this Metrics class, including the - // submit delay or frequency! - // WARNING: Modifying this code will get your plugin banned on bStats. Just - // don't do it! - long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); - long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); - scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); - scheduler.scheduleAtFixedRate( - submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); - } - - private void submitData() { - final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); - appendPlatformDataConsumer.accept(baseJsonBuilder); - final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); - appendServiceDataConsumer.accept(serviceJsonBuilder); - JsonObjectBuilder.JsonObject[] chartData = - customCharts.stream() - .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) - .filter(Objects::nonNull) - .toArray(JsonObjectBuilder.JsonObject[]::new); - serviceJsonBuilder.appendField("id", serviceId); - serviceJsonBuilder.appendField("customCharts", chartData); - baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); - baseJsonBuilder.appendField("serverUUID", serverUuid); - baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); - JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); - scheduler.execute( - () -> { - try { - // Send the data - sendData(data); - } catch (Exception e) { - // Something went wrong! :( - if (logErrors) { - errorLogger.accept("Could not submit bStats metrics data", e); - } - } - }); - } - - private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { - if (logSentData) { - infoLogger.accept("Sent bStats metrics data: " + data.toString()); - } - String url = String.format(REPORT_URL, platform); - HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); - // Compress the data to save bandwidth - byte[] compressedData = compress(data.toString()); - connection.setRequestMethod("POST"); - connection.addRequestProperty("Accept", "application/json"); - connection.addRequestProperty("Connection", "close"); - connection.addRequestProperty("Content-Encoding", "gzip"); - connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", "Metrics-Service/1"); - connection.setDoOutput(true); - try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { - outputStream.write(compressedData); - } - StringBuilder builder = new StringBuilder(); - try (BufferedReader bufferedReader = - new BufferedReader(new InputStreamReader(connection.getInputStream()))) { - String line; - while ((line = bufferedReader.readLine()) != null) { - builder.append(line); - } - } - if (logResponseStatusText) { - infoLogger.accept("Sent data to bStats and received response: " + builder); - } - } - - /** - * Checks that the class was properly relocated. - */ - private void checkRelocation() { - // You can use the property to disable the check in your test environment - if (System.getProperty("bstats.relocatecheck") == null - || !System.getProperty("bstats.relocatecheck").equals("false")) { - // Maven's Relocate is clever and changes strings, too. So we have to use this - // little "trick" ... :D - final String defaultPackage = - new String(new byte[]{'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); - final String examplePackage = - new String(new byte[]{'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); - // We want to make sure no one just copy & pastes the example and uses the wrong - // package names - if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) - || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { - throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); - } - } - } - - /** - * Gzips the given string. - * - * @param str The string to gzip. - * @return The gzipped string. - */ - private static byte[] compress(final String str) throws IOException { - if (str == null) { - return null; - } - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { - gzip.write(str.getBytes(StandardCharsets.UTF_8)); - } - return outputStream.toByteArray(); - } - } - - public static class AdvancedBarChart extends CustomChart { - - private final Callable> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public AdvancedBarChart(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObjectBuilder.JsonObject getChartData() throws Exception { - JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue().length == 0) { - // Skip this invalid - continue; - } - allSkipped = false; - valuesBuilder.appendField(entry.getKey(), entry.getValue()); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); - } - } - - public static class SimplePie extends CustomChart { - - private final Callable callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public SimplePie(String chartId, Callable callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObjectBuilder.JsonObject getChartData() throws Exception { - String value = callable.call(); - if (value == null || value.isEmpty()) { - // Null = skip the chart - return null; - } - return new JsonObjectBuilder().appendField("value", value).build(); - } - } - - public static class DrilldownPie extends CustomChart { - - private final Callable>> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public DrilldownPie(String chartId, Callable>> callable) { - super(chartId); - this.callable = callable; - } - - @Override - public JsonObjectBuilder.JsonObject getChartData() throws Exception { - JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); - Map> map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean reallyAllSkipped = true; - for (Map.Entry> entryValues : map.entrySet()) { - JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); - boolean allSkipped = true; - for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { - valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); - allSkipped = false; - } - if (!allSkipped) { - reallyAllSkipped = false; - valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); - } - } - if (reallyAllSkipped) { - // Null = skip the chart - return null; - } - return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); - } - } - - public static class SingleLineChart extends CustomChart { - - private final Callable callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public SingleLineChart(String chartId, Callable callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObjectBuilder.JsonObject getChartData() throws Exception { - int value = callable.call(); - if (value == 0) { - // Null = skip the chart - return null; - } - return new JsonObjectBuilder().appendField("value", value).build(); - } - } - - public static class MultiLineChart extends CustomChart { - - private final Callable> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public MultiLineChart(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObjectBuilder.JsonObject getChartData() throws Exception { - JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() == 0) { - // Skip this invalid - continue; - } - allSkipped = false; - valuesBuilder.appendField(entry.getKey(), entry.getValue()); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); - } - } - - public static class AdvancedPie extends CustomChart { - - private final Callable> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public AdvancedPie(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObjectBuilder.JsonObject getChartData() throws Exception { - JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() == 0) { - // Skip this invalid - continue; - } - allSkipped = false; - valuesBuilder.appendField(entry.getKey(), entry.getValue()); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); - } - } - - public abstract static class CustomChart { - - private final String chartId; - - protected CustomChart(String chartId) { - if (chartId == null) { - throw new IllegalArgumentException("chartId must not be null"); - } - this.chartId = chartId; - } - - public JsonObjectBuilder.JsonObject getRequestJsonObject( - BiConsumer errorLogger, boolean logErrors) { - JsonObjectBuilder builder = new JsonObjectBuilder(); - builder.appendField("chartId", chartId); - try { - JsonObjectBuilder.JsonObject data = getChartData(); - if (data == null) { - // If the data is null we don't send the chart. - return null; - } - builder.appendField("data", data); - } catch (Throwable t) { - if (logErrors) { - errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); - } - return null; - } - return builder.build(); - } - - protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; - } - - public static class SimpleBarChart extends CustomChart { - - private final Callable> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public SimpleBarChart(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObjectBuilder.JsonObject getChartData() throws Exception { - JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - for (Map.Entry entry : map.entrySet()) { - valuesBuilder.appendField(entry.getKey(), new int[]{entry.getValue()}); - } - return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); - } - } - - /** - * An extremely simple JSON builder. - * - *

While this class is neither feature-rich nor the most performant one, it's sufficient enough - * for its use-case. - */ - public static class JsonObjectBuilder { - - private StringBuilder builder = new StringBuilder(); - - private boolean hasAtLeastOneField = false; - - public JsonObjectBuilder() { - builder.append("{"); - } - - /** - * Appends a null field to the JSON. - * - * @param key The key of the field. - * @return A reference to this object. - */ - public JsonObjectBuilder appendNull(String key) { - appendFieldUnescaped(key, "null"); - return this; - } - - /** - * Appends a string field to the JSON. - * - * @param key The key of the field. - * @param value The value of the field. - * @return A reference to this object. - */ - public JsonObjectBuilder appendField(String key, String value) { - if (value == null) { - throw new IllegalArgumentException("JSON value must not be null"); - } - appendFieldUnescaped(key, "\"" + escape(value) + "\""); - return this; - } - - /** - * Appends an integer field to the JSON. - * - * @param key The key of the field. - * @param value The value of the field. - * @return A reference to this object. - */ - public JsonObjectBuilder appendField(String key, int value) { - appendFieldUnescaped(key, String.valueOf(value)); - return this; - } - - /** - * Appends an object to the JSON. - * - * @param key The key of the field. - * @param object The object. - * @return A reference to this object. - */ - public JsonObjectBuilder appendField(String key, JsonObject object) { - if (object == null) { - throw new IllegalArgumentException("JSON object must not be null"); - } - appendFieldUnescaped(key, object.toString()); - return this; - } - - /** - * Appends a string array to the JSON. - * - * @param key The key of the field. - * @param values The string array. - * @return A reference to this object. - */ - public JsonObjectBuilder appendField(String key, String[] values) { - if (values == null) { - throw new IllegalArgumentException("JSON values must not be null"); - } - String escapedValues = - Arrays.stream(values) - .map(value -> "\"" + escape(value) + "\"") - .collect(Collectors.joining(",")); - appendFieldUnescaped(key, "[" + escapedValues + "]"); - return this; - } - - /** - * Appends an integer array to the JSON. - * - * @param key The key of the field. - * @param values The integer array. - * @return A reference to this object. - */ - public JsonObjectBuilder appendField(String key, int[] values) { - if (values == null) { - throw new IllegalArgumentException("JSON values must not be null"); - } - String escapedValues = - Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); - appendFieldUnescaped(key, "[" + escapedValues + "]"); - return this; - } - - /** - * Appends an object array to the JSON. - * - * @param key The key of the field. - * @param values The integer array. - * @return A reference to this object. - */ - public JsonObjectBuilder appendField(String key, JsonObject[] values) { - if (values == null) { - throw new IllegalArgumentException("JSON values must not be null"); - } - String escapedValues = - Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); - appendFieldUnescaped(key, "[" + escapedValues + "]"); - return this; - } - - /** - * Appends a field to the object. - * - * @param key The key of the field. - * @param escapedValue The escaped value of the field. - */ - private void appendFieldUnescaped(String key, String escapedValue) { - if (builder == null) { - throw new IllegalStateException("JSON has already been built"); - } - if (key == null) { - throw new IllegalArgumentException("JSON key must not be null"); - } - if (hasAtLeastOneField) { - builder.append(","); - } - builder.append("\"").append(escape(key)).append("\":").append(escapedValue); - hasAtLeastOneField = true; - } - - /** - * Builds the JSON string and invalidates this builder. - * - * @return The built JSON string. - */ - public JsonObject build() { - if (builder == null) { - throw new IllegalStateException("JSON has already been built"); - } - JsonObject object = new JsonObject(builder.append("}").toString()); - builder = null; - return object; - } - - /** - * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. - * - *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. - * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). - * - * @param value The value to escape. - * @return The escaped value. - */ - private static String escape(String value) { - final StringBuilder builder = new StringBuilder(); - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - if (c == '"') { - builder.append("\\\""); - } else if (c == '\\') { - builder.append("\\\\"); - } else if (c <= '\u000F') { - builder.append("\\u000").append(Integer.toHexString(c)); - } else if (c <= '\u001F') { - builder.append("\\u00").append(Integer.toHexString(c)); - } else { - builder.append(c); - } - } - return builder.toString(); - } - - /** - * A super simple representation of a JSON object. - * - *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not - * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, - * JsonObject)}. - */ - public static class JsonObject { - - private final String value; - - private JsonObject(String value) { - this.value = value; - } - - @Override - public String toString() { - return value; - } - } - } -} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 24b9126..4174f6d 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -9,7 +9,8 @@ defaults: # overwriting the vanilla implementation provided yourself. enabled: true # How often should we refresh tags (in milliseconds) - refresh-every: 1 + # Do not go lower than 50ms (1 tick) or clients may be kicked. + refresh-every: 500 # Lines of text to display. text: - "%player_name%" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index c972974..ab38f9a 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -12,7 +12,10 @@ softdepend: - TAB - SkinsRestorer +libraries: + - com.github.ben-manes.caffeine:caffeine:3.2.0 + commands: - nametags-reload: - description: Reload the config - permission: nametags.command.reload + nametags: + description: Reload and debug commands + permission: nametags.command.admin