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: ++ * ++ * @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(); + }