From fdcbbc445865039fb99ed69d946c39b37cdde90b Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Wed, 19 Jun 2024 13:54:43 +0100 Subject: [PATCH] refactor(spigot): use protocollib accessors for bossbar packets Fixes bossbars not working on MC 1.20.6 and above. --- .../ProtocolLibListener.java | 99 +------- .../protocollib/BossBarPacketHandler.java | 231 ++++++++++++++++++ 2 files changed, 238 insertions(+), 92 deletions(-) create mode 100644 core/src/main/java/com/rexcantor64/triton/packetinterceptor/protocollib/BossBarPacketHandler.java diff --git a/core/src/main/java/com/rexcantor64/triton/packetinterceptor/ProtocolLibListener.java b/core/src/main/java/com/rexcantor64/triton/packetinterceptor/ProtocolLibListener.java index 4f701d6a..a3a6518c 100644 --- a/core/src/main/java/com/rexcantor64/triton/packetinterceptor/ProtocolLibListener.java +++ b/core/src/main/java/com/rexcantor64/triton/packetinterceptor/ProtocolLibListener.java @@ -26,6 +26,7 @@ import com.rexcantor64.triton.Triton; import com.rexcantor64.triton.language.item.SignLocation; import com.rexcantor64.triton.packetinterceptor.protocollib.AdvancementsPacketHandler; +import com.rexcantor64.triton.packetinterceptor.protocollib.BossBarPacketHandler; import com.rexcantor64.triton.packetinterceptor.protocollib.EntitiesPacketHandler; import com.rexcantor64.triton.packetinterceptor.protocollib.HandlerFunction; import com.rexcantor64.triton.packetinterceptor.protocollib.SignPacketHandler; @@ -36,11 +37,9 @@ import com.rexcantor64.triton.wrappers.AdventureComponentWrapper; import com.rexcantor64.triton.wrappers.WrappedClientConfiguration; import com.rexcantor64.triton.wrappers.WrappedPlayerChatMessage; -import lombok.SneakyThrows; import lombok.val; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.chat.TranslatableComponent; import net.md_5.bungee.chat.ComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.World; @@ -84,9 +83,10 @@ public class ProtocolLibListener implements PacketListener, PacketInterceptor { private final HandlerFunction ASYNC_PASSTHROUGH = asAsync((_packet, _player) -> { }); - private final SignPacketHandler signPacketHandler = new SignPacketHandler(); private final AdvancementsPacketHandler advancementsPacketHandler = AdvancementsPacketHandler.newInstance(); + private final BossBarPacketHandler bossBarPacketHandler = new BossBarPacketHandler(); private final EntitiesPacketHandler entitiesPacketHandler = new EntitiesPacketHandler(); + private final SignPacketHandler signPacketHandler = new SignPacketHandler(); private final SpigotMLP main; private final List allowedTypes; @@ -202,22 +202,18 @@ private void setupPacketHandlers() { } packetHandlers.put(PacketType.Play.Server.WINDOW_ITEMS, asAsync(this::handleWindowItems)); packetHandlers.put(PacketType.Play.Server.SET_SLOT, asAsync(this::handleSetSlot)); - if (MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { // 1.9+ - // Bossbars were only added on MC 1.9 - packetHandlers.put(PacketType.Play.Server.BOSS, asAsync(this::handleBoss)); - } if (MinecraftVersion.VILLAGE_UPDATE.atOrAbove()) { // 1.14+ // Villager merchant interface redesign on 1.14 packetHandlers.put(PacketType.Play.Server.OPEN_WINDOW_MERCHANT, asAsync(this::handleMerchantItems)); } - // External Packet Handlers - signPacketHandler.registerPacketTypes(packetHandlers); if (advancementsPacketHandler != null) { advancementsPacketHandler.registerPacketTypes(packetHandlers); } + bossBarPacketHandler.registerPacketTypes(packetHandlers); entitiesPacketHandler.registerPacketTypes(packetHandlers); + signPacketHandler.registerPacketTypes(packetHandlers); } /* PACKET HANDLERS */ @@ -583,58 +579,6 @@ private void handleSetSlot(PacketEvent packet, SpigotLanguagePlayer languagePlay packet.getPacket().getItemModifier().writeSafely(0, item); } - @SneakyThrows - private void handleBoss(PacketEvent packet, SpigotLanguagePlayer languagePlayer) { - if (!main.getConf().isBossbars()) return; - - val uuid = packet.getPacket().getUUIDs().readSafely(0); - WrappedChatComponent bossbar; - Object actionObj = null; - - if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+ - actionObj = packet.getPacket().getModifier().readSafely(1); - val method = actionObj.getClass().getMethod("a"); - method.setAccessible(true); - val actionEnum = ((Enum) method.invoke(actionObj)).ordinal(); - if (actionEnum == 1) { - languagePlayer.removeBossbar(uuid); - return; - } - if (actionEnum != 0 && actionEnum != 3) return; - - bossbar = WrappedChatComponent.fromHandle(NMSUtils.getDeclaredField(actionObj, "a")); - } else { - Action action = packet.getPacket().getEnumModifier(Action.class, 1).readSafely(0); - if (action == Action.REMOVE) { - languagePlayer.removeBossbar(uuid); - return; - } - if (action != Action.ADD && action != Action.UPDATE_NAME) return; - - bossbar = packet.getPacket().getChatComponents().readSafely(0); - } - - - try { - languagePlayer.setBossbar(uuid, bossbar.getJson()); - BaseComponent[] result = main.getLanguageParser().parseComponent(languagePlayer, - main.getConf().getBossbarSyntax(), ComponentSerializer.parse(bossbar.getJson())); - if (result == null) - result = new BaseComponent[]{new TranslatableComponent("")}; - bossbar.setJson(ComponentSerializer.toString(result)); - if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+ - NMSUtils.setDeclaredField(actionObj, "a", bossbar.getHandle()); - } else { - packet.getPacket().getChatComponents().writeSafely(0, bossbar); - } - } catch (RuntimeException e) { - // Catch 1.16 Hover 'contents' not being parsed correctly - // Has been fixed in newer versions of Spigot 1.16 - Triton.get().getLogger() - .logError(e, "Could not parse a bossbar, so it was ignored. Bossbar: %1", bossbar.getJson()); - } - } - @SuppressWarnings({"unchecked"}) private void handleMerchantItems(PacketEvent packet, SpigotLanguagePlayer languagePlayer) { if (!main.getConf().isItems()) return; @@ -905,30 +849,8 @@ public void refreshTabHeaderFooter(SpigotLanguagePlayer player, String header, S } @Override - @SneakyThrows - public void refreshBossbar(SpigotLanguagePlayer player, UUID uuid, String json) { - if (!MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { - // bossbar only works on 1.9+ - return; - } - - val bukkitPlayerOpt = player.toBukkit(); - if (!bukkitPlayerOpt.isPresent()) return; - val bukkitPlayer = bukkitPlayerOpt.get(); - - PacketContainer packet = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.BOSS); - packet.getUUIDs().writeSafely(0, uuid); - if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+ - val msg = WrappedChatComponent.fromJson(json); - val constructor = BOSSBAR_UPDATE_TITLE_ACTION_CLASS.getDeclaredConstructor(msg.getHandleType()); - constructor.setAccessible(true); - val action = constructor.newInstance(msg.getHandle()); - packet.getModifier().writeSafely(1, action); - } else { - packet.getEnumModifier(Action.class, 1).writeSafely(0, Action.UPDATE_NAME); - packet.getChatComponents().writeSafely(0, WrappedChatComponent.fromJson(json)); - } - ProtocolLibrary.getProtocolManager().sendServerPacket(bukkitPlayer, packet, true); + public void refreshBossbar(SpigotLanguagePlayer player, UUID uuid, String text) { + bossBarPacketHandler.refreshBossbar(player, uuid, text); } @Override @@ -1060,11 +982,4 @@ private boolean isPlayerInventoryOpen(Player player) { } } - /** - * BossBar packet Action wrapper - */ - public enum Action { - ADD, REMOVE, UPDATE_PCT, UPDATE_NAME, UPDATE_STYLE, UPDATE_PROPERTIES - } - } diff --git a/core/src/main/java/com/rexcantor64/triton/packetinterceptor/protocollib/BossBarPacketHandler.java b/core/src/main/java/com/rexcantor64/triton/packetinterceptor/protocollib/BossBarPacketHandler.java new file mode 100644 index 00000000..d93a6e07 --- /dev/null +++ b/core/src/main/java/com/rexcantor64/triton/packetinterceptor/protocollib/BossBarPacketHandler.java @@ -0,0 +1,231 @@ +package com.rexcantor64.triton.packetinterceptor.protocollib; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.reflect.EquivalentConverter; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.ConstructorAccessor; +import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.comphenix.protocol.reflect.accessors.MethodAccessor; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; +import com.comphenix.protocol.wrappers.BukkitConverters; +import com.comphenix.protocol.wrappers.EnumWrappers; +import com.comphenix.protocol.wrappers.WrappedChatComponent; +import com.rexcantor64.triton.player.SpigotLanguagePlayer; +import lombok.val; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TranslatableComponent; +import net.md_5.bungee.chat.ComponentSerializer; + +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; + +import static com.rexcantor64.triton.packetinterceptor.protocollib.HandlerFunction.asAsync; + +public class BossBarPacketHandler extends PacketHandler { + + private final Class ACTION_ENUM_CLASS; + private final Class OPERATION_INTERFACE; + private final MethodAccessor GET_OPERATION_ACTION_METHOD; + private final FieldAccessor ADD_COMPONENT_FIELD; + private final FieldAccessor UPDATE_COMPONENT_FIELD; + private final ConstructorAccessor UPDATE_CONSTRUCTOR; + private final EquivalentConverter ACTION_CONVERTER; + private final EquivalentConverter COMPONENT_CONVERTER = BukkitConverters.getWrappedChatComponentConverter(); + + public BossBarPacketHandler() { + if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+ + val innerClasses = PacketType.Play.Server.BOSS.getPacketClass().getDeclaredClasses(); + ACTION_ENUM_CLASS = Arrays.stream(innerClasses) + .filter(Enum.class::isAssignableFrom) + .findAny() + .orElseThrow(() -> new RuntimeException("Cannot find boss bar action enum class")); + + // the operation interface has a method that returns the operation type + OPERATION_INTERFACE = Arrays.stream(innerClasses) + .filter(Class::isInterface) + .filter(c -> !FuzzyReflection.fromClass(c).getMethodListByParameters(ACTION_ENUM_CLASS).isEmpty()) + .findAny() + .orElseThrow(() -> new RuntimeException("Cannot find boss bar operation interface class")); + GET_OPERATION_ACTION_METHOD = Accessors.getMethodAccessor( + FuzzyReflection.fromClass(OPERATION_INTERFACE) + .getMethodByReturnTypeAndParameters("getType", ACTION_ENUM_CLASS) + ); + + // the add operation class has a chat component field, but does not have a chat component constructor + val addOperationClass = Arrays.stream(innerClasses) + .filter(OPERATION_INTERFACE::isAssignableFrom) + .filter(c -> !FuzzyReflection.fromClass(c, true).getFieldListByType(MinecraftReflection.getIChatBaseComponentClass()).isEmpty()) + .filter(c -> FuzzyReflection.fromClass(c, true) + .getConstructorList( + FuzzyMethodContract.newBuilder() + .parameterExactType(MinecraftReflection.getIChatBaseComponentClass()) + .build() + ) + .isEmpty() + ) + .findAny() + .orElseThrow(() -> new RuntimeException("Cannot find boss bar add operation class")); + ADD_COMPONENT_FIELD = Accessors.getFieldAccessor(addOperationClass, MinecraftReflection.getIChatBaseComponentClass(), true); + + // the update operation class has a chat component field, and also has a chat component constructor + val updateOperationClass = Arrays.stream(innerClasses) + .filter(OPERATION_INTERFACE::isAssignableFrom) + .filter(c -> !FuzzyReflection.fromClass(c, true).getFieldListByType(MinecraftReflection.getIChatBaseComponentClass()).isEmpty()) + .filter(c -> !FuzzyReflection.fromClass(c, true) + .getConstructorList( + FuzzyMethodContract.newBuilder() + .parameterExactType(MinecraftReflection.getIChatBaseComponentClass()) + .build() + ) + .isEmpty() + ) + .findAny() + .orElseThrow(() -> new RuntimeException("Cannot find boss bar add operation class")); + UPDATE_COMPONENT_FIELD = Accessors.getFieldAccessor(updateOperationClass, MinecraftReflection.getIChatBaseComponentClass(), true); + + UPDATE_CONSTRUCTOR = Accessors.getConstructorAccessor( + FuzzyReflection.fromClass(updateOperationClass, true) + .getConstructor( + FuzzyMethodContract.newBuilder() + .parameterExactType(MinecraftReflection.getIChatBaseComponentClass()) + .build() + ) + ); + + ACTION_CONVERTER = new EnumWrappers.EnumConverter<>(ACTION_ENUM_CLASS, Action.class); + } else { + ACTION_ENUM_CLASS = null; + OPERATION_INTERFACE = null; + GET_OPERATION_ACTION_METHOD = null; + ADD_COMPONENT_FIELD = null; + UPDATE_COMPONENT_FIELD = null; + UPDATE_CONSTRUCTOR = null; + ACTION_CONVERTER = null; + } + } + + /** + * @return Whether the plugin should attempt to translate boss bars + */ + private boolean areBossBarsDisabled() { + return !getMain().getConf().isBossbars(); + } + + private void handleBoss_1_9(PacketEvent packet, SpigotLanguagePlayer languagePlayer) { + if (areBossBarsDisabled()) return; + + val uuid = packet.getPacket().getUUIDs().readSafely(0); + Action action = packet.getPacket().getEnumModifier(Action.class, 1).readSafely(0); + if (action == Action.REMOVE) { + languagePlayer.removeBossbar(uuid); + return; + } + if (action != Action.ADD && action != Action.UPDATE_NAME) return; + + WrappedChatComponent component = packet.getPacket().getChatComponents().readSafely(0); + try { + translateAndSaveBossBar(languagePlayer, uuid, component); + packet.getPacket().getChatComponents().writeSafely(0, component); + } catch (RuntimeException e) { + // Catch 1.16 Hover 'contents' not being parsed correctly + // Has been fixed in newer versions of Spigot 1.16 + logger().logError(e, "Could not parse a bossbar, so it was ignored. Bossbar: %1", component.getJson()); + } + } + + private void handleBoss_1_17(PacketEvent packet, SpigotLanguagePlayer languagePlayer) { + if (areBossBarsDisabled()) return; + + val uuid = packet.getPacket().getUUIDs().readSafely(0); + val operation = packet.getPacket().getSpecificModifier(OPERATION_INTERFACE).readSafely(0); + Action action = ACTION_CONVERTER.getSpecific(GET_OPERATION_ACTION_METHOD.invoke(operation)); + + if (action == Action.REMOVE) { + languagePlayer.removeBossbar(uuid); + return; + } + + WrappedChatComponent component; + if (action == Action.ADD) { + component = COMPONENT_CONVERTER.getSpecific(ADD_COMPONENT_FIELD.get(operation)); + } else if (action == Action.UPDATE_NAME) { + component = COMPONENT_CONVERTER.getSpecific(UPDATE_COMPONENT_FIELD.get(operation)); + } else { + return; + } + + translateAndSaveBossBar(languagePlayer, uuid, component); + + if (action == Action.ADD) { + ADD_COMPONENT_FIELD.set(operation, COMPONENT_CONVERTER.getGeneric(component)); + } else { + UPDATE_COMPONENT_FIELD.set(operation, COMPONENT_CONVERTER.getGeneric(component)); + } + } + + private void translateBossBar(SpigotLanguagePlayer languagePlayer, WrappedChatComponent component) { + BaseComponent[] result = getLanguageParser().parseComponent( + languagePlayer, + getConfig().getBossbarSyntax(), + ComponentSerializer.parse(component.getJson()) + ); + if (result == null) { + result = new BaseComponent[]{new TranslatableComponent("")}; + } + component.setJson(ComponentSerializer.toString(result)); + } + + private void translateAndSaveBossBar(SpigotLanguagePlayer languagePlayer, UUID uuid, WrappedChatComponent component) { + languagePlayer.setBossbar(uuid, component.getJson()); + translateBossBar(languagePlayer, component); + } + + public void refreshBossbar(SpigotLanguagePlayer player, UUID uuid, String json) { + if (!MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { + // bossbar only works on 1.9+ + return; + } + + val bukkitPlayerOpt = player.toBukkit(); + if (!bukkitPlayerOpt.isPresent()) return; + val bukkitPlayer = bukkitPlayerOpt.get(); + + PacketContainer packet = createPacket(PacketType.Play.Server.BOSS); + packet.getUUIDs().writeSafely(0, uuid); + val msg = WrappedChatComponent.fromJson(json); + translateBossBar(player, msg); + if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+ + Object operation = UPDATE_CONSTRUCTOR.invoke(COMPONENT_CONVERTER.getGeneric(msg)); + // noinspection unchecked + packet.getSpecificModifier((Class) OPERATION_INTERFACE).writeSafely(0, operation); + } else { + packet.getEnumModifier(Action.class, 1).writeSafely(0, Action.UPDATE_NAME); + packet.getChatComponents().writeSafely(0, msg); + } + sendPacket(bukkitPlayer, packet, false); + } + + @Override + public void registerPacketTypes(Map registry) { + if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+ + // Rework of packet to use subclasses instead of having all data on packet itself + registry.put(PacketType.Play.Server.BOSS, asAsync(this::handleBoss_1_17)); + } else if (MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { // 1.9+ + // Bossbars were only added on MC 1.9 + registry.put(PacketType.Play.Server.BOSS, asAsync(this::handleBoss_1_9)); + } + } + + /** + * BossBar packet Action wrapper + */ + public enum Action { + ADD, REMOVE, UPDATE_PCT, UPDATE_NAME, UPDATE_STYLE, UPDATE_PROPERTIES + } +}