diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2aea337..ca145c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: hangar-token: ${{ secrets.HANGAR_API_KEY }} hangar-version-type: Alpha hangar-game-versions: | - 3.3 + 3.4 files: target/Velocitab-*.jar name: Velocitab v${{ env.version_name }} version: ${{ env.version_name }} @@ -124,4 +124,5 @@ jobs: 1.20.6 1.21 1.21.1 + 1.21.2 java: 17 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c486073..692fc1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,7 @@ jobs: hangar-token: ${{ secrets.HANGAR_API_KEY }} hangar-version-type: Release hangar-game-versions: | - 3.3 + 3.4 files: target/Velocitab-*.jar name: Velocitab v${{ github.event.release.tag_name }} version: ${{ github.event.release.tag_name }} @@ -113,4 +113,5 @@ jobs: 1.20.6 1.21 1.21.1 + 1.21.2 java: 17 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 81b7d8c..d4e12c4 100644 --- a/build.gradle +++ b/build.gradle @@ -2,15 +2,15 @@ import org.apache.tools.ant.filters.ReplaceTokens plugins { id 'xyz.jpenilla.run-velocity' version '2.3.1' - id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'com.gradleup.shadow' version '8.3.3' id 'org.cadixdev.licenser' version '0.6.1' - id 'org.ajoberstar.grgit' version '5.2.2' + id 'org.ajoberstar.grgit' version '5.3.0' id 'maven-publish' id 'java' } group 'net.william278' -version "$ext.plugin_version${versionMetadata()}" +version "$ext.plugin_version" description "$ext.plugin_description" defaultTasks 'licenseFormat', 'build' @@ -20,23 +20,24 @@ ext { set 'velocity_api_version', velocity_api_version.toString() set 'velocity_minimum_build', velocity_minimum_build.toString() + set 'papi_proxy_bridge_minimum_version', papi_proxy_bridge_minimum_version.toString() } repositories { mavenCentral() maven { url = 'https://repo.william278.net/velocity/' } - maven { url = 'https://repo.papermc.io/repository/maven-public/' } - maven { url = 'https://jitpack.io' } maven { url = 'https://repo.william278.net/releases/' } maven { url = 'https://repo.william278.net/snapshots/' } + maven { url = 'https://repo.papermc.io/repository/maven-public/' } maven { url = 'https://repo.minebench.de/' } + maven { url = 'https://jitpack.io' } } dependencies { compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT" compileOnly "com.velocitypowered:velocity-proxy:${velocity_api_version}-SNAPSHOT" - compileOnly 'io.netty:netty-codec-http:4.1.112.Final' + compileOnly 'io.netty:netty-codec-http:4.1.114.Final' compileOnly 'org.projectlombok:lombok:1.18.34' compileOnly 'net.luckperms:api:5.4' compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.2.3' @@ -71,7 +72,6 @@ license { } logger.lifecycle("Building Velocitab ${version} by William278") - version rootProject.version archivesBaseName = "${rootProject.name}" @@ -160,11 +160,12 @@ publishing { } tasks { + var papi = papi_proxy_bridge_minimum_version runVelocity { velocityVersion("${velocity_api_version}-SNAPSHOT") downloadPlugins { - modrinth ("papiproxybridge", "1.6.1") + modrinth ("papiproxybridge", papi) modrinth ("miniplaceholders", "2.2.4") } } diff --git a/docs/Plugin-Message-API.md b/docs/Plugin-Message-API.md index d8cf1a8..e8d12db 100644 --- a/docs/Plugin-Message-API.md +++ b/docs/Plugin-Message-API.md @@ -1,10 +1,17 @@ -Velocitab provides a plugin message API. +Velocitab provides a plugin message API, to let you do things with Velocitab from your backend servers. + +> **Note:** This feature requires sending Update Teams packets. `send_scoreboard_packets` must be enabled in the [`config.yml` file](config-file) for this to work. [More details...](sorting#compatibility-issues) +> +## Prerequisites +To use the Velocitab plugin message API, you must first turn it on and ensure the following: + +* That `enable_plugin_message_api` and `send_scoreboard_packets` is set to `true` in your Velocitab [[config file]] +* That `bungee-plugin-message-channel` is set to `true` in your **Velocity proxy config** TOML (see [Velocity config reference](https://docs.papermc.io/velocity/configuration)). ## API Requests from Backend Plugins -### 1 Changing player's username in the TAB List -To change a player's username in the tablist, you can send a plugin message with the channel `velocitab:update_custom_name` and as data `customName`. -Remember to replace `customName` with the desired name. +### 1 Changing player's username in the TAB list +To change a player's username in the TAB list, you can send a plugin message on the channel `velocitab:update_custom_name` with a `customName` string, where `customName` is the new desired display name.
Example — Changing player's username in the TAB List @@ -13,10 +20,10 @@ player.sendPluginMessage(plugin, "velocitab:update_custom_name", "Steve".getByte ```
-### 2 Update team color -To change a player's team color in the TAB List, you can send a plugin message with the channel `velocitab:update_team_color` and as data `teamColor`. -You can only use legacy color codes, for example `a` for green, `b` for aqua, etc. -This option overrides the glow effect if set +### 2 Update color of player's nametag +To change player's [nametag](nametags) color, you can send a plugin message on the channel `velocitab:update_team_color` with `teamColor` string, where `teamColor` is the new desired name tag color. + +You can only use legacy color codes, for example `a` for green, `b` for aqua, etc. Please note this option overrides the color of the glow potion effect if set. [Check here](https://wiki.vg/index.php?title=Text_formatting&oldid=18983#Colors) for a list of supported colors (The value under the "Code" header on the table is what you need).
Example — Changing player's team color diff --git a/docs/Setup.md b/docs/Setup.md index 3bb4919..0359856 100644 --- a/docs/Setup.md +++ b/docs/Setup.md @@ -1,7 +1,7 @@ This page will walk you through installing Velocitab on a Velocity proxy server. ## Requirements -* A Velocity proxy server (running Velocity 3.3.0 or newer) +* A Velocity proxy server (running Velocity 3.4.0 or newer) * Backend Minecraft servers. The following Minecraft server versions are fully supported: - Minecraft 1.8—1.8.9 - Minecraft 1.12.2—latest diff --git a/gradle.properties b/gradle.properties index 53ca3ce..72b8ddd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,9 +3,10 @@ javaVersion=17 org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true -plugin_version=1.7.1 +plugin_version=1.7.2 plugin_archive=velocitab plugin_description=A beautiful and versatile TAB list plugin for Velocity proxies -velocity_api_version=3.3.0 -velocity_minimum_build=400 \ No newline at end of file +velocity_api_version=3.4.0 +velocity_minimum_build=444 +papi_proxy_bridge_minimum_version=1.7 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22c..9355b41 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ 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.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ 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. +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 @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/src/main/java/net/william278/velocitab/Velocitab.java b/src/main/java/net/william278/velocitab/Velocitab.java index 7b88c45..3cf2fa3 100644 --- a/src/main/java/net/william278/velocitab/Velocitab.java +++ b/src/main/java/net/william278/velocitab/Velocitab.java @@ -59,7 +59,6 @@ import org.slf4j.event.Level; import java.nio.file.Path; import java.util.List; -import java.util.Optional; @Plugin(id = "velocitab") @Getter @@ -144,9 +143,9 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv return this; } - @NotNull - public Optional getScoreboardManager() { - return Optional.ofNullable(scoreboardManager); + @Override + public ScoreboardManager getScoreboardManager() { + return scoreboardManager; } private void prepareAPI() { diff --git a/src/main/java/net/william278/velocitab/config/ConfigProvider.java b/src/main/java/net/william278/velocitab/config/ConfigProvider.java index 16744bf..ad24a17 100644 --- a/src/main/java/net/william278/velocitab/config/ConfigProvider.java +++ b/src/main/java/net/william278/velocitab/config/ConfigProvider.java @@ -19,6 +19,8 @@ package net.william278.velocitab.config; +import com.velocitypowered.api.plugin.PluginContainer; +import com.velocitypowered.api.plugin.PluginDescription; import de.exlll.configlib.NameFormatters; import de.exlll.configlib.YamlConfigurationProperties; import de.exlll.configlib.YamlConfigurations; @@ -129,16 +131,24 @@ public interface ConfigProvider { } } + @SuppressWarnings("OptionalIsPresent") default void checkCompatibility() { if (getSkipCompatibilityCheck().orElse(false)) { - getPlugin().getLogger().warn("Skipping compatibility check"); + getPlugin().getLogger().warn("Skipping compatibility checks"); return; } + // Validate Velocity platform version final Metadata metadata = getMetadata(); final Version proxyVersion = getVelocityVersion(); metadata.validateApiVersion(proxyVersion); metadata.validateBuild(proxyVersion); + + // Validate PAPIProxyBridge hook version + final Optional papiProxyBridgeVersion = getPapiProxyBridgeVersion(); + if (papiProxyBridgeVersion.isPresent()) { + metadata.validatePapiProxyBridgeVersion(papiProxyBridgeVersion.get()); + } } @NotNull @@ -151,6 +161,12 @@ public interface ConfigProvider { .findFirst(); } + default Optional getPapiProxyBridgeVersion() { + return getPlugin().getServer().getPluginManager() + .getPlugin("papiproxybridge").map(PluginContainer::getDescription) + .flatMap(PluginDescription::getVersion).map(Version::fromString); + } + @NotNull Version getVelocityVersion(); diff --git a/src/main/java/net/william278/velocitab/config/Formatter.java b/src/main/java/net/william278/velocitab/config/Formatter.java index 9948cdb..b739b23 100644 --- a/src/main/java/net/william278/velocitab/config/Formatter.java +++ b/src/main/java/net/william278/velocitab/config/Formatter.java @@ -26,7 +26,7 @@ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.william278.velocitab.Velocitab; import net.william278.velocitab.player.TabPlayer; import net.william278.velocitab.util.QuadFunction; -import net.william278.velocitab.util.SerializerUtil; +import net.william278.velocitab.util.SerializationUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -57,11 +57,11 @@ public enum Formatter { MiniMessage.miniMessage()::serialize ), LEGACY( - (text, player, viewer, plugin) -> SerializerUtil.LEGACY_SERIALIZER.deserialize(text), + (text, player, viewer, plugin) -> SerializationUtil.LEGACY_SERIALIZER.deserialize(text), Function.identity(), "Legacy Text", - SerializerUtil.LEGACY_SERIALIZER::deserialize, - SerializerUtil.LEGACY_SERIALIZER::serialize + SerializationUtil.LEGACY_SERIALIZER::deserialize, + SerializationUtil.LEGACY_SERIALIZER::serialize ); diff --git a/src/main/java/net/william278/velocitab/config/Metadata.java b/src/main/java/net/william278/velocitab/config/Metadata.java index 815e2b4..e249d99 100644 --- a/src/main/java/net/william278/velocitab/config/Metadata.java +++ b/src/main/java/net/william278/velocitab/config/Metadata.java @@ -37,6 +37,7 @@ public class Metadata { private String velocityApiVersion; private int velocityMinimumBuild; + private String papiProxyBridgeMinimumVersion; public void validateApiVersion(@NotNull Version version) { if (version.compareTo(Version.fromString(velocityApiVersion)) < 0) { @@ -56,6 +57,14 @@ public class Metadata { } } + public void validatePapiProxyBridgeVersion(@NotNull Version version) { + if (version.compareTo(Version.fromString(papiProxyBridgeMinimumVersion)) < 0) { + final String serverVersion = version.toStringWithoutMetadata(); + throw new IllegalStateException("Your PAPIProxyBridge version (" + serverVersion + ") is not supported! " + + "Disabling Velocitab. Please update to at least PAPIProxyBridge v" + papiProxyBridgeMinimumVersion + "."); + } + } + private int getBuildNumber(@NotNull String proxyVersion) { final Matcher matcher = Pattern.compile(".*-b(\\d+).*").matcher(proxyVersion); if (matcher.find(1)) { diff --git a/src/main/java/net/william278/velocitab/config/ServerUrl.java b/src/main/java/net/william278/velocitab/config/ServerUrl.java index b4c519f..778fd8e 100644 --- a/src/main/java/net/william278/velocitab/config/ServerUrl.java +++ b/src/main/java/net/william278/velocitab/config/ServerUrl.java @@ -45,7 +45,7 @@ public record ServerUrl( (type) -> CompletableFuture.completedFuture(ServerLink.serverLink(type, url())) ).orElseGet( () -> Placeholder.replace(label(), plugin, player) - .thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin)) + .thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin)) .thenApply(formatted -> ServerLink.serverLink(formatted, url())) ); } diff --git a/src/main/java/net/william278/velocitab/config/TabGroups.java b/src/main/java/net/william278/velocitab/config/TabGroups.java index 6a65239..f671566 100644 --- a/src/main/java/net/william278/velocitab/config/TabGroups.java +++ b/src/main/java/net/william278/velocitab/config/TabGroups.java @@ -51,7 +51,7 @@ public class TabGroups implements ConfigValidator { List.of("Running Velocitab by William278 & AlexDev_"), List.of("There are currently %players_online%/%max_players_online% players online"), "[%server%] %prefix%%username%", - new Nametag("%prefix%", "%suffix%"), + new Nametag("", ""), Set.of("lobby", "survival", "creative", "minigames", "skyblock", "prison", "hub"), List.of("%role_weight%", "%username_lower%"), false, @@ -63,11 +63,12 @@ public class TabGroups implements ConfigValidator { public List groups = List.of(DEFAULT_GROUP); @NotNull + @SuppressWarnings("unused") public Group getGroupFromName(@NotNull String name) { return groups.stream() .filter(group -> group.name().equals(name)) .findFirst() - .orElseThrow(() -> new IllegalStateException("No group with name " + name + " found")); + .orElseThrow(() -> new IllegalStateException("No group with name %s found".formatted(name))); } public Optional getGroup(@NotNull String name) { diff --git a/src/main/java/net/william278/velocitab/hook/miniconditions/MiniConditionManager.java b/src/main/java/net/william278/velocitab/hook/miniconditions/MiniConditionManager.java index 6937dac..103de61 100644 --- a/src/main/java/net/william278/velocitab/hook/miniconditions/MiniConditionManager.java +++ b/src/main/java/net/william278/velocitab/hook/miniconditions/MiniConditionManager.java @@ -49,10 +49,10 @@ public class MiniConditionManager { public final static Map REPLACE_2 = Map.of( "*LESS3*", "<", - "*GREATER3*",">", + "*GREATER3*", ">", "*LESS2*", "<", "*GREATER2*", ">" - ); + ); private final static Map REPLACE_3 = Map.of( "?dp?", ":" diff --git a/src/main/java/net/william278/velocitab/packet/PacketRegistration.java b/src/main/java/net/william278/velocitab/packet/PacketRegistration.java index 631f950..2affa37 100644 --- a/src/main/java/net/william278/velocitab/packet/PacketRegistration.java +++ b/src/main/java/net/william278/velocitab/packet/PacketRegistration.java @@ -138,20 +138,31 @@ public final class PacketRegistration

{ final MethodHandles.Lookup lookup = MethodHandles.lookup(); try { final MethodHandles.Lookup stateRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.class, lookup); - STATE_REGISTRY$clientBound = stateRegistryLookup.findGetter(StateRegistry.class, "clientbound", StateRegistry.PacketRegistry.class); - STATE_REGISTRY$serverBound = stateRegistryLookup.findGetter(StateRegistry.class, "serverbound", StateRegistry.PacketRegistry.class); + STATE_REGISTRY$clientBound = stateRegistryLookup.findGetter( + StateRegistry.class, "clientbound", StateRegistry.PacketRegistry.class); + STATE_REGISTRY$serverBound = stateRegistryLookup.findGetter( + StateRegistry.class, "serverbound", StateRegistry.PacketRegistry.class); - final MethodType mapType = MethodType.methodType(StateRegistry.PacketMapping.class, Integer.TYPE, ProtocolVersion.class, Boolean.TYPE); - PACKET_MAPPING$map = stateRegistryLookup.findStatic(StateRegistry.class, "map", mapType); + final MethodType mapType = MethodType.methodType( + StateRegistry.PacketMapping.class, Integer.TYPE, ProtocolVersion.class, Boolean.TYPE); + PACKET_MAPPING$map = stateRegistryLookup.findStatic( + StateRegistry.class, "map", mapType); - final MethodHandles.Lookup packetRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.class, lookup); - final MethodType registerType = MethodType.methodType(void.class, Class.class, Supplier.class, StateRegistry.PacketMapping[].class); - PACKET_REGISTRY$register = packetRegistryLookup.findVirtual(StateRegistry.PacketRegistry.class, "register", registerType); - PACKET_REGISTRY$versions = packetRegistryLookup.findGetter(StateRegistry.PacketRegistry.class, "versions", Map.class); + final MethodHandles.Lookup packetRegistryLookup = MethodHandles.privateLookupIn( + StateRegistry.PacketRegistry.class, lookup); + final MethodType registerType = MethodType.methodType( + void.class, Class.class, Supplier.class, StateRegistry.PacketMapping[].class); + PACKET_REGISTRY$register = packetRegistryLookup.findVirtual( + StateRegistry.PacketRegistry.class, "register", registerType); + PACKET_REGISTRY$versions = packetRegistryLookup.findGetter( + StateRegistry.PacketRegistry.class, "versions", Map.class); - final MethodHandles.Lookup protocolRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.ProtocolRegistry.class, lookup); - PACKET_REGISTRY$packetIdToSupplier = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetIdToSupplier", IntObjectMap.class); - PACKET_REGISTRY$packetClassToId = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetClassToId", Object2IntMap.class); + final MethodHandles.Lookup protocolRegistryLookup = MethodHandles.privateLookupIn( + StateRegistry.PacketRegistry.ProtocolRegistry.class, lookup); + PACKET_REGISTRY$packetIdToSupplier = protocolRegistryLookup.findGetter( + StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetIdToSupplier", IntObjectMap.class); + PACKET_REGISTRY$packetClassToId = protocolRegistryLookup.findGetter( + StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetClassToId", Object2IntMap.class); } catch (Throwable e) { diff --git a/src/main/java/net/william278/velocitab/packet/PlayerChannelHandler.java b/src/main/java/net/william278/velocitab/packet/PlayerChannelHandler.java index 37c78e5..dce1432 100644 --- a/src/main/java/net/william278/velocitab/packet/PlayerChannelHandler.java +++ b/src/main/java/net/william278/velocitab/packet/PlayerChannelHandler.java @@ -45,8 +45,8 @@ public class PlayerChannelHandler extends ChannelDuplexHandler { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof final UpdateTeamsPacket updateTeamsPacket && plugin.getSettings().isSendScoreboardPackets()) { - final Optional scoreboardManager = plugin.getScoreboardManager(); - if (scoreboardManager.isEmpty()) { + final ScoreboardManager scoreboardManager = plugin.getScoreboardManager(); + if (!scoreboardManager.handleTeams()) { super.write(ctx, msg, promise); return; } @@ -56,7 +56,7 @@ public class PlayerChannelHandler extends ChannelDuplexHandler { return; } - if (scoreboardManager.get().isInternalTeam(updateTeamsPacket.teamName())) { + if (scoreboardManager.isInternalTeam(updateTeamsPacket.teamName())) { super.write(ctx, msg, promise); return; } @@ -74,8 +74,8 @@ public class PlayerChannelHandler extends ChannelDuplexHandler { // Cancel packet if the backend is trying to send a team packet with an online player. // This is to prevent conflicts with Velocitab teams. plugin.getLogger().warn("Cancelled team \"{}\" packet from backend for player {}. " + - "We suggest disabling \"send_scoreboard_packets\" in Velocitab's config.yml file, " + - "but note this will disable TAB sorting", + "We suggest disabling \"send_scoreboard_packets\" in Velocitab's config.yml file, " + + "but note this will disable TAB sorting", updateTeamsPacket.teamName(), player.getUsername()); return; } diff --git a/src/main/java/net/william278/velocitab/packet/Protocol48Adapter.java b/src/main/java/net/william278/velocitab/packet/Protocol48Adapter.java index f922456..a9d045e 100644 --- a/src/main/java/net/william278/velocitab/packet/Protocol48Adapter.java +++ b/src/main/java/net/william278/velocitab/packet/Protocol48Adapter.java @@ -104,6 +104,7 @@ public class Protocol48Adapter extends TeamsPacketAdapter { /** * Returns a shortened version of the given string, with a maximum length of 16 characters. * This is used to ensure that the team name, display name, prefix and suffix are not too long for the client. + * * @param string the string to be shortened * @return the shortened string */ diff --git a/src/main/java/net/william278/velocitab/packet/Protocol765Adapter.java b/src/main/java/net/william278/velocitab/packet/Protocol765Adapter.java index d80bc18..4817874 100644 --- a/src/main/java/net/william278/velocitab/packet/Protocol765Adapter.java +++ b/src/main/java/net/william278/velocitab/packet/Protocol765Adapter.java @@ -32,7 +32,7 @@ import org.jetbrains.annotations.NotNull; import java.util.Set; /** - * Adapter for handling the UpdateTeamsPacket for Minecraft 1.20.3-1.20.5 + * Adapter for handling the UpdateTeamsPacket for Minecraft 1.20.3-1.21.2 */ public class Protocol765Adapter extends Protocol404Adapter { @@ -40,7 +40,8 @@ public class Protocol765Adapter extends Protocol404Adapter { super(plugin, Set.of( ProtocolVersion.MINECRAFT_1_20_3, ProtocolVersion.MINECRAFT_1_20_5, - ProtocolVersion.MINECRAFT_1_21 + ProtocolVersion.MINECRAFT_1_21, + ProtocolVersion.MINECRAFT_1_21_2 )); } @@ -51,7 +52,9 @@ public class Protocol765Adapter extends Protocol404Adapter { @NotNull protected Component readComponent(@NotNull ByteBuf buf) { - return GsonComponentSerializer.gson().deserializeFromTree(ComponentHolder.deserialize(ProtocolUtils.readBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, null))); + return GsonComponentSerializer.gson().deserializeFromTree(ComponentHolder.deserialize( + ProtocolUtils.readBinaryTag(buf, ProtocolVersion.MINECRAFT_1_20_3, null) + )); } } diff --git a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java index 6fa9690..9ee0dd3 100644 --- a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java +++ b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java @@ -30,18 +30,18 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; +import lombok.Getter; import net.kyori.adventure.text.Component; import net.william278.velocitab.Velocitab; import net.william278.velocitab.config.Group; import net.william278.velocitab.player.TabPlayer; +import net.william278.velocitab.sorting.SortedSet; import net.william278.velocitab.tab.Nametag; import org.jetbrains.annotations.NotNull; import org.slf4j.event.Level; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; +import java.util.concurrent.CompletableFuture; import static com.velocitypowered.api.network.ProtocolVersion.*; @@ -49,29 +49,43 @@ public class ScoreboardManager { private PacketRegistration packetRegistration; private final Velocitab plugin; - private final Set versions; + private final boolean teams; + private final Map versions; + @Getter private final Map createdTeams; private final Map nametags; private final Multimap trackedTeams; + @Getter + private final SortedSet sortedTeams; - public ScoreboardManager(@NotNull Velocitab velocitab) { + public ScoreboardManager(@NotNull Velocitab velocitab, boolean teams) { this.plugin = velocitab; + this.teams = teams; this.createdTeams = Maps.newConcurrentMap(); this.nametags = Maps.newConcurrentMap(); - this.versions = Sets.newHashSet(); + this.versions = Maps.newHashMap(); this.trackedTeams = Multimaps.synchronizedMultimap(Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet)); + this.sortedTeams = new SortedSet(Comparator.reverseOrder()); this.registerVersions(); } + public boolean handleTeams() { + return teams; + } + private void registerVersions() { try { - versions.add(new Protocol765Adapter(plugin)); - versions.add(new Protocol735Adapter(plugin)); - versions.add(new Protocol404Adapter(plugin)); - versions.add(new Protocol48Adapter(plugin)); + final Protocol765Adapter protocol765Adapter = new Protocol765Adapter(plugin); + protocol765Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol765Adapter)); + final Protocol735Adapter protocol735Adapter = new Protocol735Adapter(plugin); + protocol735Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol735Adapter)); + final Protocol404Adapter protocol404Adapter = new Protocol404Adapter(plugin); + protocol404Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol404Adapter)); + final Protocol48Adapter protocol48Adapter = new Protocol48Adapter(plugin); + protocol48Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol48Adapter)); } catch (NoSuchFieldError e) { throw new IllegalStateException("Failed to register Scoreboard Teams packets." + - " Velocitab probably does not (yet) support your Proxy version.", e); + " Velocitab probably does not (yet) support your Proxy version.", e); } } @@ -79,11 +93,13 @@ public class ScoreboardManager { return nametags.containsKey(teamName); } + public int getPosition(@NotNull String teamName) { + return sortedTeams.getPosition(teamName); + } + @NotNull public TeamsPacketAdapter getPacketAdapter(@NotNull ProtocolVersion version) { - return versions.stream() - .filter(adapter -> adapter.getProtocolVersions().contains(version)) - .findFirst() + return Optional.ofNullable(versions.get(version)) .orElseThrow(() -> new IllegalArgumentException("No adapter found for protocol version " + version)); } @@ -94,6 +110,7 @@ public class ScoreboardManager { public void resetCache(@NotNull Player player) { final String team = createdTeams.remove(player.getUniqueId()); if (team != null) { + removeSortedTeam(team); plugin.getTabList().getTabPlayer(player).ifPresent(tabPlayer -> dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), tabPlayer) ); @@ -104,10 +121,18 @@ public class ScoreboardManager { public void resetCache(@NotNull Player player, @NotNull Group group) { final String team = createdTeams.remove(player.getUniqueId()); if (team != null) { + removeSortedTeam(team); dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), group); } } + private void removeSortedTeam(@NotNull String teamName) { + final boolean result = sortedTeams.removeTeam(teamName); + if (!result) { + plugin.log(Level.ERROR, "Failed to remove team " + teamName + " from sortedTeams"); + } + } + public void vanishPlayer(@NotNull TabPlayer tabPlayer) { this.handleVanish(tabPlayer, true); } @@ -127,12 +152,13 @@ public class ScoreboardManager { return; } final Set siblings = tabPlayer.getGroup().registeredServers(plugin); + final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty(); final Optional cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null)); cachedTag.ifPresent(nametag -> siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player) .forEach(connected -> { if (vanish && !plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername())) { - dispatchPacket(UpdateTeamsPacket.removeTeam(plugin, teamName), connected); + sendPacket(connected, UpdateTeamsPacket.removeTeam(plugin, teamName), isNameTagEmpty); trackedTeams.remove(connected.getUniqueId(), teamName); } else { dispatchGroupCreatePacket(plugin, tabPlayer, teamName, nametag, player.getUsername()); @@ -147,14 +173,14 @@ public class ScoreboardManager { * @param role The new role of the player. Must not be null. * @param force Whether to force the update even if the player's nametag is the same. */ - public void updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) { + public CompletableFuture updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) { final Player player = tabPlayer.getPlayer(); if (!player.isActive()) { plugin.getTabList().removeOfflinePlayer(player); - return; + return CompletableFuture.completedFuture(null); } - final String name = player.getUsername(); + final CompletableFuture future = new CompletableFuture<>(); tabPlayer.getNametag(plugin).thenAccept(newTag -> { if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) { if (createdTeams.containsKey(player.getUniqueId())) { @@ -163,8 +189,15 @@ public class ScoreboardManager { tabPlayer ); } - + final String oldRole = createdTeams.remove(player.getUniqueId()); + if (oldRole != null) { + removeSortedTeam(oldRole); + } createdTeams.put(player.getUniqueId(), role); + final boolean a = sortedTeams.addTeam(role); + if (!a) { + plugin.log(Level.ERROR, "Failed to add team " + role + " to sortedTeams"); + } this.nametags.put(role, newTag); dispatchGroupCreatePacket(plugin, tabPlayer, role, newTag, name); } else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) { @@ -173,10 +206,13 @@ public class ScoreboardManager { } else { updatePlaceholders(tabPlayer); } + future.complete(null); }).exceptionally(e -> { plugin.log(Level.ERROR, "Failed to update role for " + player.getUsername(), e); return null; }); + + return future; } public void updatePlaceholders(@NotNull TabPlayer tabPlayer) { @@ -192,6 +228,9 @@ public class ScoreboardManager { } public void resendAllTeams(@NotNull TabPlayer tabPlayer) { + if (!teams) { + return; + } if (!plugin.getSettings().isSendScoreboardPackets()) { return; } @@ -238,6 +277,9 @@ public class ScoreboardManager { private void dispatchGroupCreatePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull String teamName, @NotNull Nametag nametag, @NotNull String... teamMembers) { + if (!teams) { + return; + } tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> { if (!viewer.getPlayer().isActive()) { return; @@ -252,6 +294,9 @@ public class ScoreboardManager { @NotNull String teamName, @NotNull Nametag nametag, @NotNull TabPlayer viewer, @NotNull String... teamMembers) { + if (!teams) { + return; + } final boolean canSee = plugin.getVanishManager().canSee(viewer.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername()); if (!canSee) { return; @@ -259,12 +304,17 @@ public class ScoreboardManager { final UpdateTeamsPacket packet = UpdateTeamsPacket.create(plugin, tabPlayer, teamName, nametag, viewer, teamMembers); trackedTeams.put(viewer.getPlayer().getUniqueId(), teamName); - dispatchPacket(packet, viewer.getPlayer()); + final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty(); + sendPacket(viewer.getPlayer(), packet, isNameTagEmpty); } private void dispatchGroupChangePacket(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull String teamName, @NotNull Nametag nametag) { + if (!teams) { + return; + } + final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty(); tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer).forEach(viewer -> { if (viewer == tabPlayer || !viewer.getPlayer().isActive()) { return; @@ -289,30 +339,20 @@ public class ScoreboardManager { return; } tabPlayer.setRelationalNametag(viewer.getPlayer().getUniqueId(), prefix, suffix); - dispatchPacket(packet, viewer.getPlayer()); + sendPacket(viewer.getPlayer(), packet, isNameTagEmpty); }); } - private void dispatchPacket(@NotNull UpdateTeamsPacket packet, @NotNull Player player) { - if (!player.isActive()) { - plugin.getTabList().removeOfflinePlayer(player); + private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Group group) { + if (!teams) { return; } - try { - final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player; - connectedPlayer.getConnection().write(packet); - } catch (Throwable e) { - plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e); - } - } - - private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Group group) { final boolean isRemove = packet.isRemoveTeam(); + final boolean isNameTagEmpty = group.nametag().isEmpty(); group.registeredServers(plugin).forEach(server -> server.getPlayersConnected().forEach(connected -> { try { - final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected; - connectedPlayer.getConnection().write(packet); + sendPacket(connected, packet, isNameTagEmpty); if (isRemove) { trackedTeams.remove(connected.getUniqueId(), packet.teamName()); } @@ -323,6 +363,9 @@ public class ScoreboardManager { } private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull TabPlayer tabPlayer) { + if (!teams) { + return; + } final Player player = tabPlayer.getPlayer(); final Optional optionalServerConnection = player.getCurrentServer(); if (optionalServerConnection.isEmpty()) { @@ -330,6 +373,7 @@ public class ScoreboardManager { } final Set players = tabPlayer.getGroup().getPlayers(plugin); + final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty(); players.forEach(connected -> { try { final boolean canSee = plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername()); @@ -337,15 +381,30 @@ public class ScoreboardManager { return; } - final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected; - connectedPlayer.getConnection().write(packet); + sendPacket(connected, packet, isNameTagEmpty); } catch (Throwable e) { plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e); } }); } + private void sendPacket(@NotNull Player player, @NotNull UpdateTeamsPacket packet, boolean isNameTagEmpty) { + if (!player.isActive()) { + plugin.getTabList().removeOfflinePlayer(player); + return; + } + if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2) && isNameTagEmpty) { + return; + } + + final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player; + connectedPlayer.getConnection().write(packet); + } + public void registerPacket() { + if (!teams) { + return; + } try { packetRegistration = PacketRegistration.of(UpdateTeamsPacket.class) .direction(ProtocolUtils.Direction.CLIENTBOUND) @@ -362,7 +421,8 @@ public class ScoreboardManager { .mapping(0x5A, MINECRAFT_1_19_4, false) .mapping(0x5C, MINECRAFT_1_20_2, false) .mapping(0x5E, MINECRAFT_1_20_3, false) - .mapping(0x60, MINECRAFT_1_20_5, false); + .mapping(0x60, MINECRAFT_1_20_5, false) + .mapping(0x67, MINECRAFT_1_21_2, false); packetRegistration.register(); } catch (Throwable e) { plugin.log(Level.ERROR, "Failed to register UpdateTeamsPacket", e); @@ -389,6 +449,9 @@ public class ScoreboardManager { * @param canSee A boolean indicating whether the player can see the target player. */ public void recalculateVanishForPlayer(TabPlayer tabPlayer, TabPlayer target, boolean canSee) { + if (!teams) { + return; + } final Player player = tabPlayer.getPlayer(); final String team = createdTeams.get(target.getPlayer().getUniqueId()); if (team == null) { @@ -396,7 +459,8 @@ public class ScoreboardManager { } final UpdateTeamsPacket removeTeam = UpdateTeamsPacket.removeTeam(plugin, team); - dispatchPacket(removeTeam, player); + final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty(); + sendPacket(player, removeTeam, isNameTagEmpty); trackedTeams.remove(player.getUniqueId(), team); if (canSee) { diff --git a/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java b/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java index 6e43de8..6bf6782 100644 --- a/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java +++ b/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java @@ -36,7 +36,6 @@ import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; @Getter @@ -208,22 +207,14 @@ public class UpdateTeamsPacket implements MinecraftPacket { @Override public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - final Optional optionalManager = plugin.getScoreboardManager(); - if (optionalManager.isEmpty()) { - return; - } - - optionalManager.get().getPacketAdapter(protocolVersion).decode(byteBuf, this, protocolVersion); + final ScoreboardManager scoreboardManager = plugin.getScoreboardManager(); + scoreboardManager.getPacketAdapter(protocolVersion).decode(byteBuf, this, protocolVersion); } @Override public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { - final Optional optionalManager = plugin.getScoreboardManager(); - if (optionalManager.isEmpty()) { - return; - } - - optionalManager.get().getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion); + final ScoreboardManager scoreboardManager = plugin.getScoreboardManager(); + scoreboardManager.getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion); } @Override diff --git a/src/main/java/net/william278/velocitab/player/TabPlayer.java b/src/main/java/net/william278/velocitab/player/TabPlayer.java index b32a949..8e1648e 100644 --- a/src/main/java/net/william278/velocitab/player/TabPlayer.java +++ b/src/main/java/net/william278/velocitab/player/TabPlayer.java @@ -36,7 +36,10 @@ import org.apache.commons.lang3.ObjectUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -58,10 +61,13 @@ public final class TabPlayer implements Comparable { private final Map relationalDisplayNames; private final Map relationalNametags; private final Map cachedPlaceholders; + private final Map cachedListOrders; private String lastDisplayName; private Component lastHeader; private Component lastFooter; private String teamName; + @Setter + private int listOrder = -1; @Nullable @Setter private UpdateTeamsPacket.TeamColor teamColor; @@ -86,6 +92,7 @@ public final class TabPlayer implements Comparable { this.relationalDisplayNames = Maps.newConcurrentMap(); this.relationalNametags = Maps.newConcurrentMap(); this.cachedPlaceholders = Maps.newConcurrentMap(); + this.cachedListOrders = Maps.newConcurrentMap(); } @NotNull @@ -237,6 +244,10 @@ public final class TabPlayer implements Comparable { relationalNametags.remove(target); } + public void unsetTabListOrder(@NotNull UUID target) { + cachedListOrders.remove(target); + } + public Optional getRelationalNametag(@NotNull UUID target) { return Optional.ofNullable(relationalNametags.get(target)); } @@ -248,6 +259,8 @@ public final class TabPlayer implements Comparable { lastHeader = null; lastFooter = null; role = Role.DEFAULT_ROLE; + teamName = null; + cachedListOrders.clear(); } /** diff --git a/src/main/java/net/william278/velocitab/providers/LoggerProvider.java b/src/main/java/net/william278/velocitab/providers/LoggerProvider.java index 92a7662..0e1c36a 100644 --- a/src/main/java/net/william278/velocitab/providers/LoggerProvider.java +++ b/src/main/java/net/william278/velocitab/providers/LoggerProvider.java @@ -30,6 +30,7 @@ public interface LoggerProvider { * * @return the logger for the class */ + @NotNull Logger getLogger(); /** diff --git a/src/main/java/net/william278/velocitab/providers/MetricProvider.java b/src/main/java/net/william278/velocitab/providers/MetricProvider.java index 7436515..51b64a2 100644 --- a/src/main/java/net/william278/velocitab/providers/MetricProvider.java +++ b/src/main/java/net/william278/velocitab/providers/MetricProvider.java @@ -36,7 +36,8 @@ public interface MetricProvider { /** * Retrieves the Velocitab plugin instance. - * @return + * + * @return The Velocitab plugin instance. */ Velocitab getPlugin(); diff --git a/src/main/java/net/william278/velocitab/providers/ScoreboardProvider.java b/src/main/java/net/william278/velocitab/providers/ScoreboardProvider.java index 81733fd..5c0b9eb 100644 --- a/src/main/java/net/william278/velocitab/providers/ScoreboardProvider.java +++ b/src/main/java/net/william278/velocitab/providers/ScoreboardProvider.java @@ -24,7 +24,6 @@ import net.william278.velocitab.packet.ScoreboardManager; import net.william278.velocitab.sorting.SortingManager; import net.william278.velocitab.tab.PlayerTabList; -import java.util.Optional; import java.util.concurrent.TimeUnit; public interface ScoreboardProvider { @@ -37,11 +36,10 @@ public interface ScoreboardProvider { Velocitab getPlugin(); /** - * Retrieves the optional scoreboard manager. - * - * @return An {@code Optional} object that may contain a {@code ScoreboardManager} instance. + * Retrieves the scoreboard manager + * @return The scoreboard manager */ - Optional getScoreboardManager(); + ScoreboardManager getScoreboardManager(); /** * Sets the scoreboard manager. @@ -85,11 +83,9 @@ public interface ScoreboardProvider { * */ default void prepareScoreboard() { - if (getPlugin().getSettings().isSendScoreboardPackets()) { - final ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin()); - setScoreboardManager(scoreboardManager); - scoreboardManager.registerPacket(); - } + final ScoreboardManager scoreboardManager = new ScoreboardManager(getPlugin(), getPlugin().getSettings().isSendScoreboardPackets()); + setScoreboardManager(scoreboardManager); + scoreboardManager.registerPacket(); final PlayerTabList tabList = new PlayerTabList(getPlugin()); setTabList(tabList); @@ -105,10 +101,8 @@ public interface ScoreboardProvider { * Disables the ScoreboardManager and closes the tab list for the player. */ default void disableScoreboardManager() { - if (getScoreboardManager().isPresent() && getPlugin().getSettings().isSendScoreboardPackets()) { - getScoreboardManager().get().close(); - getScoreboardManager().get().unregisterPacket(); - } + getScoreboardManager().close(); + getScoreboardManager().unregisterPacket(); getTabList().close(); } diff --git a/src/main/java/net/william278/velocitab/sorting/SortedSet.java b/src/main/java/net/william278/velocitab/sorting/SortedSet.java new file mode 100644 index 0000000..fe9b8d6 --- /dev/null +++ b/src/main/java/net/william278/velocitab/sorting/SortedSet.java @@ -0,0 +1,74 @@ +/* + * This file is part of Velocitab, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.william278.velocitab.sorting; + +import com.google.common.collect.Maps; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.ConcurrentSkipListSet; + +public class SortedSet { + + private final ConcurrentSkipListSet sortedTeams; + private final Map positionMap; + + public SortedSet(@NotNull Comparator comparator) { + sortedTeams = new ConcurrentSkipListSet<>(comparator); + positionMap = Maps.newConcurrentMap(); + } + + public synchronized boolean addTeam(@NotNull String teamName) { + final boolean result = sortedTeams.add(teamName); + if (!result) { + return false; + } + updatePositions(); + return true; + } + + public synchronized boolean removeTeam(@NotNull String teamName) { + final boolean result = sortedTeams.remove(teamName); + if (!result) { + return false; + } + updatePositions(); + return true; + } + + private synchronized void updatePositions() { + int index = 0; + positionMap.clear(); + for (final String team : sortedTeams) { + positionMap.put(team, index); + index++; + } + } + + public synchronized int getPosition(@NotNull String teamName) { + return positionMap.getOrDefault(teamName, -1); + } + + @Override + public String toString() { + return sortedTeams.toString(); + } +} diff --git a/src/main/java/net/william278/velocitab/tab/GroupTasks.java b/src/main/java/net/william278/velocitab/tab/GroupTasks.java index fc8879a..ce4318b 100644 --- a/src/main/java/net/william278/velocitab/tab/GroupTasks.java +++ b/src/main/java/net/william278/velocitab/tab/GroupTasks.java @@ -22,7 +22,8 @@ package net.william278.velocitab.tab; import com.velocitypowered.api.scheduler.ScheduledTask; import org.jetbrains.annotations.Nullable; -public record GroupTasks(@Nullable ScheduledTask updateTask, @Nullable ScheduledTask headerFooterTask, @Nullable ScheduledTask latencyTask) { +public record GroupTasks(@Nullable ScheduledTask updateTask, @Nullable ScheduledTask headerFooterTask, + @Nullable ScheduledTask latencyTask) { public void cancel() { if (updateTask != null) { diff --git a/src/main/java/net/william278/velocitab/tab/Nametag.java b/src/main/java/net/william278/velocitab/tab/Nametag.java index cd29b25..9fef606 100644 --- a/src/main/java/net/william278/velocitab/tab/Nametag.java +++ b/src/main/java/net/william278/velocitab/tab/Nametag.java @@ -50,4 +50,8 @@ public record Nametag(@NotNull String prefix, @NotNull String suffix) { return (prefix.equals(other.prefix)) && (suffix.equals(other.suffix)); } + public boolean isEmpty() { + return prefix.isEmpty() && suffix.isEmpty(); + } + } diff --git a/src/main/java/net/william278/velocitab/tab/PlayerTabList.java b/src/main/java/net/william278/velocitab/tab/PlayerTabList.java index 472d134..c763cb1 100644 --- a/src/main/java/net/william278/velocitab/tab/PlayerTabList.java +++ b/src/main/java/net/william278/velocitab/tab/PlayerTabList.java @@ -26,6 +26,8 @@ import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import lombok.AccessLevel; import lombok.Getter; import net.kyori.adventure.text.Component; @@ -34,6 +36,7 @@ import net.william278.velocitab.api.PlayerAddedToTabEvent; import net.william278.velocitab.config.Group; import net.william278.velocitab.config.Placeholder; import net.william278.velocitab.config.ServerUrl; +import net.william278.velocitab.packet.ScoreboardManager; import net.william278.velocitab.player.Role; import net.william278.velocitab.player.TabPlayer; import org.jetbrains.annotations.NotNull; @@ -46,10 +49,13 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER; + /** - * The main class for tracking the server TAB list + * The main class for tracking the server TAB list for a map of {@link TabPlayer}s */ public class PlayerTabList { + private final Velocitab plugin; @Getter private final VanishTabList vanishTabList; @@ -141,6 +147,7 @@ public class PlayerTabList { players.values().forEach(p -> { p.unsetRelationalDisplayName(player.getUniqueId()); p.unsetRelationalNametag(player.getUniqueId()); + p.unsetTabListOrder(player.getUniqueId()); }); } @@ -220,10 +227,9 @@ public class PlayerTabList { } iteratedPlayer.sendHeaderAndFooter(this); } - plugin.getScoreboardManager().ifPresent(s -> { - s.resendAllTeams(tabPlayer); - tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(tabPlayer, t, false)); - }); + final ScoreboardManager scoreboardManager = plugin.getScoreboardManager(); + scoreboardManager.resendAllTeams(tabPlayer); + updateSorting(tabPlayer, false); fixDuplicateEntries(joined); // Fire event without listening for result plugin.getServer().getEventManager().fireAndForget(new PlayerAddedToTabEvent(tabPlayer, group)); @@ -278,7 +284,7 @@ public class PlayerTabList { .filter(entry -> !entry.getKey().equals(target.getUniqueId())) .forEach(entry -> target.getTabList().removeEntry(entry.getKey())); } catch (Throwable error) { - plugin.log(Level.ERROR, "Failed to fix duplicate entries for class " + target.getTabList().getClass().getName() , error); + plugin.log(Level.ERROR, "Failed to fix duplicate entries for class " + target.getTabList().getClass().getName(), error); } } @@ -288,12 +294,13 @@ public class PlayerTabList { /** * Remove a player from the tab list - * @param uuid + * + * @param uuid {@link UUID} of the {@link TabPlayer player} to remove */ - protected void removeTablistUUID(@NotNull UUID uuid) { - getPlayers().forEach((key, value) -> { - value.getPlayer().getTabList().getEntry(uuid).ifPresent(entry -> value.getPlayer().getTabList().removeEntry(uuid)); - }); + protected void removeTabListUUID(@NotNull UUID uuid) { + getPlayers().forEach((key, value) -> value.getPlayer().getTabList().getEntry(uuid).ifPresent( + entry -> value.getPlayer().getTabList().removeEntry(uuid) + )); } protected void removePlayer(@NotNull Player target, @Nullable RegisteredServer server) { @@ -318,7 +325,7 @@ public class PlayerTabList { .delay(250, TimeUnit.MILLISECONDS) .schedule(); // Delete player team - plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(target)); + plugin.getScoreboardManager().resetCache(target); //remove player from tab list cache getPlayers().remove(uuid); } @@ -389,13 +396,25 @@ public class PlayerTabList { return; } + updateSorting(tabPlayer, force); + } + + private void updateSorting(@NotNull TabPlayer tabPlayer, boolean force) { tabPlayer.getTeamName(plugin).thenAccept(teamName -> { if (teamName.isBlank()) { return; } - plugin.getScoreboardManager().ifPresent(manager -> manager.updateRole( - tabPlayer, teamName, force - )); + plugin.getScoreboardManager().updateRole(tabPlayer, teamName, force).thenAccept(v -> { + final int order = plugin.getScoreboardManager().getPosition(teamName); + if (order == -1) { + plugin.log(Level.ERROR, "Failed to get position for " + tabPlayer.getPlayer().getUsername()); + return; + } + + tabPlayer.setListOrder(order); + final Set players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer); + players.forEach(p -> recalculateSortingForPlayer(p, players)); + }); }); } @@ -533,4 +552,44 @@ public class PlayerTabList { players.remove(player.getUniqueId()); } + /** + * Whether the player can use server-side specified TAB list ordering (Minecraft 1.21.2+) + * + * @param tabPlayer player to check + * @return {@code true} if the user is on Minecraft 1.21.2+; {@code false} + */ + private boolean hasListOrder(@NotNull TabPlayer tabPlayer) { + return tabPlayer.getPlayer().getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2); + } + + private void updateSorting(@NotNull TabPlayer tabPlayer, @NotNull UUID uuid, int position) { + if (!tabPlayer.getPlayer().getTabList().containsEntry(uuid)) { + return; + } + if (tabPlayer.getCachedListOrders().containsKey(uuid) && tabPlayer.getCachedListOrders().get(uuid) == position) { + return; + } + tabPlayer.getCachedListOrders().put(uuid, position); + final UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(UPDATE_LIST_ORDER); + final UpsertPlayerInfoPacket.Entry entry = new UpsertPlayerInfoPacket.Entry(uuid); + entry.setListOrder(position); + packet.addEntry(entry); + ((ConnectedPlayer) tabPlayer.getPlayer()).getConnection().write(packet); + } + + private String getPlayerName(UUID uuid) { + return plugin.getServer().getPlayer(uuid).map(Player::getUsername).orElse("Unknown"); + } + + public synchronized void recalculateSortingForPlayer(@NotNull TabPlayer tabPlayer, @NotNull Set players) { + if (!hasListOrder(tabPlayer)) { + return; + } + + players.forEach(p -> { + final int order = p.getListOrder(); + updateSorting(tabPlayer, p.getPlayer().getUniqueId(), order); + }); + } + } diff --git a/src/main/java/net/william278/velocitab/tab/TabListListener.java b/src/main/java/net/william278/velocitab/tab/TabListListener.java index ed231bc..d03848a 100644 --- a/src/main/java/net/william278/velocitab/tab/TabListListener.java +++ b/src/main/java/net/william278/velocitab/tab/TabListListener.java @@ -48,7 +48,8 @@ public class TabListListener { private final Velocitab plugin; private final PlayerTabList tabList; - // In 1.8 there is a packet delay problem + + // Set of UUIDs of users who just left the game - fixes packet delay problem on Minecraft 1.8.x private final Set justQuit; public TabListListener(@NotNull Velocitab plugin, @NotNull PlayerTabList tabList) { @@ -102,7 +103,7 @@ public class TabListListener { plugin.getTabList().clearCachedData(joined); if (!plugin.getSettings().isShowAllPlayersFromAllGroups() && previousGroup.isPresent() - && (groupOptional.isPresent() && !previousGroup.get().equals(groupOptional.get()) + && (groupOptional.isPresent() && !previousGroup.get().equals(groupOptional.get()) || groupOptional.isEmpty())) { tabList.removeOldEntry(previousGroup.get(), joined.getUniqueId()); } @@ -126,7 +127,7 @@ public class TabListListener { final Component currentHeader = joined.getPlayerListHeader(); final Component currentFooter = joined.getPlayerListFooter(); if ((header.equals(currentHeader) && footer.equals(currentFooter)) || - (currentHeader.equals(Component.empty()) && currentFooter.equals(Component.empty())) + (currentHeader.equals(Component.empty()) && currentFooter.equals(Component.empty())) ) { joined.sendPlayerListHeaderAndFooter(Component.empty(), Component.empty()); joined.getCurrentServer().ifPresent(serverConnection -> serverConnection.getServer().getPlayersConnected().forEach(player -> @@ -144,7 +145,7 @@ public class TabListListener { } final Group group = groupOptional.get(); - plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group)); + plugin.getScoreboardManager().resetCache(joined, group); if (justQuit.contains(joined.getUniqueId())) { plugin.getServer().getScheduler().buildTask(plugin, () -> tabList.joinPlayer(joined, group)) @@ -156,7 +157,8 @@ public class TabListListener { tabList.joinPlayer(joined, group); } - @Subscribe(order = PostOrder.LAST) + @SuppressWarnings("deprecation") + @Subscribe(order = PostOrder.CUSTOM, priority = Short.MIN_VALUE) public void onPlayerQuit(@NotNull DisconnectEvent event) { if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) { checkDelayedDisconnect(event); @@ -178,7 +180,7 @@ public class TabListListener { return; } - tabList.removeTablistUUID(event.getPlayer().getUniqueId()); + tabList.removeTabListUUID(event.getPlayer().getUniqueId()); }).delay(750, TimeUnit.MILLISECONDS).schedule(); } diff --git a/src/main/java/net/william278/velocitab/tab/VanishTabList.java b/src/main/java/net/william278/velocitab/tab/VanishTabList.java index 4f692cb..10374f2 100644 --- a/src/main/java/net/william278/velocitab/tab/VanishTabList.java +++ b/src/main/java/net/william278/velocitab/tab/VanishTabList.java @@ -89,21 +89,21 @@ public class VanishTabList { final String serverName = target.getServerName(); if (tabPlayer.getGroup().onlyListPlayersInSameServer() - && !tabPlayer.getServerName().equals(serverName)) { + && !tabPlayer.getServerName().equals(serverName)) { return; } final boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) || - plugin.getVanishManager().canSee(player.getUsername(), p.getUsername()); + plugin.getVanishManager().canSee(player.getUsername(), p.getUsername()); if (!canSee) { player.getTabList().removeEntry(p.getUniqueId()); - plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, false)); + plugin.getScoreboardManager().recalculateVanishForPlayer(tabPlayer, target, false); } else { if (!player.getTabList().containsEntry(p.getUniqueId())) { final TabListEntry tabListEntry = tabList.createEntry(target, player.getTabList(), tabPlayer); player.getTabList().addEntry(tabListEntry); - plugin.getScoreboardManager().ifPresent(s -> s.recalculateVanishForPlayer(tabPlayer, target, true)); + plugin.getScoreboardManager().recalculateVanishForPlayer(tabPlayer, target, true); } } }); diff --git a/src/main/java/net/william278/velocitab/util/QuadFunction.java b/src/main/java/net/william278/velocitab/util/QuadFunction.java index b7c07b3..285bae4 100644 --- a/src/main/java/net/william278/velocitab/util/QuadFunction.java +++ b/src/main/java/net/william278/velocitab/util/QuadFunction.java @@ -32,4 +32,5 @@ public interface QuadFunction { * @return the function result */ R apply(T t, U u, V v, S s); + } \ No newline at end of file diff --git a/src/main/java/net/william278/velocitab/util/SerializerUtil.java b/src/main/java/net/william278/velocitab/util/SerializationUtil.java similarity index 96% rename from src/main/java/net/william278/velocitab/util/SerializerUtil.java rename to src/main/java/net/william278/velocitab/util/SerializationUtil.java index 6f863b7..4c0ed73 100644 --- a/src/main/java/net/william278/velocitab/util/SerializerUtil.java +++ b/src/main/java/net/william278/velocitab/util/SerializationUtil.java @@ -21,7 +21,7 @@ package net.william278.velocitab.util; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -public final class SerializerUtil { +public final class SerializationUtil { public final static LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder() .hexCharacter('#') diff --git a/src/main/java/net/william278/velocitab/vanish/VanishManager.java b/src/main/java/net/william278/velocitab/vanish/VanishManager.java index 607b36d..e2d73dd 100644 --- a/src/main/java/net/william278/velocitab/vanish/VanishManager.java +++ b/src/main/java/net/william278/velocitab/vanish/VanishManager.java @@ -61,7 +61,7 @@ public class VanishManager { } plugin.getTabList().getVanishTabList().vanishPlayer(tabPlayer.get()); - plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.vanishPlayer(tabPlayer.get())); + plugin.getScoreboardManager().vanishPlayer(tabPlayer.get()); } public void unVanishPlayer(@NotNull Player player) { @@ -72,6 +72,6 @@ public class VanishManager { } plugin.getTabList().getVanishTabList().unVanishPlayer(tabPlayer.get()); - plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.unVanishPlayer(tabPlayer.get())); + plugin.getScoreboardManager().unVanishPlayer(tabPlayer.get()); } } diff --git a/src/main/resources/metadata.yml b/src/main/resources/metadata.yml index 9fa88f5..b1aafc2 100644 --- a/src/main/resources/metadata.yml +++ b/src/main/resources/metadata.yml @@ -1,2 +1,3 @@ velocity_api_version: '${velocity_api_version}' -velocity_minimum_build: ${velocity_minimum_build} \ No newline at end of file +velocity_minimum_build: ${velocity_minimum_build} +papi_proxy_bridge_minimum_version: '${papi_proxy_bridge_minimum_version}' \ No newline at end of file