mirror of
https://github.com/WiIIiam278/Velocitab.git
synced 2025-03-13 13:30:01 +01:00
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:
parent
73de08eea9
commit
6f140e4708
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -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
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -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
|
17
build.gradle
17
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")
|
||||
}
|
||||
}
|
||||
|
@ -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 — 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 — Changing player's team color</summary>
|
||||
|
@ -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
|
||||
|
@ -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
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
20
gradlew.bat
vendored
@ -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
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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()))
|
||||
);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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?", ":"
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,6 +30,7 @@ public interface LoggerProvider {
|
||||
*
|
||||
* @return the logger for the class
|
||||
*/
|
||||
@NotNull
|
||||
Logger getLogger();
|
||||
|
||||
/**
|
||||
|
@ -36,7 +36,8 @@ public interface MetricProvider {
|
||||
|
||||
/**
|
||||
* Retrieves the Velocitab plugin instance.
|
||||
* @return
|
||||
*
|
||||
* @return The Velocitab plugin instance.
|
||||
*/
|
||||
Velocitab getPlugin();
|
||||
|
||||
|
@ -24,7 +24,6 @@ import net.william278.velocitab.packet.ScoreboardManager;
|
||||
import net.william278.velocitab.sorting.SortingManager;
|
||||
import net.william278.velocitab.tab.PlayerTabList;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public interface ScoreboardProvider {
|
||||
@ -37,11 +36,10 @@ public interface ScoreboardProvider {
|
||||
Velocitab getPlugin();
|
||||
|
||||
/**
|
||||
* Retrieves the optional scoreboard manager.
|
||||
*
|
||||
* @return An {@code Optional} object that may contain a {@code ScoreboardManager} instance.
|
||||
* Retrieves the scoreboard manager
|
||||
* @return The scoreboard manager
|
||||
*/
|
||||
Optional<ScoreboardManager> getScoreboardManager();
|
||||
ScoreboardManager getScoreboardManager();
|
||||
|
||||
/**
|
||||
* Sets the scoreboard manager.
|
||||
@ -85,11 +83,9 @@ public interface ScoreboardProvider {
|
||||
*
|
||||
*/
|
||||
default void prepareScoreboard() {
|
||||
if (getPlugin().getSettings().isSendScoreboardPackets()) {
|
||||
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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
}
|
@ -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('#')
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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}'
|
Loading…
Reference in New Issue
Block a user