diff --git a/build.gradle b/build.gradle index 029cca6..e2c55c6 100644 --- a/build.gradle +++ b/build.gradle @@ -23,8 +23,8 @@ repositories { maven { url = 'https://repo.william278.net/releases/' } maven { url = 'https://jitpack.io/' } maven { url = 'https://repo.minebench.de/' } + maven { url = 'https://maven.elytrium.net/repo/' } maven { url = 'https://mvn.exceptionflug.de/repository/exceptionflug-public/' } - maven { url = "https://maven.elytrium.net/repo/" } } dependencies { diff --git a/src/main/java/net/william278/velocitab/Velocitab.java b/src/main/java/net/william278/velocitab/Velocitab.java index 6e1a228..edf81ca 100644 --- a/src/main/java/net/william278/velocitab/Velocitab.java +++ b/src/main/java/net/william278/velocitab/Velocitab.java @@ -28,12 +28,13 @@ import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.annotation.DataDirectory; -import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.scheduler.ScheduledTask; +import lombok.Getter; import net.william278.annotaml.Annotaml; import net.william278.desertwell.util.UpdateChecker; import net.william278.desertwell.util.Version; +import net.william278.velocitab.api.VelocitabAPI; import net.william278.velocitab.commands.VelocitabCommand; import net.william278.velocitab.config.Formatter; import net.william278.velocitab.config.Settings; @@ -42,10 +43,9 @@ import net.william278.velocitab.hook.LuckPermsHook; import net.william278.velocitab.hook.MiniPlaceholdersHook; import net.william278.velocitab.hook.PAPIProxyBridgeHook; import net.william278.velocitab.packet.ScoreboardManager; -import net.william278.velocitab.player.Role; -import net.william278.velocitab.player.TabPlayer; import net.william278.velocitab.sorting.SortingManager; import net.william278.velocitab.tab.PlayerTabList; +import net.william278.velocitab.vanish.VanishManager; import org.bstats.charts.SimplePie; import org.bstats.velocity.Metrics; import org.jetbrains.annotations.NotNull; @@ -75,6 +75,8 @@ public class Velocitab { private List hooks; private ScoreboardManager scoreboardManager; private SortingManager sortingManager; + @Getter + private VanishManager vanishManager; @Inject public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path dataDirectory) { @@ -90,9 +92,11 @@ public class Velocitab { prepareScoreboardManager(); prepareTabList(); prepareSortingManager(); + prepareVanishManager(); registerCommands(); registerMetrics(); checkForUpdates(); + prepareAPI(); logger.info("Successfully enabled Velocitab"); } @@ -101,6 +105,7 @@ public class Velocitab { server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel); disableScoreboardManager(); getLuckPermsHook().ifPresent(LuckPermsHook::close); + VelocitabAPI.unregister(); logger.info("Successfully disabled Velocitab"); } @@ -173,6 +178,10 @@ public class Velocitab { } } + private void prepareVanishManager() { + this.vanishManager = new VanishManager(this); + } + private void prepareSortingManager() { this.sortingManager = new SortingManager(this); } @@ -197,16 +206,8 @@ public class Velocitab { server.getEventManager().register(this, tabList); } - @NotNull - public TabPlayer getTabPlayer(@NotNull Player player) { - return new TabPlayer(player, - getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE) - ); - } - - @SuppressWarnings("unused") - public Optional getTabPlayer(String name) { - return server.getPlayer(name).map(this::getTabPlayer); + private void prepareAPI() { + VelocitabAPI.register(this); } private void registerCommands() { diff --git a/src/main/java/net/william278/velocitab/api/VelocitabAPI.java b/src/main/java/net/william278/velocitab/api/VelocitabAPI.java new file mode 100644 index 0000000..e0f2aeb --- /dev/null +++ b/src/main/java/net/william278/velocitab/api/VelocitabAPI.java @@ -0,0 +1,181 @@ +/* + * This file is part of Velocitab, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package net.william278.velocitab.api; + +import com.velocitypowered.api.proxy.Player; +import net.william278.velocitab.Velocitab; +import net.william278.velocitab.player.TabPlayer; +import net.william278.velocitab.tab.PlayerTabList; +import net.william278.velocitab.vanish.VanishIntegration; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +@SuppressWarnings("unused") + +/** + * The Velocitab API class. + * Retrieve an instance of the API class via {@link #getInstance()}. + */ public class VelocitabAPI { + + // Instance of the plugin + private final Velocitab plugin; + private static VelocitabAPI instance; + + @ApiStatus.Internal + protected VelocitabAPI(@NotNull Velocitab plugin) { + this.plugin = plugin; + } + + /** + * Entrypoint to the {@link VelocitabAPI} API - returns an instance of the API + * + * @return instance of the HuskSync API + * @since 1.5.2 + */ + @NotNull + public static VelocitabAPI getInstance() { + if (instance == null) { + throw new NotRegisteredException(); + } + return instance; + } + + /** + * (Internal use only) - Register the API. + * + * @param plugin the plugin instance + * @since 3.0 + */ + @ApiStatus.Internal + public static void register(@NotNull Velocitab plugin) { + instance = new VelocitabAPI(plugin); + } + + /** + * (Internal use only) - Unregister the API. + */ + @ApiStatus.Internal + public static void unregister() { + instance = null; + } + + /** + * Returns an option of {@link TabPlayer} instance for the given Velocity {@link Player}. + * + * @param player the Velocity player to get the {@link TabPlayer} instance for + * @return the {@link TabPlayer} instance for the given player or an empty optional if the player is not in a group server + * @since 2.0 + */ + public Optional getUser(@NotNull Player player) { + return plugin.getTabList().getTabPlayer(player); + } + + /** + * Sets the custom name for a player. + * This will only be visible in the tab list and not in the nametag. + * + * @param player The player for whom to set the custom name + * @param name The custom name to set + */ + public void setCustomPlayerName(@NotNull Player player, @Nullable String name) { + getUser(player).ifPresent(tabPlayer -> { + tabPlayer.setCustomName(name); + plugin.getTabList().updatePlayerDisplayName(tabPlayer); + }); + } + + /** + * Returns the custom name of the TabPlayer, if it has been set. + * + * @param player The player for whom to get the custom name + * @return An Optional object containing the custom name, or empty if no custom name has been set. + */ + public Optional getCustomPlayerName(@NotNull Player player) { + return getUser(player).flatMap(TabPlayer::getCustomName); + } + + /** + * {@link PlayerTabList} handles the tab list for all players on the server groups. + * + * @return the {@link PlayerTabList} global instance. + */ + @NotNull + public PlayerTabList getTabList() { + return plugin.getTabList(); + } + + /** + * Sets the VanishIntegration for the VelocitabAPI. + * + * @param vanishIntegration the VanishIntegration to set + */ + public void setVanishIntegration(@NotNull VanishIntegration vanishIntegration) { + plugin.getVanishManager().setIntegration(vanishIntegration); + } + + /** + * Retrieves the VanishIntegration associated with the VelocitabAPI instance. + * This integration allows checking if a player can see another player and if a player is vanished. + * + * @return The VanishIntegration instance associated with the VelocitabAPI + */ + @NotNull + public VanishIntegration getVanishIntegration() { + return plugin.getVanishManager().getIntegration(); + } + + /** + * Vanishes the player by hiding them from the tab list and scoreboard if enabled. + * + * @param player The player to vanish + */ + public void vanishPlayer(@NotNull Player player) { + plugin.getVanishManager().vanishPlayer(player); + } + + /** + * Un-vanishes the given player by showing them in the tab list and scoreboard if enabled. + * + * @param player The player to unvanish + */ + public void unVanishPlayer(@NotNull Player player) { + plugin.getVanishManager().unVanishPlayer(player); + } + + + static final class NotRegisteredException extends IllegalStateException { + + private static final String MESSAGE = """ + Could not access the Velocitab API as it has not yet been registered. This could be because: + 1) Velocitab has failed to enable successfully + 2) You are attempting to access Velocitab on plugin construction/before your plugin has enabled. + 3) You have shaded Velocitab into your plugin jar and need to fix your maven/gradle/build script + to only include Velocitab as a dependency and not as a shaded dependency."""; + + NotRegisteredException() { + super(MESSAGE); + } + + } +} diff --git a/src/main/java/net/william278/velocitab/commands/VelocitabCommand.java b/src/main/java/net/william278/velocitab/commands/VelocitabCommand.java index bca71c8..9492845 100644 --- a/src/main/java/net/william278/velocitab/commands/VelocitabCommand.java +++ b/src/main/java/net/william278/velocitab/commands/VelocitabCommand.java @@ -20,15 +20,21 @@ package net.william278.velocitab.commands; import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; import net.william278.desertwell.about.AboutMenu; import net.william278.velocitab.Velocitab; +import net.william278.velocitab.player.TabPlayer; import org.jetbrains.annotations.NotNull; +import java.util.Optional; + public final class VelocitabCommand { private static final TextColor MAIN_COLOR = TextColor.color(0x00FB9A); private final AboutMenu aboutMenu; @@ -69,6 +75,29 @@ public final class VelocitabCommand { return Command.SINGLE_SUCCESS; }) ) + .then(LiteralArgumentBuilder.literal("name") + .then(RequiredArgumentBuilder.argument("name", StringArgumentType.word()) + .executes(ctx -> { + if (!(ctx.getSource() instanceof Player player)) { + ctx.getSource().sendMessage(Component.text("You must be a player to use this command!", MAIN_COLOR)); + return Command.SINGLE_SUCCESS; + } + + String name = StringArgumentType.getString(ctx, "name"); + Optional tabPlayer = plugin.getTabList().getTabPlayer(player); + + if (tabPlayer.isEmpty()) { + ctx.getSource().sendMessage(Component.text("You must in a correct server!", MAIN_COLOR)); + return Command.SINGLE_SUCCESS; + } + + tabPlayer.get().setCustomName(name); + plugin.getTabList().updatePlayerDisplayName(tabPlayer.get()); + + return Command.SINGLE_SUCCESS; + }) + ) + ) .then(LiteralArgumentBuilder.literal("reload") .requires(src -> src.hasPermission("velocitab.command.reload")) .executes(ctx -> { @@ -91,7 +120,7 @@ public final class VelocitabCommand { } ctx.getSource().sendMessage(Component .text("An update for velocitab is available. " + - "Please update to " + checked.getLatestVersion(), MAIN_COLOR)); + "Please update to " + checked.getLatestVersion(), MAIN_COLOR)); }); return Command.SINGLE_SUCCESS; }) diff --git a/src/main/java/net/william278/velocitab/config/Placeholder.java b/src/main/java/net/william278/velocitab/config/Placeholder.java index d20434f..0014f08 100644 --- a/src/main/java/net/william278/velocitab/config/Placeholder.java +++ b/src/main/java/net/william278/velocitab/config/Placeholder.java @@ -43,7 +43,7 @@ public enum Placeholder { .orElse("")), CURRENT_DATE((plugin, player) -> DateTimeFormatter.ofPattern("dd MMM yyyy").format(LocalDateTime.now())), CURRENT_TIME((plugin, player) -> DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now())), - USERNAME((plugin, player) -> plugin.getFormatter().escape(player.getPlayer().getUsername())), + USERNAME((plugin, player) -> plugin.getFormatter().escape(player.getCustomName().orElse(player.getPlayer().getUsername()))), SERVER((plugin, player) -> player.getServerDisplayName(plugin)), PING((plugin, player) -> Long.toString(player.getPlayer().getPing())), PREFIX((plugin, player) -> player.getRole().getPrefix().orElse("")), @@ -83,10 +83,4 @@ public enum Placeholder { return replaced; }); } - - @NotNull - public static String formatSortableInt(int value, int maxValue) { - return String.format("%0" + Integer.toString(maxValue).length() + "d", maxValue - value); - } - } diff --git a/src/main/java/net/william278/velocitab/hook/LuckPermsHook.java b/src/main/java/net/william278/velocitab/hook/LuckPermsHook.java index e01b547..9498aa4 100644 --- a/src/main/java/net/william278/velocitab/hook/LuckPermsHook.java +++ b/src/main/java/net/william278/velocitab/hook/LuckPermsHook.java @@ -34,12 +34,14 @@ import net.william278.velocitab.tab.PlayerTabList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; public class LuckPermsHook extends Hook { - private int highestWeight = Role.DEFAULT_WEIGHT; private final LuckPerms api; private final EventSubscription event; private final Map lastUpdate; @@ -87,13 +89,9 @@ public class LuckPermsHook extends Hook { plugin.getServer().getPlayer(event.getUser().getUniqueId()) .ifPresent(player -> plugin.getServer().getScheduler() .buildTask(plugin, () -> { - final TabPlayer updatedPlayer = new TabPlayer( - player, - getRoleFromMetadata(event.getData().getMetaData()) - ); - tabList.replacePlayer(updatedPlayer); - tabList.updatePlayer(updatedPlayer); - tabList.updatePlayerDisplayName(updatedPlayer); + final TabPlayer tabPlayer = tabList.getTabPlayer(player).orElseThrow(); + tabPlayer.setRole(getRoleFromMetadata(event.getData().getMetaData())); + tabList.updatePlayerDisplayName(tabPlayer); }) .delay(500, TimeUnit.MILLISECONDS) .schedule()); diff --git a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java index 08b5bbc..f207a07 100644 --- a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java +++ b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java @@ -74,6 +74,63 @@ public class ScoreboardManager { } } + public void vanishPlayer(@NotNull Player player) { + if (!plugin.getSettings().isSortPlayers()) { + return; + } + + final Optional optionalServerConnection = player.getCurrentServer(); + if (optionalServerConnection.isEmpty()) { + return; + } + + final RegisteredServer serverInfo = optionalServerConnection.get().getServer(); + final List siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName()); + UpdateTeamsPacket packet = UpdateTeamsPacket.removeTeam(plugin, createdTeams.get(player.getUniqueId())); + + siblings.forEach(server -> server.getPlayersConnected().forEach(connected -> { + boolean canSee = !plugin.getVanishManager().isVanished(connected.getUsername()) + || plugin.getVanishManager().canSee(player.getUsername(), player.getUsername()); + + if (!canSee) { + return; + } + + dispatchPacket(packet, connected); + })); + } + + public void unVanishPlayer(@NotNull Player player) { + if (!plugin.getSettings().isSortPlayers()) { + return; + } + + final Optional optionalServerConnection = player.getCurrentServer(); + if (optionalServerConnection.isEmpty()) { + return; + } + + final RegisteredServer serverInfo = optionalServerConnection.get().getServer(); + final List siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName()); + + final String role = createdTeams.getOrDefault(player.getUniqueId(), ""); + if (role.isEmpty()) { + return; + } + + final String nametag = nametags.getOrDefault(role, ""); + if (nametag.isEmpty()) { + return; + } + + final String[] split = nametag.split(NAMETAG_DELIMITER, 2); + final String prefix = split[0]; + final String suffix = split.length > 1 ? split[1] : ""; + final UpdateTeamsPacket packet = UpdateTeamsPacket.create(plugin, createdTeams.get(player.getUniqueId()), "", prefix, suffix, player.getUsername()); + + siblings.forEach(server -> server.getPlayersConnected().forEach(connected -> dispatchPacket(packet, connected))); + } + public void updateRole(@NotNull Player player, @NotNull String role) { if (!player.isActive()) { plugin.getTabList().removeOfflinePlayer(player); @@ -81,11 +138,11 @@ public class ScoreboardManager { } final String name = player.getUsername(); - final TabPlayer tabPlayer = plugin.getTabPlayer(player); + final TabPlayer tabPlayer = plugin.getTabList().getTabPlayer(player).orElseThrow(); tabPlayer.getNametag(plugin).thenAccept(nametag -> { - String[] split = nametag.split(player.getUsername(), 2); - String prefix = split[0]; - String suffix = split.length > 1 ? split[1] : ""; + final String[] split = nametag.split(player.getUsername(), 2); + final String prefix = split[0]; + final String suffix = split.length > 1 ? split[1] : ""; if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) { if (createdTeams.containsKey(player.getUniqueId())) { @@ -106,8 +163,8 @@ public class ScoreboardManager { } - public void resendAllNameTags(Player player) { - if (!plugin.getSettings().doNametags()) { + public void resendAllTeams(@NotNull Player player) { + if (!plugin.getSettings().isSendScoreboardPackets()) { return; } @@ -122,16 +179,31 @@ public class ScoreboardManager { .map(RegisteredServer::getPlayersConnected) .flatMap(Collection::stream) .toList(); + + final List roles = new ArrayList<>(); + players.forEach(p -> { if (p == player || !p.isActive()) { return; } + if (plugin.getVanishManager().isVanished(p.getUsername()) || + !plugin.getVanishManager().canSee(player.getUsername(), p.getUsername())) { + return; + } + final String role = createdTeams.getOrDefault(p.getUniqueId(), ""); if (role.isEmpty()) { return; } + // Prevent duplicate packets + if (roles.contains(role)) { + return; + } + + roles.add(role); + final String nametag = nametags.getOrDefault(role, ""); if (nametag.isEmpty()) { return; @@ -168,6 +240,12 @@ public class ScoreboardManager { final List siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName()); siblings.forEach(server -> server.getPlayersConnected().forEach(connected -> { try { + final boolean canSee = !plugin.getVanishManager().isVanished(connected.getUsername()) + || plugin.getVanishManager().canSee(player.getUsername(), player.getUsername()); + if (!canSee) { + return; + } + final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected; connectedPlayer.getConnection().write(packet); } catch (Throwable e) { diff --git a/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java b/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java index 1097dda..fe42990 100644 --- a/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java +++ b/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java @@ -69,7 +69,7 @@ public class UpdateTeamsPacket implements MinecraftPacket { .mode(UpdateMode.CREATE_TEAM) .displayName(displayName) .friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY)) - .nameTagVisibility(NameTagVisibility.ALWAYS) + .nameTagVisibility(isNametagPresent(prefix, suffix) ? NameTagVisibility.ALWAYS : NameTagVisibility.NEVER) .collisionRule(CollisionRule.ALWAYS) .color(getLastColor(prefix)) .prefix(prefix == null ? "" : prefix) @@ -77,6 +77,10 @@ public class UpdateTeamsPacket implements MinecraftPacket { .entities(Arrays.asList(teamMembers)); } + private static boolean isNametagPresent(@Nullable String prefix, @Nullable String suffix) { + return prefix != null && !prefix.isEmpty() || suffix != null && !suffix.isEmpty(); + } + @NotNull protected static UpdateTeamsPacket changeNameTag(@NotNull Velocitab plugin, @NotNull String teamName, @Nullable String prefix, @Nullable String suffix) { @@ -85,7 +89,7 @@ public class UpdateTeamsPacket implements MinecraftPacket { .mode(UpdateMode.UPDATE_INFO) .displayName(teamName) .friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY)) - .nameTagVisibility(NameTagVisibility.ALWAYS) + .nameTagVisibility(isNametagPresent(prefix, suffix) ? NameTagVisibility.ALWAYS : NameTagVisibility.NEVER) .collisionRule(CollisionRule.ALWAYS) .color(getLastColor(prefix)) .prefix(prefix == null ? "" : prefix) diff --git a/src/main/java/net/william278/velocitab/player/TabPlayer.java b/src/main/java/net/william278/velocitab/player/TabPlayer.java index 7e12803..4ac46ce 100644 --- a/src/main/java/net/william278/velocitab/player/TabPlayer.java +++ b/src/main/java/net/william278/velocitab/player/TabPlayer.java @@ -21,18 +21,21 @@ package net.william278.velocitab.player; import com.velocitypowered.api.proxy.Player; import lombok.Getter; +import lombok.Setter; import net.kyori.adventure.text.Component; import net.william278.velocitab.Velocitab; import net.william278.velocitab.config.Placeholder; import net.william278.velocitab.tab.PlayerTabList; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Optional; import java.util.concurrent.CompletableFuture; public final class TabPlayer implements Comparable { private final Player player; - private final Role role; + @Setter + private Role role; @Getter private int headerIndex = 0; @Getter @@ -40,6 +43,8 @@ public final class TabPlayer implements Comparable { @Getter private Component lastDisplayname; private String teamName; + @Nullable + private String customName; public TabPlayer(@NotNull Player player, @NotNull Role role) { this.player = player; @@ -153,6 +158,24 @@ public final class TabPlayer implements Comparable { } } + /** + * Returns the custom name of the TabPlayer, if it has been set. + * + * @return An Optional object containing the custom name, or empty if no custom name has been set. + */ + public Optional getCustomName() { + return Optional.ofNullable(customName); + } + + /** + * Sets the custom name of the TabPlayer. + * + * @param customName The custom name to set + */ + public void setCustomName(@Nullable String customName) { + this.customName = customName; + } + @Override public int compareTo(@NotNull TabPlayer o) { final int roleDifference = role.compareTo(o.role); diff --git a/src/main/java/net/william278/velocitab/tab/PlayerTabList.java b/src/main/java/net/william278/velocitab/tab/PlayerTabList.java index 7523aba..941c96b 100644 --- a/src/main/java/net/william278/velocitab/tab/PlayerTabList.java +++ b/src/main/java/net/william278/velocitab/tab/PlayerTabList.java @@ -33,6 +33,7 @@ import com.velocitypowered.api.scheduler.ScheduledTask; import net.kyori.adventure.text.Component; import net.william278.velocitab.Velocitab; import net.william278.velocitab.config.Placeholder; +import net.william278.velocitab.player.Role; import net.william278.velocitab.player.TabPlayer; import org.jetbrains.annotations.NotNull; @@ -63,17 +64,17 @@ public class PlayerTabList { } } + public Optional getTabPlayer(@NotNull Player player) { + return players.stream().filter(tabPlayer -> tabPlayer.getPlayer().getUniqueId().equals(player.getUniqueId())).findFirst(); + } + @SuppressWarnings("UnstableApiUsage") @Subscribe public void onPlayerJoin(@NotNull ServerPostConnectEvent event) { final Player joined = event.getPlayer(); plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined)); - // Remove the player from the tracking list if they are switching servers final RegisteredServer previousServer = event.getPreviousServer(); - if (previousServer != null) { - players.removeIf(player -> player.getPlayer().getUniqueId().equals(joined.getUniqueId())); - } // Get the servers in the group from the joined server name // If the server is not in a group, use fallback @@ -90,10 +91,11 @@ public class PlayerTabList { return; } - // Add the player to the tracking list - final TabPlayer tabPlayer = plugin.getTabPlayer(joined); + // Add the player to the tracking list if they are not already listed + final TabPlayer tabPlayer = getTabPlayer(joined).orElseGet(() -> createTabPlayer(joined)); players.add(tabPlayer); + final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername()); // Update lists plugin.getServer().getScheduler() .buildTask(plugin, () -> { @@ -104,22 +106,31 @@ public class PlayerTabList { && !serversInGroup.get().contains(player.getServerName())) { continue; } - - // Create or update TAB list entries for all players - tabList.getEntries().stream() - .filter(e -> e.getProfile().getId().equals(player.getPlayer().getUniqueId())) - .findFirst().ifPresentOrElse( - entry -> player.getDisplayName(plugin).thenAccept(entry::setDisplayName), - () -> createEntry(player, tabList).thenAccept(tabList::addEntry) - ); - addPlayerToTabList(player, tabPlayer); + // check if current player can see the joined player + if (!isVanished || plugin.getVanishManager().canSee(player.getPlayer().getUsername(), joined.getUsername())) { + addPlayerToTabList(player, tabPlayer); + } else { + player.getPlayer().getTabList().removeEntry(joined.getUniqueId()); + } + // check if joined player can see current player + if ((plugin.getVanishManager().isVanished(player.getPlayer().getUsername()) || + !plugin.getVanishManager().canSee(joined.getUsername(), player.getPlayer().getUsername())) && player.getPlayer() != joined) { + tabList.removeEntry(player.getPlayer().getUniqueId()); + } else { + tabList.getEntries().stream() + .filter(e -> e.getProfile().getId().equals(player.getPlayer().getUniqueId())).findFirst() + .ifPresentOrElse( + entry -> player.getDisplayName(plugin).thenAccept(entry::setDisplayName), + () -> createEntry(player, tabList).thenAccept(tabList::addEntry) + ); + } player.sendHeaderAndFooter(this); } plugin.getScoreboardManager().ifPresent(s -> { - s.resendAllNameTags(joined); - plugin.getTabPlayer(joined).getTeamName(plugin).thenAccept(t -> s.updateRole(joined, t)); + s.resendAllTeams(joined); + tabPlayer.getTeamName(plugin).thenAccept(t -> s.updateRole(joined, t)); }); }) .delay(500, TimeUnit.MILLISECONDS) @@ -136,6 +147,16 @@ public class PlayerTabList { .build()); } + @NotNull + private TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, Component displayName) { + return TabListEntry.builder() + .profile(player.getPlayer().getGameProfile()) + .displayName(displayName) + .latency(0) + .tabList(tabList) + .build(); + } + private void addPlayerToTabList(@NotNull TabPlayer player, @NotNull TabPlayer newPlayer) { if (newPlayer.getPlayer().getUniqueId().equals(player.getPlayer().getUniqueId())) { return; @@ -152,6 +173,20 @@ public class PlayerTabList { } + private void addPlayerToTabList(@NotNull TabPlayer player, @NotNull TabPlayer newPlayer, TabListEntry entry) { + if (newPlayer.getPlayer().getUniqueId().equals(player.getPlayer().getUniqueId())) { + return; + } + + final boolean isPresent = player.getPlayer() + .getTabList().getEntries().stream() + .noneMatch(e -> e.getProfile().getId().equals(newPlayer.getPlayer().getUniqueId())); + + if (isPresent) { + player.getPlayer().getTabList().addEntry(entry); + } + } + @Subscribe public void onPlayerQuit(@NotNull DisconnectEvent event) { if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) { @@ -181,10 +216,11 @@ public class PlayerTabList { } - // Replace a player in the tab list - public void replacePlayer(@NotNull TabPlayer tabPlayer) { - players.removeIf(player -> player.getPlayer().getUniqueId().equals(tabPlayer.getPlayer().getUniqueId())); - players.add(tabPlayer); + @NotNull + public TabPlayer createTabPlayer(@NotNull Player player) { + return new TabPlayer(player, + plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE) + ); } // Update a player's name in the tab list @@ -204,19 +240,25 @@ public class PlayerTabList { }); } - public void updatePlayerDisplayName(TabPlayer tabPlayer) { + public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) { final Component lastDisplayName = tabPlayer.getLastDisplayname(); tabPlayer.getDisplayName(plugin).thenAccept(displayName -> { if (displayName == null || displayName.equals(lastDisplayName)) { return; } - players.forEach(player -> - player.getPlayer().getTabList().getEntries().stream() - .filter(e -> e.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId())).findFirst() - .ifPresent(entry -> entry.setDisplayName(displayName))); - }); + boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername()); + players.forEach(player -> { + if (isVanished && !plugin.getVanishManager().canSee(player.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) { + return; + } + + player.getPlayer().getTabList().getEntries().stream() + .filter(e -> e.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId())).findFirst() + .ifPresent(entry -> entry.setDisplayName(displayName)); + }); + }); } // Update the display names of all listed players @@ -293,7 +335,7 @@ public class PlayerTabList { * @return The servers in the same group as the given server, empty if the server is not in a group and fallback is disabled */ @NotNull - public Optional> getGroupNames(String serverName) { + public Optional> getGroupNames(@NotNull String serverName) { return plugin.getSettings().getServerGroups().values().stream() .filter(servers -> servers.contains(serverName)) .findFirst() @@ -319,7 +361,7 @@ public class PlayerTabList { * @return The servers in the same group as the given server, empty if the server is not in a group and fallback is disabled */ @NotNull - public List getGroupServers(String serverName) { + public List getGroupServers(@NotNull String serverName) { return plugin.getServer().getAllServers().stream() .filter(server -> plugin.getSettings().getServerGroups().values().stream() .filter(servers -> servers.contains(serverName)) @@ -342,4 +384,33 @@ public class PlayerTabList { public void removeOfflinePlayer(@NotNull Player player) { players.removeIf(tabPlayer -> tabPlayer.getPlayer().getUniqueId().equals(player.getUniqueId())); } + + public void vanishPlayer(@NotNull TabPlayer tabPlayer) { + players.forEach(p -> { + if (p.getPlayer().equals(tabPlayer.getPlayer())) { + return; + } + + if (!plugin.getVanishManager().canSee(p.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) { + p.getPlayer().getTabList().removeEntry(tabPlayer.getPlayer().getUniqueId()); + } + }); + } + + public void unVanishPlayer(@NotNull TabPlayer tabPlayer) { + final UUID uuid = tabPlayer.getPlayer().getUniqueId(); + + tabPlayer.getDisplayName(plugin).thenAccept(c -> { + players.forEach(p -> { + if (p.getPlayer().equals(tabPlayer.getPlayer())) { + return; + } + + if (!p.getPlayer().getTabList().containsEntry(uuid)) { + createEntry(tabPlayer, p.getPlayer().getTabList(), c); + } + }); + }); + + } } diff --git a/src/main/java/net/william278/velocitab/vanish/DefaultVanishIntegration.java b/src/main/java/net/william278/velocitab/vanish/DefaultVanishIntegration.java new file mode 100644 index 0000000..f959356 --- /dev/null +++ b/src/main/java/net/william278/velocitab/vanish/DefaultVanishIntegration.java @@ -0,0 +1,39 @@ +/* + * This file is part of Velocitab, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.william278.velocitab.vanish; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public final class DefaultVanishIntegration implements VanishIntegration { + + @Override + public boolean canSee(@NotNull String name, @NotNull String otherName) { + return true; + } + + @Override + public boolean isVanished(@NotNull String name) { + return false; + } + +} diff --git a/src/main/java/net/william278/velocitab/vanish/VanishIntegration.java b/src/main/java/net/william278/velocitab/vanish/VanishIntegration.java new file mode 100644 index 0000000..7b4ef2a --- /dev/null +++ b/src/main/java/net/william278/velocitab/vanish/VanishIntegration.java @@ -0,0 +1,30 @@ +/* + * This file is part of Velocitab, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.william278.velocitab.vanish; + +import org.jetbrains.annotations.NotNull; + +public interface VanishIntegration { + + boolean canSee(@NotNull String name, @NotNull String otherName); + + boolean isVanished(@NotNull String name); + +} diff --git a/src/main/java/net/william278/velocitab/vanish/VanishManager.java b/src/main/java/net/william278/velocitab/vanish/VanishManager.java new file mode 100644 index 0000000..688c19d --- /dev/null +++ b/src/main/java/net/william278/velocitab/vanish/VanishManager.java @@ -0,0 +1,75 @@ +/* + * This file is part of Velocitab, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.william278.velocitab.vanish; + +import com.velocitypowered.api.proxy.Player; +import net.william278.velocitab.Velocitab; +import net.william278.velocitab.player.TabPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public class VanishManager { + + private final Velocitab plugin; + private VanishIntegration integration; + + public VanishManager(@NotNull Velocitab plugin) { + this.plugin = plugin; + setIntegration(new DefaultVanishIntegration()); + } + + public void setIntegration(@NotNull VanishIntegration integration) { + this.integration = integration; + } + + @NotNull + public VanishIntegration getIntegration() { + return integration; + } + + public boolean canSee(@NotNull String name, @NotNull String otherName) { + return integration.canSee(name, otherName); + } + + public boolean isVanished(@NotNull String name) { + return integration.isVanished(name); + } + + public void vanishPlayer(@NotNull Player player) { + final Optional tabPlayer = plugin.getTabList().getTabPlayer(player); + if (tabPlayer.isEmpty()) { + return; + } + + plugin.getTabList().vanishPlayer(tabPlayer.get()); + plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.vanishPlayer(player)); + } + + public void unVanishPlayer(@NotNull Player player) { + final Optional tabPlayer = plugin.getTabList().getTabPlayer(player); + if (tabPlayer.isEmpty()) { + return; + } + + plugin.getTabList().unVanishPlayer(tabPlayer.get()); + plugin.getScoreboardManager().ifPresent(scoreboardManager -> scoreboardManager.unVanishPlayer(player)); + } +}