feat: Add support for Minecraft 1.21.2/3 (#228)

* First step for 1.21.2

* fix

* feat: start preparing 1.21.2 support

bumps gradle and various build deps

* build: now requires Velocity 3.4.0

* build: use Velocity 3.4.0 from maven

* refactor: cleanup, fix wrong protocol ver in 765

* refactor: minor code cleanup & reformat

* refactor: further code cleanup

* refactor: more minor refactoring work

* docs: document prerequisites for using the plugin message API

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

* docs: further grammar fixes to plugin message API docs

* refactor: adjust PPB version checking logic

* build: simplify PPB test logic

* refactor: remove unused code

* refactor: adjust formatting

* refactor: make nametag empty by default

* refactor: suppress warning

* fix: `ConfigurationException` deserializing minimum PPB version string

* refactor: remove unused import

* Bug fixes

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

* Fixed problem with data structure

* Removed synchronized

* fix: subscriber order not taking effect

* refactor: minor code style tweaks

---------

Co-authored-by: AlexDev_ <56083016+alexdev03@users.noreply.github.com>
This commit is contained in:
William 2024-10-28 18:17:36 +00:00 committed by GitHub
parent 73de08eea9
commit 6f140e4708
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 422 additions and 165 deletions

View File

@ -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

View File

@ -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

View File

@ -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")
}
}

View File

@ -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.
<details>
<summary>Example &mdash; Changing player's username in the TAB List</summary>
@ -13,10 +20,10 @@ player.sendPluginMessage(plugin, "velocitab:update_custom_name", "Steve".getByte
```
</details>
### 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).
<details>
<summary>Example &mdash; Changing player's team color</summary>

View File

@ -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&mdash;1.8.9
- Minecraft 1.12.2&mdash;latest

View File

@ -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
velocity_api_version=3.4.0
velocity_minimum_build=444
papi_proxy_bridge_minimum_version=1.7

View File

@ -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

20
gradlew.bat vendored
View File

@ -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

View File

@ -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<ScoreboardManager> getScoreboardManager() {
return Optional.ofNullable(scoreboardManager);
@Override
public ScoreboardManager getScoreboardManager() {
return scoreboardManager;
}
private void prepareAPI() {

View File

@ -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<Version> papiProxyBridgeVersion = getPapiProxyBridgeVersion();
if (papiProxyBridgeVersion.isPresent()) {
metadata.validatePapiProxyBridgeVersion(papiProxyBridgeVersion.get());
}
}
@NotNull
@ -151,6 +161,12 @@ public interface ConfigProvider {
.findFirst();
}
default Optional<Version> getPapiProxyBridgeVersion() {
return getPlugin().getServer().getPluginManager()
.getPlugin("papiproxybridge").map(PluginContainer::getDescription)
.flatMap(PluginDescription::getVersion).map(Version::fromString);
}
@NotNull
Version getVelocityVersion();

View File

@ -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
);

View File

@ -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)) {

View File

@ -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()))
);
}

View File

@ -51,7 +51,7 @@ public class TabGroups implements ConfigValidator {
List.of("<rainbow:!2>Running Velocitab by William278 & AlexDev_</rainbow>"),
List.of("<gray>There are currently %players_online%/%max_players_online% players online</gray>"),
"<gray>[%server%] %prefix%%username%</gray>",
new Nametag("<white>%prefix%</white>", "<white>%suffix%</white>"),
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<Group> groups = List.of(DEFAULT_GROUP);
@NotNull
@SuppressWarnings("unused")
public Group getGroupFromName(@NotNull String name) {
return groups.stream()
.filter(group -> group.name().equals(name))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No group with name " + name + " found"));
.orElseThrow(() -> new IllegalStateException("No group with name %s found".formatted(name)));
}
public Optional<Group> getGroup(@NotNull String name) {

View File

@ -49,10 +49,10 @@ public class MiniConditionManager {
public final static Map<String, String> REPLACE_2 = Map.of(
"*LESS3*", "<",
"*GREATER3*",">",
"*GREATER3*", ">",
"*LESS2*", "<",
"*GREATER2*", ">"
);
);
private final static Map<String, String> REPLACE_3 = Map.of(
"?dp?", ":"

View File

@ -138,20 +138,31 @@ public final class PacketRegistration<P extends MinecraftPacket> {
final MethodHandles.Lookup lookup = MethodHandles.lookup();
try {
final MethodHandles.Lookup stateRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.class, lookup);
STATE_REGISTRY$clientBound = stateRegistryLookup.findGetter(StateRegistry.class, "clientbound", StateRegistry.PacketRegistry.class);
STATE_REGISTRY$serverBound = stateRegistryLookup.findGetter(StateRegistry.class, "serverbound", StateRegistry.PacketRegistry.class);
STATE_REGISTRY$clientBound = stateRegistryLookup.findGetter(
StateRegistry.class, "clientbound", StateRegistry.PacketRegistry.class);
STATE_REGISTRY$serverBound = stateRegistryLookup.findGetter(
StateRegistry.class, "serverbound", StateRegistry.PacketRegistry.class);
final MethodType mapType = MethodType.methodType(StateRegistry.PacketMapping.class, Integer.TYPE, ProtocolVersion.class, Boolean.TYPE);
PACKET_MAPPING$map = stateRegistryLookup.findStatic(StateRegistry.class, "map", mapType);
final MethodType mapType = MethodType.methodType(
StateRegistry.PacketMapping.class, Integer.TYPE, ProtocolVersion.class, Boolean.TYPE);
PACKET_MAPPING$map = stateRegistryLookup.findStatic(
StateRegistry.class, "map", mapType);
final MethodHandles.Lookup packetRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.class, lookup);
final MethodType registerType = MethodType.methodType(void.class, Class.class, Supplier.class, StateRegistry.PacketMapping[].class);
PACKET_REGISTRY$register = packetRegistryLookup.findVirtual(StateRegistry.PacketRegistry.class, "register", registerType);
PACKET_REGISTRY$versions = packetRegistryLookup.findGetter(StateRegistry.PacketRegistry.class, "versions", Map.class);
final MethodHandles.Lookup packetRegistryLookup = MethodHandles.privateLookupIn(
StateRegistry.PacketRegistry.class, lookup);
final MethodType registerType = MethodType.methodType(
void.class, Class.class, Supplier.class, StateRegistry.PacketMapping[].class);
PACKET_REGISTRY$register = packetRegistryLookup.findVirtual(
StateRegistry.PacketRegistry.class, "register", registerType);
PACKET_REGISTRY$versions = packetRegistryLookup.findGetter(
StateRegistry.PacketRegistry.class, "versions", Map.class);
final MethodHandles.Lookup protocolRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.ProtocolRegistry.class, lookup);
PACKET_REGISTRY$packetIdToSupplier = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetIdToSupplier", IntObjectMap.class);
PACKET_REGISTRY$packetClassToId = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetClassToId", Object2IntMap.class);
final MethodHandles.Lookup protocolRegistryLookup = MethodHandles.privateLookupIn(
StateRegistry.PacketRegistry.ProtocolRegistry.class, lookup);
PACKET_REGISTRY$packetIdToSupplier = protocolRegistryLookup.findGetter(
StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetIdToSupplier", IntObjectMap.class);
PACKET_REGISTRY$packetClassToId = protocolRegistryLookup.findGetter(
StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetClassToId", Object2IntMap.class);
} catch (Throwable e) {

View File

@ -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> 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;
}

View File

@ -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
*/

View File

@ -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)
));
}
}

View File

@ -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<UpdateTeamsPacket> packetRegistration;
private final Velocitab plugin;
private final Set<TeamsPacketAdapter> versions;
private final boolean teams;
private final Map<ProtocolVersion, TeamsPacketAdapter> versions;
@Getter
private final Map<UUID, String> createdTeams;
private final Map<String, Nametag> nametags;
private final Multimap<UUID, String> trackedTeams;
@Getter
private final SortedSet sortedTeams;
public ScoreboardManager(@NotNull Velocitab velocitab) {
public ScoreboardManager(@NotNull Velocitab velocitab, boolean teams) {
this.plugin = velocitab;
this.teams = teams;
this.createdTeams = Maps.newConcurrentMap();
this.nametags = Maps.newConcurrentMap();
this.versions = Sets.newHashSet();
this.versions = Maps.newHashMap();
this.trackedTeams = Multimaps.synchronizedMultimap(Multimaps.newSetMultimap(Maps.newConcurrentMap(), Sets::newConcurrentHashSet));
this.sortedTeams = new SortedSet(Comparator.reverseOrder());
this.registerVersions();
}
public boolean handleTeams() {
return teams;
}
private void registerVersions() {
try {
versions.add(new Protocol765Adapter(plugin));
versions.add(new Protocol735Adapter(plugin));
versions.add(new Protocol404Adapter(plugin));
versions.add(new Protocol48Adapter(plugin));
final Protocol765Adapter protocol765Adapter = new Protocol765Adapter(plugin);
protocol765Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol765Adapter));
final Protocol735Adapter protocol735Adapter = new Protocol735Adapter(plugin);
protocol735Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol735Adapter));
final Protocol404Adapter protocol404Adapter = new Protocol404Adapter(plugin);
protocol404Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol404Adapter));
final Protocol48Adapter protocol48Adapter = new Protocol48Adapter(plugin);
protocol48Adapter.getProtocolVersions().forEach(version -> versions.put(version, protocol48Adapter));
} catch (NoSuchFieldError e) {
throw new IllegalStateException("Failed to register Scoreboard 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<RegisteredServer> siblings = tabPlayer.getGroup().registeredServers(plugin);
final boolean isNameTagEmpty = tabPlayer.getGroup().nametag().isEmpty();
final Optional<Nametag> cachedTag = Optional.ofNullable(nametags.getOrDefault(teamName, null));
cachedTag.ifPresent(nametag -> siblings.forEach(server -> server.getPlayersConnected().stream().filter(p -> p != player)
.forEach(connected -> {
if (vanish && !plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername())) {
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<Void> updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
final Player player = tabPlayer.getPlayer();
if (!player.isActive()) {
plugin.getTabList().removeOfflinePlayer(player);
return;
return CompletableFuture.completedFuture(null);
}
final String name = player.getUsername();
final CompletableFuture<Void> future = new CompletableFuture<>();
tabPlayer.getNametag(plugin).thenAccept(newTag -> {
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
if (createdTeams.containsKey(player.getUniqueId())) {
@ -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<ServerConnection> optionalServerConnection = player.getCurrentServer();
if (optionalServerConnection.isEmpty()) {
@ -330,6 +373,7 @@ public class ScoreboardManager {
}
final Set<Player> 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) {

View File

@ -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<ScoreboardManager> 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<ScoreboardManager> optionalManager = plugin.getScoreboardManager();
if (optionalManager.isEmpty()) {
return;
}
optionalManager.get().getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion);
final ScoreboardManager scoreboardManager = plugin.getScoreboardManager();
scoreboardManager.getPacketAdapter(protocolVersion).encode(byteBuf, this, protocolVersion);
}
@Override

View File

@ -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<TabPlayer> {
private final Map<UUID, Component> relationalDisplayNames;
private final Map<UUID, Component[]> relationalNametags;
private final Map<String, String> cachedPlaceholders;
private final Map<UUID, Integer> cachedListOrders;
private String lastDisplayName;
private Component lastHeader;
private Component lastFooter;
private String teamName;
@Setter
private int listOrder = -1;
@Nullable
@Setter
private UpdateTeamsPacket.TeamColor teamColor;
@ -86,6 +92,7 @@ public final class TabPlayer implements Comparable<TabPlayer> {
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<TabPlayer> {
relationalNametags.remove(target);
}
public void unsetTabListOrder(@NotNull UUID target) {
cachedListOrders.remove(target);
}
public Optional<Component[]> getRelationalNametag(@NotNull UUID target) {
return Optional.ofNullable(relationalNametags.get(target));
}
@ -248,6 +259,8 @@ public final class TabPlayer implements Comparable<TabPlayer> {
lastHeader = null;
lastFooter = null;
role = Role.DEFAULT_ROLE;
teamName = null;
cachedListOrders.clear();
}
/**

View File

@ -30,6 +30,7 @@ public interface LoggerProvider {
*
* @return the logger for the class
*/
@NotNull
Logger getLogger();
/**

View File

@ -36,7 +36,8 @@ public interface MetricProvider {
/**
* Retrieves the Velocitab plugin instance.
* @return
*
* @return The Velocitab plugin instance.
*/
Velocitab getPlugin();

View File

@ -24,7 +24,6 @@ import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.sorting.SortingManager;
import net.william278.velocitab.tab.PlayerTabList;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public interface ScoreboardProvider {
@ -37,11 +36,10 @@ public interface ScoreboardProvider {
Velocitab getPlugin();
/**
* Retrieves the optional scoreboard manager.
*
* @return An {@code Optional} object that may contain a {@code ScoreboardManager} instance.
* Retrieves the scoreboard manager
* @return The scoreboard manager
*/
Optional<ScoreboardManager> getScoreboardManager();
ScoreboardManager getScoreboardManager();
/**
* Sets the scoreboard manager.
@ -85,11 +83,9 @@ public interface ScoreboardProvider {
*
*/
default void prepareScoreboard() {
if (getPlugin().getSettings().isSendScoreboardPackets()) {
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();
}

View File

@ -0,0 +1,74 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.sorting;
import com.google.common.collect.Maps;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListSet;
public class SortedSet {
private final ConcurrentSkipListSet<String> sortedTeams;
private final Map<String, Integer> positionMap;
public SortedSet(@NotNull Comparator<String> comparator) {
sortedTeams = new ConcurrentSkipListSet<>(comparator);
positionMap = Maps.newConcurrentMap();
}
public synchronized boolean addTeam(@NotNull String teamName) {
final boolean result = sortedTeams.add(teamName);
if (!result) {
return false;
}
updatePositions();
return true;
}
public synchronized boolean removeTeam(@NotNull String teamName) {
final boolean result = sortedTeams.remove(teamName);
if (!result) {
return false;
}
updatePositions();
return true;
}
private synchronized void updatePositions() {
int index = 0;
positionMap.clear();
for (final String team : sortedTeams) {
positionMap.put(team, index);
index++;
}
}
public synchronized int getPosition(@NotNull String teamName) {
return positionMap.getOrDefault(teamName, -1);
}
@Override
public String toString() {
return sortedTeams.toString();
}
}

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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<TabPlayer> 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<TabPlayer> players) {
if (!hasListOrder(tabPlayer)) {
return;
}
players.forEach(p -> {
final int order = p.getListOrder();
updateSorting(tabPlayer, p.getPlayer().getUniqueId(), order);
});
}
}

View File

@ -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<UUID> 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();
}

View File

@ -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);
}
}
});

View File

@ -32,4 +32,5 @@ public interface QuadFunction<T, U, V, S, R> {
* @return the function result
*/
R apply(T t, U u, V v, S s);
}

View File

@ -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('#')

View File

@ -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());
}
}

View File

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