From de5d8a76fee2c8b0f5c98fece077b17397d5680c Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Fri, 21 May 2021 15:55:54 -0700
Subject: [PATCH] Enhance (Async)ChatEvent with per-viewer rendering API
 (#5684)

---
 Spigot-API-Patches/Adventure.patch            | 269 +++++++++++++++++-
 Spigot-Server-Patches/Adventure.patch         | 115 ++++++--
 ...nilla-per-world-scoreboard-coloring-.patch |   2 +-
 3 files changed, 346 insertions(+), 40 deletions(-)

diff --git a/Spigot-API-Patches/Adventure.patch b/Spigot-API-Patches/Adventure.patch
index f9a91368ac..5c4dc1b51b 100644
--- a/Spigot-API-Patches/Adventure.patch
+++ b/Spigot-API-Patches/Adventure.patch
@@ -116,7 +116,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +/**
 + * A chat composer is responsible for composing chat messages sent by {@link Player}s to the server.
++ *
++ * @deprecated for removal with 1.17, in favor of {@link ChatRenderer}
 + */
++@Deprecated
 +@FunctionalInterface
 +public interface ChatComposer {
 +    ChatComposer DEFAULT = (player, displayName, message) -> Component.translatable("chat.type.text", displayName, message);
@@ -128,7 +131,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @param displayName the display name of the {@link Player} sending the message
 +     * @param message the chat message
 +     * @return a composed chat message
++     * @deprecated for removal with 1.17
 +     */
++    @Deprecated
 +    @NotNull
 +    Component composeChat(final @NotNull Player source, final @NotNull Component displayName, final @NotNull Component message);
 +}
@@ -147,7 +152,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +/**
 + * A chat formatter is responsible for the formatting of chat messages sent by {@link Player}s to the server.
 + *
-+ * @deprecated in favour of {@link ChatComposer}
++ * @deprecated for removal with 1.17, in favour of {@link ChatRenderer}
 + */
 +@Deprecated
 +@FunctionalInterface
@@ -161,11 +166,85 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @param displayName the display name of the {@link Player} sending the message
 +     * @param message the chat message
 +     * @return a formatted chat message
++     * @deprecated for removal with 1.17
 +     */
 +    @Deprecated
 +    @NotNull
 +    Component chat(final @NotNull Component displayName, final @NotNull Component message);
 +}
+diff --git a/src/main/java/io/papermc/paper/chat/ChatRenderer.java b/src/main/java/io/papermc/paper/chat/ChatRenderer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/chat/ChatRenderer.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;
++
++/**
++ * A chat renderer is responsible for rendering chat messages sent by {@link Player}s to the server.
++ */
++@FunctionalInterface
++public interface ChatRenderer {
++    ChatRenderer DEFAULT = viewerUnaware((source, sourceDisplayName, message) -> Component.translatable("chat.type.text", sourceDisplayName, message));
++
++    /**
++     * Renders a chat message. This will be called once for each receiving {@link Audience}.
++     *
++     * @param source the message source
++     * @param sourceDisplayName the display name of the source player
++     * @param message the chat message
++     * @param viewer the receiving {@link Audience}
++     * @return a rendered chat message
++     */
++    @NotNull
++    Component render(@NotNull Player source, @NotNull Component sourceDisplayName, @NotNull Component message, @NotNull Audience viewer);
++
++    /**
++     * Creates a new viewer-unaware {@link ChatRenderer}, which will render the chat message a single time,
++     * displaying the same rendered message to every viewing {@link Audience}.
++     *
++     * @param renderer the viewer unaware renderer
++     * @return a new {@link ChatRenderer}
++     */
++    @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;
++            }
++        };
++    }
++
++    /**
++     * Similar to {@link ChatRenderer}, but without knowledge of the message viewer.
++     *
++     * @see ChatRenderer#viewerUnaware(ViewerUnaware)
++     */
++    interface ViewerUnaware {
++        /**
++         * Renders a chat message.
++         *
++         * @param source the message source
++         * @param sourceDisplayName the display name of the source player
++         * @param message the chat message
++         * @return a rendered chat message
++         */
++        @NotNull
++        Component render(@NotNull Player source, @NotNull Component sourceDisplayName, @NotNull Component message);
++    }
++}
 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
@@ -176,11 +255,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +import io.papermc.paper.chat.ChatComposer;
 +import io.papermc.paper.chat.ChatFormatter;
++import java.util.HashSet;
 +import java.util.Set;
++import io.papermc.paper.chat.ChatRenderer;
++import net.kyori.adventure.audience.Audience;
++import net.kyori.adventure.audience.ForwardingAudience;
 +import net.kyori.adventure.text.Component;
++import org.bukkit.Bukkit;
 +import org.bukkit.entity.Player;
 +import org.bukkit.event.Cancellable;
 +import org.bukkit.event.player.PlayerEvent;
++import org.checkerframework.checker.nullness.qual.NonNull;
 +import org.checkerframework.checker.nullness.qual.Nullable;
 +import org.jetbrains.annotations.NotNull;
 +
@@ -190,25 +275,83 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 + * An abstract implementation of a chat event, handling shared logic.
 + */
 +public abstract class AbstractChatEvent extends PlayerEvent implements Cancellable {
-+    private final Set<Player> recipients;
++    private final Set<Audience> viewers;
++    @Deprecated private final Set<Player> recipients;
 +    private boolean cancelled = false;
-+    private ChatComposer composer;
++    private ChatRenderer renderer;
++    @Deprecated private @Nullable ChatComposer composer;
 +    @Deprecated private @Nullable ChatFormatter formatter;
++    private final Component originalMessage;
 +    private Component message;
 +
++    AbstractChatEvent(final boolean async, final @NotNull Player player, final @NotNull Set<Audience> viewers, final @NotNull ChatRenderer renderer, final @NotNull Component message) {
++        super(player, async);
++        this.viewers = viewers;
++        this.recipients = new HashSet<>(Bukkit.getOnlinePlayers());
++        this.renderer = renderer;
++        this.message = message;
++        this.originalMessage = message;
++    }
++
++    /**
++     * @deprecated for removal with 1.17
++     */
++    @Deprecated
++    AbstractChatEvent(final boolean async, final @NotNull Player player, final @NotNull Set<Player> recipients, final @NotNull Set<Audience> viewers, final @NotNull ChatRenderer renderer, final @NotNull Component message) {
++        super(player, async);
++        this.recipients = recipients;
++        this.viewers = viewers;
++        this.renderer = renderer;
++        this.message = message;
++        this.originalMessage = message;
++    }
++
++    /**
++     * @deprecated for removal with 1.17
++     */
++    @Deprecated
 +    AbstractChatEvent(final boolean async, final @NotNull Player player, final @NotNull Set<Player> recipients, final @NotNull ChatComposer composer, final @NotNull Component message) {
 +        super(player, async);
 +        this.recipients = recipients;
++        final Set<Audience> audiences = new HashSet<>(recipients);
++        audiences.add(Bukkit.getConsoleSender());
++        this.viewers = audiences;
 +        this.composer = composer;
 +        this.message = message;
++        this.originalMessage = message;
 +    }
 +
++    /**
++     * @deprecated for removal with 1.17
++     */
 +    @Deprecated
 +    AbstractChatEvent(final boolean async, final @NotNull Player player, final @NotNull Set<Player> recipients, final @NotNull ChatFormatter formatter, final @NotNull Component message) {
 +        super(player, async);
 +        this.recipients = recipients;
++        final Set<Audience> audiences = new HashSet<>(recipients);
++        audiences.add(Bukkit.getConsoleSender());
++        this.viewers = audiences;
 +        this.formatter = formatter;
 +        this.message = message;
++        this.originalMessage = message;
++    }
++
++    /**
++     * Gets a set of {@link Audience audiences} that this chat message will be displayed to.
++     *
++     * <p>The set returned is not guaranteed to be mutable and may auto-populate
++     * on access. Any listener accessing the returned set should be aware that
++     * it may reduce performance for a lazy set implementation.</p>
++     *
++     * <p>Listeners should be aware that modifying the list may throw {@link
++     * UnsupportedOperationException} if the event caller provides an
++     * unmodifiable set.</p>
++     *
++     * @return a set of {@link Audience audiences} who will receive the chat message
++     */
++    @NotNull
++    public final Set<Audience> viewers() {
++        return this.viewers;
 +    }
 +
 +    /**
@@ -223,22 +366,60 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * unmodifiable set.</p>
 +     *
 +     * @return a set of players who will receive the chat message
++     * @deprecated for removal with 1.17, in favor of {@link #viewers()}
 +     */
++    @Deprecated
 +    @NotNull
 +    public final Set<Player> recipients() {
 +        return this.recipients;
 +    }
 +
 +    /**
++     * Sets the chat renderer.
++     *
++     * @param renderer the chat renderer
++     * @throws NullPointerException if {@code renderer} is {@code null}
++     */
++    public final void renderer(final @NotNull ChatRenderer renderer) {
++        this.renderer = requireNonNull(renderer, "renderer");
++        this.formatter = null;
++        this.composer = null;
++    }
++
++    /**
++     * Gets the chat renderer.
++     *
++     * @return the chat renderer
++     */
++    @NotNull
++    public final ChatRenderer renderer() {
++        if(this.renderer == null) {
++            if(this.composer != null) {
++                this.renderer = ChatRenderer.viewerUnaware((source, displayName, message) -> this.composer.composeChat(source, source.displayName(), message));
++            } else {
++                requireNonNull(this.formatter, "renderer, composer, and formatter");
++                this.renderer = ChatRenderer.viewerUnaware((source, displayName, message) -> this.formatter.chat(source.displayName(), message));
++            }
++        }
++        return this.renderer;
++    }
++
++    /**
 +     * Gets the chat composer.
 +     *
 +     * @return the chat composer
++     * @deprecated for removal with 1.17, in favour of {@link #renderer()}
 +     */
++    @Deprecated
 +    @NotNull
 +    public final ChatComposer composer() {
 +        if(this.composer == null) {
-+            requireNonNull(this.formatter, "composer and formatter");
-+            this.composer = (source, displayName, message) -> this.formatter.chat(displayName, message);
++            if(this.renderer != null) {
++                this.composer = (source, displayName, message) -> this.renderer.render(source, displayName, message, this.legacyForwardingAudience());
++            } else {
++                requireNonNull(this.formatter, "renderer, composer, and formatter");
++                this.composer = (source, displayName, message) -> this.formatter.chat(displayName, message);
++            }
 +        }
 +        return this.composer;
 +    }
@@ -248,23 +429,31 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     *
 +     * @param composer the chat composer
 +     * @throws NullPointerException if {@code composer} is {@code null}
++     * @deprecated for removal with 1.17, in favour of {@link #renderer(ChatRenderer)}
 +     */
++    @Deprecated
 +    public final void composer(final @NotNull ChatComposer composer) {
 +        this.composer = requireNonNull(composer, "composer");
 +        this.formatter = null;
++        this.renderer = null;
 +    }
 +
 +    /**
 +     * Gets the chat formatter.
 +     *
 +     * @return the chat formatter
-+     * @deprecated in favour of {@link #composer()}
++     * @deprecated for removal with 1.17, in favour of {@link #renderer()}
 +     */
 +    @Deprecated
 +    @NotNull
 +    public final ChatFormatter formatter() {
 +        if(this.formatter == null) {
-+            this.formatter = (displayName, message) -> this.composer.composeChat(this.player, displayName, message);
++            if(this.renderer != null) {
++                this.formatter = (displayName, message) -> this.renderer.render(this.player, displayName, message, this.legacyForwardingAudience());
++            } else {
++                requireNonNull(this.composer, "renderer, composer, and formatter");
++                this.formatter = (displayName, message) -> this.composer.composeChat(this.player, displayName, message);
++            }
 +        }
 +        return this.formatter;
 +    }
@@ -274,16 +463,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     *
 +     * @param formatter the chat formatter
 +     * @throws NullPointerException if {@code formatter} is {@code null}
-+     * @deprecated in favour of {@link #composer(ChatComposer)}
++     * @deprecated for removal with 1.17, in favour of {@link #renderer(ChatRenderer)}
 +     */
 +    @Deprecated
 +    public final void formatter(final @NotNull ChatFormatter formatter) {
 +        this.formatter = requireNonNull(formatter, "formatter");
-+        this.composer = (source, displayName, message) -> formatter.chat(displayName, message);
++        this.composer = null;
++        this.renderer = null;
 +    }
 +
 +    /**
 +     * Gets the user-supplied message.
++     * The return value will reflect changes made using {@link #message(Component)}.
 +     *
 +     * @return the user-supplied message
 +     */
@@ -302,6 +493,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.message = requireNonNull(message, "message");
 +    }
 +
++    /**
++     * Gets the original and unmodified user-supplied message.
++     * The return value will <b>not</b> reflect changes made using
++     * {@link #message(Component)}.
++     *
++     * @return the original user-supplied message
++     */
++    @NotNull
++    public final Component originalMessage() {
++        return this.originalMessage;
++    }
++
 +    @Override
 +    public final boolean isCancelled() {
 +        return this.cancelled;
@@ -311,6 +514,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public final void setCancelled(final boolean cancelled) {
 +        this.cancelled = cancelled;
 +    }
++
++    private @NotNull Audience legacyForwardingAudience() {
++        return new ForwardingAudience() {
++            @Override
++            public @NonNull Iterable<? extends Audience> audiences() {
++                return AbstractChatEvent.this.viewers;
++            }
++        };
++    }
 +}
 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
@@ -323,6 +535,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import io.papermc.paper.chat.ChatComposer;
 +import io.papermc.paper.chat.ChatFormatter;
 +import java.util.Set;
++import io.papermc.paper.chat.ChatRenderer;
++import net.kyori.adventure.audience.Audience;
 +import net.kyori.adventure.text.Component;
 +import org.bukkit.entity.Player;
 +import org.bukkit.event.HandlerList;
@@ -334,12 +548,28 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +public final class AsyncChatEvent extends AbstractChatEvent {
 +    private static final HandlerList HANDLERS = new HandlerList();
 +
++    public AsyncChatEvent(final boolean async, final @NotNull Player player, final @NotNull Set<Audience> viewers, final @NotNull ChatRenderer renderer, final @NotNull Component message) {
++        super(async, player, viewers, renderer, message);
++    }
++
++    /**
++     * @deprecated for removal with 1.17, use {@link #AsyncChatEvent(boolean, Player, Set, ChatRenderer, Component)}
++     */
++    @Deprecated
++    public AsyncChatEvent(final boolean async, final @NotNull Player player, final @NotNull Set<Player> recipients, final @NotNull Set<Audience> viewers, final @NotNull ChatRenderer renderer, final @NotNull Component message) {
++        super(async, player, recipients, viewers, renderer, message);
++    }
++
++    /**
++     * @deprecated for removal with 1.17, use {@link #AsyncChatEvent(boolean, Player, Set, ChatRenderer, Component)}
++     */
++    @Deprecated
 +    public AsyncChatEvent(final boolean async, final @NotNull Player player, final @NotNull Set<Player> recipients, final @NotNull ChatComposer composer, final @NotNull Component message) {
 +        super(async, player, recipients, composer, message);
 +    }
 +
 +    /**
-+     * @deprecated use {@link #AsyncChatEvent(boolean, Player, Set, ChatComposer, Component)}
++     * @deprecated for removal with 1.17, use {@link #AsyncChatEvent(boolean, Player, Set, ChatRenderer, Component)}
 +     */
 +    @Deprecated
 +    public AsyncChatEvent(final boolean async, final @NotNull Player player, final @NotNull Set<Player> recipients, final @NotNull ChatFormatter formatter, final @NotNull Component message) {
@@ -368,6 +598,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import io.papermc.paper.chat.ChatComposer;
 +import io.papermc.paper.chat.ChatFormatter;
 +import java.util.Set;
++import io.papermc.paper.chat.ChatRenderer;
++import net.kyori.adventure.audience.Audience;
 +import net.kyori.adventure.text.Component;
 +import org.bukkit.Warning;
 +import org.bukkit.entity.Player;
@@ -384,12 +616,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +public final class ChatEvent extends AbstractChatEvent {
 +    private static final HandlerList HANDLERS = new HandlerList();
 +
++    public ChatEvent(final @NotNull Player player, final @NotNull Set<Audience> viewers, final @NotNull ChatRenderer renderer, final @NotNull Component message) {
++        super(false, player, viewers, renderer, message);
++    }
++
++    /**
++     * @deprecated for removal with 1.17, use {@link #ChatEvent(Player, Set, ChatRenderer, Component)}
++     */
++    public ChatEvent(final @NotNull Player player, final @NotNull Set<Player> recipients, final @NotNull Set<Audience> viewers, final @NotNull ChatRenderer renderer, final @NotNull Component message) {
++        super(false, player, recipients, viewers, renderer, message);
++    }
++
++    /**
++     * @deprecated for removal with 1.17, use {@link #ChatEvent(Player, Set, ChatRenderer, Component)}
++     */
++    @Deprecated
 +    public ChatEvent(final @NotNull Player player, final @NotNull Set<Player> recipients, final @NotNull ChatComposer composer, final @NotNull Component message) {
 +        super(false, player, recipients, composer, message);
 +    }
 +
 +    /**
-+     * @deprecated use {@link #ChatEvent(Player, Set, ChatComposer, Component)}
++     * @deprecated for removal with 1.17, use {@link #ChatEvent(Player, Set, ChatRenderer, Component)}
 +     */
 +    @Deprecated
 +    public ChatEvent(final @NotNull Player player, final @NotNull Set<Player> recipients, final @NotNull ChatFormatter formatter, final @NotNull Component message) {
diff --git a/Spigot-Server-Patches/Adventure.patch b/Spigot-Server-Patches/Adventure.patch
index f2f279c1e1..72ab1a1017 100644
--- a/Spigot-Server-Patches/Adventure.patch
+++ b/Spigot-Server-Patches/Adventure.patch
@@ -111,7 +111,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package io.papermc.paper.adventure;
 +
-+import io.papermc.paper.chat.ChatComposer;
++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;
@@ -120,11 +120,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import java.util.function.Consumer;
 +import java.util.regex.Pattern;
 +
++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.minecraft.network.chat.ChatMessageType;
 +import net.minecraft.network.chat.IChatBaseComponent;
 +import net.minecraft.server.MinecraftServer;
 +import net.minecraft.server.level.EntityPlayer;
@@ -171,7 +171,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // continuing from AsyncPlayerChatEvent (without PlayerChatEvent)
 +            event -> {
 +                this.processModern(
-+                    legacyComposer(event.getFormat()),
++                    legacyRenderer(event.getFormat()),
 +                    event.getRecipients(),
 +                    PaperAdventure.LEGACY_SECTION_UXRC.deserialize(event.getMessage()),
 +                    event.isCancelled()
@@ -180,7 +180,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // continuing from AsyncPlayerChatEvent and PlayerChatEvent
 +            event -> {
 +                this.processModern(
-+                    legacyComposer(event.getFormat()),
++                    legacyRenderer(event.getFormat()),
 +                    event.getRecipients(),
 +                    PaperAdventure.LEGACY_SECTION_UXRC.deserialize(event.getMessage()),
 +                    event.isCancelled()
@@ -189,7 +189,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // no legacy events called, all nice and fresh!
 +            () -> {
 +                this.processModern(
-+                    ChatComposer.DEFAULT,
++                    ChatRenderer.DEFAULT,
 +                    new LazyPlayerSet(this.server),
 +                    Component.text(this.message).replaceText(URL_REPLACEMENT_CONFIG),
 +                    false
@@ -229,8 +229,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +
-+    private void processModern(final ChatComposer composer, final Set<Player> recipients, final Component message, final boolean cancelled) {
-+        final AsyncChatEvent ae = this.createAsync(composer, recipients, message);
++    private void processModern(final ChatRenderer renderer, final Set<Player> recipients, final Component message, final boolean cancelled) {
++        final AsyncChatEvent ae = this.createAsync(renderer, recipients, new LazyChatAudienceSet(), message);
 +        ae.setCancelled(cancelled); // propagate cancelled state
 +        post(ae);
 +        final boolean listenersOnSyncEvent = anyListeners(ChatEvent.getHandlerList());
@@ -245,7 +245,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.queueIfAsyncOrRunImmediately(new Waitable<Void>() {
 +            @Override
 +            protected Void evaluate() {
-+                final ChatEvent se = ChatProcessor.this.createSync(ae.composer(), ae.recipients(), ae.message());
++                final ChatEvent se = ChatProcessor.this.createSync(ae.renderer(), ae.recipients(), ae.viewers(), ae.message());
 +                se.setCancelled(ae.isCancelled()); // propagate cancelled state
 +                post(se);
 +                ChatProcessor.this.complete(se);
@@ -260,33 +260,31 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        final CraftPlayer player = this.player.getBukkitEntity();
++        final Component displayName = displayName(player);
++        final Component message = event.message();
++        final ChatRenderer renderer = event.renderer();
 +
-+        final Component message = event.composer().composeChat(
-+            event.getPlayer(),
-+            displayName(player),
-+            event.message()
-+        );
-+
-+        this.server.console.sendMessage(message);
-+
-+        if (((LazyPlayerSet) event.recipients()).isLazy()) {
-+            final IChatBaseComponent vanilla = PaperAdventure.asVanilla(message);
-+            for (final EntityPlayer recipient : this.server.getPlayerList().players) {
-+                recipient.sendMessage(vanilla, ChatMessageType.CHAT, this.player.getUniqueID());
++        final Set<Audience> viewers = event.viewers();
++        final Set<Player> recipients = event.recipients();
++        if (viewers instanceof LazyChatAudienceSet && recipients instanceof LazyPlayerSet &&
++            (!((LazyChatAudienceSet) viewers).isLazy() || ((LazyPlayerSet) recipients).isLazy())) {
++            for (final Audience viewer : viewers) {
++                viewer.sendMessage(player, renderer.render(player, displayName, message, viewer), MessageType.CHAT);
 +            }
 +        } else {
-+            for (final Player recipient : event.recipients()) {
-+                recipient.sendMessage(player, message, MessageType.CHAT);
++            this.server.console.sendMessage(player, renderer.render(player, displayName, message, this.server.console), MessageType.CHAT);
++            for (final Player recipient : recipients) {
++                recipient.sendMessage(player, renderer.render(player, displayName, message, recipient), MessageType.CHAT);
 +            }
 +        }
 +    }
 +
-+    private AsyncChatEvent createAsync(final ChatComposer composer, final Set<Player> recipients, final Component message) {
-+        return new AsyncChatEvent(this.async, this.player.getBukkitEntity(), recipients, composer, message);
++    private AsyncChatEvent createAsync(final ChatRenderer renderer, final Set<Player> recipients, final Set<Audience> viewers, final Component message) {
++        return new AsyncChatEvent(this.async, this.player.getBukkitEntity(), recipients, viewers, renderer, message);
 +    }
 +
-+    private ChatEvent createSync(final ChatComposer composer, final Set<Player> recipients, final Component message) {
-+        return new ChatEvent(this.player.getBukkitEntity(), recipients, composer, message);
++    private ChatEvent createSync(final ChatRenderer renderer, final Set<Player> recipients, final Set<Audience> viewers, final Component message) {
++        return new ChatEvent(this.player.getBukkitEntity(), recipients, viewers, renderer, message);
 +    }
 +
 +    private static String legacyDisplayName(final CraftPlayer player) {
@@ -297,8 +295,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return player.displayName();
 +    }
 +
-+    private static ChatComposer legacyComposer(final String format) {
-+        return (player, displayName, message) -> PaperAdventure.LEGACY_SECTION_UXRC.deserialize(String.format(format, legacyDisplayName((CraftPlayer) player), PaperAdventure.LEGACY_SECTION_UXRC.serialize(message))).replaceText(URL_REPLACEMENT_CONFIG);
++    private static ChatRenderer legacyRenderer(final String format) {
++        return (player, displayName, message, recipient) -> PaperAdventure.LEGACY_SECTION_UXRC.deserialize(String.format(format, legacyDisplayName((CraftPlayer) player), PaperAdventure.LEGACY_SECTION_UXRC.serialize(message))).replaceText(URL_REPLACEMENT_CONFIG);
 +    }
 +
 +    private void queueIfAsyncOrRunImmediately(final Waitable<Void> waitable) {
@@ -352,6 +350,33 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return PaperAdventure.LEGACY_SECTION_UXRC.serialize(player.adventure$displayName);
 +    }
 +}
+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
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.adventure;
++
++import net.kyori.adventure.audience.Audience;
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.Bukkit;
++import org.bukkit.craftbukkit.util.LazyHashSet;
++import org.bukkit.craftbukkit.util.LazyPlayerSet;
++import org.bukkit.entity.Player;
++
++import java.util.HashSet;
++import java.util.Set;
++
++final class LazyChatAudienceSet extends LazyHashSet<Audience> {
++    @Override
++    protected Set<Audience> makeReference() {
++        final Set<Player> playerSet = LazyPlayerSet.makePlayerSet(MinecraftServer.getServer());
++        final HashSet<Audience> audiences = new HashSet<>(playerSet);
++        audiences.add(Bukkit.getConsoleSender());
++        return audiences;
++    }
++}
 diff --git a/src/main/java/io/papermc/paper/adventure/NBTLegacyHoverEventSerializer.java b/src/main/java/io/papermc/paper/adventure/NBTLegacyHoverEventSerializer.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -3235,3 +3260,37 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public static IBlockData getBlock(MaterialData material) {
          return getBlock(material.getItemType(), material.getData());
      }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java b/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java
+@@ -0,0 +0,0 @@ public abstract class LazyHashSet<E> implements Set<E> {
+         return this.reference = makeReference();
+     }
+ 
+-    abstract Set<E> makeReference();
++    protected abstract Set<E> makeReference(); // Paper - protected
+ 
+     public boolean isLazy() {
+         return reference == null;
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java b/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
+@@ -0,0 +0,0 @@ public class LazyPlayerSet extends LazyHashSet<Player> {
+     }
+ 
+     @Override
+-    HashSet<Player> makeReference() {
++    protected HashSet<Player> makeReference() { // Paper - protected
+         if (reference != null) {
+             throw new IllegalStateException("Reference already created!");
+         }
++        // Paper start
++        return makePlayerSet(this.server);
++    }
++    public static HashSet<Player> makePlayerSet(final MinecraftServer server) {
++        // Paper end
+         List<EntityPlayer> players = server.getPlayerList().players;
+         HashSet<Player> reference = new HashSet<Player>(players.size());
+         for (EntityPlayer player : players) {
diff --git a/Spigot-Server-Patches/Option-to-use-vanilla-per-world-scoreboard-coloring-.patch b/Spigot-Server-Patches/Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
index 1f1dfcd5e3..f32e2e0813 100644
--- a/Spigot-Server-Patches/Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
+++ b/Spigot-Server-Patches/Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
@@ -29,7 +29,7 @@ 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.minecraft.network.chat.ChatMessageType;
+@@ -0,0 +0,0 @@ import net.kyori.adventure.text.event.ClickEvent;
  import net.minecraft.network.chat.IChatBaseComponent;
  import net.minecraft.server.MinecraftServer;
  import net.minecraft.server.level.EntityPlayer;