From ecb0d32cae583880ef296a3632a634660e6a550d Mon Sep 17 00:00:00 2001
From: Jason
Date: Fri, 21 May 2021 15:55:54 -0700
Subject: [PATCH] Enhance (Async)ChatEvent with per-viewer rendering API
(#5684)
---
Spigot-API-Patches/0005-Adventure.patch | 289 ++++++++++++++++--
Spigot-Server-Patches/0010-Adventure.patch | 119 ++++++--
...nilla-per-world-scoreboard-coloring-.patch | 6 +-
3 files changed, 360 insertions(+), 54 deletions(-)
diff --git a/Spigot-API-Patches/0005-Adventure.patch b/Spigot-API-Patches/0005-Adventure.patch
index 67fbf8720..35a3642f5 100644
--- a/Spigot-API-Patches/0005-Adventure.patch
+++ b/Spigot-API-Patches/0005-Adventure.patch
@@ -104,10 +104,10 @@ index ef58a6c00f444bd498a2d8fc4e457236f393954f..ecd149157d4fb80444f34bf5633d74bc
}
diff --git a/src/main/java/io/papermc/paper/chat/ChatComposer.java b/src/main/java/io/papermc/paper/chat/ChatComposer.java
new file mode 100644
-index 0000000000000000000000000000000000000000..1f03ce9ff40ed12a1825c8e24dabddbbef44d6af
+index 0000000000000000000000000000000000000000..7d2fb50210246bb42d166f7e6d1c0cdfad0bdb3d
--- /dev/null
+++ b/src/main/java/io/papermc/paper/chat/ChatComposer.java
-@@ -0,0 +1,24 @@
+@@ -0,0 +1,29 @@
+package io.papermc.paper.chat;
+
+import net.kyori.adventure.text.Component;
@@ -116,7 +116,10 @@ index 0000000000000000000000000000000000000000..1f03ce9ff40ed12a1825c8e24dabddbb
+
+/**
+ * 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,16 +131,18 @@ index 0000000000000000000000000000000000000000..1f03ce9ff40ed12a1825c8e24dabddbb
+ * @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);
+}
diff --git a/src/main/java/io/papermc/paper/chat/ChatFormatter.java b/src/main/java/io/papermc/paper/chat/ChatFormatter.java
new file mode 100644
-index 0000000000000000000000000000000000000000..ae811175089009be6b1db6941e9c5a24b2b1f027
+index 0000000000000000000000000000000000000000..ba784ab037adfd37b01b222ea33abea00161ecae
--- /dev/null
+++ b/src/main/java/io/papermc/paper/chat/ChatFormatter.java
-@@ -0,0 +1,28 @@
+@@ -0,0 +1,29 @@
+package io.papermc.paper.chat;
+
+import net.kyori.adventure.text.Component;
@@ -147,7 +152,7 @@ index 0000000000000000000000000000000000000000..ae811175089009be6b1db6941e9c5a24
+/**
+ * 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,26 +166,106 @@ index 0000000000000000000000000000000000000000..ae811175089009be6b1db6941e9c5a24
+ * @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..ec50931f02d8be3e32aa6bc8a2204a4f2e4cabb4
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/chat/ChatRenderer.java
+@@ -0,0 +1,67 @@
++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..f77597786f201b57ac18e14099f7b84f1e4e4cf3
+index 0000000000000000000000000000000000000000..dc6db537ceba0baf6e7cc532cfb58290b241e096
--- /dev/null
+++ b/src/main/java/io/papermc/paper/event/player/AbstractChatEvent.java
-@@ -0,0 +1,140 @@
+@@ -0,0 +1,273 @@
+package io.papermc.paper.event.player;
+
+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..f77597786f201b57ac18e14099f7b84f
+ * An abstract implementation of a chat event, handling shared logic.
+ */
+public abstract class AbstractChatEvent extends PlayerEvent implements Cancellable {
-+ private final Set recipients;
++ private final Set viewers;
++ @Deprecated private final Set 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 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 recipients, final @NotNull Set 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 recipients, final @NotNull ChatComposer composer, final @NotNull Component message) {
+ super(player, async);
+ this.recipients = recipients;
++ final Set 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 recipients, final @NotNull ChatFormatter formatter, final @NotNull Component message) {
+ super(player, async);
+ this.recipients = recipients;
++ final Set 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.
++ *
++ * 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.
++ *
++ * Listeners should be aware that modifying the list may throw {@link
++ * UnsupportedOperationException} if the event caller provides an
++ * unmodifiable set.
++ *
++ * @return a set of {@link Audience audiences} who will receive the chat message
++ */
++ @NotNull
++ public final Set viewers() {
++ return this.viewers;
+ }
+
+ /**
@@ -223,22 +366,60 @@ index 0000000000000000000000000000000000000000..f77597786f201b57ac18e14099f7b84f
+ * unmodifiable set.
+ *
+ * @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 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..f77597786f201b57ac18e14099f7b84f
+ *
+ * @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..f77597786f201b57ac18e14099f7b84f
+ *
+ * @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..f77597786f201b57ac18e14099f7b84f
+ this.message = requireNonNull(message, "message");
+ }
+
++ /**
++ * Gets the original and unmodified user-supplied message.
++ * The return value will not 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,18 +514,29 @@ index 0000000000000000000000000000000000000000..f77597786f201b57ac18e14099f7b84f
+ 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
-index 0000000000000000000000000000000000000000..a0f748957f4472103dd27fc95a711a42de7fae89
+index 0000000000000000000000000000000000000000..d85006591808b61518545c9f5b0b5915c19e0a9d
--- /dev/null
+++ b/src/main/java/io/papermc/paper/event/player/AsyncChatEvent.java
-@@ -0,0 +1,39 @@
+@@ -0,0 +1,57 @@
+package io.papermc.paper.event.player;
+
+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..a0f748957f4472103dd27fc95a711a42
+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 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 recipients, final @NotNull Set 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 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 recipients, final @NotNull ChatFormatter formatter, final @NotNull Component message) {
@@ -359,15 +589,17 @@ index 0000000000000000000000000000000000000000..a0f748957f4472103dd27fc95a711a42
+}
diff --git a/src/main/java/io/papermc/paper/event/player/ChatEvent.java b/src/main/java/io/papermc/paper/event/player/ChatEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..13c5df5fb8ce1d0203d99e88dd691019146a8f52
+index 0000000000000000000000000000000000000000..6219aabaf40ab89f8e08c256d1255bf5522db4d3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/event/player/ChatEvent.java
-@@ -0,0 +1,44 @@
+@@ -0,0 +1,61 @@
+package io.papermc.paper.event.player;
+
+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..13c5df5fb8ce1d0203d99e88dd691019
+public final class ChatEvent extends AbstractChatEvent {
+ private static final HandlerList HANDLERS = new HandlerList();
+
++ public ChatEvent(final @NotNull Player player, final @NotNull Set 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 recipients, final @NotNull Set 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 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 recipients, final @NotNull ChatFormatter formatter, final @NotNull Component message) {
diff --git a/Spigot-Server-Patches/0010-Adventure.patch b/Spigot-Server-Patches/0010-Adventure.patch
index 06cdf6b82..8af26635b 100644
--- a/Spigot-Server-Patches/0010-Adventure.patch
+++ b/Spigot-Server-Patches/0010-Adventure.patch
@@ -105,13 +105,13 @@ index 0000000000000000000000000000000000000000..89597b4a3064c3c6001c7e927a848ee7
+}
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..0cb9368dcffe08a1ab004c6e2803b43eb655ac43
+index 0000000000000000000000000000000000000000..bab8156acf87731ccdc841de5b341176ddedaae6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
-@@ -0,0 +1,215 @@
+@@ -0,0 +1,213 @@
+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..0cb9368dcffe08a1ab004c6e2803b43e
+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..0cb9368dcffe08a1ab004c6e2803b43e
+ // 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..0cb9368dcffe08a1ab004c6e2803b43e
+ // 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..0cb9368dcffe08a1ab004c6e2803b43e
+ // 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..0cb9368dcffe08a1ab004c6e2803b43e
+ }
+ }
+
-+ private void processModern(final ChatComposer composer, final Set recipients, final Component message, final boolean cancelled) {
-+ final AsyncChatEvent ae = this.createAsync(composer, recipients, message);
++ private void processModern(final ChatRenderer renderer, final Set 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..0cb9368dcffe08a1ab004c6e2803b43e
+ this.queueIfAsyncOrRunImmediately(new Waitable() {
+ @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..0cb9368dcffe08a1ab004c6e2803b43e
+ }
+
+ 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 viewers = event.viewers();
++ final Set 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 recipients, final Component message) {
-+ return new AsyncChatEvent(this.async, this.player.getBukkitEntity(), recipients, composer, message);
++ private AsyncChatEvent createAsync(final ChatRenderer renderer, final Set recipients, final Set viewers, final Component message) {
++ return new AsyncChatEvent(this.async, this.player.getBukkitEntity(), recipients, viewers, renderer, message);
+ }
+
-+ private ChatEvent createSync(final ChatComposer composer, final Set recipients, final Component message) {
-+ return new ChatEvent(this.player.getBukkitEntity(), recipients, composer, message);
++ private ChatEvent createSync(final ChatRenderer renderer, final Set recipients, final Set 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..0cb9368dcffe08a1ab004c6e2803b43e
+ 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 waitable) {
@@ -352,6 +350,33 @@ index 0000000000000000000000000000000000000000..b1d9d6276eb577ed3c66df1f89b3266d
+ 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..10f08e2b73610ab06928d1f63348920fef8e91fa
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/adventure/LazyChatAudienceSet.java
+@@ -0,0 +1,21 @@
++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 {
++ @Override
++ protected Set makeReference() {
++ final Set playerSet = LazyPlayerSet.makePlayerSet(MinecraftServer.getServer());
++ final HashSet 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..caa9708f321f04cd02534161231c05999bda4acd
@@ -3235,3 +3260,37 @@ index 65131f0977fa55c4761c34ce52720170feb61a72..8f737f63f280c00c1276bd1dc3ecf604
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 f194cf2663919ea18309a0501ddfab5e2ed639dd..4b110d6c6f22ff7c2fa0fd4b459820797066199d 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/LazyHashSet.java
+@@ -80,7 +80,7 @@ public abstract class LazyHashSet implements Set {
+ return this.reference = makeReference();
+ }
+
+- abstract Set makeReference();
++ protected abstract Set 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 e7b9250ebdd0d9034ef18a96a6cacc83e6db69c2..20ee8468bcf305139a51da61f5f9026794da27f6 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/LazyPlayerSet.java
+@@ -15,10 +15,15 @@ public class LazyPlayerSet extends LazyHashSet {
+ }
+
+ @Override
+- HashSet makeReference() {
++ protected HashSet makeReference() { // Paper - protected
+ if (reference != null) {
+ throw new IllegalStateException("Reference already created!");
+ }
++ // Paper start
++ return makePlayerSet(this.server);
++ }
++ public static HashSet makePlayerSet(final MinecraftServer server) {
++ // Paper end
+ List players = server.getPlayerList().players;
+ HashSet reference = new HashSet(players.size());
+ for (EntityPlayer player : players) {
diff --git a/Spigot-Server-Patches/0083-Option-to-use-vanilla-per-world-scoreboard-coloring-.patch b/Spigot-Server-Patches/0083-Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
index 7b7292001..a43e32ce4 100644
--- a/Spigot-Server-Patches/0083-Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
+++ b/Spigot-Server-Patches/0083-Option-to-use-vanilla-per-world-scoreboard-coloring-.patch
@@ -26,10 +26,10 @@ index db2dddd12f54e6d15916c4cee623676541de37fb..1942f5224aaebb18adb591d6f70a419c
+ }
}
diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
-index 84dcca67ccd2e52881b4a97de0f061b396ab5f35..b9bdf74e02f414ac0cf265f37b8a7883cab4fdd0 100644
+index bab8156acf87731ccdc841de5b341176ddedaae6..ab0e91e1e2b293dc51b251ded8bbfd386f08b3dc 100644
--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
+++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java
-@@ -17,7 +17,11 @@ import net.minecraft.network.chat.ChatMessageType;
+@@ -17,7 +17,11 @@ import net.kyori.adventure.text.event.ClickEvent;
import net.minecraft.network.chat.IChatBaseComponent;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.EntityPlayer;
@@ -41,7 +41,7 @@ index 84dcca67ccd2e52881b4a97de0f061b396ab5f35..b9bdf74e02f414ac0cf265f37b8a7883
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.util.LazyPlayerSet;
import org.bukkit.craftbukkit.util.Waitable;
-@@ -179,10 +183,22 @@ public final class ChatProcessor {
+@@ -177,10 +181,22 @@ public final class ChatProcessor {
}
private static String legacyDisplayName(final CraftPlayer player) {