From 764367e5a05bee7daeefcb8630d06feff70d637a Mon Sep 17 00:00:00 2001 From: Alexander <88254229+Fulminazzo@users.noreply.github.com> Date: Sat, 9 Mar 2024 15:29:22 +0100 Subject: [PATCH] Push dev branch to main (#8) * Removed unnecessary section wrapping of sortings and replaced with list format * Created base PacketListener to handle interception of TAB list * Loaded PacketListener * Added write packet interception and recognition of update info packet * Added filtering of game mode enum by spectator * Added replacement logic * Completed first working implementation of replacing packet * Switched FoliaTpsProvider system to provide global TPS * Added hide-spectator-mode-self option * Implemented hide-spectator-mode-self option * Fixed constructor call * Fixed invalid uuid retrieval * Extracted method should hide self Fixed some inconsistencies * Removed hide-spectator-self option * Set the folia TPS provider to 15 seconds --------- Co-authored-by: Netherwhal --- .../simplytab/SimplyTabPlugin.java | 8 + .../simplytab/packets/PacketListener.java | 164 ++++++++++++++++++ .../simplytab/rank/TablistRankHandler.java | 83 +++++---- .../simplytab/tab/FoliaTpsProvider.java | 8 +- src/main/resources/config.yml | 34 ++-- 5 files changed, 233 insertions(+), 64 deletions(-) create mode 100644 src/main/java/net/simplyvanilla/simplytab/packets/PacketListener.java diff --git a/src/main/java/net/simplyvanilla/simplytab/SimplyTabPlugin.java b/src/main/java/net/simplyvanilla/simplytab/SimplyTabPlugin.java index a263e57..7d3d4b2 100644 --- a/src/main/java/net/simplyvanilla/simplytab/SimplyTabPlugin.java +++ b/src/main/java/net/simplyvanilla/simplytab/SimplyTabPlugin.java @@ -3,13 +3,16 @@ import net.megavex.scoreboardlibrary.api.ScoreboardLibrary; import net.megavex.scoreboardlibrary.api.exception.NoPacketAdapterAvailableException; import net.megavex.scoreboardlibrary.api.noop.NoopScoreboardLibrary; +import net.simplyvanilla.simplytab.packets.PacketListener; import net.simplyvanilla.simplytab.rank.TablistRankHandler; import net.simplyvanilla.simplytab.tab.TablistManager; +import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; public class SimplyTabPlugin extends JavaPlugin { private ScoreboardLibrary scoreboardLibrary; + private PacketListener packetListener; @Override public void onEnable() { @@ -25,10 +28,15 @@ public void onEnable() { new TablistManager(this); new TablistRankHandler(this); + + this.packetListener = new PacketListener(); + this.packetListener.loadAll(); + Bukkit.getPluginManager().registerEvents(this.packetListener, this); } @Override public void onDisable() { + if (this.packetListener != null) this.packetListener.unloadAll(); this.scoreboardLibrary.close(); } diff --git a/src/main/java/net/simplyvanilla/simplytab/packets/PacketListener.java b/src/main/java/net/simplyvanilla/simplytab/packets/PacketListener.java new file mode 100644 index 0000000..9c0d3d2 --- /dev/null +++ b/src/main/java/net/simplyvanilla/simplytab/packets/PacketListener.java @@ -0,0 +1,164 @@ +package net.simplyvanilla.simplytab.packets; + +import io.netty.channel.*; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Stream; + +public class PacketListener implements Listener { + private static final String UPDATE_INFO_PACKET = "ClientboundPlayerInfoUpdatePacket"; + private static final String UPDATE_GAME_MODE_PACKET = "UPDATE_GAME_MODE"; + private static final String GAME_MODE_ENUM_NAME = "EnumGamemode"; + private static final String SPECTATOR_ENUM_NAME = "SPECTATOR"; + private static final String REPLACEMENT_ENUM = "c"; // ADVENTURE + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + injectPlayer(event.getPlayer()); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + removePlayer(event.getPlayer()); + } + + public void loadAll() { + Bukkit.getOnlinePlayers().forEach(this::injectPlayer); + } + + public void unloadAll() { + Bukkit.getOnlinePlayers().forEach(this::removePlayer); + } + + private void injectPlayer(Player player) { + ChannelDuplexHandler channelDuplexHandler = new ChannelDuplexHandler() { + + @Override + public void write(ChannelHandlerContext channelHandlerContext, Object packet, ChannelPromise promise) throws Exception { + try { + if (!packet.toString().contains(UPDATE_INFO_PACKET)) return; + + EnumSet actions = getFieldObject(packet, EnumSet.class.getSimpleName()); + List previousEntries = getFieldObject(packet, List.class.getSimpleName()); + List entries = new LinkedList<>(previousEntries); + if (actions.stream().noneMatch(p -> p.name().equals(UPDATE_GAME_MODE_PACKET))) return; + + for (int i = 0; i < entries.size(); i++) { + Object entry = entries.get(i); + + Field gameTypeField = getField(entry, GAME_MODE_ENUM_NAME); + Enum gameType = (Enum) gameTypeField.get(entry); + if (gameType == null) continue; + if (!gameType.name().equals(SPECTATOR_ENUM_NAME)) continue; + + UUID uuid = getFieldObject(entry, UUID.class.getSimpleName()); + if (player.getUniqueId().equals(uuid)) continue; + + Class clazz = gameType.getClass(); + Field replaceField = clazz.getDeclaredField(REPLACEMENT_ENUM); + replaceField.setAccessible(true); + Enum replacement = (Enum) replaceField.get(clazz); + + Class entryClass = entry.getClass(); + Field[] fields = entryClass.getDeclaredFields(); + Object[] objects = Arrays.stream(fields).peek(f -> f.setAccessible(true)).map(f -> { + try { + if (f.getType().getSimpleName().equals(GAME_MODE_ENUM_NAME)) return replacement; + return f.get(entry); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }).toList().toArray(new Object[0]); + Class[] classes = Arrays.stream(fields).map(Field::getType).toList().toArray(new Class[0]); + + Constructor entryConstructor = entryClass.getDeclaredConstructor(classes); + Object newEntry = entryConstructor.newInstance(objects); + entries.set(i, newEntry); + } + + if (previousEntries.equals(entries)) return; + + Class packetClass = packet.getClass(); + Constructor constructor = packetClass.getDeclaredConstructor(EnumSet.class, List.class); + constructor.setAccessible(true); + packet = constructor.newInstance(actions, entries); + + } finally { + super.write(channelHandlerContext, packet, promise); + } + } + + }; + + ChannelPipeline pipeline = getPlayerChannel(player).pipeline(); + pipeline.addBefore("packet_handler", player.getName(), channelDuplexHandler); + } + + private void removePlayer(Player player) { + Channel channel = getPlayerChannel(player); + channel.eventLoop().submit(() -> channel.pipeline().remove(player.getName())); + } + + private Channel getPlayerChannel(Player player) { + try { + if (player == null) throw new NullPointerException("Player cannot be null"); + + // ((CraftPlayer) player).getHandle() + Method getHandle = Arrays.stream(player.getClass().getMethods()) + .filter(m -> m.getParameterCount() == 0) + .filter(m -> m.getReturnType().getSimpleName().equals("EntityPlayer")) + .findFirst().orElse(null); + if (getHandle == null) + throw new NoSuchMethodException(String.format("Could not find method EntityPlayer() in %s", player.getClass().getCanonicalName())); + Object handle = getHandle.invoke(player); + + // handle.playerConnection + Object playerConnection = getFieldObject(handle, "PlayerConnection"); + + // playerConnection.networkManager + Object networkManager = getFieldObject(playerConnection, "NetworkManager"); + + // networkManager.channel + return getFieldObject(networkManager, "Channel"); + } catch (Exception e) { + if (e instanceof RuntimeException) throw (RuntimeException) e; + else throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + private T getFieldObject(Object object, String typeName) { + try { + Field field = getField(object, typeName); + return (T) field.get(object); + } catch (Exception e) { + if (e instanceof RuntimeException) throw (RuntimeException) e; + throw new RuntimeException(e); + } + } + + private Field getField(Object object, String typeName) { + try { + Field field = Stream.concat(Arrays.stream(object.getClass().getFields()), + Arrays.stream(object.getClass().getDeclaredFields())) + .filter(f -> f.getType().getSimpleName().equals(typeName)) + .findFirst().orElse(null); + if (field == null) + throw new NoSuchFieldException(String.format("Could not find field %s in %s", typeName, object.getClass().getCanonicalName())); + field.setAccessible(true); + return field; + } catch (Exception e) { + if (e instanceof RuntimeException) throw (RuntimeException) e; + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/net/simplyvanilla/simplytab/rank/TablistRankHandler.java b/src/main/java/net/simplyvanilla/simplytab/rank/TablistRankHandler.java index 5f5ba13..953fc35 100644 --- a/src/main/java/net/simplyvanilla/simplytab/rank/TablistRankHandler.java +++ b/src/main/java/net/simplyvanilla/simplytab/rank/TablistRankHandler.java @@ -17,8 +17,7 @@ import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; -import java.util.ArrayList; -import java.util.List; +import java.util.*; public class TablistRankHandler implements Listener { private final SimplyTabPlugin plugin; @@ -76,49 +75,49 @@ public void createTab(Player player) { } private String getTeamName(Player onlinePlayer) { - ConfigurationSection sortings = this.plugin.getConfig().getConfigurationSection("sortings"); + List> tmp = (List>) this.plugin.getConfig().getList("sortings"); int sortingId = 0; - for (String key : sortings.getKeys(false)) { - var section = sortings.getConfigurationSection(key); - var type = section.getString("type"); - - switch (type.toLowerCase()) { - case "index": - var placeholder = section.getString("placeholder", ""); - var resolvedValue = PlainTextComponentSerializer.plainText().serialize( - MiniMessage.miniMessage().deserialize(placeholder, MiniPlaceholders.getAudiencePlaceholders(onlinePlayer)) - ); - var values = section.getStringList("values"); - int index = values.indexOf(resolvedValue); - if (index == -1) { - index = values.size() + 1; - } - sortingId += index * 100; - break; - case "alphabetical": - placeholder = section.getString("placeholder", ""); - resolvedValue = PlainTextComponentSerializer.plainText().serialize( - MiniMessage.miniMessage().deserialize(placeholder, MiniPlaceholders.getAudiencePlaceholders(onlinePlayer)) - ); - - List playerValues = new ArrayList<>(Bukkit.getOnlinePlayers().stream() - .map(player -> PlainTextComponentSerializer.plainText().serialize( - MiniMessage.miniMessage().deserialize(placeholder, MiniPlaceholders.getAudiencePlaceholders(player)) - )).toList()); - - // sort player values alphabetically - playerValues.sort(String::compareToIgnoreCase); - index = playerValues.indexOf(resolvedValue); - if (index == -1) { - index = playerValues.size() + 1; - } - sortingId += index; - break; - default: - break; + if (tmp != null) + for (var section : tmp) { + var type = (String) section.get("type"); + + switch (type.toLowerCase()) { + case "index": + var placeholder = (String) section.getOrDefault("placeholder", ""); + var resolvedValue = PlainTextComponentSerializer.plainText().serialize( + MiniMessage.miniMessage().deserialize(placeholder, MiniPlaceholders.getAudiencePlaceholders(onlinePlayer)) + ); + var values = (List) section.get("values"); + int index = values.indexOf(resolvedValue); + if (index == -1) { + index = values.size() + 1; + } + sortingId += index * 100; + break; + case "alphabetical": + placeholder = (String) section.getOrDefault("placeholder", ""); + resolvedValue = PlainTextComponentSerializer.plainText().serialize( + MiniMessage.miniMessage().deserialize(placeholder, MiniPlaceholders.getAudiencePlaceholders(onlinePlayer)) + ); + + List playerValues = new ArrayList<>(Bukkit.getOnlinePlayers().stream() + .map(player -> PlainTextComponentSerializer.plainText().serialize( + MiniMessage.miniMessage().deserialize(placeholder, MiniPlaceholders.getAudiencePlaceholders(player)) + )).toList()); + + // sort player values alphabetically + playerValues.sort(String::compareToIgnoreCase); + index = playerValues.indexOf(resolvedValue); + if (index == -1) { + index = playerValues.size() + 1; + } + sortingId += index; + break; + default: + break; + } } - } // create team name like "0001_name" String formattedName = String.format("%05d_%s", sortingId, onlinePlayer.getName()); // team name can only be 16 characters long diff --git a/src/main/java/net/simplyvanilla/simplytab/tab/FoliaTpsProvider.java b/src/main/java/net/simplyvanilla/simplytab/tab/FoliaTpsProvider.java index 6deb86f..6b71c0a 100644 --- a/src/main/java/net/simplyvanilla/simplytab/tab/FoliaTpsProvider.java +++ b/src/main/java/net/simplyvanilla/simplytab/tab/FoliaTpsProvider.java @@ -2,17 +2,17 @@ import io.papermc.paper.threadedregions.TickData; import io.papermc.paper.threadedregions.TickRegionScheduler; +import org.bukkit.Bukkit; public class FoliaTpsProvider implements TpsProvider { @Override public double getTps() { + TickData.TickReportData report = TickRegionScheduler.getCurrentRegion() .getData() .getRegionSchedulingHandle() - .getTickReport5s(System.nanoTime()); - - double average = report.tpsData().segmentAll().average(); + .getTickReport15s(System.nanoTime()); - return report == null ? 0D : average; + return report.tpsData().segmentAll().average(); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index b6dc4d7..dc4ba27 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -20,24 +20,22 @@ tab: '*': '%value%' sortings: - '1': - - type: 'index' - placeholder: '' - values: - - Owner - - Admin - - Mod - - JrMod - - Creator - - OG - - Donator - - DonatorNitro - - DonatorVoter - - default - - Designer - '2': - - type: 'alphabetical' - placeholder: '' + - type: 'index' + placeholder: '' + values: + - Owner + - Admin + - Mod + - JrMod + - Creator + - OG + - Donator + - DonatorNitro + - DonatorVoter + - default + - Designer + - type: 'alphabetical' + placeholder: '' group-prefix: ' ' group-suffix: ''