Move to using Protocolize for scoreboard team handling using fake scoreboard teams

This commit is contained in:
William 2023-02-19 23:08:17 +00:00
parent 8ad443f4fc
commit 51c7853b7b
No known key found for this signature in database
8 changed files with 373 additions and 96 deletions

View File

@ -1,4 +1,4 @@
# Velocitab
[![Discord](https://img.shields.io/discord/818135932103557162?color=7289da&logo=discord)](https://discord.gg/tVYhJfyDWG)
A very simple (sorted) Velocity TAB plugin
A very simple (sorted) Velocity TAB plugin. Requires [Protocolize](https://github.com/Exceptionflug/protocolize) v2.2.5 to be installed on your proxy.

View File

@ -12,18 +12,23 @@ repositories {
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://jitpack.io' }
maven { url = 'https://repo.minebench.de/' }
maven { url = 'https://mvn.exceptionflug.de/repository/exceptionflug-public/' }
}
dependencies {
compileOnly 'com.velocitypowered:velocity-api:3.1.1'
compileOnly 'net.luckperms:api:5.4'
compileOnly 'dev.simplix:protocolize-api:2.2.5'
compileOnly 'io.netty:netty-codec-http:4.1.86.Final'
implementation 'org.apache.commons:commons-text:1.10.0'
implementation 'net.william278:Annotaml:2.0.1'
implementation 'dev.dejvokep:boosted-yaml:1.3.1'
implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
implementation 'org.projectlombok:lombok:1.18.26'
annotationProcessor 'com.velocitypowered:velocity-api:3.1.1'
annotationProcessor 'org.projectlombok:lombok:1.18.26'
}
shadowJar {

View File

@ -11,6 +11,7 @@ import com.velocitypowered.api.proxy.ProxyServer;
import net.william278.annotaml.Annotaml;
import net.william278.velocitab.config.Settings;
import net.william278.velocitab.luckperms.LuckPermsHook;
import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.PlayerTabList;
@ -30,7 +31,10 @@ import java.util.Optional;
description = "Simple velocity TAB menu plugin",
url = "https://william278.net/",
authors = {"William278"},
dependencies = {@Dependency(id = "luckperms", optional = true)}
dependencies = {
@Dependency(id = "protocolize"),
@Dependency(id = "luckperms", optional = true)
}
)
public class Velocitab {
@ -40,6 +44,7 @@ public class Velocitab {
private final Path dataDirectory;
private PlayerTabList tabList;
private LuckPermsHook luckPerms;
private ScoreboardManager scoreboardManager;
@Inject
public Velocitab(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) {
@ -52,6 +57,7 @@ public class Velocitab {
public void onProxyInitialization(ProxyInitializeEvent event) {
loadSettings();
loadHooks();
prepareScoreboardManager();
prepareTabList();
logger.info("Successfully enabled Velocitab v" + BuildConstants.VERSION);
}
@ -92,6 +98,16 @@ public class Velocitab {
}
}
private void prepareScoreboardManager() {
this.scoreboardManager = new ScoreboardManager(this);
scoreboardManager.registerPacket();
}
@NotNull
public ScoreboardManager getScoreboardManager() {
return scoreboardManager;
}
@NotNull
public PlayerTabList getTabList() {
return tabList;

View File

@ -10,7 +10,6 @@ import net.luckperms.api.model.group.Group;
import net.luckperms.api.model.user.User;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.tab.PlayerTabList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -45,12 +44,8 @@ public class LuckPermsHook {
@Subscribe
public void onLuckPermsGroupUpdate(@NotNull UserDataRecalculateEvent event) {
plugin.getServer().getPlayer(event.getUser().getUniqueId()).ifPresent(player -> {
final PlayerTabList tabList = plugin.getTabList();
tabList.removePlayer(player);
tabList.addPlayer(plugin.getTabPlayer(player));
tabList.refreshHeaderAndFooter();
});
plugin.getServer().getPlayer(event.getUser().getUniqueId())
.ifPresent(player -> plugin.getTabList().updatePlayer(plugin.getTabPlayer(player)));
}
private OptionalInt getWeight(@Nullable String groupName) {

View File

@ -0,0 +1,62 @@
package net.william278.velocitab.packet;
import com.velocitypowered.api.proxy.Player;
import dev.simplix.protocolize.api.PacketDirection;
import dev.simplix.protocolize.api.Protocol;
import dev.simplix.protocolize.api.Protocolize;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.UUID;
public class ScoreboardManager {
private final Velocitab plugin;
private final HashMap<UUID, String> fauxTeams;
public ScoreboardManager(@NotNull Velocitab velocitab) {
this.plugin = velocitab;
this.fauxTeams = new HashMap<>();
}
public void registerPacket() {
Protocolize.protocolRegistration().registerPacket(
UpdateTeamsPacket.MAPPINGS,
Protocol.PLAY,
PacketDirection.CLIENTBOUND,
UpdateTeamsPacket.class
);
}
public void setPlayerTeam(@NotNull TabPlayer player) {
removeTeam(player.getPlayer());
createTeam(player.getTeamName(), player.getPlayer());
}
private void createTeam(@NotNull String teamName, @NotNull Player member) {
final UUID uuid = member.getUniqueId();
final UpdateTeamsPacket createTeamPacket = UpdateTeamsPacket.create(teamName, member.getUsername());
plugin.getServer().getAllPlayers().stream()
.map(Player::getUniqueId)
.map(Protocolize.playerProvider()::player)
.forEach(protocolPlayer -> protocolPlayer.sendPacket(createTeamPacket));
fauxTeams.put(uuid, teamName);
}
public void removeTeam(@NotNull Player member) {
final UUID uuid = member.getUniqueId();
if (!fauxTeams.containsKey(uuid)) {
return;
}
final UpdateTeamsPacket removeTeamPacket = UpdateTeamsPacket.remove(fauxTeams.get(uuid));
plugin.getServer().getAllPlayers().stream()
.map(Player::getUniqueId)
.map(Protocolize.playerProvider()::player)
.forEach(protocolPlayer -> protocolPlayer.sendPacket(removeTeamPacket));
fauxTeams.remove(uuid);
}
}

View File

@ -0,0 +1,226 @@
package net.william278.velocitab.packet;
import dev.simplix.protocolize.api.PacketDirection;
import dev.simplix.protocolize.api.mapping.AbstractProtocolMapping;
import dev.simplix.protocolize.api.mapping.ProtocolIdMapping;
import dev.simplix.protocolize.api.packet.AbstractPacket;
import dev.simplix.protocolize.api.util.ProtocolUtil;
import io.netty.buffer.ByteBuf;
import lombok.*;
import lombok.experimental.Accessors;
import org.apache.commons.text.StringEscapeUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static dev.simplix.protocolize.api.util.ProtocolVersions.MINECRAFT_1_19;
import static dev.simplix.protocolize.api.util.ProtocolVersions.MINECRAFT_1_19_3;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Accessors(fluent = true)
public class UpdateTeamsPacket extends AbstractPacket {
protected static final List<ProtocolIdMapping> MAPPINGS = List.of(
AbstractProtocolMapping.rangedIdMapping(MINECRAFT_1_19, MINECRAFT_1_19_3, 0x56)
);
private String teamName;
private UpdateMode mode;
private String displayName;
private List<FriendlyFlag> friendlyFlags;
private NameTagVisibility nameTagVisibility;
private CollisionRule collisionRule;
private int color;
private String prefix;
private String suffix;
private List<String> entities;
@NotNull
public static UpdateTeamsPacket create(@NotNull String teamName, @NotNull String member) {
if (teamName.length() > 16) {
teamName = teamName.substring(0, 16);
}
final UpdateTeamsPacket updateTeamsPacket = new UpdateTeamsPacket();
updateTeamsPacket.teamName(teamName);
updateTeamsPacket.mode(UpdateMode.CREATE);
updateTeamsPacket.displayName(getChatString(teamName));
updateTeamsPacket.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY));
updateTeamsPacket.nameTagVisibility(NameTagVisibility.ALWAYS);
updateTeamsPacket.collisionRule(CollisionRule.ALWAYS);
updateTeamsPacket.color(0);
updateTeamsPacket.prefix(getChatString(""));
updateTeamsPacket.suffix(getChatString(""));
updateTeamsPacket.entities(List.of(member));
return updateTeamsPacket;
}
@NotNull
public static UpdateTeamsPacket remove(@NotNull String teamName) {
if (teamName.length() > 16) {
teamName = teamName.substring(0, 16);
}
final UpdateTeamsPacket updateTeamsPacket = new UpdateTeamsPacket();
updateTeamsPacket.teamName(teamName);
updateTeamsPacket.mode(UpdateMode.REMOVE);
return updateTeamsPacket;
}
@Override
public void read(ByteBuf byteBuf, PacketDirection packetDirection, int i) {
teamName = ProtocolUtil.readString(byteBuf);
mode = UpdateMode.byId(byteBuf.readByte());
if (mode == UpdateMode.REMOVE) {
return;
}
if (mode == UpdateMode.CREATE || mode == UpdateMode.UPDATE_INFO) {
displayName = ProtocolUtil.readString(byteBuf);
friendlyFlags = FriendlyFlag.fromBitMask(byteBuf.readByte());
nameTagVisibility = NameTagVisibility.byId(ProtocolUtil.readString(byteBuf));
collisionRule = CollisionRule.byId(ProtocolUtil.readString(byteBuf));
color = byteBuf.readByte();
prefix = ProtocolUtil.readString(byteBuf);
suffix = ProtocolUtil.readString(byteBuf);
}
if (mode == UpdateMode.CREATE || mode == UpdateMode.ADD_PLAYERS || mode == UpdateMode.REMOVE_PLAYERS) {
int entityCount = ProtocolUtil.readVarInt(byteBuf);
for (int j = 0; j < entityCount; j++) {
entities.add(ProtocolUtil.readString(byteBuf));
}
}
}
@Override
public void write(ByteBuf byteBuf, PacketDirection packetDirection, int i) {
ProtocolUtil.writeString(byteBuf, teamName);
byteBuf.writeByte(mode.id());
if (mode == UpdateMode.REMOVE) {
return;
}
if (mode == UpdateMode.CREATE || mode == UpdateMode.UPDATE_INFO) {
ProtocolUtil.writeString(byteBuf, displayName);
byteBuf.writeByte(FriendlyFlag.toBitMask(friendlyFlags));
ProtocolUtil.writeString(byteBuf, nameTagVisibility.id());
ProtocolUtil.writeString(byteBuf, collisionRule.id());
byteBuf.writeByte(color);
ProtocolUtil.writeString(byteBuf, prefix);
ProtocolUtil.writeString(byteBuf, suffix);
}
if (mode == UpdateMode.CREATE || mode == UpdateMode.ADD_PLAYERS || mode == UpdateMode.REMOVE_PLAYERS) {
ProtocolUtil.writeVarInt(byteBuf, entities.size());
for (String entity : entities) {
ProtocolUtil.writeString(byteBuf, entity);
}
}
}
@NotNull
private static String getChatString(@NotNull String string) {
return "{\"text\":\"" + StringEscapeUtils.escapeJson(string) + "\"}";
}
public enum UpdateMode {
CREATE(0),
REMOVE(1),
UPDATE_INFO(2),
ADD_PLAYERS(3),
REMOVE_PLAYERS(4);
private final int id;
UpdateMode(int id) {
this.id = id;
}
public int id() {
return id;
}
public static UpdateMode byId(int id) {
return Arrays.stream(values())
.filter(mode -> mode.id == id)
.findFirst()
.orElse(null);
}
}
public enum FriendlyFlag {
CAN_HURT_FRIENDLY(0x01),
CAN_HURT_FRIENDLY_FIRE(0x02);
private final int id;
FriendlyFlag(int id) {
this.id = id;
}
@NotNull
public static List<FriendlyFlag> fromBitMask(int bitMask) {
return Arrays.stream(values())
.filter(flag -> (bitMask & flag.id) != 0)
.collect(Collectors.toList());
}
public static int toBitMask(@NotNull List<FriendlyFlag> friendlyFlags) {
int bitMask = 0;
for (FriendlyFlag friendlyFlag : friendlyFlags) {
bitMask |= friendlyFlag.id;
}
return bitMask;
}
}
public enum NameTagVisibility {
ALWAYS("always"),
NEVER("never"),
HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"),
HIDE_FOR_OWN_TEAM("hideForOwnTeam");
private final String id;
NameTagVisibility(String id) {
this.id = id;
}
public String id() {
return id;
}
public static NameTagVisibility byId(String id) {
return Arrays.stream(values())
.filter(visibility -> visibility.id.equals(id))
.findFirst()
.orElse(null);
}
}
public enum CollisionRule {
ALWAYS("always"),
NEVER("never"),
PUSH_OTHER_TEAMS("pushOtherTeams"),
PUSH_OWN_TEAM("pushOwnTeam");
private final String id;
CollisionRule(String id) {
this.id = id;
}
public String id() {
return id;
}
public static CollisionRule byId(String id) {
return Arrays.stream(values())
.filter(rule -> rule.id.equals(id))
.findFirst()
.orElse(null);
}
}
}

View File

@ -1,9 +1,6 @@
package net.william278.velocitab.player;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.player.TabList;
import com.velocitypowered.api.proxy.player.TabListEntry;
import com.velocitypowered.api.util.GameProfile;
import de.themoep.minedown.adventure.MineDown;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
@ -11,26 +8,15 @@ import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.tab.PlayerTabList;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public final class TabPlayer implements Comparable<TabPlayer> {
private static final int DEFAULT_LATENCY = 3;
private final Player player;
private final Role role;
private final GameProfile profile;
private final int highestWeight;
public TabPlayer(@NotNull Player player, @NotNull Role role, int highestWeight) {
this.player = player;
this.role = role;
final String profileName = role.getStringComparableWeight(highestWeight) + getServerName() + player.getUsername();
this.profile = new GameProfile(
new UUID(0, new Random().nextLong()),
profileName.length() > 16 ? profileName.substring(0, 16) : profileName,
player.getGameProfileProperties()
);
this.highestWeight = highestWeight;
}
@NotNull
@ -43,11 +29,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
return role;
}
@NotNull
public GameProfile getProfile() {
return profile;
}
@NotNull
public String getServerName() {
return player.getCurrentServer()
@ -56,41 +37,19 @@ public final class TabPlayer implements Comparable<TabPlayer> {
}
@NotNull
private Component getDisplayName(@NotNull Velocitab plugin) {
public Component getDisplayName(@NotNull Velocitab plugin) {
return new MineDown(Placeholder.format(plugin.getSettings().getFormat(), plugin, this)).toComponent();
}
private TabListEntry getEntry(@NotNull Velocitab plugin, @NotNull TabList tabList) {
return TabListEntry.builder()
.displayName(getDisplayName(plugin))
.latency(DEFAULT_LATENCY)
.profile(profile)
.tabList(tabList)
.build();
@NotNull
public String getTeamName() {
return role.getStringComparableWeight(highestWeight) + "-" + getServerName() + "-" + player.getUsername();
}
public void sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
this.player.sendPlayerListHeaderAndFooter(tabList.getHeader(this), tabList.getFooter(this));
}
public void addPlayer(@NotNull TabPlayer player, @NotNull Velocitab plugin) {
this.player.getTabList().addEntry(player.getEntry(plugin, this.player.getTabList()));
removeUuidPlayer(plugin, player.getPlayer().getUniqueId());
}
public void removePlayer(@NotNull TabPlayer player, @NotNull Velocitab plugin) {
this.player.getTabList().removeEntry(player.getProfile().getId());
removeUuidPlayer(plugin, player.getPlayer().getUniqueId());
}
public void removeUuidPlayer(@NotNull Velocitab plugin, @NotNull UUID... uuid) {
plugin.getServer().getScheduler()
.buildTask(plugin, () -> Arrays.stream(uuid).forEach(this.player.getTabList()::removeEntry))
.delay(500, TimeUnit.MILLISECONDS)
.schedule();
}
@Override
public int compareTo(@NotNull TabPlayer o) {
final int roleDifference = role.compareTo(o.role);

View File

@ -3,7 +3,6 @@ package net.william278.velocitab.tab;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
import com.velocitypowered.api.proxy.Player;
import de.themoep.minedown.adventure.MineDown;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
@ -13,7 +12,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public class PlayerTabList {
private final Velocitab plugin;
@ -27,33 +26,66 @@ public class PlayerTabList {
@SuppressWarnings("UnstableApiUsage")
@Subscribe
public void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
// Remove previous Tab entries for players when they move servers
if (event.getPreviousServer() != null) {
removePlayer(event.getPlayer());
// Remove the player from the tracking list if they are switching servers
if (event.getPreviousServer() == null) {
players.removeIf(player -> player.getPlayer().getUniqueId().equals(event.getPlayer().getUniqueId()));
}
final TabPlayer player = plugin.getTabPlayer(event.getPlayer());
// Add the player to the tracking list
players.add(plugin.getTabPlayer(event.getPlayer()));
// Reset existing tab list
player.getPlayer().getTabList().clearHeaderAndFooter();
if (!player.getPlayer().getTabList().getEntries().isEmpty()) {
player.getPlayer().getTabList().getEntries().clear();
}
// Show existing list to new player
players.forEach(listPlayer -> player.addPlayer(listPlayer, plugin));
addPlayer(player);
refreshHeaderAndFooter();
// Update the tab list of all players
plugin.getServer().getScheduler().buildTask(plugin, this::updateList)
.delay(500, TimeUnit.MILLISECONDS)
.schedule();
}
@Subscribe
public void onPlayerQuit(@NotNull DisconnectEvent event) {
try {
removePlayer(event.getPlayer());
refreshHeaderAndFooter();
} catch (Exception ignored) {
// Ignore when server shutting down
}
// Remove the player from the tracking list
players.removeIf(player -> player.getPlayer().getUniqueId().equals(event.getPlayer().getUniqueId()));
// Remove the player from the tab list of all other players
plugin.getScoreboardManager().removeTeam(event.getPlayer());
plugin.getServer().getAllPlayers().forEach(player -> {
if (player.getTabList().containsEntry(event.getPlayer().getUniqueId())) {
player.getTabList().removeEntry(event.getPlayer().getUniqueId());
}
});
// Update the tab list of all players
plugin.getServer().getScheduler().buildTask(plugin, this::updateList)
.delay(500, TimeUnit.MILLISECONDS)
.schedule();
}
public void updatePlayer(@NotNull TabPlayer tabPlayer) {
// Remove the existing player from the tracking list
players.removeIf(player -> player.getPlayer().getUniqueId().equals(tabPlayer.getPlayer().getUniqueId()));
// Add the player to the tracking list
players.add(tabPlayer);
// Update the player's team sorting
plugin.getScoreboardManager().removeTeam(tabPlayer.getPlayer());
// Update the tab list of all players
plugin.getServer().getScheduler().buildTask(plugin, this::updateList)
.delay(500, TimeUnit.MILLISECONDS)
.schedule();
}
private void updateList() {
players.forEach(player -> {
player.sendHeaderAndFooter(this);
player.getPlayer().getTabList().getEntries()
.forEach(entry -> players.stream()
.filter(p -> p.getPlayer().getGameProfile().getId().equals(entry.getProfile().getId()))
.findFirst().ifPresent(tabPlayer -> {
entry.setDisplayName(tabPlayer.getDisplayName(plugin));
plugin.getScoreboardManager().setPlayerTeam(tabPlayer);
}));
});
}
@NotNull
@ -66,22 +98,4 @@ public class PlayerTabList {
return new MineDown(Placeholder.format(plugin.getSettings().getFooter(), plugin, player)).toComponent();
}
// Add a new tab player to the list and update for online players
public void addPlayer(@NotNull TabPlayer player) {
players.add(player);
players.forEach(tabPlayer -> tabPlayer.addPlayer(player, plugin));
}
public void removePlayer(@NotNull Player playerToRemove) {
final Optional<TabPlayer> quitTabPlayer = players.stream()
.filter(player -> player.getPlayer().equals(playerToRemove)).findFirst();
if (quitTabPlayer.isPresent()) {
players.remove(quitTabPlayer.get());
players.forEach(tabPlayer -> tabPlayer.removePlayer(quitTabPlayer.get(), plugin));
}
}
public void refreshHeaderAndFooter() {
players.forEach(tabPlayer -> tabPlayer.sendHeaderAndFooter(this));
}
}