Compare commits

...
Author SHA1 Message Date
Owen 138af2a09c Fix typo 2025-05-25 14:30:36 -04:00
Owen1212055 1fd5542373 Don't cache ViewerUnawareImpl
This would cause issues for those who cache ChatRenders. It does not make sense to cache this, as this is only called once during chat processing.
2025-05-25 14:17:16 -04:00
Owen1212055 520566367f Vanilla Chat Rendering API 2025-05-25 14:16:49 -04:00
11 changed files with 421 additions and 43 deletions

No files matched your search

@@ -1,7 +1,12 @@
package io.papermc.paper;
import io.papermc.paper.chat.ChatRenderer;
import io.papermc.paper.chat.ChatTypeParameter;
import io.papermc.paper.chat.vanilla.ChatTypeRenderer;
import io.papermc.paper.world.damagesource.CombatEntry;
import io.papermc.paper.world.damagesource.FallLocationType;
import net.kyori.adventure.chat.ChatType;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.util.Services;
import org.bukkit.block.Biome;
import org.bukkit.damage.DamageEffect;
@@ -10,6 +15,7 @@ import org.bukkit.entity.LivingEntity;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.util.List;
/**
* Static bridge to the server internals.
@@ -73,5 +79,9 @@ public interface InternalAPIBridge {
* @return combat entry
*/
CombatEntry createCombatEntry(DamageSource damageSource, float damage, @Nullable FallLocationType fallLocationType, float fallDistance);
ChatType createAdventureChatType(String format, String narration, Style style, List<ChatTypeParameter> parameters);
ChatRenderer createVanillaChatRenderer(ChatTypeRenderer renderer);
}
@@ -1,5 +1,7 @@
package io.papermc.paper.chat;
import io.papermc.paper.InternalAPIBridge;
import io.papermc.paper.chat.vanilla.ChatTypeRenderer;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
@@ -67,4 +69,27 @@ public interface ChatRenderer {
@ApiStatus.OverrideOnly
Component render(Player source, Component sourceDisplayName, Component message);
}
/**
* Creates a {@link ChatRenderer} that renders using the vanilla chat renderer that is
* viewer aware.
*
* @param renderer the custom provided {@link ChatTypeRenderer.ViewerAware} implementation
* @return a {@link ChatRenderer} that delegates to the given viewer‐aware renderer
*/
static ChatRenderer chatTypeRenderer(final ChatTypeRenderer.ViewerAware renderer) {
return InternalAPIBridge.get().createVanillaChatRenderer(renderer);
}
/**
* Creates a {@link ChatRenderer} that renders using the vanilla chat renderer that is
* viewer unaware.
*
* @param renderer the custom provided {@link ChatTypeRenderer.ViewerUnaware} implementation
* @return a {@link ChatRenderer} that delegates to the given viewer‐unaware renderer
*/
static ChatRenderer chatTypeRendererViewerUnaware(final ChatTypeRenderer.ViewerUnaware renderer) {
return InternalAPIBridge.get().createVanillaChatRenderer(renderer);
}
}
@@ -0,0 +1,16 @@
package io.papermc.paper.chat;
import net.kyori.adventure.text.format.Style;
import java.util.List;
/**
* The available parameters for vanilla chat formatting.
* This used to determine the order of how chat content is rendered.
*
* @see io.papermc.paper.chat.vanilla.ChatTypeRenderer#vanillaChatType(String, String, Style, List)
*/
public enum ChatTypeParameter {
SENDER,
TARGET,
CONTENT
}
@@ -11,7 +11,6 @@ import org.jspecify.annotations.Nullable;
@NullMarked
sealed class ViewerUnawareImpl implements ChatRenderer, ChatRenderer.ViewerUnaware permits ViewerUnawareImpl.Default {
private final ViewerUnaware unaware;
private @Nullable Component message;
ViewerUnawareImpl(final ViewerUnaware unaware) {
this.unaware = unaware;
@@ -24,10 +23,7 @@ sealed class ViewerUnawareImpl implements ChatRenderer, ChatRenderer.ViewerUnawa
@Override
public Component render(final Player source, final Component sourceDisplayName, final Component message) {
if (this.message == null) {
this.message = this.unaware.render(source, sourceDisplayName, message);
}
return this.message;
return this.unaware.render(source, sourceDisplayName, message);
}
static final class Default extends ViewerUnawareImpl implements ChatRenderer.Default {
@@ -0,0 +1,53 @@
package io.papermc.paper.chat.vanilla;
import net.kyori.adventure.chat.ChatType;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
* The result of rendering a chat message into a specific {@link ChatType}.
*/
@NullMarked
@ApiStatus.NonExtendable
public sealed interface ChatTypeRenderResult permits TypeRenderResultImpl {
/**
* Create a render result with no unsigned content component.
*
* @param bound the {@link ChatType.Bound} containing the formatted message
* component and narration text
* @return a new {@link ChatTypeRenderResult}
*/
static ChatTypeRenderResult of(ChatType.Bound bound) {
return new TypeRenderResultImpl(bound, null);
}
/**
* Create a render result including an unsigned content component.
*
* @param bound the {@link ChatType.Bound}
* @param unsignedContent an optional {@link Component} representing unsigned content, or {@code null}
* @return a new {@link ChatTypeRenderResult}
*/
static ChatTypeRenderResult of(ChatType.Bound bound, @Nullable Component unsignedContent) {
return new TypeRenderResultImpl(bound, unsignedContent);
}
/**
* Retrieves the optional unsigned content component.
*
* @return the unsigned {@link Component}, or {@code null} if none was provided
*/
@Nullable
Component unsignedContent();
/**
* Retrieves the bound chat type result.
*
* @return the {@link ChatType.Bound} instance for this render
*/
ChatType.Bound boundChat();
}
@@ -0,0 +1,80 @@
package io.papermc.paper.chat.vanilla;
import io.papermc.paper.InternalAPIBridge;
import io.papermc.paper.chat.ChatTypeParameter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.chat.ChatType;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.Style;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import java.util.List;
/**
* Represents a renderer implementation that utilizes Minecraft's {@link ChatType} system for
* rendering on the client.
* <p>
* Using this API allows additional flexibility such as configurable narration.
* <p>
* Implementations are either viewer-unaware or viewer-aware, depending on
* whether they need to know the audience when rendering.
* </p>
*/
@ApiStatus.NonExtendable
public sealed interface ChatTypeRenderer permits ChatTypeRenderer.ViewerUnaware, ChatTypeRenderer.ViewerAware {
/**
* A chat type renderer that does not consider the viewer when rendering.
*/
non-sealed interface ViewerUnaware extends ChatTypeRenderer {
/**
* Renders a chat message without regard to a recipient.
*
* @param source the player who sent the message
* @param sourceDisplayName the display name component of the sender
* @param message the message content component
* @return the rendering result
*/
@ApiStatus.OverrideOnly
ChatTypeRenderResult render(Player source, Component sourceDisplayName, Component message);
}
/**
* A chat type renderer that may customize output based on the viewer.
*/
non-sealed interface ViewerAware extends ChatTypeRenderer {
/**
* Render a chat message while taking into account who is receiving the message.
*
* @param source the player who sent the message
* @param sourceDisplayName the display name component of the sender
* @param message the message content component
* @param viewer the audience receiving the message
* @return the rendering result
*/
@ApiStatus.OverrideOnly
ChatTypeRenderResult render(Player source, Component sourceDisplayName, Component message, Audience viewer);
}
/**
* Creates a vanilla {@link ChatType} with custom message formatting and narration.
* <p>
* The position of each {@link ChatTypeParameter} directly determines which
* placeholder it substitutes in both the chat message and the narration output.
*
* @param format the pattern for rendering chat messages (e.g. "<%s> %s")
* @param narration the pattern for the narration of chat messages (e.g. "%s says %s")
* @param style the {@link Style} to apply to the rendered message
* @param parameters an ordered list of {@link ChatTypeParameter}s
*
* @return a new {@link ChatType} instance with the specified format, narration,style, and parameters
*/
static ChatType vanillaChatType(String format, String narration, Style style, List<ChatTypeParameter> parameters) {
return InternalAPIBridge.get().createAdventureChatType(format, narration, style, parameters);
}
}
@@ -0,0 +1,10 @@
package io.papermc.paper.chat.vanilla;
import net.kyori.adventure.chat.ChatType;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
@ApiStatus.Internal
record TypeRenderResultImpl(ChatType.Bound boundChat, @Nullable Component unsignedContent) implements ChatTypeRenderResult {
}
@@ -28206,10 +28206,10 @@ index b30f56fbc1fd17259a1d05dc9155fffcab292ca1..11fed81a4696ba18440e755c3b8a5ca3
this.generatingStep = generatingStep;
this.cache = cache;
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
index eda176c96bcf3d67650722ffce33863edfbdea9e..a7a07ebe6ceed99fa768b6834e350fe2f51a6950 100644
index b80e6d91b8dafc9782bb7796f9855b360fd86f65..990945a9f23830f11bb1e07485fb81b0470dc2e8 100644
--- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java
@@ -1332,7 +1332,7 @@ public abstract class PlayerList {
@@ -1338,7 +1338,7 @@ public abstract class PlayerList {
public void setViewDistance(int viewDistance) {
this.viewDistance = viewDistance;
@@ -28218,7 +28218,7 @@ index eda176c96bcf3d67650722ffce33863edfbdea9e..a7a07ebe6ceed99fa768b6834e350fe2
for (ServerLevel serverLevel : this.server.getAllLevels()) {
if (serverLevel != null) {
@@ -1343,7 +1343,7 @@ public abstract class PlayerList {
@@ -1349,7 +1349,7 @@ public abstract class PlayerList {
public void setSimulationDistance(int simulationDistance) {
this.simulationDistance = simulationDistance;
@@ -1013,45 +1013,51 @@
public void broadcastSystemMessage(Component message, boolean bypassHiddenChat) {
this.broadcastSystemMessage(message, serverPlayer -> message, bypassHiddenChat);
@@ -756,20 +_,39 @@
@@ -756,20 +_,45 @@
}
public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound boundChatType) {
- this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, boundChatType);
+ // Paper start
+ this.broadcastChatMessage(message, sender, boundChatType, null);
+ this.broadcastChatMessage(message, sender, io.papermc.paper.adventure.ChatProcessor.ChatContentProvider.ofStaticViewerUnaware(boundChatType, null));
+ }
+ public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound boundChatType, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) {
+ public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, io.papermc.paper.adventure.ChatProcessor.ChatContentProvider chatContentProvider) {
+ // Paper end
+ this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, boundChatType, unsignedFunction); // Paper
+ this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, chatContentProvider); // Paper
}
private void broadcastChatMessage(
PlayerChatMessage message, Predicate<ServerPlayer> shouldFilterMessageTo, @Nullable ServerPlayer sender, ChatType.Bound boundChatType
) {
+ // Paper start
+ this.broadcastChatMessage(message, shouldFilterMessageTo, sender, boundChatType, null);
+ this.broadcastChatMessage(message, shouldFilterMessageTo, sender, io.papermc.paper.adventure.ChatProcessor.ChatContentProvider.ofStaticViewerUnaware(boundChatType, null));
+ }
+ public void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldFilterMessageTo, @Nullable ServerPlayer sender, ChatType.Bound boundChatType, @Nullable Function<net.kyori.adventure.audience.Audience, Component> unsignedFunction) {
+ private void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldFilterMessageTo, @Nullable ServerPlayer sender, io.papermc.paper.adventure.ChatProcessor.ChatContentProvider chatContentProvider) {
+ io.papermc.paper.adventure.ChatProcessor.RenderedResult consoleChat = chatContentProvider.render(this.server.console);
+ boolean isViewerUnaware = chatContentProvider.isViewerUnaware();
+ // Paper end
boolean flag = this.verifyChatTrusted(message);
- this.server.logChatMessage(message.decoratedContent(), boundChatType, flag ? null : "Not Secure");
+ this.server.logChatMessage((unsignedFunction == null ? message.decoratedContent() : unsignedFunction.apply(this.server.console)), boundChatType, flag ? null : "Not Secure"); // Paper
+ this.server.logChatMessage(consoleChat.unsignedOrElse(message.decoratedContent()), consoleChat.bound(), flag ? null : "Not Secure"); // Paper
OutgoingChatMessage outgoingChatMessage = OutgoingChatMessage.create(message);
boolean flag1 = false;
+ Packet<?> disguised = sender != null && unsignedFunction == null ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(outgoingChatMessage.content(), boundChatType) : null; // Paper - don't send player chat packets from vanished players
+ Packet<?> disguised = sender != null && isViewerUnaware ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(outgoingChatMessage.content(), consoleChat.bound()) : null; // Paper - don't send player chat packets from vanished players
for (ServerPlayer serverPlayer : this.players) {
boolean flag2 = shouldFilterMessageTo.test(serverPlayer);
- serverPlayer.sendChatMessage(outgoingChatMessage, flag2, boundChatType);
+ // Paper start - don't send player chat packets from vanished players
+ if (sender != null && !serverPlayer.getBukkitEntity().canSee(sender.getBukkitEntity())) {
+ serverPlayer.connection.send(unsignedFunction != null
+ ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(unsignedFunction.apply(serverPlayer.getBukkitEntity()), boundChatType)
+ : disguised);
+ if (isViewerUnaware) {
+ serverPlayer.connection.send(disguised);
+ } else {
+ io.papermc.paper.adventure.ChatProcessor.RenderedResult renderedFrame = chatContentProvider.render(serverPlayer.getBukkitEntity());
+ serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(renderedFrame.unsignedOrElse(outgoingChatMessage.content()), renderedFrame.bound()));
+ }
+ continue;
+ }
+ serverPlayer.sendChatMessage(outgoingChatMessage, flag2, boundChatType, unsignedFunction == null ? null : unsignedFunction.apply(serverPlayer.getBukkitEntity()));
+ io.papermc.paper.adventure.ChatProcessor.RenderedResult playerChat = isViewerUnaware ? consoleChat : chatContentProvider.render(serverPlayer.getBukkitEntity());
+ serverPlayer.sendChatMessage(outgoingChatMessage, flag2, playerChat.bound(), playerChat.unsigned());
+ // Paper end
flag1 |= flag2 && message.isFullyFiltered();
}
@@ -1,10 +1,21 @@
package io.papermc.paper;
import io.papermc.paper.adventure.ChatProcessor;
import io.papermc.paper.adventure.PaperAdventure;
import io.papermc.paper.chat.ChatRenderer;
import io.papermc.paper.chat.ChatTypeParameter;
import io.papermc.paper.chat.vanilla.ChatTypeRenderer;
import io.papermc.paper.util.MCUtil;
import io.papermc.paper.world.damagesource.CombatEntry;
import io.papermc.paper.world.damagesource.FallLocationType;
import io.papermc.paper.world.damagesource.PaperCombatEntryWrapper;
import io.papermc.paper.world.damagesource.PaperCombatTrackerWrapper;
import net.kyori.adventure.chat.ChatType;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.format.Style;
import net.minecraft.Optionull;
import net.minecraft.core.Holder;
import net.minecraft.network.chat.ChatTypeDecoration;
import net.minecraft.world.damagesource.FallLocation;
import org.bukkit.block.Biome;
import org.bukkit.craftbukkit.block.CraftBiome;
@@ -14,8 +25,10 @@ import org.bukkit.craftbukkit.entity.CraftLivingEntity;
import org.bukkit.damage.DamageEffect;
import org.bukkit.damage.DamageSource;
import org.bukkit.entity.LivingEntity;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.util.List;
@NullMarked
public class PaperServerInternalAPIBridge implements InternalAPIBridge {
@@ -71,4 +84,29 @@ public class PaperServerInternalAPIBridge implements InternalAPIBridge {
damageSource, damage, fallLocation, fallDistance
));
}
@Override
public ChatType createAdventureChatType(final String format, final String narration, final Style style, final List<ChatTypeParameter> parameters) {
List<ChatTypeDecoration.Parameter> parameterList = MCUtil.transformUnmodifiable(parameters, chatTypeParameter -> ChatTypeDecoration.Parameter.values()[chatTypeParameter.ordinal()]);
net.minecraft.network.chat.Style nmsType = PaperAdventure.asVanilla(style);
net.minecraft.network.chat.ChatType chatType = new net.minecraft.network.chat.ChatType(
new ChatTypeDecoration(format, parameterList, nmsType),
new ChatTypeDecoration(narration, parameterList, nmsType)
);
return new PaperInternalChatType(Holder.direct(chatType));
}
public record PaperInternalChatType(Holder<net.minecraft.network.chat.ChatType> chatType) implements ChatType {
@Override
public @NotNull Key key() {
throw new IllegalStateException("Cannot get key for this registry item, because it is not registered.");
}
}
@Override
public ChatRenderer createVanillaChatRenderer(final ChatTypeRenderer renderer) {
return ChatProcessor.PaperVanillaChatRenderer.of(renderer);
}
}
@@ -1,6 +1,9 @@
package io.papermc.paper.adventure;
import io.papermc.paper.PaperServerInternalAPIBridge;
import io.papermc.paper.chat.ChatRenderer;
import io.papermc.paper.chat.vanilla.ChatTypeRenderResult;
import io.papermc.paper.chat.vanilla.ChatTypeRenderer;
import io.papermc.paper.event.player.AbstractChatEvent;
import io.papermc.paper.event.player.AsyncChatEvent;
import io.papermc.paper.event.player.ChatEvent;
@@ -12,6 +15,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
@@ -21,8 +25,8 @@ import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.minecraft.ChatFormatting;
import net.minecraft.Optionull;
import net.minecraft.Util;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.ChatType;
import net.minecraft.network.chat.OutgoingChatMessage;
@@ -63,7 +67,8 @@ public final class ChatProcessor {
static final int MESSAGE_CHANGED = 1;
static final int FORMAT_CHANGED = 2;
static final int SENDER_CHANGED = 3; // Not used
private final BitSet flags = new BitSet(3);
static final int USES_CHAT_TYPE_RENDERER = 4;
private final BitSet flags = new BitSet(4);
public ChatProcessor(final MinecraftServer server, final ServerPlayer player, final PlayerChatMessage message, final boolean async) {
this.server = server;
@@ -171,7 +176,11 @@ public final class ChatProcessor {
private void readModernModifications(final AbstractChatEvent chatEvent, final ChatRenderer originalRenderer) {
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);
if (chatEvent.renderer() instanceof PaperVanillaChatRenderer) {
this.flags.set(USES_CHAT_TYPE_RENDERER, true);
} else {
this.flags.set(FORMAT_CHANGED, true);
}
}
}
@@ -190,7 +199,12 @@ public final class ChatProcessor {
final ChatType.Bound chatType = ChatType.bind(chatTypeKey, this.player.level().registryAccess(), PaperAdventure.asVanilla(displayName(player)));
OutgoingChat outgoingChat = viewers instanceof LazyChatAudienceSet lazyAudienceSet && lazyAudienceSet.isLazy() ? new ServerOutgoingChat() : new ViewersOutgoingChat();
if (this.flags.get(FORMAT_CHANGED)) {
if (this.flags.get(USES_CHAT_TYPE_RENDERER)) {
if (!(renderer instanceof PaperVanillaChatRenderer vanillaChatRenderer)) {
throw new IllegalStateException("BUG: This should only be a vanilla renderer");
}
outgoingChat.sendChatTypeRendered(player, displayName, message, viewers, vanillaChatRenderer, vanillaChatRenderer.viewerUnaware());
} 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 {
@@ -212,6 +226,8 @@ public final class ChatProcessor {
this.sendMessageChanged(player, renderedMessage, viewers, chatType);
}
void sendChatTypeRendered(CraftPlayer player, Component displayName, Component message, Set<Audience> viewers, PaperVanillaChatRenderer vanillaChatRenderer, boolean viewerUnaware);
void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType);
void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType);
@@ -220,9 +236,15 @@ public final class ChatProcessor {
}
final class ServerOutgoingChat implements OutgoingChat {
@Override
public void sendChatTypeRendered(final CraftPlayer player, final Component displayName, final Component message, final Set<Audience> viewers, final PaperVanillaChatRenderer vanillaChatRenderer, final boolean viewerUnaware) {
ChatContentProvider provider = viewerUnaware ? ChatContentProvider.ofStaticViewerUnaware(vanillaChatRenderer.compute(player, displayName, message, null)) : ChatContentProvider.ofViewerAware((viewer) -> vanillaChatRenderer.compute(player, displayName, message, viewer));
ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, provider);
}
@Override
public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> 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)));
ChatProcessor.this.server.getPlayerList().broadcastChatMessage(ChatProcessor.this.message, ChatProcessor.this.player, ChatContentProvider.ofViewerAware((viewer) -> new RenderedResult(chatType, PaperAdventure.asVanilla(renderer.render(player, displayName, message, viewer)))));
}
@Override
@@ -237,27 +259,36 @@ public final class ChatProcessor {
}
final class ViewersOutgoingChat implements OutgoingChat {
@Override
public void sendChatTypeRendered(final CraftPlayer player, final Component displayName, final Component message, final Set<Audience> viewers, final PaperVanillaChatRenderer vanillaChatRenderer, final boolean viewerUnaware) {
ChatContentProvider provider = viewerUnaware ? ChatContentProvider.ofStaticViewerUnaware(vanillaChatRenderer.compute(player, displayName, message, null)) : ChatContentProvider.ofViewerAware((viewer) -> vanillaChatRenderer.compute(player, displayName, message, viewer));
this.broadcastToViewers(viewers, provider);
}
@Override
public void sendFormatChangedViewerAware(CraftPlayer player, Component displayName, Component message, ChatRenderer renderer, Set<Audience> viewers, ChatType.Bound chatType) {
this.broadcastToViewers(viewers, chatType, v -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, v)));
this.broadcastToViewers(viewers, ChatContentProvider.ofViewerAware((v) -> PaperAdventure.asVanilla(renderer.render(player, displayName, message, v)), chatType));
}
@Override
public void sendMessageChanged(CraftPlayer player, net.minecraft.network.chat.Component renderedMessage, Set<Audience> viewers, ChatType.Bound chatType) {
this.broadcastToViewers(viewers, chatType, $ -> renderedMessage);
this.broadcastToViewers(viewers, ChatContentProvider.ofStaticViewerUnaware(chatType, renderedMessage));
}
@Override
public void sendOriginal(CraftPlayer player, Set<Audience> viewers, ChatType.Bound chatType) {
this.broadcastToViewers(viewers, chatType, null);
this.broadcastToViewers(viewers, ChatContentProvider.ofStaticViewerUnaware(chatType, null));
}
private void broadcastToViewers(Collection<Audience> viewers, final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) {
private void broadcastToViewers(Collection<Audience> viewers, final ChatContentProvider provider) {
for (Audience viewer : viewers) {
if (acceptsNative(viewer)) {
this.sendNative(viewer, chatType, msgFunction);
this.sendNative(viewer, provider);
} else {
final net.minecraft.network.chat.@Nullable Component unsigned = Optionull.map(msgFunction, f -> f.apply(viewer));
final RenderedResult frame = provider.render(viewer);
final ChatType.Bound chatType = frame.bound();
final net.minecraft.network.chat.@Nullable Component unsigned = frame.unsigned();
final PlayerChatMessage msg = unsigned == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(unsigned);
viewer.sendMessage(msg.adventureView(), this.adventure(chatType));
}
@@ -303,21 +334,25 @@ public final class ChatProcessor {
return false;
}
private void sendNative(final Audience viewer, final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> 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, Optionull.map(msgFunction, f -> f.apply(viewer)));
} else if (viewer instanceof ForwardingAudience.Single single) {
this.sendNative(single.audience(), chatType, msgFunction);
} else {
throw new IllegalStateException("Should only be a Player or Console or ForwardingAudience.Single pointing to one!");
private void sendNative(final Audience viewer, final ChatContentProvider provider) {
switch (viewer) {
case final ConsoleCommandSender sender -> this.sendToServer(provider);
case final CraftPlayer craftPlayer -> {
final RenderedResult frame = provider.render(viewer);
final ChatType.Bound toChatType = frame.bound();
final net.minecraft.network.chat.@Nullable Component messageContent = frame.unsigned();
craftPlayer.getHandle().sendChatMessage(ChatProcessor.this.outgoing, ChatProcessor.this.player.shouldFilterMessageTo(craftPlayer.getHandle()), toChatType, messageContent);
}
case final ForwardingAudience.Single single -> this.sendNative(single.audience(), provider);
default -> throw new IllegalStateException("Should only be a Player or Console or ForwardingAudience.Single pointing to one!");
}
}
private void sendToServer(final ChatType.Bound chatType, final @Nullable Function<Audience, net.minecraft.network.chat.Component> msgFunction) {
final PlayerChatMessage toConsoleMessage = msgFunction == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(msgFunction.apply(ChatProcessor.this.server.console));
ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) ? null : "Not Secure");
private void sendToServer(final ChatContentProvider chatContentProvider) {
final RenderedResult frame = chatContentProvider.render(ChatProcessor.this.server.console);
final PlayerChatMessage toConsoleMessage = frame.unsigned() == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(frame.unsigned());
ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), frame.bound(), ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) ? null : "Not Secure");
}
}
@@ -381,4 +416,113 @@ public final class ChatProcessor {
static boolean canYouHearMe(final HandlerList handlers) {
return handlers.getRegisteredListeners().length > 0;
}
public record PaperVanillaChatRenderer(ChatTypeRenderer.ViewerAware vanillaChatTypeRenderer, boolean viewerUnaware) implements ChatRenderer {
/*
We wrap the viewer unaware implementation in a viewer aware implementation in order to prevent code complexity.
In this case, the viewerUnaware flag on this renderer indicates that optimizations can occur.
We just ignore the viewer param as its treated as effectively undefined when viewerUnaware is true.
*/
public static ChatRenderer of(ChatTypeRenderer renderer) {
return switch (renderer) {
case ChatTypeRenderer.ViewerAware viewerAware -> new PaperVanillaChatRenderer(viewerAware, false);
case ChatTypeRenderer.ViewerUnaware viewerUnaware -> new PaperVanillaChatRenderer((source, sourceDisplayName, message, viewer) -> viewerUnaware.render(source, sourceDisplayName, message), true);
};
}
// api compatability renderer
@Override
public Component render(final Player source, final Component sourceDisplayName, final Component message, final Audience viewer) {
ChatType.Bound nms = compute(source, sourceDisplayName, message, viewer).bound();
ChatType chatTypeHolder = nms.chatType().value();
return PaperAdventure.asAdventure(chatTypeHolder.chat().decorate(PaperAdventure.asVanilla(message), nms));
}
private RenderedResult compute(final Player source, final Component sourceDisplayName, final Component message, final Audience viewer) {
ChatTypeRenderResult result = vanillaChatTypeRenderer.render(source, sourceDisplayName, message, viewer);
return new RenderedResult(toHandle(((CraftPlayer) source).getHandle(), result.boundChat()), PaperAdventure.asVanilla(result.unsignedContent()));
}
public static net.minecraft.network.chat.ChatType.Bound toHandle(ServerPlayer player, net.kyori.adventure.chat.ChatType.Bound boundChatType) {
Holder<ChatType> chatTypeHolder = toNmsChatType(player, boundChatType.type());
return new net.minecraft.network.chat.ChatType.Bound(
chatTypeHolder,
io.papermc.paper.adventure.PaperAdventure.asVanilla(boundChatType.name()),
Optional.ofNullable(io.papermc.paper.adventure.PaperAdventure.asVanilla(boundChatType.target()))
);
}
private static Holder<ChatType> toNmsChatType(ServerPlayer player, net.kyori.adventure.chat.ChatType chatType) {
net.minecraft.core.Registry<net.minecraft.network.chat.ChatType> chatTypeRegistry = player.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.CHAT_TYPE);
if (chatType instanceof PaperServerInternalAPIBridge.PaperInternalChatType paperInternalChatType) {
return paperInternalChatType.chatType();
}
return chatTypeRegistry.get(ResourceKey.create(Registries.CHAT_TYPE, PaperAdventure.asVanilla(chatType.key())))
.orElseThrow(() -> new IllegalStateException("Expected a registered chat type!"));
}
}
/*
This class is used for providing chat type / unsigned content to users.
The isViewerUnaware flag indicates that render may be called with an undefined audience, which can be
used for slight optimizations.
*/
public interface ChatContentProvider {
static ChatContentProvider ofViewerAware(Function<Audience, net.minecraft.network.chat.Component> function, ChatType.Bound chatType) {
return ofViewerAware((v) -> new RenderedResult(chatType, function.apply(v)));
}
static ChatContentProvider ofViewerAware(Function<Audience, RenderedResult> function) {
return new ChatContentProvider() {
@Override
public RenderedResult render(final Audience audience) {
return function.apply(audience);
}
@Override
public boolean isViewerUnaware() {
return false;
}
};
}
static ChatContentProvider ofStaticViewerUnaware(ChatType.Bound chatType, net.minecraft.network.chat.@Nullable Component unsignedContent) {
return ofStaticViewerUnaware(new RenderedResult(chatType, unsignedContent));
}
static ChatContentProvider ofStaticViewerUnaware(RenderedResult result) {
record ViewerUnawareStatic(RenderedResult staticFrame) implements ChatContentProvider {
@Override
public RenderedResult render(final Audience audience) {
return this.staticFrame;
}
@Override
public boolean isViewerUnaware() {
return true;
}
}
return new ViewerUnawareStatic(result);
}
RenderedResult render(Audience audience);
boolean isViewerUnaware();
}
public record RenderedResult(ChatType.Bound bound, net.minecraft.network.chat.@Nullable Component unsigned) {
public net.minecraft.network.chat.Component unsignedOrElse(net.minecraft.network.chat.Component component) {
return this.unsigned != null ? this.unsigned : component;
}
}
}