diff --git a/patches/api/Adventure.patch b/patches/api/Adventure.patch
index 5eb93e45d2..41d941bbfd 100644
--- a/patches/api/Adventure.patch
+++ b/patches/api/Adventure.patch
@@ -86,7 +86,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import org.bukkit.entity.Player;
-+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
@@ -115,7 +114,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ */
+ @NotNull
+ static ChatRenderer defaultRenderer() {
-+ return viewerUnaware((source, sourceDisplayName, message) -> Component.translatable("chat.type.text", sourceDisplayName, message));
++ return new ViewerUnawareImpl.Default((source, sourceDisplayName, message) -> Component.translatable("chat.type.text", sourceDisplayName, message));
++ }
++
++ @ApiStatus.Internal
++ sealed interface Default extends ChatRenderer, ViewerUnaware permits ViewerUnawareImpl.Default {
+ }
+
+ /**
@@ -127,17 +130,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ */
+ @NotNull
+ static ChatRenderer viewerUnaware(final @NotNull ViewerUnaware renderer) {
-+ return new ChatRenderer() {
-+ private @MonotonicNonNull Component message;
-+
-+ @Override
-+ public @NotNull Component render(final @NotNull Player source, final @NotNull Component sourceDisplayName, final @NotNull Component message, final @NotNull Audience viewer) {
-+ if (this.message == null) {
-+ this.message = renderer.render(source, sourceDisplayName, message);
-+ }
-+ return this.message;
-+ }
-+ };
++ return new ViewerUnawareImpl(renderer);
+ }
+
+ /**
@@ -159,6 +152,50 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ Component render(@NotNull Player source, @NotNull Component sourceDisplayName, @NotNull Component message);
+ }
+}
+diff --git a/src/main/java/io/papermc/paper/chat/ViewerUnawareImpl.java b/src/main/java/io/papermc/paper/chat/ViewerUnawareImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/chat/ViewerUnawareImpl.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.chat;
++
++import net.kyori.adventure.audience.Audience;
++import net.kyori.adventure.text.Component;
++import org.bukkit.entity.Player;
++import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
++import org.jetbrains.annotations.NotNull;
++
++sealed class ViewerUnawareImpl implements ChatRenderer, ChatRenderer.ViewerUnaware permits ViewerUnawareImpl.Default {
++
++ private final ViewerUnaware unaware;
++
++ private @MonotonicNonNull Component message;
++
++ ViewerUnawareImpl(final ViewerUnaware unaware) {
++ this.unaware = unaware;
++ }
++
++ @Override
++ public @NotNull Component render(final @NotNull Player source, final @NotNull Component sourceDisplayName, final @NotNull Component message, final @NotNull Audience viewer) {
++ return this.render(source, sourceDisplayName, message);
++ }
++
++ @Override
++ public @NotNull Component render(final @NotNull Player source, final @NotNull Component sourceDisplayName, final @NotNull Component message) {
++ if (this.message == null) {
++ this.message = this.unaware.render(source, sourceDisplayName, message);
++ }
++ return this.message;
++ }
++
++ static final class Default extends ViewerUnawareImpl implements ChatRenderer.Default {
++
++ Default(final ViewerUnaware unaware) {
++ super(unaware);
++ }
++ }
++}
diff --git a/src/main/java/io/papermc/paper/event/player/AbstractChatEvent.java b/src/main/java/io/papermc/paper/event/player/AbstractChatEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -277,6 +314,164 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ this.cancelled = cancelled;
+ }
+}
+diff --git a/src/main/java/io/papermc/paper/event/player/AsyncChatCommandDecorateEvent.java b/src/main/java/io/papermc/paper/event/player/AsyncChatCommandDecorateEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/event/player/AsyncChatCommandDecorateEvent.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.event.player;
++
++import net.kyori.adventure.text.Component;
++import org.bukkit.entity.Player;
++import org.bukkit.event.HandlerList;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++@ApiStatus.Experimental
++public class AsyncChatCommandDecorateEvent extends AsyncChatDecorateEvent {
++
++ private static final HandlerList HANDLER_LIST = new HandlerList();
++
++ public AsyncChatCommandDecorateEvent(boolean async, @Nullable Player player, @NotNull Component originalMessage, boolean isPreview, @NotNull Component result) {
++ super(async, player, originalMessage, isPreview, result);
++ }
++
++ @Override
++ public @NotNull HandlerList getHandlers() {
++ return HANDLER_LIST;
++ }
++
++ public static @NotNull HandlerList getHandlerList() {
++ return HANDLER_LIST;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/event/player/AsyncChatDecorateEvent.java b/src/main/java/io/papermc/paper/event/player/AsyncChatDecorateEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/event/player/AsyncChatDecorateEvent.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.event.player;
++
++import net.kyori.adventure.text.Component;
++import org.bukkit.entity.Player;
++import org.bukkit.event.Cancellable;
++import org.bukkit.event.HandlerList;
++import org.bukkit.event.server.ServerEvent;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++/**
++ * This event is fired when the server decorates a component for chat purposes. It can be called
++ * under the following circumstances:
++ *
++ * - Previewing: If the client requests a preview response, this event is fired to decorate the component
++ * before it is sent back to the client for signing.
++ * - Chat: If the client sends a chat packet without having signed a preview (the client could have previews
++ * disabled or they sent the message too quickly) this event is fired to generated the decorated component. Note
++ * that when this is the case, the message will show up as modified as the decorated component wasn't signed
++ * by the client.
++ *
++ * @see AsyncChatCommandDecorateEvent for the decoration of messages sent via commands
++ */
++@ApiStatus.Experimental
++public class AsyncChatDecorateEvent extends ServerEvent implements Cancellable {
++
++ private static final HandlerList HANDLER_LIST = new HandlerList();
++
++ private final Player player;
++ private final Component originalMessage;
++ private final boolean isPreview;
++ private Component result;
++ private boolean cancelled;
++
++ @ApiStatus.Internal
++ public AsyncChatDecorateEvent(final boolean async, final @Nullable Player player, final @NotNull Component originalMessage, final boolean isPreview, final @NotNull Component result) {
++ super(async);
++ this.player = player;
++ this.originalMessage = originalMessage;
++ this.isPreview = isPreview;
++ this.result = result;
++ }
++
++ /**
++ * Gets the player (if available) associated with this event.
++ *
++ * Certain commands request decorations without a player context
++ * which is why this is possibly null.
++ *
++ * @return the player or null
++ */
++ public @Nullable Player player() {
++ return this.player;
++ }
++
++ /**
++ * Gets the original decoration input
++ *
++ * @return the input
++ */
++ public @NotNull Component originalMessage() {
++ return this.originalMessage;
++ }
++
++ /**
++ * 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 org.bukkit.event.player.AsyncPlayerChatPreviewEvent}
++ * changed the result.
++ *
++ * @return the result
++ */
++ public @NotNull Component result() {
++ return this.result;
++ }
++
++ /**
++ * Sets the resulting decorated component.
++ *
++ * @param result the result
++ */
++ public void result(@NotNull Component result) {
++ this.result = result;
++ }
++
++ /**
++ * If this decorating is part of a preview request/response.
++ *
++ * @return true if part of previewing
++ */
++ public boolean isPreview() {
++ return this.isPreview;
++ }
++
++ @Override
++ public boolean isCancelled() {
++ return this.cancelled;
++ }
++
++ /**
++ * A cancelled decorating event means that no changes to the result component
++ * will have any effect. The decorated component will be equal to the original
++ * component.
++ */
++ @Override
++ public void setCancelled(boolean cancel) {
++ this.cancelled = cancel;
++ }
++
++ @Override
++ public @NotNull HandlerList getHandlers() {
++ return HANDLER_LIST;
++ }
++
++ public static @NotNull HandlerList getHandlerList() {
++ return HANDLER_LIST;
++ }
++}
diff --git a/src/main/java/io/papermc/paper/event/player/AsyncChatEvent.java b/src/main/java/io/papermc/paper/event/player/AsyncChatEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
diff --git a/patches/server/Adventure.patch b/patches/server/Adventure.patch
index 8c1a36d0d5..0152d895b3 100644
--- a/patches/server/Adventure.patch
+++ b/patches/server/Adventure.patch
@@ -98,6 +98,153 @@ 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.Util;
++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;
++ final boolean isPreview;
++
++ public ChatDecorationProcessor(final MinecraftServer server, final @Nullable ServerPlayer player, final @Nullable CommandSourceStack commandSourceStack, final net.minecraft.network.chat.Component originalMessage, final boolean isPreview) {
++ this.server = server;
++ this.player = player;
++ this.commandSourceStack = commandSourceStack;
++ this.originalMessage = PaperAdventure.asAdventure(originalMessage);
++ this.isPreview = isPreview;
++ }
++
++ public CompletableFuture process() {
++ return CompletableFuture.supplyAsync(() -> {
++ ChatDecorator.Result result = new ChatDecorator.ModernResult(this.originalMessage, true, false);
++ if (canYouHearMe(AsyncPlayerChatPreviewEvent.getHandlerList())) {
++ result = this.processLegacy(result);
++ }
++ return this.processModern(result);
++ }, this.server.chatExecutor);
++ }
++
++ 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 = Util.mapNullable(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, this.isPreview, initialResult);
++ } else {
++ event = new AsyncChatDecorateEvent(true, player, this.originalMessage, this.isPreview, 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
@@ -106,22 +253,32 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@@ -0,0 +0,0 @@
+package io.papermc.paper.adventure;
+
++import com.google.common.base.Suppliers;
+import io.papermc.paper.chat.ChatRenderer;
+import io.papermc.paper.event.player.AbstractChatEvent;
+import io.papermc.paper.event.player.AsyncChatEvent;
+import io.papermc.paper.event.player.ChatEvent;
++import java.util.BitSet;
++import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
-+import java.util.regex.Pattern;
++import java.util.function.Function;
++import java.util.function.Supplier;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.audience.MessageType;
+import net.kyori.adventure.text.Component;
-+import net.kyori.adventure.text.TextReplacementConfig;
-+import net.kyori.adventure.text.event.ClickEvent;
-+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
++import net.minecraft.Util;
++import net.minecraft.network.chat.ChatDecorator;
++import net.minecraft.network.chat.ChatMessageContent;
++import net.minecraft.network.chat.ChatType;
++import net.minecraft.network.chat.OutgoingPlayerChatMessage;
++import net.minecraft.network.chat.PlayerChatMessage;
++import net.minecraft.resources.ResourceKey;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
++import org.bukkit.command.CommandSender;
++import org.bukkit.command.ConsoleCommandSender;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.craftbukkit.util.LazyPlayerSet;
+import org.bukkit.craftbukkit.util.Waitable;
@@ -130,35 +287,54 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.player.AsyncPlayerChatEvent;
+import org.bukkit.event.player.PlayerChatEvent;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
+
++import static net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection;
++
++@DefaultQualifier(NonNull.class)
+public final class ChatProcessor {
-+ // <-- copied from adventure-text-serializer-legacy
-+ private static final Pattern DEFAULT_URL_PATTERN = Pattern.compile("(?:(https?)://)?([-\\w_.]+\\.\\w{2,})(/\\S*)?");
-+ private static final Pattern URL_SCHEME_PATTERN = Pattern.compile("^[a-z][a-z0-9+\\-.]*:");
-+ private static final TextReplacementConfig URL_REPLACEMENT_CONFIG = TextReplacementConfig.builder()
-+ .match(DEFAULT_URL_PATTERN)
-+ .replacement(url -> {
-+ String clickUrl = url.content();
-+ if (!URL_SCHEME_PATTERN.matcher(clickUrl).find()) {
-+ clickUrl = "http://" + clickUrl;
-+ }
-+ return url.clickEvent(ClickEvent.openUrl(clickUrl));
-+ })
-+ .build();
-+ // copied from adventure-text-serializer-legacy -->
-+ private static final String DEFAULT_LEGACY_FORMAT = "<%1$s> %2$s"; // copied from PlayerChatEvent/AsyncPlayerChatEvent
++ static final String DEFAULT_LEGACY_FORMAT = "<%1$s> %2$s"; // copied from PlayerChatEvent/AsyncPlayerChatEvent
+ final MinecraftServer server;
+ final ServerPlayer player;
-+ final String message;
++ final PlayerChatMessage message;
+ final boolean async;
-+ final Component originalMessage;
++ final String craftbukkit$originalMessage;
++ final Component paper$originalMessage;
++ final OutgoingPlayerChatMessage outgoing;
+
-+ public ChatProcessor(final MinecraftServer server, final ServerPlayer player, final String message, final boolean async) {
++ 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;
-+ this.originalMessage = Component.text(message);
++ if (this.message.signedContent().decorationResult().modernized()) {
++ this.craftbukkit$originalMessage = this.message.signedContent().decorationResult().message().legacyMessage();
++ } else {
++ this.craftbukkit$originalMessage = message.signedContent().plain();
++ }
++ /*
++ 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.signedContent().decorationResult().message().component();
++ this.outgoing = OutgoingPlayerChatMessage.create(this.message);
+ }
+
+ @SuppressWarnings("deprecated")
@@ -167,7 +343,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ final boolean listenersOnSyncEvent = canYouHearMe(PlayerChatEvent.getHandlerList());
+ if (listenersOnAsyncEvent || listenersOnSyncEvent) {
+ final CraftPlayer player = this.player.getBukkitEntity();
-+ final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.message, new LazyPlayerSet(this.server));
++ final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server));
+ this.post(ae);
+ if (listenersOnSyncEvent) {
+ final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients());
@@ -179,33 +355,73 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return null;
+ }
+ });
++ this.readLegacyModifications(se.getMessage(), se.getFormat(), se.getPlayer());
+ this.processModern(
-+ legacyRenderer(se.getFormat()),
++ this.modernRenderer(se.getFormat()),
+ this.viewersFromLegacy(se.getRecipients()),
-+ LegacyComponentSerializer.legacySection().deserialize(se.getMessage()),
++ this.modernMessage(se.getMessage()),
++ se.getPlayer(),
+ se.isCancelled()
+ );
+ } else {
++ this.readLegacyModifications(ae.getMessage(), ae.getFormat(), ae.getPlayer());
+ this.processModern(
-+ legacyRenderer(ae.getFormat()),
++ this.modernRenderer(ae.getFormat()),
+ this.viewersFromLegacy(ae.getRecipients()),
-+ LegacyComponentSerializer.legacySection().deserialize(ae.getMessage()),
++ this.modernMessage(ae.getMessage()),
++ ae.getPlayer(),
+ ae.isCancelled()
+ );
+ }
+ } else {
+ this.processModern(
-+ ChatRenderer.defaultRenderer(),
++ defaultRenderer(),
+ new LazyChatAudienceSet(this.server),
-+ Component.text(this.message).replaceText(URL_REPLACEMENT_CONFIG),
++ this.paper$originalMessage,
++ this.player.getBukkitEntity(),
+ false
+ );
+ }
+ }
+
-+ private void processModern(final ChatRenderer renderer, final Set viewers, final Component message, final boolean cancelled) {
-+ final CraftPlayer player = this.player.getBukkitEntity();
-+ final AsyncChatEvent ae = new AsyncChatEvent(this.async, player, viewers, renderer, message, this.originalMessage);
++ private ChatRenderer modernRenderer(final String format) {
++ if (this.flags.get(FORMAT_CHANGED)) {
++ return legacyRenderer(format);
++ } else if (this.message.signedContent().decorationResult() instanceof ChatDecorator.LegacyResult legacyResult) {
++ return legacyRenderer(legacyResult.format());
++ } else {
++ return defaultRenderer();
++ }
++ }
++
++ private Component modernMessage(final String legacyMessage) {
++ if (this.flags.get(MESSAGE_CHANGED)) {
++ return legacySection().deserialize(legacyMessage);
++ } else if (this.message.unsignedContent().isEmpty() && this.message.signedContent().decorationResult() 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) {
++ final ChatMessageContent content = this.message.signedContent();
++ if (content.decorationResult() instanceof ChatDecorator.LegacyResult result) {
++ if ((content.isDecorated() || this.message.unsignedContent().isPresent()) && !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(SENDER_CHANGED, playerSender != this.player.getBukkitEntity());
++ }
++
++ private void processModern(final ChatRenderer renderer, final Set viewers, final Component message, final Player player, final boolean cancelled) {
++ final AsyncChatEvent ae = new AsyncChatEvent(this.async, player, viewers, renderer, message, this.paper$originalMessage);
+ ae.setCancelled(cancelled); // propagate cancelled state
+ this.post(ae);
+ final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList());
@@ -213,39 +429,145 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ this.queueIfAsyncOrRunImmediately(new Waitable() {
+ @Override
+ protected Void evaluate() {
-+ final ChatEvent se = new ChatEvent(player, ae.viewers(), ae.renderer(), ae.message(), ChatProcessor.this.originalMessage);
++ final ChatEvent se = new ChatEvent(player, ae.viewers(), ae.renderer(), ae.message(), ChatProcessor.this.paper$originalMessage/*, ae.usePreviewComponent()*/);
+ se.setCancelled(ae.isCancelled()); // propagate cancelled state
+ ChatProcessor.this.post(se);
++ ChatProcessor.this.readModernModifications(se, renderer);
+ ChatProcessor.this.complete(se);
+ return null;
+ }
+ });
+ } else {
++ this.readModernModifications(ae, renderer);
+ this.complete(ae);
+ }
+ }
+
++ private void readModernModifications(final AbstractChatEvent chatEvent, final ChatRenderer originalRenderer) {
++ if (this.message.signedContent().isDecorated()) {
++ this.flags.set(MESSAGE_CHANGED, !chatEvent.message().equals(this.message.signedContent().decorationResult().message().component()));
++ } else {
++ 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) {
+ if (event.isCancelled()) {
++ this.outgoing.sendHeadersToRemainingPlayers(this.server.getPlayerList());
+ return;
+ }
+
-+ final CraftPlayer player = this.player.getBukkitEntity();
++ final CraftPlayer player = ((CraftPlayer) event.getPlayer());
+ final Component displayName = displayName(player);
+ final Component message = event.message();
+ final ChatRenderer renderer = event.renderer();
+
+ final Set viewers = event.viewers();
++ final ResourceKey chatTypeKey = renderer instanceof ChatRenderer.Default ? ChatType.CHAT : ChatType.RAW;
++ final ChatType.Bound chatType = ChatType.bind(chatTypeKey, this.player.level.registryAccess(), PaperAdventure.asVanilla(displayName(player)));
+
-+ if (viewers instanceof LazyChatAudienceSet lazyAudienceSet && lazyAudienceSet.isLazy()) {
-+ this.server.console.sendMessage(player, renderer.render(player, displayName, message, this.server.console), MessageType.CHAT);
-+ for (final ServerPlayer viewer : this.server.getPlayerList().getPlayers()) {
-+ final Player bukkit = viewer.getBukkitEntity();
-+ bukkit.sendMessage(player, renderer.render(player, displayName, message, bukkit), MessageType.CHAT);
++ 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);
++ } else {
++ outgoingChat.sendFormatChangedViewerAware(player, displayName, message, renderer, viewers, chatType);
+ }
++ } 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");
++ }
++ final Component renderedComponent = chatTypeKey == ChatType.CHAT ? message : unaware.render(player, displayName, message);
++ outgoingChat.sendMessageChanged(player, PaperAdventure.asVanilla(renderedComponent), viewers, chatType);
+ } else {
-+ for (final Audience viewer : viewers) {
-+ viewer.sendMessage(player, renderer.render(player, displayName, message, viewer), MessageType.CHAT);
++ outgoingChat.sendOriginal(player, viewers, chatType);
++ }
++ }
++
++ interface OutgoingChat {
++ default void sendFormatChangedViewerUnaware(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set viewers, ChatType.Bound chatType) {
++ this.sendMessageChanged(player, renderedMessage, viewers, chatType);
++ }
++
++ void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set viewers, ChatType.Bound chatType);
++
++ void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set viewers, ChatType.Bound chatType);
++
++ void sendOriginal(CraftPlayer player, Set viewers, ChatType.Bound chatType);
++ }
++
++ final class ServerOutgoingChat implements OutgoingChat {
++ @Override
++ public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set viewers, ChatType.Bound chatType) {
++ ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, chatType, viewer -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, viewer)));
++ }
++
++ @Override
++ public void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set viewers, ChatType.Bound chatType) {
++ ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message.withUnsignedContent(renderedMessage), ChatProcessor.this.player, chatType);
++ }
++
++ @Override
++ public void sendOriginal(CraftPlayer player, Set viewers, ChatType.Bound chatType) {
++ ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, chatType);
++ }
++ }
++
++ final class ViewersOutgoingChat implements OutgoingChat {
++ @Override
++ public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set viewers, ChatType.Bound chatType) {
++ this.broadcastToViewers(viewers, player, chatType, v -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, v)));
++ }
++
++ @Override
++ public void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set viewers, ChatType.Bound chatType) {
++ this.broadcastToViewers(viewers, player, chatType, new ConstantFunction(renderedMessage));
++ }
++
++ @Override
++ public void sendOriginal(CraftPlayer player, Set viewers, ChatType.Bound chatType) {
++ this.broadcastToViewers(viewers, player, chatType, null);
++ }
++
++ private void broadcastToViewers(Collection viewers, final Player source, final ChatType.Bound chatType, final @Nullable Function msgFunction) {
++ final Supplier fallbackSupplier = Suppliers.memoize(() -> PaperAdventure.asAdventure(msgFunction instanceof ConstantFunction constantFunction ? constantFunction.component : ChatProcessor.this.message.serverContent()));
++ final Function audienceMsgFunction = !(msgFunction instanceof ConstantFunction || msgFunction == null) ? msgFunction.andThen(PaperAdventure::asAdventure) : viewer -> fallbackSupplier.get();
++ for (Audience viewer : viewers) {
++ if (viewer instanceof Player || viewer instanceof ConsoleCommandSender) {
++ // players and console have builtin PlayerChatMessage sending support while other audiences do not
++ this.sendToViewer((CommandSender) viewer, chatType, msgFunction);
++ } else {
++ viewer.sendMessage(source, audienceMsgFunction.apply(viewer), MessageType.CHAT);
++ }
++ }
++ }
++
++ private void sendToViewer(final CommandSender viewer, final ChatType.Bound chatType, final @Nullable Function msgFunction) {
++ if (viewer instanceof ConsoleCommandSender) {
++ this.sendToServer(chatType, msgFunction);
++ } else if (viewer instanceof CraftPlayer craftPlayer) {
++ craftPlayer.getHandle().sendChatMessage(ChatProcessor.this.outgoing, ChatProcessor.this.player.shouldFilterMessageTo(craftPlayer.getHandle()), chatType, Util.mapNullable(msgFunction, f -> f.apply(viewer)));
++ } else {
++ throw new IllegalStateException("Should only be a Player or Console");
++ }
++ }
++
++ private void sendToServer(final ChatType.Bound chatType, final @Nullable Function msgFunction) {
++ final PlayerChatMessage toConsoleMessage = msgFunction == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(msgFunction.apply(ChatProcessor.this.server.console));
++ ChatProcessor.this.server.logChatMessage(toConsoleMessage.serverContent(), chatType, ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage, ChatProcessor.this.player.asChatSender()) ? null : "Not Secure");
++ }
++
++ record ConstantFunction(net.minecraft.network.chat.Component component) implements Function {
++ @Override
++ public net.minecraft.network.chat.Component apply(Audience audience) {
++ return this.component;
+ }
+ }
+ }
@@ -259,19 +581,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return viewers;
+ }
+
-+ private static String legacyDisplayName(final CraftPlayer player) {
++ static String legacyDisplayName(final CraftPlayer player) {
+ return player.getDisplayName();
+ }
+
-+ private static Component displayName(final CraftPlayer player) {
++ static Component displayName(final CraftPlayer player) {
+ return player.displayName();
+ }
+
++ private static ChatRenderer.Default defaultRenderer() {
++ return (ChatRenderer.Default) ChatRenderer.defaultRenderer();
++ }
++
+ private static ChatRenderer legacyRenderer(final String format) {
+ if (DEFAULT_LEGACY_FORMAT.equals(format)) {
-+ return ChatRenderer.defaultRenderer();
++ return defaultRenderer();
+ }
-+ return ChatRenderer.viewerUnaware((player, displayName, message) -> LegacyComponentSerializer.legacySection().deserialize(String.format(format, legacyDisplayName((CraftPlayer) player), LegacyComponentSerializer.legacySection().serialize(message))).replaceText(URL_REPLACEMENT_CONFIG));
++ return ChatRenderer.viewerUnaware((player, sourceDisplayName, message) -> legacySection().deserialize(legacyFormat(format, player, legacySection().serialize(message))));
++ }
++
++ static String legacyFormat(final String format, Player player, String message) {
++ return String.format(format, legacyDisplayName((CraftPlayer) player), message);
+ }
+
+ private void queueIfAsyncOrRunImmediately(final Waitable waitable) {
@@ -293,7 +623,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ this.server.server.getPluginManager().callEvent(event);
+ }
+
-+ private static boolean canYouHearMe(final HandlerList handlers) {
++ static boolean canYouHearMe(final HandlerList handlers) {
+ return handlers.getRegisteredListeners().length > 0;
+ }
+}
@@ -384,8 +714,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+import net.kyori.adventure.text.TranslatableComponent;
+import net.kyori.adventure.text.flattener.ComponentFlattener;
+import net.kyori.adventure.text.format.TextColor;
++import net.kyori.adventure.text.serializer.ComponentSerializer;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
-+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import net.kyori.adventure.translation.GlobalTranslator;
@@ -474,7 +804,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return decoded.toString();
+ }
+ };
-+ static final WrapperAwareSerializer WRAPPER_AWARE_SERIALIZER = new WrapperAwareSerializer();
++ public static final ComponentSerializer WRAPPER_AWARE_SERIALIZER = new WrapperAwareSerializer();
+
+ private PaperAdventure() {
+ }
@@ -1090,6 +1420,28 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@Nullable
public static ChatFormatting getById(int colorIndex) {
if (colorIndex < 0) {
+diff --git a/src/main/java/net/minecraft/commands/arguments/MessageArgument.java b/src/main/java/net/minecraft/commands/arguments/MessageArgument.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/commands/arguments/MessageArgument.java
++++ b/src/main/java/net/minecraft/commands/arguments/MessageArgument.java
+@@ -0,0 +0,0 @@ public class MessageArgument implements SignedArgument
+ MinecraftServer minecraftServer = source.getServer();
+ source.getChatMessageChainer().append(() -> {
+ CompletableFuture completableFuture = this.filterPlainText(source, this.signedArgument.signedContent().plain());
+- CompletableFuture completableFuture2 = minecraftServer.getChatDecorator().decorate(source.getPlayer(), this.signedArgument);
++ CompletableFuture completableFuture2 = minecraftServer.getChatDecorator().decorate(source.getPlayer(), source,this.signedArgument); // Paper
+ return CompletableFuture.allOf(completableFuture, completableFuture2).thenAcceptAsync((void_) -> {
+ PlayerChatMessage playerChatMessage = completableFuture2.join().filter(completableFuture.join().mask());
+ callback.accept(playerChatMessage);
+@@ -0,0 +0,0 @@ public class MessageArgument implements SignedArgument
+
+ CompletableFuture resolveDecoratedComponent(CommandSourceStack source) throws CommandSyntaxException {
+ Component component = this.resolveComponent(source);
+- CompletableFuture completableFuture = source.getServer().getChatDecorator().decorate(source.getPlayer(), component);
++ CompletableFuture completableFuture = source.getServer().getChatDecorator().decorate(source.getPlayer(), source, component, true).thenApply(net.minecraft.network.chat.ChatDecorator.Result::component); // Paper
+ MessageArgument.logResolutionFailure(source, completableFuture);
+ return completableFuture;
+ }
diff --git a/src/main/java/net/minecraft/network/FriendlyByteBuf.java b/src/main/java/net/minecraft/network/FriendlyByteBuf.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/FriendlyByteBuf.java
@@ -1147,6 +1499,168 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
try {
int i = friendlyByteBuf.writerIndex();
+diff --git a/src/main/java/net/minecraft/network/chat/ChatDecorator.java b/src/main/java/net/minecraft/network/chat/ChatDecorator.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/network/chat/ChatDecorator.java
++++ b/src/main/java/net/minecraft/network/chat/ChatDecorator.java
+@@ -0,0 +0,0 @@ public interface ChatDecorator {
+ return CompletableFuture.completedFuture(message);
+ };
+
++ @io.papermc.paper.annotation.DoNotUse // Paper
+ CompletableFuture decorate(@Nullable ServerPlayer sender, Component message);
+
++ // Paper start
++ default CompletableFuture decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message, boolean isPreview) {
++ 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, true).thenApply(Result::component);
++ }
++
++ @Override
++ public CompletableFuture decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message, boolean isPreview) {
++ return delegate.decorate(sender, commandSourceStack, message, isPreview);
++ }
++ };
++ }
++
++ @FunctionalInterface
++ interface ImprovedChatDecorator {
++ CompletableFuture decorate(@Nullable ServerPlayer sender, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, Component message, boolean isPreview);
++ }
++
++ 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 maybeAdventureComponent, 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 Component component() {
++ return this.maybeAdventureComponent instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent ? adventureComponent.deepConverted() : this.maybeAdventureComponent;
++ }
++
++ @Override
++ public MessagePair message() {
++ final net.kyori.adventure.text.Component adventureComponent = io.papermc.paper.adventure.PaperAdventure.WRAPPER_AWARE_SERIALIZER.deserialize(this.maybeAdventureComponent);
++ return new MessagePair(adventureComponent, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(adventureComponent));
++ }
++ }
++ default CompletableFuture decorate(@Nullable ServerPlayer serverPlayer, @Nullable net.minecraft.commands.CommandSourceStack commandSourceStack, PlayerChatMessage playerChatMessage) {
++ return playerChatMessage.signedContent().isDecorated() ? CompletableFuture.completedFuture(playerChatMessage) : this.decorate(serverPlayer, commandSourceStack, playerChatMessage.serverContent(), false).thenApply(result -> {
++ return new PlayerChatMessage(playerChatMessage.signedHeader(), playerChatMessage.headerSignature(), playerChatMessage.signedBody().withContent(playerChatMessage.signedContent().withDecorationResult(result)), playerChatMessage.unsignedContent(), playerChatMessage.filterMask()).withUnsignedContent(result.component());
++ });
++ }
++
++ // Paper end
++
++ @io.papermc.paper.annotation.DoNotUse // Paper
+ default CompletableFuture decorate(@Nullable ServerPlayer serverPlayer, PlayerChatMessage playerChatMessage) {
+- return playerChatMessage.signedContent().isDecorated() ? CompletableFuture.completedFuture(playerChatMessage) : this.decorate(serverPlayer, playerChatMessage.serverContent()).thenApply(playerChatMessage::withUnsignedContent);
++ return this.decorate(serverPlayer, null, playerChatMessage); // Paper
+ }
+
+ static PlayerChatMessage attachIfNotDecorated(PlayerChatMessage playerChatMessage, Component component) {
+diff --git a/src/main/java/net/minecraft/network/chat/ChatMessageContent.java b/src/main/java/net/minecraft/network/chat/ChatMessageContent.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/network/chat/ChatMessageContent.java
++++ b/src/main/java/net/minecraft/network/chat/ChatMessageContent.java
+@@ -0,0 +0,0 @@ package net.minecraft.network.chat;
+ import java.util.Objects;
+ import net.minecraft.network.FriendlyByteBuf;
+
+-public record ChatMessageContent(String plain, Component decorated) {
++// Paper start
++public record ChatMessageContent(String plain, Component decorated, ChatDecorator.Result decorationResult) {
++
++ public ChatMessageContent(String plain, Component decorated) {
++ this(plain, decorated, new ChatDecorator.ModernResult(decorated, true, false));
++ }
++
++ public ChatMessageContent withDecorationResult(ChatDecorator.Result result) {
++ return new ChatMessageContent(this.plain, this.decorated, result);
++ }
++ // Paper end
+ public ChatMessageContent(String content) {
+ this(content, Component.literal(content));
+ }
+diff --git a/src/main/java/net/minecraft/network/chat/ChatPreviewCache.java b/src/main/java/net/minecraft/network/chat/ChatPreviewCache.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/network/chat/ChatPreviewCache.java
++++ b/src/main/java/net/minecraft/network/chat/ChatPreviewCache.java
+@@ -0,0 +0,0 @@
+ package net.minecraft.network.chat;
+
+ import javax.annotation.Nullable;
++import net.minecraft.Util;
+
+ public class ChatPreviewCache {
+ @Nullable
+ private ChatPreviewCache.Result result;
+
+ public void set(String query, Component preview) {
+- this.result = new ChatPreviewCache.Result(query, preview);
++ // Paper start
++ this.set(query, new ChatDecorator.ModernResult(java.util.Objects.requireNonNull(preview), true, false));
++ }
++ public void set(String query, ChatDecorator.Result decoratorResult) {
++ this.result = new ChatPreviewCache.Result(query, java.util.Objects.requireNonNull(decoratorResult));
++ // Paper end
+ }
+
+ @Nullable
+ public Component pull(String query) {
++ // Paper start
++ return net.minecraft.Util.mapNullable(this.pullFull(query), Result::preview);
++ }
++ public @Nullable Result pullFull(String query) {
++ // Paper end
+ ChatPreviewCache.Result result = this.result;
+ if (result != null && result.matches(query)) {
+ this.result = null;
+- return result.preview();
++ return result; // Paper
+ } else {
+ return null;
+ }
+ }
+
+- static record Result(String query, Component preview) {
++ // Paper start
++ public record Result(String query, ChatDecorator.Result decoratorResult) {
++
++ public Component preview() {
++ return this.decoratorResult.component();
++ }
++ // Paper end
+ public boolean matches(String query) {
+ return this.query.equals(query);
+ }
diff --git a/src/main/java/net/minecraft/network/chat/Component.java b/src/main/java/net/minecraft/network/chat/Component.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/chat/Component.java
@@ -1175,6 +1689,54 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
JsonObject jsonobject = new JsonObject();
if (!ichatbasecomponent.getStyle().isEmpty()) {
+diff --git a/src/main/java/net/minecraft/network/chat/OutgoingPlayerChatMessage.java b/src/main/java/net/minecraft/network/chat/OutgoingPlayerChatMessage.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/network/chat/OutgoingPlayerChatMessage.java
++++ b/src/main/java/net/minecraft/network/chat/OutgoingPlayerChatMessage.java
+@@ -0,0 +0,0 @@ public interface OutgoingPlayerChatMessage {
+ Component serverContent();
+
+ void sendToPlayer(ServerPlayer serverPlayer, boolean bl, ChatType.Bound bound);
++ // Paper start
++ default void sendToPlayer(ServerPlayer serverPlayer, boolean shouldFilter, ChatType.Bound bound, @javax.annotation.Nullable Component unsigned) {
++ this.sendToPlayer(serverPlayer, shouldFilter, bound);
++ }
++ // Paper end
+
+ void sendHeadersToRemainingPlayers(PlayerList playerManager);
+
+@@ -0,0 +0,0 @@ public interface OutgoingPlayerChatMessage {
+
+ @Override
+ public void sendToPlayer(ServerPlayer serverPlayer, boolean bl, ChatType.Bound bound) {
++ // Paper start
++ this.sendToPlayer(serverPlayer, bl, bound, null);
++ }
++
++ @Override
++ public void sendToPlayer(ServerPlayer serverPlayer, boolean bl, ChatType.Bound bound, @javax.annotation.Nullable Component unsigned) {
++ // Paper end
+ PlayerChatMessage playerChatMessage = this.message.filter(bl);
++ playerChatMessage = unsigned != null ? playerChatMessage.withUnsignedContent(unsigned) : playerChatMessage; // Paper
+ if (!playerChatMessage.isFullyFiltered()) {
+ RegistryAccess registryAccess = serverPlayer.level.registryAccess();
+ ChatType.BoundNetwork boundNetwork = bound.toNetwork(registryAccess);
+@@ -0,0 +0,0 @@ public interface OutgoingPlayerChatMessage {
+
+ @Override
+ public void sendToPlayer(ServerPlayer serverPlayer, boolean bl, ChatType.Bound bound) {
++ // Paper start
++ this.sendToPlayer(serverPlayer, bl, bound, null);
++ }
++
++ @Override
++ public void sendToPlayer(ServerPlayer serverPlayer, boolean bl, ChatType.Bound bound, @javax.annotation.Nullable Component unsigned) {
++ // Paper end
+ PlayerChatMessage playerChatMessage = this.message.filter(bl);
++ playerChatMessage = unsigned != null ? playerChatMessage.withUnsignedContent(unsigned) : playerChatMessage; // Paper
+ if (!playerChatMessage.isFullyFiltered()) {
+ this.playersWithFullMessage.add(serverPlayer);
+ RegistryAccess registryAccess = serverPlayer.level.registryAccess();
diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetActionBarTextPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetActionBarTextPacket.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetActionBarTextPacket.java
@@ -1356,6 +1918,41 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
}
public boolean previewsChat() {
+@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {
+- // SPIGOT-7127: Console /say and similar
+- if (entityplayer == null) {
+- return CompletableFuture.completedFuture(ichatbasecomponent);
+- }
+-
+- return CompletableFuture.supplyAsync(() -> {
+- AsyncPlayerChatPreviewEvent event = new AsyncPlayerChatPreviewEvent(true, entityplayer.getBukkitEntity(), CraftChatMessage.fromComponent(ichatbasecomponent), new LazyPlayerSet(this));
+- String originalFormat = event.getFormat(), originalMessage = event.getMessage();
+- this.server.getPluginManager().callEvent(event);
+-
+- if (originalFormat.equals(event.getFormat()) && originalMessage.equals(event.getMessage()) && event.getPlayer().getName().equalsIgnoreCase(event.getPlayer().getDisplayName())) {
+- return ichatbasecomponent;
+- }
+-
+- return CraftChatMessage.fromStringOrNull(String.format(event.getFormat(), event.getPlayer().getDisplayName(), event.getMessage()));
+- }, chatExecutor);
+- };
++ // Paper start - moved to ChatPreviewProcessor
++ return ChatDecorator.create((sender, commandSourceStack, message, isPreview) -> {
++ final io.papermc.paper.adventure.ChatDecorationProcessor processor = new io.papermc.paper.adventure.ChatDecorationProcessor(this, sender, commandSourceStack, message, isPreview);
++ return processor.process();
++ });
++ // Paper end
+ // CraftBukkit end
+ }
+
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -1414,6 +2011,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@@ -0,0 +0,0 @@ public class ServerPlayer extends Player {
}
+ public void sendChatMessage(OutgoingPlayerChatMessage message, boolean flag, ChatType.Bound chatmessagetype_a) {
++ // Paper start
++ this.sendChatMessage(message, flag, chatmessagetype_a, null);
++ }
++ public void sendChatMessage(OutgoingPlayerChatMessage message, boolean flag, ChatType.Bound chatmessagetype_a, @Nullable Component unsigned) {
++ // Paper end
+ if (this.acceptsChatMessages()) {
+- message.sendToPlayer(this, flag, chatmessagetype_a);
++ message.sendToPlayer(this, flag, chatmessagetype_a, unsigned); // Paper
+ }
+
+ }
+@@ -0,0 +0,0 @@ public class ServerPlayer extends Player {
+ }
+
public String locale = "en_us"; // CraftBukkit - add, lowercase
+ public java.util.Locale adventure$locale = java.util.Locale.US; // Paper
public void updateOptions(ServerboundClientInformationPacket packet) {
@@ -1512,6 +2124,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
}
// CraftBukkit end
this.player.getTextFilter().leave();
+@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
+ if (this.verifyChatMessage(playerchatmessage)) {
+ this.chatMessageChain.append(() -> {
+ CompletableFuture completablefuture = this.filterTextPacket(playerchatmessage.signedContent().plain());
+- CompletableFuture completablefuture1 = this.server.getChatDecorator().decorate(this.player, playerchatmessage);
++ CompletableFuture completablefuture1 = this.server.getChatDecorator().decorate(this.player, null, playerchatmessage); // Paper
+
+ return CompletableFuture.allOf(completablefuture, completablefuture1).thenAcceptAsync((ovoid) -> {
+ FilterMask filtermask = ((FilteredText) completablefuture.join()).mask();
@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
this.handleCommand(s);
} else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) {
@@ -1519,13 +2140,59 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
- } else {
+ // Paper start
+ } else if (true) {
-+ final ChatProcessor cp = new ChatProcessor(this.server, this.player, s, async);
++ final ChatProcessor cp = new ChatProcessor(this.server, this.player, original, async);
+ cp.process();
+ // Paper end
+ } else if (false) { // Paper
Player player = this.getCraftPlayer();
AsyncPlayerChatEvent event = new AsyncPlayerChatEvent(async, player, s, new LazyPlayerSet(this.server));
String originalFormat = event.getFormat(), originalMessage = event.getMessage();
+@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
+ }
+
+ private ChatMessageContent getSignedContent(ServerboundChatPacket packet) {
+- Component ichatbasecomponent = this.chatPreviewCache.pull(packet.message());
++ // Paper start
++ final net.minecraft.network.chat.ChatPreviewCache.Result result = this.chatPreviewCache.pullFull(packet.message());
++ Component ichatbasecomponent = result != null ? result.preview() : null;
++ // Paper end
+
+- return packet.signedPreview() && ichatbasecomponent != null ? new ChatMessageContent(packet.message(), ichatbasecomponent) : new ChatMessageContent(packet.message());
++ return packet.signedPreview() && ichatbasecomponent != null ? new ChatMessageContent(packet.message(), ichatbasecomponent, result.decoratorResult()) : new ChatMessageContent(packet.message()); // Paper end
+ }
+
+ private void broadcastChatMessage(PlayerChatMessage playerchatmessage) {
+@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
+
+ private CompletableFuture queryChatPreview(String query) {
+ MutableComponent ichatmutablecomponent = Component.literal(query);
+- CompletableFuture completablefuture = this.server.getChatDecorator().decorate(this.player, (Component) ichatmutablecomponent).thenApply((ichatbasecomponent) -> {
+- return !ichatmutablecomponent.equals(ichatbasecomponent) ? ichatbasecomponent : null;
++ // Paper start
++ final CompletableFuture result = this.server.getChatDecorator().decorate(this.player, null, ichatmutablecomponent, true);
++ CompletableFuture completablefuture = result.thenApply((res) -> {
++ return !ichatmutablecomponent.equals(res.component()) ? res : null;
++ // Paper end
+ });
+
+ completablefuture.thenAcceptAsync((ichatbasecomponent) -> {
+- this.chatPreviewCache.set(query, ichatbasecomponent);
++ if (ichatbasecomponent != null) this.chatPreviewCache.set(query, ichatbasecomponent); // Paper
+ }, this.server);
+- return completablefuture;
++ return completablefuture.thenApply(net.minecraft.network.chat.ChatDecorator.Result::component); // paper
+ }
+
+ private CompletableFuture queryCommandPreview(String query) {
+@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
+ CompletableFuture completablefuture = this.getPreviewedArgument(commandlistenerwrapper, PreviewableCommand.of(parseresults));
+
+ completablefuture.thenAcceptAsync((ichatbasecomponent) -> {
+- this.chatPreviewCache.set(query, ichatbasecomponent);
++ if (ichatbasecomponent != null) this.chatPreviewCache.set(query, ichatbasecomponent); // Paper
+ }, this.server);
+ return completablefuture;
+ }
@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
return;
}
@@ -1721,6 +2388,52 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
}
// CraftBukkit end
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+ }
+
+ public void broadcastChatMessage(PlayerChatMessage playerchatmessage, ServerPlayer sender, ChatType.Bound params) {
++ // Paper start
++ this.broadcastChatMessage(playerchatmessage, sender, params, null);
++ }
++ public void broadcastChatMessage(PlayerChatMessage playerchatmessage, ServerPlayer sender, ChatType.Bound params, @Nullable Function unsignedFunction) {
++ // Paper end
+ Objects.requireNonNull(sender);
+- this.broadcastChatMessage(playerchatmessage, sender::shouldFilterMessageTo, sender, sender.asChatSender(), params);
++ this.broadcastChatMessage(playerchatmessage, sender::shouldFilterMessageTo, sender, sender.asChatSender(), params, unsignedFunction); // Paper
+ }
+
+ private void broadcastChatMessage(PlayerChatMessage playerchatmessage, Predicate shouldSendFiltered, @Nullable ServerPlayer entityplayer, ChatSender chatsender, ChatType.Bound chatmessagetype_a) {
++ // Paper start
++ this.broadcastChatMessage(playerchatmessage, shouldSendFiltered, entityplayer, chatsender, chatmessagetype_a, null);
++ }
++
++ private void broadcastChatMessage(PlayerChatMessage playerchatmessage, Predicate shouldSendFiltered, @Nullable ServerPlayer entityplayer, ChatSender chatsender, ChatType.Bound chatmessagetype_a, @Nullable Function unsignedFunction) {
++ // Paper end
+ boolean flag = this.verifyChatTrusted(playerchatmessage, chatsender);
+
+- this.server.logChatMessage(playerchatmessage.serverContent(), chatmessagetype_a, flag ? null : "Not Secure");
++ this.server.logChatMessage((unsignedFunction == null ? playerchatmessage : playerchatmessage.withUnsignedContent(unsignedFunction.apply(this.server.console))).serverContent(), chatmessagetype_a, flag ? null : "Not Secure"); // Paper
+ OutgoingPlayerChatMessage outgoingplayerchatmessage = OutgoingPlayerChatMessage.create(playerchatmessage);
+ boolean flag1 = playerchatmessage.isFullyFiltered();
+ boolean flag2 = false;
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+ ServerPlayer entityplayer1 = (ServerPlayer) iterator.next();
+ boolean flag3 = shouldSendFiltered.test(entityplayer1);
+
+- entityplayer1.sendChatMessage(outgoingplayerchatmessage, flag3, chatmessagetype_a);
++ entityplayer1.sendChatMessage(outgoingplayerchatmessage, flag3, chatmessagetype_a, unsignedFunction == null ? null : unsignedFunction.apply(entityplayer1.getBukkitEntity()));
+ if (entityplayer != entityplayer1) {
+ flag2 |= flag1 && flag3;
+ }
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+
+ }
+
+- private boolean verifyChatTrusted(PlayerChatMessage message, ChatSender profile) {
++ public boolean verifyChatTrusted(PlayerChatMessage message, ChatSender profile) { // Paper - private -> public
+ return !message.hasExpiredServer(Instant.now()) && message.verify(profile);
+ }
+
diff --git a/src/main/java/net/minecraft/world/BossEvent.java b/src/main/java/net/minecraft/world/BossEvent.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/world/BossEvent.java
diff --git a/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch b/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch
index 02bd3cd8f0..289e89a58b 100644
--- a/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch
+++ b/patches/server/Deobfuscate-stacktraces-in-log-messages-crash-report.patch
@@ -522,19 +522,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
});
private final PacketFlow receiving;
private final Queue queue = Queues.newConcurrentLinkedQueue();
-diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
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 882e35ba34..d8bc643f24 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,11 +15,13 @@ 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.text.event.ClickEvent;
- import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+@@ -0,0 +0,0 @@ import net.minecraft.network.chat.PlayerChatMessage;
+ import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.ChatColor;
+ import org.bukkit.command.CommandSender;
+ import org.bukkit.command.ConsoleCommandSender;
+import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.util.LazyPlayerSet;
@@ -27,14 +29,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@@ -0,0 +0,0 @@ public final class ChatProcessor {
}
- private static String legacyDisplayName(final CraftPlayer player) {
+ static String legacyDisplayName(final CraftPlayer player) {
+ if (((org.bukkit.craftbukkit.CraftWorld) player.getWorld()).getHandle().paperConfig().scoreboards.useVanillaWorldScoreboardNameColoring) {
-+ return LegacyComponentSerializer.legacySection().serialize(player.teamDisplayName()) + ChatColor.RESET;
++ return legacySection().serialize(player.teamDisplayName()) + ChatColor.RESET;
+ }
return player.getDisplayName();
}
- private static Component displayName(final CraftPlayer player) {
+ static Component displayName(final CraftPlayer player) {
+ if (((CraftWorld) player.getWorld()).getHandle().paperConfig().scoreboards.useVanillaWorldScoreboardNameColoring) {
+ return player.teamDisplayName();
+ }