diff --git a/patches/api/Adventure.patch b/patches/api/Adventure.patch
index b17b7ff027..fa47823c6a 100644
--- a/patches/api/Adventure.patch
+++ b/patches/api/Adventure.patch
@@ -359,8 +359,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
+ @ApiStatus.Internal
-+ public AsyncChatCommandDecorateEvent(boolean async, @Nullable Player player, @NotNull Component originalMessage, @NotNull Component result) {
-+ super(async, player, originalMessage, result);
++ public AsyncChatCommandDecorateEvent(@Nullable Player player, @NotNull Component originalMessage) {
++ super(player, originalMessage);
+ }
+
+ @Override
@@ -384,7 +384,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import org.bukkit.entity.Player;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
-+import org.bukkit.event.player.AsyncPlayerChatPreviewEvent;
+import org.bukkit.event.server.ServerEvent;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
@@ -412,11 +411,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ private boolean cancelled;
+
+ @ApiStatus.Internal
-+ public AsyncChatDecorateEvent(final boolean async, final @Nullable Player player, final @NotNull Component originalMessage, final @NotNull Component result) {
-+ super(async);
++ public AsyncChatDecorateEvent(final @Nullable Player player, final @NotNull Component originalMessage) {
++ super(true);
+ this.player = player;
+ this.originalMessage = originalMessage;
-+ this.result = result;
++ this.result = originalMessage;
+ }
+
+ /**
@@ -443,7 +442,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ /**
+ * Gets the decoration result. This may already be different from
+ * {@link #originalMessage()} if some other listener to this event
-+ * OR the legacy preview event ({@link AsyncPlayerChatPreviewEvent})
+ * changed the result.
+ *
+ * @return the result
diff --git a/patches/server/Adventure.patch b/patches/server/Adventure.patch
index 47707978b1..bab4ef08f7 100644
--- a/patches/server/Adventure.patch
+++ b/patches/server/Adventure.patch
@@ -416,7 +416,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ });
+ }
+
-+ static @Nullable String tryCollapseToString(final Component component) {
++ public static @Nullable String tryCollapseToString(final Component component) {
+ if (component instanceof final TextComponent textComponent) {
+ if (component.children().isEmpty() && component.style().isEmpty()) {
+ return textComponent.content();
@@ -611,157 +611,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ }
+ }
+}
-diff --git a/src/main/java/io/papermc/paper/adventure/ChatDecorationProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatDecorationProcessor.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/adventure/ChatDecorationProcessor.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.adventure;
-+
-+import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent;
-+import io.papermc.paper.event.player.AsyncChatDecorateEvent;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.concurrent.CompletableFuture;
-+import java.util.regex.Pattern;
-+import net.kyori.adventure.text.Component;
-+import net.kyori.adventure.text.minimessage.MiniMessage;
-+import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
-+import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
-+import net.minecraft.Optionull;
-+import net.minecraft.commands.CommandSourceStack;
-+import net.minecraft.network.chat.ChatDecorator;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerPlayer;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.craftbukkit.util.LazyPlayerSet;
-+import org.bukkit.event.Event;
-+import org.bukkit.event.player.AsyncPlayerChatPreviewEvent;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.checker.nullness.qual.Nullable;
-+import org.checkerframework.framework.qual.DefaultQualifier;
-+
-+import static io.papermc.paper.adventure.ChatProcessor.DEFAULT_LEGACY_FORMAT;
-+import static io.papermc.paper.adventure.ChatProcessor.canYouHearMe;
-+import static io.papermc.paper.adventure.ChatProcessor.displayName;
-+import static net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection;
-+
-+@DefaultQualifier(NonNull.class)
-+public final class ChatDecorationProcessor {
-+
-+ private static final String DISPLAY_NAME_TAG = "---paper_dn---";
-+ private static final Pattern DISPLAY_NAME_PATTERN = Pattern.compile("%(1\\$)?s");
-+ private static final String CONTENT_TAG = "---paper_content---";
-+ private static final Pattern CONTENT_PATTERN = Pattern.compile("%(2\\$)?s");
-+
-+ final MinecraftServer server;
-+ final @Nullable ServerPlayer player;
-+ final @Nullable CommandSourceStack commandSourceStack;
-+ final Component originalMessage;
-+
-+ public ChatDecorationProcessor(final MinecraftServer server, final @Nullable ServerPlayer player, final @Nullable CommandSourceStack commandSourceStack, final net.minecraft.network.chat.Component originalMessage) {
-+ this.server = server;
-+ this.player = player;
-+ this.commandSourceStack = commandSourceStack;
-+ this.originalMessage = PaperAdventure.asAdventure(originalMessage);
-+ }
-+
-+ public CompletableFuture process() {
-+ return CompletableFuture.supplyAsync(() -> {
-+ ChatDecorator.Result result = new ChatDecorator.ModernResult(this.originalMessage, true, false);
-+ if (listenToLegacy()) {
-+ result = this.processLegacy(result);
-+ }
-+ return this.processModern(result);
-+ }, this.server.chatExecutor);
-+ }
-+
-+ @SuppressWarnings("deprecation")
-+ private static boolean listenToLegacy() {
-+ return canYouHearMe(AsyncPlayerChatPreviewEvent.getHandlerList());
-+ }
-+
-+ @SuppressWarnings("deprecation")
-+ private ChatDecorator.Result processLegacy(final ChatDecorator.Result input) {
-+ if (this.player != null) {
-+ final CraftPlayer player = this.player.getBukkitEntity();
-+ final String originalMessage = legacySection().serialize(this.originalMessage);
-+ final AsyncPlayerChatPreviewEvent event = new AsyncPlayerChatPreviewEvent(true, player, originalMessage, new LazyPlayerSet(this.server));
-+ this.post(event);
-+
-+ final boolean isDefaultFormat = DEFAULT_LEGACY_FORMAT.equals(event.getFormat());
-+ if (event.isCancelled() || (isDefaultFormat && originalMessage.equals(event.getMessage()))) {
-+ return input;
-+ } else {
-+ final Component message = legacySection().deserialize(event.getMessage());
-+ final Component component = isDefaultFormat ? message : legacyFormat(event.getFormat(), ((CraftPlayer) event.getPlayer()), legacySection().deserialize(event.getMessage()));
-+ return legacy(component, event.getFormat(), new ChatDecorator.MessagePair(message, event.getMessage()), isDefaultFormat);
-+ }
-+ }
-+ return input;
-+ }
-+
-+ private ChatDecorator.Result processModern(final ChatDecorator.Result input) {
-+ final @Nullable CraftPlayer player = Optionull.map(this.player, ServerPlayer::getBukkitEntity);
-+
-+ final Component initialResult = input.message().component();
-+ final AsyncChatDecorateEvent event;
-+ if (this.commandSourceStack != null) {
-+ // TODO more command decorate context
-+ event = new AsyncChatCommandDecorateEvent(true, player, this.originalMessage, initialResult);
-+ } else {
-+ event = new AsyncChatDecorateEvent(true, player, this.originalMessage, initialResult);
-+ }
-+ this.post(event);
-+ if (!event.isCancelled() && !event.result().equals(initialResult)) {
-+ if (input instanceof ChatDecorator.LegacyResult legacyResult) {
-+ if (legacyResult.hasNoFormatting()) {
-+ /*
-+ The MessagePair in the decoration result may be different at this point. This is because the legacy
-+ decoration system requires the same modifications be made to the message, so we can't have the initial
-+ message value for the legacy chat events be changed by the modern decorate event.
-+ */
-+ return noFormatting(event.result(), legacyResult.format(), legacyResult.message().legacyMessage());
-+ } else {
-+ final Component formatted = legacyFormat(legacyResult.format(), player, event.result());
-+ return withFormatting(formatted, legacyResult.format(), event.result(), legacyResult.message().legacyMessage());
-+ }
-+ } else {
-+ return new ChatDecorator.ModernResult(event.result(), true, false);
-+ }
-+ }
-+ return input;
-+ }
-+
-+ private void post(final Event event) {
-+ this.server.server.getPluginManager().callEvent(event);
-+ }
-+
-+ private static Component legacyFormat(final String format, final @Nullable CraftPlayer player, final Component message) {
-+ final List args = new ArrayList<>(player != null ? 2 : 1);
-+ if (player != null) {
-+ args.add(Placeholder.component(DISPLAY_NAME_TAG, displayName(player)));
-+ }
-+ args.add(Placeholder.component(CONTENT_TAG, message));
-+ String miniMsg = MiniMessage.miniMessage().serialize(legacySection().deserialize(format));
-+ miniMsg = DISPLAY_NAME_PATTERN.matcher(miniMsg).replaceFirst("<" + DISPLAY_NAME_TAG + ">");
-+ miniMsg = CONTENT_PATTERN.matcher(miniMsg).replaceFirst("<" + CONTENT_TAG + ">");
-+ return MiniMessage.miniMessage().deserialize(miniMsg, TagResolver.resolver(args));
-+ }
-+
-+ public static ChatDecorator.LegacyResult legacy(final Component maybeFormatted, final String format, final ChatDecorator.MessagePair message, final boolean hasNoFormatting) {
-+ return new ChatDecorator.LegacyResult(maybeFormatted, format, message, hasNoFormatting, false);
-+ }
-+
-+ public static ChatDecorator.LegacyResult noFormatting(final Component component, final String format, final String legacyMessage) {
-+ return new ChatDecorator.LegacyResult(component, format, new ChatDecorator.MessagePair(component, legacyMessage), true, true);
-+ }
-+
-+ public static ChatDecorator.LegacyResult withFormatting(final Component formatted, final String format, final Component message, final String legacyMessage) {
-+ return new ChatDecorator.LegacyResult(formatted, format, new ChatDecorator.MessagePair(message, legacyMessage), false, true);
-+ }
-+}
diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -789,10 +638,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import net.kyori.adventure.audience.ForwardingAudience;
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.minecraft.Optionull;
+import net.minecraft.Util;
+import net.minecraft.core.registries.Registries;
-+import net.minecraft.network.chat.ChatDecorator;
+import net.minecraft.network.chat.ChatType;
+import net.minecraft.network.chat.OutgoingChatMessage;
+import net.minecraft.network.chat.PlayerChatMessage;
@@ -812,6 +661,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.framework.qual.DefaultQualifier;
++import org.intellij.lang.annotations.Subst;
+
+import static net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection;
+
@@ -830,33 +680,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ static final int MESSAGE_CHANGED = 1;
+ static final int FORMAT_CHANGED = 2;
+ static final int SENDER_CHANGED = 3; // Not used
-+ // static final int FORCE_PREVIEW_USE = 4; // TODO (future, maybe?)
+ private final BitSet flags = new BitSet(3);
+
+ public ChatProcessor(final MinecraftServer server, final ServerPlayer player, final PlayerChatMessage message, final boolean async) {
+ this.server = server;
+ this.player = player;
-+ /*
-+ CraftBukkit's preview/decoration system relies on both the "decorate" and chat event making the same modifications. If
-+ there is unsigned content in the legacyMessage, that is because the player sent the legacyMessage without it being
-+ previewed (probably by sending it too quickly). We can just ignore that because the same changes will
-+ happen in the chat event.
-+
-+ If unsigned content is present, it will be the same as `this.legacyMessage.signedContent().previewResult().component()`.
-+ */
+ this.message = message;
+ this.async = async;
-+ if (this.message.requireResult().modernized()) {
-+ this.craftbukkit$originalMessage = this.message.requireResult().message().legacyMessage();
-+ } else {
-+ this.craftbukkit$originalMessage = message.signedContent();
-+ }
-+ /*
-+ this.paper$originalMessage is the input to paper's chat events. This should be the decorated message component.
-+ Even if the legacy preview event modified the format, and the client signed the formatted message, this should
-+ still just be the message component.
-+ */
-+ this.paper$originalMessage = this.message.requireResult().message().component();
++ this.craftbukkit$originalMessage = message.unsignedContent() != null ? LegacyComponentSerializer.legacySection().serialize(PaperAdventure.asAdventure(message.unsignedContent())) : message.signedContent();
++ this.paper$originalMessage = PaperAdventure.asAdventure(this.message.decoratedContent());
+ this.outgoing = OutgoingChatMessage.create(this.message);
+ }
+
@@ -910,8 +742,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ private ChatRenderer modernRenderer(final String format) {
+ if (this.flags.get(FORMAT_CHANGED)) {
+ return legacyRenderer(format);
-+ } else if (this.message.requireResult() instanceof ChatDecorator.LegacyResult legacyResult) {
-+ return legacyRenderer(legacyResult.format());
+ } else {
+ return defaultRenderer();
+ }
@@ -920,25 +750,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ private Component modernMessage(final String legacyMessage) {
+ if (this.flags.get(MESSAGE_CHANGED)) {
+ return legacySection().deserialize(legacyMessage);
-+ } else if (this.message.unsignedContent() == null && this.message.requireResult() instanceof ChatDecorator.LegacyResult legacyResult) {
-+ return legacyResult.message().component();
+ } else {
+ return this.paper$originalMessage;
+ }
+ }
+
+ private void readLegacyModifications(final String message, final String format, final Player playerSender) {
-+ if (this.message.requireResult() instanceof ChatDecorator.LegacyResult result) {
-+ if (this.message.unsignedContent() != null && !result.modernized()) {
-+ this.flags.set(MESSAGE_CHANGED, !message.equals(result.message().legacyMessage()));
-+ } else {
-+ this.flags.set(MESSAGE_CHANGED, !message.equals(this.craftbukkit$originalMessage));
-+ }
-+ this.flags.set(FORMAT_CHANGED, !format.equals(result.format()));
-+ } else {
-+ this.flags.set(MESSAGE_CHANGED, !message.equals(this.craftbukkit$originalMessage));
-+ this.flags.set(FORMAT_CHANGED, !format.equals(DEFAULT_LEGACY_FORMAT));
-+ }
++ this.flags.set(MESSAGE_CHANGED, !message.equals(this.craftbukkit$originalMessage));
++ this.flags.set(FORMAT_CHANGED, !format.equals(DEFAULT_LEGACY_FORMAT));
+ this.flags.set(SENDER_CHANGED, playerSender != this.player.getBukkitEntity());
+ }
+
@@ -967,15 +786,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ }
+
+ private void readModernModifications(final AbstractChatEvent chatEvent, final ChatRenderer originalRenderer) {
-+ if (this.message.unsignedContent() != null) {
-+ this.flags.set(MESSAGE_CHANGED, !chatEvent.message().equals(this.message.requireResult().message().component()));
-+ } else {
-+ this.flags.set(MESSAGE_CHANGED, !chatEvent.message().equals(this.paper$originalMessage));
-+ }
++ this.flags.set(MESSAGE_CHANGED, !chatEvent.message().equals(this.paper$originalMessage));
+ if (originalRenderer != chatEvent.renderer()) { // don't set to false if it hasn't changed
+ this.flags.set(FORMAT_CHANGED, true);
+ }
-+ // this.flags.set(FORCE_PREVIEW_USE, chatEvent.usePreviewComponent()); // TODO (future, maybe?)
+ }
+
+ private void complete(final AbstractChatEvent event) {
@@ -993,9 +807,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ final ChatType.Bound chatType = ChatType.bind(chatTypeKey, this.player.level().registryAccess(), PaperAdventure.asVanilla(displayName(player)));
+
+ OutgoingChat outgoingChat = viewers instanceof LazyChatAudienceSet lazyAudienceSet && lazyAudienceSet.isLazy() ? new ServerOutgoingChat() : new ViewersOutgoingChat();
-+ /* if (this.flags.get(FORCE_PREVIEW_USE)) { // TODO (future, maybe?)
-+ outgoingChat.sendOriginal(player, viewers, chatType);
-+ } else */
+ if (this.flags.get(FORMAT_CHANGED)) {
+ if (renderer instanceof ChatRenderer.ViewerUnaware unaware) {
+ outgoingChat.sendFormatChangedViewerUnaware(player, PaperAdventure.asVanilla(unaware.render(player, displayName, message)), viewers, chatType);
@@ -1004,7 +815,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ }
+ } else if (this.flags.get(MESSAGE_CHANGED)) {
+ if (!(renderer instanceof ChatRenderer.ViewerUnaware unaware)) {
-+ throw new IllegalStateException("BUG: There should not be a non-legacy renderer at this point");
++ throw new IllegalStateException("BUG: This should be a ViewerUnaware renderer at this point");
+ }
+ final Component renderedComponent = chatTypeKey == ChatType.CHAT ? message : unaware.render(player, displayName, message);
+ outgoingChat.sendMessageChanged(player, PaperAdventure.asVanilla(renderedComponent), viewers, chatType);
@@ -1085,7 +896,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ });
+
+ private net.kyori.adventure.chat.ChatType.Bound adventure(ChatType.Bound chatType) {
-+ final String stringKey = Objects.requireNonNull(
++ @Subst("key:value") final String stringKey = Objects.requireNonNull(
+ ChatProcessor.this.server.registryAccess().registryOrThrow(Registries.CHAT_TYPE).getKey(chatType.chatType()),
+ () -> "No key for '%s' in CHAT_TYPE registry.".formatted(chatType)
+ ).toString();
@@ -1213,6 +1024,67 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return LegacyComponentSerializer.legacySection().serialize(player.adventure$displayName);
+ }
+}
+diff --git a/src/main/java/io/papermc/paper/adventure/ImprovedChatDecorator.java b/src/main/java/io/papermc/paper/adventure/ImprovedChatDecorator.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/ImprovedChatDecorator.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.adventure;
++
++import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent;
++import io.papermc.paper.event.player.AsyncChatDecorateEvent;
++import java.util.concurrent.CompletableFuture;
++import net.minecraft.commands.CommandSourceStack;
++import net.minecraft.network.chat.ChatDecorator;
++import net.minecraft.network.chat.Component;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerPlayer;
++import org.bukkit.craftbukkit.entity.CraftPlayer;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public final class ImprovedChatDecorator implements ChatDecorator {
++ private final MinecraftServer server;
++
++ public ImprovedChatDecorator(final MinecraftServer server) {
++ this.server = server;
++ }
++
++ @Override
++ public CompletableFuture decorate(final @Nullable ServerPlayer sender, final Component message) {
++ return decorate(this.server, sender, null, message);
++ }
++
++ @Override
++ public CompletableFuture decorate(final @Nullable ServerPlayer sender, final @Nullable CommandSourceStack commandSourceStack, final Component message) {
++ return decorate(this.server, sender, commandSourceStack, message);
++ }
++
++ private static CompletableFuture decorate(final MinecraftServer server, final @Nullable ServerPlayer player, final @Nullable CommandSourceStack commandSourceStack, final Component originalMessage) {
++ return CompletableFuture.supplyAsync(() -> {
++ final net.kyori.adventure.text.Component initialResult = PaperAdventure.asAdventure(originalMessage);
++
++ final @Nullable CraftPlayer craftPlayer = player == null ? null : player.getBukkitEntity();
++
++ final AsyncChatDecorateEvent event;
++ if (commandSourceStack != null) {
++ // TODO more command decorate context
++ event = new AsyncChatCommandDecorateEvent(craftPlayer, initialResult);
++ } else {
++ event = new AsyncChatDecorateEvent(craftPlayer, initialResult);
++ }
++
++ if (event.callEvent()) {
++ return PaperAdventure.asVanilla(event.result());
++ }
++
++ return originalMessage;
++ }, server.chatExecutor);
++ }
++}
diff --git a/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java b/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -1401,7 +1273,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+
+ // Component
+
-+ public static Component asAdventure(final net.minecraft.network.chat.Component component) {
++ public static @NotNull Component asAdventure(@Nullable final net.minecraft.network.chat.Component component) {
+ return component == null ? Component.empty() : WRAPPER_AWARE_SERIALIZER.deserialize(component);
+ }
+
@@ -1429,7 +1301,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return jsons;
+ }
+
-+ public static net.minecraft.network.chat.Component asVanilla(final Component component) {
++ public static net.minecraft.network.chat.Component asVanilla(@Nullable final Component component) {
+ if (component == null) return null;
+ if (true) return new AdventureComponent(component);
+ return WRAPPER_AWARE_SERIALIZER.serialize(component);
@@ -2142,9 +2014,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
- source.getChatMessageChainer().append(completableFuture, filtered -> {
- PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(component).filter(filtered.mask());
+ // Paper start - support asynchronous chat decoration
-+ CompletableFuture componentFuture = minecraftServer.getChatDecorator().decorate(source.getPlayer(), source, message.decoratedContent());
++ CompletableFuture componentFuture = minecraftServer.getChatDecorator().decorate(source.getPlayer(), source, message.decoratedContent());
+ source.getChatMessageChainer().append(CompletableFuture.allOf(completableFuture, componentFuture), filtered -> {
-+ PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(componentFuture.join().component()).filter(completableFuture.join().mask());
++ PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(componentFuture.join()).filter(completableFuture.join().mask());
+ // Paper end - support asynchronous chat decoration
callback.accept(playerChatMessage2);
});
@@ -2155,8 +2027,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
- Component component = chatDecorator.decorate(source.getPlayer(), message.decoratedContent());
- callback.accept(message.withUnsignedContent(component));
+ // Paper start - support asynchronous chat decoration
-+ CompletableFuture componentFuture = chatDecorator.decorate(source.getPlayer(), source, message.decoratedContent());
-+ source.getChatMessageChainer().append(componentFuture, (result) -> callback.accept(message.withUnsignedContent(result.component())));
++ CompletableFuture componentFuture = chatDecorator.decorate(source.getPlayer(), source, message.decoratedContent());
++ source.getChatMessageChainer().append(componentFuture, (result) -> callback.accept(message.withUnsignedContent(result)));
+ // Paper end - support asynchronous chat decoration
}
@@ -2249,68 +2121,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@FunctionalInterface
public interface ChatDecorator {
- ChatDecorator PLAIN = (sender, message) -> message;
-+ ChatDecorator PLAIN = (sender, message) -> CompletableFuture.completedFuture(message); // Paper - adventure; support async chat decoration events;
++ ChatDecorator PLAIN = (sender, message) -> CompletableFuture.completedFuture(message); // Paper - adventure; support async chat decoration events
- Component decorate(@Nullable ServerPlayer sender, Component message);
-+ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - adventure; support chat decoration events
++ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - adventure; support chat decoration events (callers should use the overload with CommandSourceStack)
+ CompletableFuture decorate(@Nullable ServerPlayer sender, Component message); // Paper - adventure; support async chat decoration events
+
+ // Paper start - adventure; support async chat decoration events
-+ default CompletableFuture decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message) {
++ default CompletableFuture decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message) {
+ throw new UnsupportedOperationException("Must override this implementation");
+ }
-+
-+ static ChatDecorator create(ImprovedChatDecorator delegate) {
-+ return new ChatDecorator() {
-+ @Override
-+ public CompletableFuture decorate(@Nullable ServerPlayer sender, Component message) {
-+ return this.decorate(sender, null, message).thenApply(Result::component);
-+ }
-+
-+ @Override
-+ public CompletableFuture decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message) {
-+ return delegate.decorate(sender, commandSourceStack, message);
-+ }
-+ };
-+ }
-+
-+ @FunctionalInterface
-+ interface ImprovedChatDecorator {
-+ CompletableFuture decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message);
-+ }
-+
-+ interface Result {
-+ boolean hasNoFormatting();
-+
-+ Component component();
-+
-+ MessagePair message();
-+
-+ boolean modernized();
-+ }
-+
-+ record MessagePair(net.kyori.adventure.text.Component component, String legacyMessage) { }
-+
-+ record LegacyResult(Component component, String format, MessagePair message, boolean hasNoFormatting, boolean modernized) implements Result {
-+ public LegacyResult(net.kyori.adventure.text.Component component, String format, MessagePair message, boolean hasNoFormatting, boolean modernified) {
-+ this(io.papermc.paper.adventure.PaperAdventure.asVanilla(component), format, message, hasNoFormatting, modernified);
-+ }
-+ public LegacyResult {
-+ component = component instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent ? adventureComponent.deepConverted() : component;
-+ }
-+ }
-+
-+ record ModernResult(Component component, boolean hasNoFormatting, boolean modernized) implements Result {
-+ public ModernResult(net.kyori.adventure.text.Component component, boolean hasNoFormatting, boolean modernized) {
-+ this(io.papermc.paper.adventure.PaperAdventure.asVanilla(component), hasNoFormatting, modernized);
-+ }
-+
-+ @Override
-+ public MessagePair message() {
-+ final net.kyori.adventure.text.Component adventureComponent = io.papermc.paper.adventure.PaperAdventure.WRAPPER_AWARE_SERIALIZER.deserialize(this.component);
-+ return new MessagePair(adventureComponent, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(adventureComponent));
-+ }
-+ }
+ // Paper end - adventure; support async chat decoration events
}
diff --git a/src/main/java/net/minecraft/network/chat/ComponentSerialization.java b/src/main/java/net/minecraft/network/chat/ComponentSerialization.java
@@ -2455,24 +2275,11 @@ diff --git a/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java b/s
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java
+++ b/src/main/java/net/minecraft/network/chat/PlayerChatMessage.java
-@@ -0,0 +0,0 @@ import net.minecraft.util.SignatureUpdater;
- import net.minecraft.util.SignatureValidator;
-
+@@ -0,0 +0,0 @@ import net.minecraft.util.SignatureValidator;
public record PlayerChatMessage(
-- SignedMessageLink link, @Nullable MessageSignature signature, SignedMessageBody signedBody, @Nullable Component unsignedContent, FilterMask filterMask
-+ SignedMessageLink link, @Nullable MessageSignature signature, SignedMessageBody signedBody, @Nullable Component unsignedContent, FilterMask filterMask, @Nullable net.minecraft.network.chat.ChatDecorator.Result result // Paper - adventure; support signed messages
+ SignedMessageLink link, @Nullable MessageSignature signature, SignedMessageBody signedBody, @Nullable Component unsignedContent, FilterMask filterMask
) {
+ // Paper start - adventure; support signed messages
-+ public PlayerChatMessage(SignedMessageLink link, @Nullable MessageSignature signature, SignedMessageBody signedBody, @Nullable Component unsignedContent, FilterMask filterMask) {
-+ this(link, signature, signedBody, unsignedContent, filterMask, null);
-+ }
-+ public PlayerChatMessage withResult(net.minecraft.network.chat.ChatDecorator.Result result) {
-+ final PlayerChatMessage msg = this.withUnsignedContent(result.component());
-+ return new PlayerChatMessage(msg.link, msg.signature, msg.signedBody, msg.unsignedContent, msg.filterMask, result);
-+ }
-+ public net.minecraft.network.chat.ChatDecorator.Result requireResult() {
-+ return Objects.requireNonNull(this.result, "Requires a decoration result to be set here");
-+ }
+ public final class AdventureView implements net.kyori.adventure.chat.SignedMessage {
+ private AdventureView() {
+ }
@@ -2516,7 +2323,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
public PlayerChatMessage withUnsignedContent(Component unsignedContent) {
- Component component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null;
-+ Component component = !(unsignedContent instanceof io.papermc.paper.adventure.AdventureComponent advComponent ? advComponent.deepConverted() : unsignedContent).equals(Component.literal(this.signedContent())) ? unsignedContent : null; // Paper - adventure; convert adventure component wraps
++ // Paper start - adventure
++ final Component component;
++ if (unsignedContent instanceof io.papermc.paper.adventure.AdventureComponent advComponent) {
++ component = this.signedContent().equals(io.papermc.paper.adventure.AdventureCodecs.tryCollapseToString(advComponent.adventure$component())) ? null : unsignedContent;
++ } else {
++ component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null;
++ }
++ // Paper end - adventure
return new PlayerChatMessage(this.link, this.signature, this.signedBody, component, this.filterMask);
}
@@ -2747,14 +2561,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
- // CraftBukkit end
+ new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Chat Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build()); // Paper
++ public final ChatDecorator improvedChatDecorator = new io.papermc.paper.adventure.ImprovedChatDecorator(this); // Paper - adventure
public ChatDecorator getChatDecorator() {
- return ChatDecorator.PLAIN;
-+ // Paper start - moved to ChatPreviewProcessor
-+ return ChatDecorator.create((sender, commandSourceStack, message) -> {
-+ final io.papermc.paper.adventure.ChatDecorationProcessor processor = new io.papermc.paper.adventure.ChatDecorationProcessor(this, sender, commandSourceStack, message);
-+ return processor.process();
-+ });
-+ // Paper end
++ return this.improvedChatDecorator; // Paper - support async chat decoration events
}
public boolean logIPs() {
@@ -2987,12 +2797,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
CompletableFuture completablefuture = this.filterTextPacket(playerchatmessage.signedContent()).thenApplyAsync(Function.identity(), this.server.chatExecutor); // CraftBukkit - async chat
- Component ichatbasecomponent = this.server.getChatDecorator().decorate(this.player, playerchatmessage.decoratedContent());
-+ CompletableFuture componentFuture = this.server.getChatDecorator().decorate(this.player, null, playerchatmessage.decoratedContent()); // Paper
++ CompletableFuture componentFuture = this.server.getChatDecorator().decorate(this.player, null, playerchatmessage.decoratedContent()); // Paper - Adventure
- this.chatMessageChain.append(completablefuture, (filteredtext) -> {
- PlayerChatMessage playerchatmessage1 = playerchatmessage.withUnsignedContent(ichatbasecomponent).filter(filteredtext.mask());
-+ this.chatMessageChain.append(CompletableFuture.allOf(completablefuture, componentFuture), (filteredtext) -> {
-+ PlayerChatMessage playerchatmessage1 = playerchatmessage.filter(completablefuture.join().mask()).withResult(componentFuture.join()); // Paper
++ this.chatMessageChain.append(CompletableFuture.allOf(completablefuture, componentFuture), (filteredtext) -> { // Paper - Adventure
++ PlayerChatMessage playerchatmessage1 = playerchatmessage.withUnsignedContent(componentFuture.join()).filter(completablefuture.join().mask()); // Paper - Adventure
this.broadcastChatMessage(playerchatmessage1);
});
diff --git a/patches/server/Improve-Player-chat-API-handling.patch b/patches/server/Improve-Player-chat-API-handling.patch
index 3844bee337..ec17e80cb3 100644
--- a/patches/server/Improve-Player-chat-API-handling.patch
+++ b/patches/server/Improve-Player-chat-API-handling.patch
@@ -68,7 +68,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ if (msg.startsWith("/")) {
+ this.getHandle().connection.handleCommand(msg);
+ } else {
-+ final PlayerChatMessage playerChatMessage = PlayerChatMessage.system(msg).withResult(new net.minecraft.network.chat.ChatDecorator.ModernResult(Component.literal(msg), true, false));
++ final PlayerChatMessage playerChatMessage = PlayerChatMessage.system(msg).withUnsignedContent(Component.literal(msg));
+ // TODO chat decorating
+ // TODO text filtering
+ this.getHandle().connection.chat(msg, playerChatMessage, false);
diff --git a/patches/server/Option-to-use-vanilla-per-world-scoreboard-coloring-.patch b/patches/server/Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
index 574dbdd3f9..9ee8d98d99 100644
--- a/patches/server/Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
+++ b/patches/server/Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
@@ -15,10 +15,10 @@ diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/m
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
+++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
-@@ -0,0 +0,0 @@ import net.kyori.adventure.audience.Audience;
- import net.kyori.adventure.audience.ForwardingAudience;
+@@ -0,0 +0,0 @@ import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
+ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.minecraft.ChatFormatting;
import net.minecraft.Optionull;
import net.minecraft.Util;