From 616431cc3356713e1cc80bc16aef23537946df16 Mon Sep 17 00:00:00 2001 From: vytskalt Date: Mon, 19 Feb 2024 19:11:12 +0200 Subject: [PATCH] Add several scoreboard type wrappers (#2819) --- .../protocol/events/AbstractStructure.java | 49 +++++ .../events/SerializedOfflinePlayer.java | 37 ++-- .../protocol/injector/StructureCache.java | 9 +- .../protocol/utility/MinecraftReflection.java | 64 ++++++ .../wrappers/AdventureComponentConverter.java | 38 +++- .../protocol/wrappers/BukkitConverters.java | 8 + .../protocol/wrappers/EnumWrappers.java | 196 +++++++++++++++++- .../wrappers/WrappedComponentStyle.java | 59 ++++++ .../wrappers/WrappedNumberFormat.java | 126 +++++++++++ .../wrappers/WrappedTeamParameters.java | 171 +++++++++++++++ .../wrappers/WrappedComponentStyleTest.java | 40 ++++ .../wrappers/WrappedNumberFormatTest.java | 50 +++++ .../wrappers/WrappedTeamParametersTest.java | 53 +++++ 13 files changed, 877 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/comphenix/protocol/wrappers/WrappedComponentStyle.java create mode 100644 src/main/java/com/comphenix/protocol/wrappers/WrappedNumberFormat.java create mode 100644 src/main/java/com/comphenix/protocol/wrappers/WrappedTeamParameters.java create mode 100644 src/test/java/com/comphenix/protocol/wrappers/WrappedComponentStyleTest.java create mode 100644 src/test/java/com/comphenix/protocol/wrappers/WrappedNumberFormatTest.java create mode 100644 src/test/java/com/comphenix/protocol/wrappers/WrappedTeamParametersTest.java diff --git a/src/main/java/com/comphenix/protocol/events/AbstractStructure.java b/src/main/java/com/comphenix/protocol/events/AbstractStructure.java index ec8b9f2e5..b915ad38f 100644 --- a/src/main/java/com/comphenix/protocol/events/AbstractStructure.java +++ b/src/main/java/com/comphenix/protocol/events/AbstractStructure.java @@ -870,6 +870,55 @@ public StructureModifier getChatTypes() { EnumWrappers.getChatTypeConverter()); } + /** + * Retrieve a read/write structure for the DisplaySlot enum in 1.20.2. + * @return A modifier for DisplaySlot enum fields. + */ + public StructureModifier getDisplaySlots() { + return structureModifier.withType( + EnumWrappers.getDisplaySlotClass(), + EnumWrappers.getDisplaySlotConverter()); + } + + /** + * Retrieve a read/write structure for the RenderType enum. + * @return A modifier for RenderType enum fields. + */ + public StructureModifier getRenderTypes() { + return structureModifier.withType( + EnumWrappers.getRenderTypeClass(), + EnumWrappers.getRenderTypeConverter()); + } + + /** + * Retrieve a read/write structure for the ChatFormatting enum. + * @return A modifier for ChatFormatting enum fields. + */ + public StructureModifier getChatFormattings() { + return structureModifier.withType( + EnumWrappers.getChatFormattingClass(), + EnumWrappers.getChatFormattingConverter()); + } + + /** + * Retrieve a read/write structure for optional team parameters in 1.17+. + * @return A modifier for optional team parameters fields. + */ + public StructureModifier> getOptionalTeamParameters() { + return getOptionals(BukkitConverters.getWrappedTeamParametersConverter()); + } + + /** + * Retrieve a read/write structure for the NumberFormat class in 1.20.4+. + * @return A modifier for NumberFormat fields. + */ + public StructureModifier getNumberFormats() { + return structureModifier.withType( + MinecraftReflection.getNumberFormatClass().orElse(null), + BukkitConverters.getWrappedNumberFormatConverter()); + } + + /** * Retrieve a read/write structure for the MinecraftKey class. * @return A modifier for MinecraftKey fields. diff --git a/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java b/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java index 64a20cbb5..722ea5428 100644 --- a/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java +++ b/src/main/java/com/comphenix/protocol/events/SerializedOfflinePlayer.java @@ -17,10 +17,25 @@ package com.comphenix.protocol.events; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + import com.comphenix.protocol.reflect.accessors.Accessors; import com.comphenix.protocol.reflect.accessors.MethodAccessor; import com.comphenix.protocol.utility.ByteBuddyFactory; import com.comphenix.protocol.utility.Util; + import net.bytebuddy.description.ByteCodeElement; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; @@ -35,27 +50,19 @@ import net.bytebuddy.implementation.bind.annotation.RuntimeType; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; -import org.bukkit.*; +import org.bukkit.BanEntry; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Statistic; +import org.bukkit.World; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.profile.PlayerProfile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.Duration; -import java.time.Instant; -import java.util.Date; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - /** * Represents a player object that can be serialized by Java. * diff --git a/src/main/java/com/comphenix/protocol/injector/StructureCache.java b/src/main/java/com/comphenix/protocol/injector/StructureCache.java index 6b50b465f..65f8f7a0d 100644 --- a/src/main/java/com/comphenix/protocol/injector/StructureCache.java +++ b/src/main/java/com/comphenix/protocol/injector/StructureCache.java @@ -69,9 +69,12 @@ public class StructureCache { public static Object newPacket(Class packetClass) { Supplier packetConstructor = PACKET_INSTANCE_CREATORS.computeIfAbsent(packetClass, packetClassKey -> { - PacketCreator creator = PacketCreator.forPacket(packetClassKey); - if (creator.get() != null) { - return creator; + try { + PacketCreator creator = PacketCreator.forPacket(packetClassKey); + if (creator.get() != null) { + return creator; + } + } catch (Exception ignored) { } WrappedStreamCodec streamCodec = PacketRegistry.getStreamCodec(packetClassKey); diff --git a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 7bedc2a39..6d977e854 100644 --- a/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -679,6 +679,16 @@ public static Class getChatSerializerClass() { return getMinecraftClass("network.chat.IChatBaseComponent$ChatSerializer", "network.chat.Component$Serializer", "IChatBaseComponent$ChatSerializer"); } + /** + * Retrieve the component style serializer class. + * + * @return The serializer class. + */ + public static Class getStyleSerializerClass() { + return getMinecraftClass("network.chat.ChatModifier$ChatModifierSerializer", "ChatModifier$ChatModifierSerializer"); + } + + /** * Retrieve the ServerPing class. * @@ -1020,6 +1030,60 @@ public static Class getTileEntityClass() { return getMinecraftClass("world.level.block.entity.TileEntity", "world.level.block.entity.BlockEntity", "TileEntity"); } + /** + * Retrieve the NMS team parameters class. + * + * @return The team parameters class. + */ + public static Optional> getTeamParametersClass() { + return getOptionalNMS("network.protocol.game.PacketPlayOutScoreboardTeam$b"); + } + + /** + * Retrieve the NMS component style class. + * + * @return The component style class. + */ + public static Class getComponentStyleClass() { + return getMinecraftClass("network.chat.ChatModifier", "ChatModifier"); + } + + /** + * Retrieve the NMS NumberFormat class. + * + * @return The NumberFormat class. + */ + public static Optional> getNumberFormatClass() { + return getOptionalNMS("network.chat.numbers.NumberFormat"); + } + + /** + * Retrieve the NMS BlankFormat class. + * + * @return The FixedFormat class. + */ + public static Optional> getBlankFormatClass() { + return getOptionalNMS("network.chat.numbers.BlankFormat"); + } + + /** + * Retrieve the NMS FixedFormat class. + * + * @return The FixedFormat class. + */ + public static Optional> getFixedFormatClass() { + return getOptionalNMS("network.chat.numbers.FixedFormat"); + } + + /** + * Retrieve the NMS StyledFormat class. + * + * @return The StyledFormat class. + */ + public static Optional> getStyledFormatClass() { + return getOptionalNMS("network.chat.numbers.StyledFormat"); + } + /** * Retrieve the Gson class used by Minecraft. * diff --git a/src/main/java/com/comphenix/protocol/wrappers/AdventureComponentConverter.java b/src/main/java/com/comphenix/protocol/wrappers/AdventureComponentConverter.java index 97be97270..2ebdc2180 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/AdventureComponentConverter.java +++ b/src/main/java/com/comphenix/protocol/wrappers/AdventureComponentConverter.java @@ -16,7 +16,10 @@ */ package com.comphenix.protocol.wrappers; +import com.comphenix.protocol.utility.MinecraftVersion; +import com.google.gson.JsonObject; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; /** @@ -25,7 +28,16 @@ * Note: The Adventure API Component is not included in CraftBukkit, Bukkit or Spigot and but is present in PaperMC. */ public class AdventureComponentConverter { - + private static final GsonComponentSerializer SERIALIZER; + + static { + if (MinecraftVersion.NETHER_UPDATE.atOrAbove()) { + SERIALIZER = GsonComponentSerializer.gson(); + } else { + SERIALIZER = GsonComponentSerializer.colorDownsamplingGson(); + } + } + private AdventureComponentConverter() { } @@ -35,7 +47,7 @@ private AdventureComponentConverter() { * @return Component */ public static Component fromWrapper(WrappedChatComponent wrapper) { - return GsonComponentSerializer.gson().deserialize(wrapper.getJson()); + return SERIALIZER.deserialize(wrapper.getJson()); } /** @@ -71,11 +83,29 @@ public static Object fromJsonAsObject(final String json) { * @return ProtocolLib wrapper */ public static WrappedChatComponent fromComponent(Component component) { - return WrappedChatComponent.fromJson(GsonComponentSerializer.gson().serialize(component)); + return WrappedChatComponent.fromJson(SERIALIZER.serialize(component)); + } + + /** + * Converts a {@link WrappedComponentStyle} into a {@link Style} + * @param wrapper ProtocolLib wrapper + * @return Style + */ + public static Style fromWrapper(WrappedComponentStyle wrapper) { + return SERIALIZER.serializer().fromJson(wrapper.getJson(), Style.class); + } + + /** + * Converts a {@link Style} into a ProtocolLib wrapper + * @param style Style + * @return ProtocolLib wrapper + */ + public static WrappedComponentStyle fromStyle(Style style) { + return WrappedComponentStyle.fromJson((JsonObject) SERIALIZER.serializer().toJsonTree(style)); } public static Class getComponentClass() { - return Component.class; + return Component.class; } public static Component clone(Object component) { diff --git a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index 7e714fcbe..0b29f2e19 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -640,6 +640,14 @@ public static EquivalentConverter getWrappedLig return ignoreNull(handle(WrappedLevelChunkData.LightData::getHandle, WrappedLevelChunkData.LightData::new, WrappedLevelChunkData.LightData.class)); } + public static EquivalentConverter getWrappedTeamParametersConverter() { + return ignoreNull(handle(WrappedTeamParameters::getHandle, WrappedTeamParameters::new, WrappedTeamParameters.class)); + } + + public static EquivalentConverter getWrappedNumberFormatConverter() { + return ignoreNull(handle(WrappedNumberFormat::getHandle, WrappedNumberFormat::fromHandle, WrappedNumberFormat.class)); + } + public static EquivalentConverter getPacketContainerConverter() { return ignoreNull(handle(PacketContainer::getHandle, PacketContainer::fromPacket, PacketContainer.class)); } diff --git a/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java b/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java index f9cab38f5..e697fff53 100644 --- a/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java +++ b/src/main/java/com/comphenix/protocol/wrappers/EnumWrappers.java @@ -12,6 +12,7 @@ import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.MinecraftVersion; import org.apache.commons.lang.Validate; +import org.bukkit.ChatColor; import org.bukkit.GameMode; import java.lang.reflect.Field; @@ -464,6 +465,163 @@ public static Dimension fromId(int id) { } } + /** + * @since 1.20.2 + */ + public enum DisplaySlot { + LIST, + SIDEBAR, + BELOW_NAME, + TEAM_BLACK, + TEAM_DARK_BLUE, + TEAM_DARK_GREEN, + TEAM_DARK_AQUA, + TEAM_DARK_RED, + TEAM_DARK_PURPLE, + TEAM_GOLD, + TEAM_GRAY, + TEAM_DARK_GRAY, + TEAM_BLUE, + TEAM_GREEN, + TEAM_AQUA, + TEAM_RED, + TEAM_LIGHT_PURPLE, + TEAM_YELLOW, + TEAM_WHITE; + } + + public enum RenderType { + INTEGER, + HEARTS + } + + public enum ChatFormatting { + BLACK, + DARK_BLUE, + DARK_GREEN, + DARK_AQUA, + DARK_RED, + DARK_PURPLE, + GOLD, + GRAY, + DARK_GRAY, + BLUE, + GREEN, + AQUA, + RED, + LIGHT_PURPLE, + YELLOW, + WHITE, + OBFUSCATED, + BOLD, + STRIKETHROUGH, + UNDERLINE, + ITALIC, + RESET; + + public ChatColor toBukkit() { + switch (this){ + case BLACK: + return ChatColor.BLACK; + case DARK_BLUE: + return ChatColor.DARK_BLUE; + case DARK_GREEN: + return ChatColor.DARK_GREEN; + case DARK_AQUA: + return ChatColor.DARK_AQUA; + case DARK_RED: + return ChatColor.DARK_RED; + case DARK_PURPLE: + return ChatColor.DARK_PURPLE; + case GOLD: + return ChatColor.GOLD; + case GRAY: + return ChatColor.GRAY; + case DARK_GRAY: + return ChatColor.DARK_GRAY; + case BLUE: + return ChatColor.BLUE; + case GREEN: + return ChatColor.GREEN; + case AQUA: + return ChatColor.AQUA; + case RED: + return ChatColor.RED; + case LIGHT_PURPLE: + return ChatColor.LIGHT_PURPLE; + case YELLOW: + return ChatColor.YELLOW; + case WHITE: + return ChatColor.WHITE; + case OBFUSCATED: + return ChatColor.MAGIC; + case BOLD: + return ChatColor.BOLD; + case STRIKETHROUGH: + return ChatColor.STRIKETHROUGH; + case UNDERLINE: + return ChatColor.UNDERLINE; + case ITALIC: + return ChatColor.ITALIC; + case RESET: + return ChatColor.RESET; + default: + throw new IllegalStateException("Unimplemented Bukkit equivalent for " + name()); + } + } + + public static ChatFormatting fromBukkit(ChatColor color) { + switch (color){ + case BLACK: + return ChatFormatting.BLACK; + case DARK_BLUE: + return ChatFormatting.DARK_BLUE; + case DARK_GREEN: + return ChatFormatting.DARK_GREEN; + case DARK_AQUA: + return ChatFormatting.DARK_AQUA; + case DARK_RED: + return ChatFormatting.DARK_RED; + case DARK_PURPLE: + return ChatFormatting.DARK_PURPLE; + case GOLD: + return ChatFormatting.GOLD; + case GRAY: + return ChatFormatting.GRAY; + case DARK_GRAY: + return ChatFormatting.DARK_GRAY; + case BLUE: + return ChatFormatting.BLUE; + case GREEN: + return ChatFormatting.GREEN; + case AQUA: + return ChatFormatting.AQUA; + case RED: + return ChatFormatting.RED; + case LIGHT_PURPLE: + return ChatFormatting.LIGHT_PURPLE; + case YELLOW: + return ChatFormatting.YELLOW; + case WHITE: + return ChatFormatting.WHITE; + case MAGIC: + return ChatFormatting.OBFUSCATED; + case BOLD: + return ChatFormatting.BOLD; + case STRIKETHROUGH: + return ChatFormatting.STRIKETHROUGH; + case UNDERLINE: + return ChatFormatting.UNDERLINE; + case ITALIC: + return ChatFormatting.ITALIC; + case RESET: + return ChatFormatting.RESET; + default: + throw new IllegalStateException("Unknown ChatColor " + color); + } + } + } + private static Class PROTOCOL_CLASS = null; private static Class CLIENT_COMMAND_CLASS = null; private static Class CHAT_VISIBILITY_CLASS = null; @@ -485,6 +643,9 @@ public static Dimension fromId(int id) { private static Class DIRECTION_CLASS = null; private static Class CHAT_TYPE_CLASS = null; private static Class ENTITY_POSE_CLASS = null; + private static Class DISPLAY_SLOT_CLASS = null; + private static Class RENDER_TYPE_CLASS = null; + private static Class CHAT_FORMATTING_CLASS = null; private static boolean INITIALIZED = false; private static Map, EquivalentConverter> FROM_NATIVE = new HashMap<>(); @@ -574,6 +735,9 @@ private static void initialize() { CHAT_TYPE_CLASS = getEnum(PacketType.Play.Server.CHAT.getPacketClass(), 0); ENTITY_POSE_CLASS = MinecraftReflection.getNullableNMS("world.entity.EntityPose", "world.entity.Pose", "EntityPose"); + DISPLAY_SLOT_CLASS = MinecraftReflection.getNullableNMS("world.scores.DisplaySlot"); + RENDER_TYPE_CLASS = MinecraftReflection.getNullableNMS("world.scores.criteria.IScoreboardCriteria$EnumScoreboardHealthDisplay", "IScoreboardCriteria$EnumScoreboardHealthDisplay"); + CHAT_FORMATTING_CLASS = MinecraftReflection.getNullableNMS("EnumChatFormat"); associate(PROTOCOL_CLASS, Protocol.class, getProtocolConverter()); associate(CLIENT_COMMAND_CLASS, ClientCommand.class, getClientCommandConverter()); @@ -595,6 +759,9 @@ private static void initialize() { associate(CHAT_TYPE_CLASS, ChatType.class, getChatTypeConverter()); associate(HAND_CLASS, Hand.class, getHandConverter()); associate(ENTITY_USE_ACTION_CLASS, EntityUseAction.class, getEntityUseActionConverter()); + associate(DISPLAY_SLOT_CLASS, DisplaySlot.class, getDisplaySlotConverter()); + associate(RENDER_TYPE_CLASS, RenderType.class, getRenderTypeConverter()); + associate(CHAT_FORMATTING_CLASS, ChatFormatting.class, getChatFormattingConverter()); if (ENTITY_POSE_CLASS != null) { associate(ENTITY_POSE_CLASS, EntityPose.class, getEntityPoseConverter()); @@ -746,6 +913,21 @@ public static Class getEntityPoseClass() { return ENTITY_POSE_CLASS; } + public static Class getDisplaySlotClass() { + initialize(); + return DISPLAY_SLOT_CLASS; + } + + public static Class getRenderTypeClass() { + initialize(); + return RENDER_TYPE_CLASS; + } + + public static Class getChatFormattingClass() { + initialize(); + return CHAT_FORMATTING_CLASS; + } + // Get the converters public static EquivalentConverter getProtocolConverter() { return new EnumConverter<>(getProtocolClass(), Protocol.class); @@ -826,7 +1008,19 @@ public static EquivalentConverter getDirectionConverter() { public static EquivalentConverter getChatTypeConverter() { return new EnumConverter<>(getChatTypeClass(), ChatType.class); } - + + public static EquivalentConverter getDisplaySlotConverter() { + return new EnumConverter<>(getDisplaySlotClass(), DisplaySlot.class); + } + + public static EquivalentConverter getRenderTypeConverter() { + return new EnumConverter<>(getRenderTypeClass(), RenderType.class); + } + + public static EquivalentConverter getChatFormattingConverter() { + return new EnumConverter<>(getChatFormattingClass(), ChatFormatting.class); + } + /** * @since 1.13+ * @return {@link EnumConverter} or null (if bellow 1.13 / nms EnumPose class cannot be found) diff --git a/src/main/java/com/comphenix/protocol/wrappers/WrappedComponentStyle.java b/src/main/java/com/comphenix/protocol/wrappers/WrappedComponentStyle.java new file mode 100644 index 000000000..f463a86fa --- /dev/null +++ b/src/main/java/com/comphenix/protocol/wrappers/WrappedComponentStyle.java @@ -0,0 +1,59 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; +import com.comphenix.protocol.wrappers.codecs.WrappedCodec; +import com.comphenix.protocol.wrappers.codecs.WrappedDynamicOps; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * A wrapper around the component style NMS class. + * + * @author vytskalt + */ +public class WrappedComponentStyle extends AbstractWrapper { + private static final WrappedCodec CODEC; // 1.20.4+ + private static final Gson GSON; // Below 1.20.4 + + static { + if (MinecraftVersion.v1_20_4.atOrAbove()) { + FuzzyReflection fuzzySerializer = FuzzyReflection.fromClass(MinecraftReflection.getStyleSerializerClass(), true); + Object codec = Accessors.getFieldAccessor(fuzzySerializer.getFieldByType("CODEC", MinecraftReflection.getCodecClass())).get(null); + CODEC = WrappedCodec.fromHandle(codec); + GSON = null; + } else { + FuzzyReflection fuzzySerializer = FuzzyReflection.fromClass(MinecraftReflection.getChatSerializerClass(), true); + CODEC = null; + GSON = (Gson) Accessors.getFieldAccessor(fuzzySerializer.getFieldByType("gson", Gson.class)).get(null); + } + } + + public WrappedComponentStyle(Object handle) { + super(MinecraftReflection.getComponentStyleClass()); + setHandle(handle); + } + + public JsonElement getJson() { + if (CODEC != null) { + return (JsonElement) CODEC.encode(handle, WrappedDynamicOps.json(false)) + .getOrThrow(JsonParseException::new); + } else { + return GSON.toJsonTree(handle); + } + } + + public static WrappedComponentStyle fromJson(JsonElement json) { + Object handle; + if (CODEC != null) { + handle = CODEC.parse(json, WrappedDynamicOps.json(false)) + .getOrThrow(JsonParseException::new); + } else { + handle = GSON.fromJson(json, MinecraftReflection.getComponentStyleClass()); + } + return new WrappedComponentStyle(handle); + } +} diff --git a/src/main/java/com/comphenix/protocol/wrappers/WrappedNumberFormat.java b/src/main/java/com/comphenix/protocol/wrappers/WrappedNumberFormat.java new file mode 100644 index 000000000..3caa13df9 --- /dev/null +++ b/src/main/java/com/comphenix/protocol/wrappers/WrappedNumberFormat.java @@ -0,0 +1,126 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.ConstructorAccessor; +import com.comphenix.protocol.utility.MinecraftReflection; +import org.jetbrains.annotations.NotNull; + +/** + * A wrapper around the NumberFormat NMS classes. + * + * @author vytskalt + * @since 1.20.4 + */ +@SuppressWarnings("OptionalGetWithoutIsPresent") +public class WrappedNumberFormat extends AbstractWrapper { + private static final Object BLANK; + private static final ConstructorAccessor FIXED_CONSTRUCTOR, STYLED_CONSTRUCTOR; + + static { + if (!isSupported()) { + BLANK = null; + FIXED_CONSTRUCTOR = null; + STYLED_CONSTRUCTOR = null; + } else { + Class blankClass = MinecraftReflection.getBlankFormatClass().get(); + FuzzyReflection fuzzyBlank = FuzzyReflection.fromClass(blankClass, true); + BLANK = Accessors.getFieldAccessor(fuzzyBlank.getFieldByType("INSTANCE", blankClass)).get(null); + + FIXED_CONSTRUCTOR = Accessors.getConstructorAccessor( + MinecraftReflection.getFixedFormatClass().get(), + MinecraftReflection.getIChatBaseComponentClass() + ); + + STYLED_CONSTRUCTOR = Accessors.getConstructorAccessor( + MinecraftReflection.getStyledFormatClass().get(), + MinecraftReflection.getComponentStyleClass() + ); + } + } + + /** + * @return Whether the NumberFormat classes exist on the current server version + */ + public static boolean isSupported() { + return MinecraftReflection.getNumberFormatClass().isPresent(); + } + + public static WrappedNumberFormat fromHandle(Object handle) { + throwIfUnsupported(); + if (MinecraftReflection.getBlankFormatClass().get().isInstance(handle)) { + return new Blank(handle); + } else if (MinecraftReflection.getFixedFormatClass().get().isInstance(handle)) { + return new Fixed(handle); + } else if (MinecraftReflection.getStyledFormatClass().get().isInstance(handle)) { + return new Styled(handle); + } else { + throw new IllegalArgumentException("handle is not a NumberFormat instance, but " + handle.getClass()); + } + } + + public static Blank blank() { + throwIfUnsupported(); + return new Blank(WrappedNumberFormat.BLANK); + } + + public static Fixed fixed(@NotNull WrappedChatComponent content) { + throwIfUnsupported(); + Object handle = FIXED_CONSTRUCTOR.invoke(content.getHandle()); + return new Fixed(handle); + } + + public static Styled styled(@NotNull WrappedComponentStyle style) { + throwIfUnsupported(); + Object handle = STYLED_CONSTRUCTOR.invoke(style.getHandle()); + return new Styled(handle); + } + + private static void throwIfUnsupported() { + if (!isSupported()) { + throw new IllegalStateException("NumberFormat classes don't exist on this server version"); + } + } + + private WrappedNumberFormat(Class handleType) { + super(handleType); + } + + public static class Blank extends WrappedNumberFormat { + private Blank(Object handle) { + super(MinecraftReflection.getBlankFormatClass().get()); + setHandle(handle); + } + } + + public static class Fixed extends WrappedNumberFormat { + private final StructureModifier modifier; + + private Fixed(Object handle) { + super(MinecraftReflection.getFixedFormatClass().get()); + setHandle(handle); + this.modifier = new StructureModifier<>(handle.getClass()).withTarget(handle); + } + + public WrappedChatComponent getContent() { + Object handle = modifier.withType(MinecraftReflection.getIChatBaseComponentClass()).read(0); + return WrappedChatComponent.fromHandle(handle); + } + } + + public static class Styled extends WrappedNumberFormat { + private final StructureModifier modifier; + + private Styled(Object handle) { + super(MinecraftReflection.getStyledFormatClass().get()); + setHandle(handle); + this.modifier = new StructureModifier<>(handle.getClass()).withTarget(handle); + } + + public WrappedComponentStyle getStyle() { + Object handle = modifier.withType(MinecraftReflection.getComponentStyleClass()).read(0); + return new WrappedComponentStyle(handle); + } + } +} diff --git a/src/main/java/com/comphenix/protocol/wrappers/WrappedTeamParameters.java b/src/main/java/com/comphenix/protocol/wrappers/WrappedTeamParameters.java new file mode 100644 index 000000000..a9354163c --- /dev/null +++ b/src/main/java/com/comphenix/protocol/wrappers/WrappedTeamParameters.java @@ -0,0 +1,171 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.injector.StructureCache; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A wrapper around the team parameters NMS class. + * + * @author vytskalt + * @since 1.17 + */ +public class WrappedTeamParameters extends AbstractWrapper { + public static Class getNmsClassOrThrow() { + return MinecraftReflection.getTeamParametersClass() + .orElseThrow(() -> new IllegalStateException("Team parameters class doesn't exist on this server version")); + } + + /** + * @return Whether the team parameters class exists on the current server version + */ + public static boolean isSupported() { + return MinecraftReflection.getTeamParametersClass().isPresent(); + } + + public static Builder newBuilder() { + return newBuilder(null); + } + + public static Builder newBuilder(@Nullable WrappedTeamParameters template) { + return new Builder(template); + } + + private final StructureModifier modifier; + + public WrappedTeamParameters(Object handle) { + super(getNmsClassOrThrow()); + setHandle(handle); + this.modifier = new StructureModifier<>(getNmsClassOrThrow()).withTarget(handle); + } + + @NotNull + public WrappedChatComponent getDisplayName() { + return readComponent(0); + } + + @NotNull + public WrappedChatComponent getPrefix() { + return readComponent(1); + } + + @NotNull + public WrappedChatComponent getSuffix() { + return readComponent(2); + } + + @NotNull + public String getNametagVisibility() { + return modifier.withType(String.class).read(0); + } + + @NotNull + public String getCollisionRule() { + return modifier.withType(String.class).read(1); + } + + @NotNull + public EnumWrappers.ChatFormatting getColor() { + Object handle = modifier.withType(EnumWrappers.getChatFormattingClass()).read(0); + return EnumWrappers.getChatFormattingConverter().getSpecific(handle); + } + + public int getOptions() { + return (int) modifier.withType(int.class).read(0); + } + + private WrappedChatComponent readComponent(int index) { + Object handle = modifier.withType(MinecraftReflection.getIChatBaseComponentClass()).read(index); + return WrappedChatComponent.fromHandle(handle); + } + + private void writeComponent(int index, WrappedChatComponent component) { + modifier.withType(MinecraftReflection.getIChatBaseComponentClass()).write(index, component.getHandle()); + } + + public static class Builder { + private WrappedChatComponent displayName, prefix, suffix; + private String nametagVisibility, collisionRule; + private EnumWrappers.ChatFormatting color; + private int options; + + private Builder(@Nullable WrappedTeamParameters template) { + if (template != null) { + this.displayName = template.getDisplayName(); + this.prefix = template.getDisplayName(); + this.suffix = template.getDisplayName(); + this.nametagVisibility = template.getNametagVisibility(); + this.collisionRule = template.getCollisionRule(); + this.color = template.getColor(); + this.options = template.getOptions(); + } + } + + public Builder displayName(@NotNull WrappedChatComponent displayName) { + Preconditions.checkNotNull(displayName); + this.displayName = displayName; + return this; + } + + public Builder prefix(@NotNull WrappedChatComponent prefix) { + Preconditions.checkNotNull(prefix); + this.prefix = prefix; + return this; + } + + public Builder suffix(@NotNull WrappedChatComponent suffix) { + Preconditions.checkNotNull(suffix); + this.suffix = suffix; + return this; + } + + public Builder nametagVisibility(@NotNull String nametagVisibility) { + Preconditions.checkNotNull(nametagVisibility); + this.nametagVisibility = nametagVisibility; + return this; + } + + public Builder collisionRule(@NotNull String collisionRule) { + Preconditions.checkNotNull(collisionRule); + this.collisionRule = collisionRule; + return this; + } + + public Builder color(@NotNull EnumWrappers.ChatFormatting color) { + Preconditions.checkNotNull(color); + this.color = color; + return this; + } + + public Builder options(int options) { + Preconditions.checkNotNull(collisionRule); + this.options = options; + return this; + } + + public WrappedTeamParameters build() { + Preconditions.checkNotNull(displayName, "Display name not set"); + Preconditions.checkNotNull(prefix, "Prefix not set"); + Preconditions.checkNotNull(suffix, "Suffix not set"); + Preconditions.checkNotNull(nametagVisibility, "Nametag visibility not set"); + Preconditions.checkNotNull(collisionRule, "Collision rule not set"); + Preconditions.checkNotNull(color, "Color not set"); + + // Not technically a packet, but it has a PacketDataSerializer constructor, so it works fine + Object handle = StructureCache.newPacket(getNmsClassOrThrow()); + + WrappedTeamParameters wrapped = new WrappedTeamParameters(handle); + wrapped.writeComponent(0, displayName); + wrapped.writeComponent(1, prefix); + wrapped.writeComponent(2, suffix); + wrapped.modifier.withType(String.class).write(0, nametagVisibility); + wrapped.modifier.withType(String.class).write(1, collisionRule); + wrapped.modifier.withType(EnumWrappers.getChatFormattingClass()).write(0, EnumWrappers.getChatFormattingConverter().getGeneric(color)); + wrapped.modifier.withType(int.class).write(0, options); + return wrapped; + } + } +} diff --git a/src/test/java/com/comphenix/protocol/wrappers/WrappedComponentStyleTest.java b/src/test/java/com/comphenix/protocol/wrappers/WrappedComponentStyleTest.java new file mode 100644 index 000000000..d0ff9d4b6 --- /dev/null +++ b/src/test/java/com/comphenix/protocol/wrappers/WrappedComponentStyleTest.java @@ -0,0 +1,40 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.BukkitInitialization; +import com.google.gson.JsonElement; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextDecoration; +import net.minecraft.EnumChatFormat; +import net.minecraft.network.chat.ChatModifier; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WrappedComponentStyleTest { + + @BeforeAll + public static void initializeBukkit() { + BukkitInitialization.initializeAll(); + } + + @Test + public void testComponentStyle() { + ChatModifier style = ChatModifier.a.b(EnumChatFormat.m).a(true); + WrappedComponentStyle wrapped = new WrappedComponentStyle(style); + JsonElement json = wrapped.getJson(); + assertEquals("{\"color\":\"red\",\"bold\":true}", json.toString()); + assertEquals(style, WrappedComponentStyle.fromJson(json).getHandle()); + } + + @Test + public void testStyleAdventureConversion() { + Style adventureStyle = Style.style(NamedTextColor.GREEN, TextDecoration.BOLD) + .clickEvent(ClickEvent.changePage(10)); + + WrappedComponentStyle wrapped = AdventureComponentConverter.fromStyle(adventureStyle); + assertEquals(adventureStyle, AdventureComponentConverter.fromWrapper(wrapped)); + } +} diff --git a/src/test/java/com/comphenix/protocol/wrappers/WrappedNumberFormatTest.java b/src/test/java/com/comphenix/protocol/wrappers/WrappedNumberFormatTest.java new file mode 100644 index 000000000..c8b417f8f --- /dev/null +++ b/src/test/java/com/comphenix/protocol/wrappers/WrappedNumberFormatTest.java @@ -0,0 +1,50 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.BukkitInitialization; +import net.minecraft.EnumChatFormat; +import net.minecraft.network.chat.ChatModifier; +import net.minecraft.network.chat.IChatBaseComponent; +import net.minecraft.network.chat.numbers.BlankFormat; +import net.minecraft.network.chat.numbers.FixedFormat; +import net.minecraft.network.chat.numbers.StyledFormat; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class WrappedNumberFormatTest { + + @BeforeAll + static void initializeBukkit() { + BukkitInitialization.initializeAll(); + } + + @Test + void testBlankFormat() { + assertInstanceOf(WrappedNumberFormat.Blank.class, WrappedNumberFormat.fromHandle(BlankFormat.a)); + assertEquals(BlankFormat.a, WrappedNumberFormat.blank().getHandle()); + } + + @Test + void testFixedFormat() { + IChatBaseComponent content = IChatBaseComponent.a("Fixed"); + WrappedNumberFormat wrappedHandle = WrappedNumberFormat.fromHandle(new FixedFormat(content)); + assertInstanceOf(WrappedNumberFormat.Fixed.class, wrappedHandle); + assertEquals(content, ((WrappedNumberFormat.Fixed) wrappedHandle).getContent().getHandle()); + + WrappedNumberFormat.Fixed wrapped = WrappedNumberFormat.fixed(WrappedChatComponent.fromHandle(content)); + assertEquals(content, wrapped.getContent().getHandle()); + } + + @Test + void testStyledFormat() { + ChatModifier style = ChatModifier.a.b(EnumChatFormat.g); + WrappedNumberFormat wrappedHandle = WrappedNumberFormat.fromHandle(new StyledFormat(style)); + assertInstanceOf(WrappedNumberFormat.Styled.class, wrappedHandle); + assertEquals(style, ((WrappedNumberFormat.Styled) wrappedHandle).getStyle().getHandle()); + + WrappedNumberFormat.Styled newWrapper = WrappedNumberFormat.styled(new WrappedComponentStyle(style)); + assertEquals(style, newWrapper.getStyle().getHandle()); + } +} diff --git a/src/test/java/com/comphenix/protocol/wrappers/WrappedTeamParametersTest.java b/src/test/java/com/comphenix/protocol/wrappers/WrappedTeamParametersTest.java new file mode 100644 index 000000000..c8f371fb6 --- /dev/null +++ b/src/test/java/com/comphenix/protocol/wrappers/WrappedTeamParametersTest.java @@ -0,0 +1,53 @@ +package com.comphenix.protocol.wrappers; + +import com.comphenix.protocol.BukkitInitialization; +import net.minecraft.EnumChatFormat; +import net.minecraft.network.chat.IChatBaseComponent; +import net.minecraft.network.protocol.game.PacketPlayOutScoreboardTeam; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WrappedTeamParametersTest { + @BeforeAll + static void initializeBukkit() { + BukkitInitialization.initializeAll(); + } + + @Test + void testTeamParameters() { + IChatBaseComponent displayName = IChatBaseComponent.b("display name"); + IChatBaseComponent prefix = IChatBaseComponent.b("prefix"); + IChatBaseComponent suffix = IChatBaseComponent.b("suffix"); + String nametagVisibility = "always"; + String collisionRule = "never"; + + WrappedTeamParameters wrapped = WrappedTeamParameters.newBuilder() + .displayName(WrappedChatComponent.fromHandle(displayName)) + .prefix(WrappedChatComponent.fromHandle(prefix)) + .suffix(WrappedChatComponent.fromHandle(suffix)) + .nametagVisibility(nametagVisibility) + .collisionRule(collisionRule) + .color(EnumWrappers.ChatFormatting.RED) + .options(1) + .build(); + + assertEquals(displayName, wrapped.getDisplayName().getHandle()); + assertEquals(prefix, wrapped.getPrefix().getHandle()); + assertEquals(suffix, wrapped.getSuffix().getHandle()); + assertEquals(nametagVisibility, wrapped.getNametagVisibility()); + assertEquals(collisionRule, wrapped.getCollisionRule()); + assertEquals(EnumWrappers.ChatFormatting.RED, wrapped.getColor()); + assertEquals(1, wrapped.getOptions()); + + PacketPlayOutScoreboardTeam.b handle = (PacketPlayOutScoreboardTeam.b) wrapped.getHandle(); + assertEquals(handle.a(), displayName); + assertEquals(handle.f(), prefix); + assertEquals(handle.g(), suffix); + assertEquals(handle.d(), nametagVisibility); + assertEquals(handle.e(), collisionRule); + assertEquals(handle.c(), EnumChatFormat.m); + assertEquals(handle.b(), 1); + } +}