diff --git a/core/src/main/java/com/rexcantor64/triton/config/MainConfig.java b/core/src/main/java/com/rexcantor64/triton/config/MainConfig.java index b724c095..01c563ca 100644 --- a/core/src/main/java/com/rexcantor64/triton/config/MainConfig.java +++ b/core/src/main/java/com/rexcantor64/triton/config/MainConfig.java @@ -58,6 +58,7 @@ public class MainConfig implements TritonConfig { private List commandAliases; private String disabledLine; private boolean chat; + private boolean signedChat; private FeatureSyntax chatSyntax; private boolean actionbars; private FeatureSyntax actionbarSyntax; @@ -233,6 +234,7 @@ private void setupLanguageCreation(Configuration section) { Configuration chat = section.getSection("chat"); this.chat = chat.getBoolean("enabled", true); + this.signedChat = chat.getBoolean("signed-enabled", true); this.chatSyntax = FeatureSyntax.fromSection(chat); this.chatSyntax.interactive = true; diff --git a/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/packetinterceptor/ProtocolLibListener.java b/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/packetinterceptor/ProtocolLibListener.java index 352dbe7e..fab7ec9c 100644 --- a/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/packetinterceptor/ProtocolLibListener.java +++ b/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/packetinterceptor/ProtocolLibListener.java @@ -31,6 +31,7 @@ import com.rexcantor64.triton.spigot.wrappers.WrappedClientConfiguration; import com.rexcantor64.triton.utils.ComponentUtils; import com.rexcantor64.triton.utils.ReflectionUtils; +import com.rexcantor64.triton.wrappers.WrappedPlayerChatMessage; import lombok.SneakyThrows; import lombok.val; import net.kyori.adventure.text.Component; @@ -137,11 +138,10 @@ private void setupPacketHandlers() { // Removed in 1.19.3 packetHandlers.put(PacketType.Play.Server.CHAT_PREVIEW, asAsync(this::handleChatPreview)); } - } else { - // In 1.19+, this packet is signed, so we cannot edit it. - // It's sent by the player anyway, so there's nothing to translate. - packetHandlers.put(PacketType.Play.Server.CHAT, asAsync(this::handleChat)); } + // In 1.19+, this packet is signed, but we can still edit it, since it might contain + // formatting from chat plugins. + packetHandlers.put(PacketType.Play.Server.CHAT, asAsync(this::handleChat)); if (main.getMcVersion() >= 17) { // Title packet split on 1.17 packetHandlers.put(PacketType.Play.Server.SET_TITLE_TEXT, asAsync(this::handleTitle)); @@ -185,22 +185,38 @@ private void setupPacketHandlers() { /* PACKET HANDLERS */ private void handleChat(PacketEvent packet, SpigotLanguagePlayer languagePlayer) { - boolean ab = isActionbar(packet.getPacket()); + boolean isSigned = MinecraftVersion.WILD_UPDATE.atOrAbove(); // MC 1.19+ + if (isSigned && !main.getConfig().isSignedChat()) return; + // action bars are not sent here on 1.19+ anymore + boolean ab = !isSigned && isActionbar(packet.getPacket()); // Don't bother parsing anything else if it's disabled on config if ((ab && !main.getConfig().isActionbars()) || (!ab && !main.getConfig().isChat())) return; + val chatModifier = packet.getPacket().getChatComponents(); val baseComponentModifier = packet.getPacket().getSpecificModifier(BASE_COMPONENT_ARRAY_CLASS); val adventureModifier = packet.getPacket().getSpecificModifier(ADVENTURE_COMPONENT_CLASS); + boolean hasPlayerChatMessageRecord = isSigned && !MinecraftVersion.FEATURE_PREVIEW_UPDATE.atOrAbove(); // MC 1.19-1.19.2 + WrappedPlayerChatMessage wrappedPlayerChatMessage = null; Component message = null; - if (adventureModifier.readSafely(0) != null) { + if (hasPlayerChatMessageRecord) { + // The message is wrapped in a PlayerChatMessage record + val playerChatModifier = packet.getPacket().getModifier().withType(WrappedPlayerChatMessage.getWrappedClass(), WrappedPlayerChatMessage.CONVERTER); + wrappedPlayerChatMessage = playerChatModifier.readSafely(0); + if (wrappedPlayerChatMessage != null) { + Optional msg = wrappedPlayerChatMessage.getMessage(); + if (msg.isPresent()) { + message = WrappedComponentUtils.deserialize(msg.get()); + } + } + } else if (adventureModifier.readSafely(0) != null) { message = adventureModifier.readSafely(0); } else if (baseComponentModifier.readSafely(0) != null) { message = BaseComponentUtils.deserialize(baseComponentModifier.readSafely(0)); } else { - val msg = packet.getPacket().getChatComponents().readSafely(0); + val msg = chatModifier.readSafely(0); if (msg != null) { message = WrappedComponentUtils.deserialize(msg); } @@ -212,6 +228,7 @@ private void handleChat(PacketEvent packet, SpigotLanguagePlayer languagePlayer) } // Translate the message + val wrappedPlayerChatMessageFinal = wrappedPlayerChatMessage; parser() .translateComponent( message, @@ -222,6 +239,12 @@ private void handleChat(PacketEvent packet, SpigotLanguagePlayer languagePlayer) if (adventureModifier.size() > 0) { // On a Paper or fork, so we can directly set the Adventure Component adventureModifier.writeSafely(0, result); + } else if (MinecraftVersion.FEATURE_PREVIEW_UPDATE.atOrAbove()) { // MC 1.19.3+ + // While chat is signed, we can still mess around with formatting and prefixes + chatModifier.writeSafely(0, WrappedComponentUtils.serialize(result)); + } else if (hasPlayerChatMessageRecord) { // MC 1.19-1.19.2 + // While chat is signed, we can still mess around with formatting and prefixes + wrappedPlayerChatMessageFinal.setMessage(Optional.of(WrappedComponentUtils.serialize(result))); } else { BaseComponent[] resultComponent; if (ab && !MinecraftVersion.EXPLORATION_UPDATE.atOrAbove()) { diff --git a/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/wrappers/WrappedPlayerChatMessage.java b/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/wrappers/WrappedPlayerChatMessage.java new file mode 100644 index 00000000..3ebef7e7 --- /dev/null +++ b/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/wrappers/WrappedPlayerChatMessage.java @@ -0,0 +1,61 @@ +package com.rexcantor64.triton.wrappers; + +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.FieldAccessor; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.AbstractWrapper; +import com.comphenix.protocol.wrappers.BukkitConverters; +import com.comphenix.protocol.wrappers.Converters; +import com.comphenix.protocol.wrappers.WrappedChatComponent; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + * Custom ProtocolLib Wrapper of NMS' PlayerChatMessage (added to NMS in 1.19 and removed in 1.19.3) + * Tested with 1.19.2 + */ +public class WrappedPlayerChatMessage extends AbstractWrapper { + + private static Class PLAYER_CHAT_MESSAGE = MinecraftReflection.getMinecraftClass("network.chat.PlayerChatMessage"); + private static FuzzyReflection FUZZY_REFLECTION = FuzzyReflection.fromClass(PLAYER_CHAT_MESSAGE, true); + private static FieldAccessor CHAT_COMPONENT = Accessors.getFieldAccessor(FUZZY_REFLECTION.getParameterizedField(Optional.class, MinecraftReflection.getIChatBaseComponentClass())); + private static EquivalentConverter> CHAT_COMPONENT_CONVERTER = Converters.optional(BukkitConverters.getWrappedChatComponentConverter()); + + public static final EquivalentConverter CONVERTER = Converters.ignoreNull(Converters.handle(WrappedPlayerChatMessage::getHandle, WrappedPlayerChatMessage::fromHandle, WrappedPlayerChatMessage.class)); + + /** + * Construct a new PlayerChatMessage wrapper. + */ + private WrappedPlayerChatMessage(Object handle) { + super(getWrappedClass()); + setHandle(handle); + } + + public Optional getMessage() { + return CHAT_COMPONENT_CONVERTER.getSpecific(CHAT_COMPONENT.get(handle)); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public void setMessage(Optional message) { + CHAT_COMPONENT.set(handle, CHAT_COMPONENT_CONVERTER.getGeneric(message)); + } + + /** + * Construct a player chat message from a native NMS object. + * + * @param handle - the native object. + * @return The wrapped player chat message object. + */ + @Contract("_ -> new") + public static @NotNull WrappedPlayerChatMessage fromHandle(Object handle) { + return new WrappedPlayerChatMessage(handle); + } + + public static Class getWrappedClass() { + return PLAYER_CHAT_MESSAGE; + } +} diff --git a/triton-spigot/src/main/resources/config_spigot.yml b/triton-spigot/src/main/resources/config_spigot.yml index f75e5bee..6703bb71 100644 --- a/triton-spigot/src/main/resources/config_spigot.yml +++ b/triton-spigot/src/main/resources/config_spigot.yml @@ -138,6 +138,8 @@ language-creation: chat: # Should the plugin check for placeholders in chat messages? enabled: true + # Should the plugin check for placeholders in signed chat messages (MC 1.19+)? Requires the above option to be enabled as well. + signed-enabled: true syntax-lang: lang syntax-args: args syntax-arg: arg