Skip to content

Commit

Permalink
Merge pull request CytonicMC#207 from Foxikle/feat/new-playerlist-api
Browse files Browse the repository at this point in the history
New playerlist api
  • Loading branch information
webhead1104 authored Jul 8, 2024
2 parents 752744d + 835c9d1 commit 12cc722
Show file tree
Hide file tree
Showing 12 changed files with 432 additions and 117 deletions.
3 changes: 0 additions & 3 deletions src/main/java/net/cytonic/cytosis/Cytosis.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
import net.cytonic.cytosis.logging.Logger;
import net.cytonic.cytosis.managers.*;
import net.cytonic.cytosis.messaging.MessagingManager;
import net.cytonic.cytosis.playerlist.PlayerListCategory;
import net.cytonic.cytosis.playerlist.PlayerListEntry;
import net.cytonic.cytosis.playerlist.PlayerListFavicon;
import net.cytonic.cytosis.plugins.PluginManager;
import net.cytonic.cytosis.ranks.RankManager;
import net.cytonic.cytosis.utils.Utils;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.entity.Player;

import static net.cytonic.cytosis.utils.MiniMessageTemplate.MM;

/**
Expand All @@ -29,7 +30,6 @@ public AllChatCommand() {
if (sender instanceof final Player player) {
Component message = Component.text("")
.append(Cytosis.getRankManager().getPlayerRank(player.getUuid()).orElseThrow().getPrefix())
.appendSpace()
.append(Component.text(player.getUsername(), (Cytosis.getRankManager().getPlayerRank(player.getUuid()).orElseThrow().getTeamColor())))
.append(Component.text(":", Cytosis.getRankManager().getPlayerRank(player.getUuid()).orElseThrow().getChatColor()))
.appendSpace()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public static void initServerEvents() {
if (player.hasPermission(STR."cytonic.chat.\{channel.name().toLowerCase()}")) {
sendMessage(originalMessage, channel, player);
} else {
player.sendMessage(MM."Whoops! It looks like you can't chat in the \{channel.name().toLowerCase()} channel. \uD83E\uDD14");
player.sendMessage(MM."<red>Whoops! It looks like you can't chat in the \{channel.name().toLowerCase()} channel. \uD83E\uDD14");
Cytosis.getChatManager().setChannel(player.getUuid(), ChatChannel.ALL);
}
break;
Expand Down Expand Up @@ -138,7 +138,6 @@ private static void sendMessage(String originalMessage, ChatChannel channel, Pla
Component message = Component.text("")
.append(channel.getPrefix())
.append(Cytosis.getRankManager().getPlayerRank(player.getUuid()).orElseThrow().getPrefix())
.appendSpace()
.append(Component.text(player.getUsername(), (Cytosis.getRankManager().getPlayerRank(player.getUuid()).orElseThrow().getTeamColor())))
.append(Component.text(":", Cytosis.getRankManager().getPlayerRank(player.getUuid()).orElseThrow().getChatColor()))
.appendSpace()
Expand All @@ -147,7 +146,6 @@ private static void sendMessage(String originalMessage, ChatChannel channel, Pla
} else {
Component message = Component.text("")
.append(Cytosis.getRankManager().getPlayerRank(player.getUuid()).orElseThrow().getPrefix())
.appendSpace()
.append(Component.text(player.getUsername(), (Cytosis.getRankManager().getPlayerRank(player.getUuid()).orElseThrow().getTeamColor())))
.append(Component.text(":", Cytosis.getRankManager().getPlayerRank(player.getUuid()).orElseThrow().getChatColor()))
.appendSpace()
Expand Down
240 changes: 196 additions & 44 deletions src/main/java/net/cytonic/cytosis/managers/PlayerListManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,57 @@
import lombok.Getter;
import lombok.Setter;
import net.cytonic.cytosis.Cytosis;
import net.cytonic.cytosis.playerlist.PlayerListCategory;
import net.cytonic.cytosis.playerlist.PlayerListEntry;
import net.cytonic.cytosis.playerlist.*;
import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.SendablePacket;
import net.minestom.server.network.packet.server.play.PlayerInfoRemovePacket;
import net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.PacketUtils;

import java.util.*;

import static net.cytonic.cytosis.utils.MiniMessageTemplate.MM;
import java.util.concurrent.ConcurrentHashMap;

/**
* A class that manages the player list
*/
@Setter
@Getter
@SuppressWarnings("UnstableApiUsage")
public class PlayerListManager {

private final UUID[][] listUUIDs; // <column, entry>
private final Map<UUID, Component[][]> playerComponents = new ConcurrentHashMap<>();
private final Map<UUID, PlayerInfoUpdatePacket.Property[][]> playerFavicons = new ConcurrentHashMap<>();
private PlayerlistCreator creator;
// in ticks
private int updateInterval = 20;

/**
* The default player list manager constructor
*/
public PlayerListManager() {
// do nothing
creator = new DefaultPlayerListCreator();
scheduleUpdate();
listUUIDs = new UUID[creator.getColumnCount()][20];
for (int i = 0; i < listUUIDs.length; i++) {
for (int j = 0; j < listUUIDs[i].length; j++) {
listUUIDs[i][j] = UUID.randomUUID();
}
}
}

//todo make it per player?
private List<PlayerListCategory> globalCategories = new ArrayList<>();

// Header and Footer
@Setter private static Component header = MM."<aqua><bold>CytonicMC";
@Setter private static Component footer = MM."<aqua>mc.cytonic.net";

/**
* Injects the player list packets into the player's connection
*
* @param player The player to set up
*/
public void setupPlayer(Player player) {
// setup tab header & footer
player.sendPlayerListHeaderAndFooter(header, footer);
player.sendPlayerListHeaderAndFooter(creator.header(player), creator.footer(player));

// remove them from the player list for everyone, but keep skin data
PacketUtils.broadcastPlayPacket(new PlayerInfoUpdatePacket(
Expand All @@ -56,55 +65,198 @@ public void setupPlayer(Player player) {

// remove everyone from the player


for (ServerPacket packet : createPackets()) {
player.sendPacket(packet);
}
player.sendPackets(createInjectPackets(player));
}

/**
* Creates the player list packets
*
* @return the list of packets
*/
private List<ServerPacket> createPackets() {
globalCategories.sort(Comparator.comparingInt(PlayerListCategory::getPriority));
List<ServerPacket> packets = new ArrayList<>();

char currentLetter = 'A';

for (PlayerListCategory category : globalCategories) { // category naming is "A0",then the next category is B0, etc.
category.sortEntries();
private List<SendablePacket> createInjectPackets(Player player) {
List<SendablePacket> packets = new ArrayList<>();
if (!playerComponents.containsKey(player.getUuid())) {
Component[][] components = new Component[creator.getColumnCount()][20];
for (int i = 0; i < components.length; i++) {
for (int j = 0; j < components[i].length; j++) {
components[i][j] = Component.empty();
}
}
playerComponents.put(player.getUuid(), components);
}

char categoryletter = 'a';
UUID categoryUUID = UUID.randomUUID();
if (!playerFavicons.containsKey(player.getUuid())) {
PlayerInfoUpdatePacket.Property[][] favicons = new PlayerInfoUpdatePacket.Property[creator.getColumnCount()][20];
for (PlayerInfoUpdatePacket.Property[] favicon : favicons) {
Arrays.fill(favicon, PlayerListFavicon.GREY.getProperty());
}
playerFavicons.put(player.getUuid(), favicons);
}

PlayerInfoUpdatePacket categoryPacket = new PlayerInfoUpdatePacket(
EnumSet.of(PlayerInfoUpdatePacket.Action.ADD_PLAYER, PlayerInfoUpdatePacket.Action.UPDATE_LISTED, PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME),
List.of(new PlayerInfoUpdatePacket.Entry(categoryUUID, STR."!\{currentLetter}-a", category.getFavicon().property(), true, 1, GameMode.CREATIVE, category.getName(), null)));
packets.add(categoryPacket);

for (PlayerListEntry entry : category.getEntries()) {
UUID entryUUID = UUID.randomUUID();
PlayerInfoUpdatePacket entryPacket = new PlayerInfoUpdatePacket(
for (int i = 0; i < listUUIDs.length; i++) {
char col = (char) ('A' + i);
for (int j = 0; j < listUUIDs[i].length; j++) {
char row = (char) ('a' + j);
UUID uuid = listUUIDs[i][j];
packets.add(new PlayerInfoUpdatePacket(
EnumSet.of(PlayerInfoUpdatePacket.Action.ADD_PLAYER, PlayerInfoUpdatePacket.Action.UPDATE_LISTED, PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME),
List.of(new PlayerInfoUpdatePacket.Entry(entryUUID, STR."!\{currentLetter}-\{categoryletter++}",
List.of(entry.getFavicon().getProperty()), true, 1,
GameMode.CREATIVE, entry.getName(), null)));
packets.add(entryPacket);
List.of(new PlayerInfoUpdatePacket.Entry(uuid, STR."!\{col}-\{row}", List.of(playerFavicons.get(player.getUuid())[i][j]),
true, 1, GameMode.CREATIVE, playerComponents.get(player.getUuid())[i][j], null)
)));
}
currentLetter++;
}

// remove online players too
for (Player player : Cytosis.getOnlinePlayers()) {
for (Player p : Cytosis.getOnlinePlayers()) {
packets.add(new PlayerInfoUpdatePacket(
PlayerInfoUpdatePacket.Action.UPDATE_LISTED,
new PlayerInfoUpdatePacket.Entry(player.getUuid(), player.getUsername(), List.of(
new PlayerInfoUpdatePacket.Property("textures", player.getSkin().textures(), player.getSkin().signature())
), false, player.getLatency(), player.getGameMode(), player.getDisplayName(), null)
new PlayerInfoUpdatePacket.Entry(p.getUuid(), p.getUsername(), List.of(
new PlayerInfoUpdatePacket.Property("textures", p.getSkin().textures(), p.getSkin().signature())
), false, p.getLatency(), p.getGameMode(), p.getDisplayName(), null)
));
}
return packets;
}

/**
* Update a player's player list
*
* @param player the player to update
*/
public void update(Player player) {
List<Column> columns = creator.createColumns(player);
if (columns.size() != creator.getColumnCount())
throw new IllegalArgumentException("Column count does not match size of column list");

columns.forEach(Column::sortEntries);
PlayerInfoUpdatePacket.Property[][] updatedFavicons = toFavicons(new ArrayList<>(columns));
PlayerInfoUpdatePacket.Property[][] favicons = playerFavicons.getOrDefault(player.getUuid(), updatedFavicons);
playerFavicons.put(player.getUuid(), updatedFavicons);
Component[][] updatedComponents = toComponents(columns);
Component[][] components = playerComponents.getOrDefault(player.getUuid(), updatedComponents);
playerComponents.put(player.getUuid(), updatedComponents);


List<SendablePacket> updatePackets = new ArrayList<>();

for (int i = 0; i < updatedComponents.length; i++) {
if (faviconNotEquals(favicons[i][0], updatedFavicons[i][0])) {
updatePackets.add(new PlayerInfoRemovePacket(listUUIDs[i][0]));
updatePackets.add(new PlayerInfoUpdatePacket(
EnumSet.of(PlayerInfoUpdatePacket.Action.ADD_PLAYER, PlayerInfoUpdatePacket.Action.UPDATE_LISTED, PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME),
List.of(new PlayerInfoUpdatePacket.Entry(listUUIDs[i][0], STR."!\{(char) ('A' + i)}-\{(char) ('a')}",
List.of(updatedFavicons[i][0]), true, 1, GameMode.CREATIVE, updatedComponents[i][0], null)
)));
}

if (!components[i][0].equals(columns.get(i).getName())) {
updatePackets.add(new PlayerInfoUpdatePacket(
PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, new PlayerInfoUpdatePacket.Entry(listUUIDs[i][0], STR."!\{(char) ('A' + i)}-\{(char) ('a')}",
List.of(updatedFavicons[i][0]), true, 1, GameMode.CREATIVE, updatedComponents[i][0], null)
));
}
for (int j = 1; j < updatedComponents[i].length; j++) {
if (faviconNotEquals(favicons[i][j], updatedFavicons[i][j])) {
updatePackets.add(new PlayerInfoRemovePacket(listUUIDs[i][j]));
updatePackets.add(new PlayerInfoUpdatePacket(
EnumSet.of(PlayerInfoUpdatePacket.Action.ADD_PLAYER, PlayerInfoUpdatePacket.Action.UPDATE_LISTED, PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME),
List.of(new PlayerInfoUpdatePacket.Entry(listUUIDs[i][j], STR."!\{(char) ('A' + i)}-\{(char) ('a' + j)}",
List.of(updatedFavicons[i][j]), true, 1, GameMode.CREATIVE, updatedComponents[i][j], null)
)));
}
if (!components[i][j].equals(updatedComponents[i][j])) {
components[i][j] = updatedComponents[i][j];
updatePackets.add(new PlayerInfoUpdatePacket(
PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME, new PlayerInfoUpdatePacket.Entry(listUUIDs[i][j], STR."!\{(char) ('A' + i)}-\{(char) ('a' + j)}",
List.of(updatedFavicons[i][j]), true, 1, GameMode.CREATIVE, updatedComponents[i][j], null)
));
}
}
}


player.sendPackets(updatePackets);
}

/**
* Updates all the players tab menus
*/
public void updateAll() {
Cytosis.getOnlinePlayers().forEach(this::update);
}

/**
* Shedules the next update
*/
private void scheduleUpdate() {
MinecraftServer.getSchedulerManager().buildTask(() -> {
updateAll();
scheduleUpdate();
}).delay(TaskSchedule.tick(updateInterval)).schedule();
}

/**
* Converts a list of columns to the components arrays
* @param columns the columns to convert
* @return an array with the components
*/
private Component[][] toComponents(List<Column> columns) {
Component[][] components = new Component[creator.getColumnCount()][20];
for (int i = 0; i < components.length; i++) {
components[i][0] = columns.get(i).getName();

for (int j = 1; j < components[i].length; j++) {
Column column = columns.get(i);
if (column.getEntries().size() < j) {
components[i][j] = Component.empty();
continue;
}
components[i][j] = column.getEntries().get(j - 1).getName();
}
}
return components;
}

/**
* Converts a list of columns to the favicons
* @param columns the columns to convert
* @return an array with the favicons
*/
private PlayerInfoUpdatePacket.Property[][] toFavicons(List<Column> columns) {
PlayerInfoUpdatePacket.Property[][] favicons = new PlayerInfoUpdatePacket.Property[creator.getColumnCount()][20];
for (int i = 0; i < favicons.length; i++) {
favicons[i][0] = columns.get(i).getFavicon().getProperty();
Column column = columns.get(i);
for (int j = 1; j < favicons[i].length; j++) {
List<PlayerListEntry> entries = new ArrayList<>(column.getEntries());
if (entries.size() < j) {
favicons[i][j] = PlayerListFavicon.GREY.getProperty();
continue;
}
favicons[i][j] = entries.get(j - 1).getFavicon();
}
}
return favicons;
}

/**
* A custom equals method for PlayerInfoUpdatePacket.Property
*
* @param p1 the first property
* @param p2 the second property
* @return true if the properties are not equal, indicating an update is needed
*/
private boolean faviconNotEquals(PlayerInfoUpdatePacket.Property p1, PlayerInfoUpdatePacket.Property p2) {
if (!p1.name().equals(p2.name())) {
return true;
}
if (p1.signature() == null || p2.signature() == null) {
return true;
}
if (!p1.signature().equals(p2.signature())) {
return true;
}
return !p1.value().equals(p2.value());
}
}
Loading

0 comments on commit 12cc722

Please sign in to comment.