package net.minecraft.client.gui.components; import com.google.common.collect.Lists; import com.mojang.logging.LogUtils; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.ChatFormatting; import net.minecraft.Optionull; import net.minecraft.client.GuiMessage; import net.minecraft.client.GuiMessageTag; import net.minecraft.client.Minecraft; import net.minecraft.client.GuiMessage.Line; import net.minecraft.client.GuiMessageTag.Icon; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.ChatScreen; import net.minecraft.client.multiplayer.chat.ChatListener; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MessageSignature; import net.minecraft.network.chat.Style; import net.minecraft.util.ARGB; import net.minecraft.util.ArrayListDeque; import net.minecraft.util.FormattedCharSequence; import net.minecraft.util.Mth; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.entity.player.ChatVisiblity; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class ChatComponent { private static final Logger LOGGER = LogUtils.getLogger(); private static final int MAX_CHAT_HISTORY = 100; private static final int MESSAGE_NOT_FOUND = -1; private static final int MESSAGE_INDENT = 4; private static final int MESSAGE_TAG_MARGIN_LEFT = 4; private static final int BOTTOM_MARGIN = 40; private static final int TIME_BEFORE_MESSAGE_DELETION = 60; private static final Component DELETED_CHAT_MESSAGE = Component.translatable("chat.deleted_marker").withStyle(ChatFormatting.GRAY, ChatFormatting.ITALIC); private final Minecraft minecraft; /** * A list of messages previously sent through the chat GUI */ private final ArrayListDeque recentChat = new ArrayListDeque<>(100); /** * Chat lines to be displayed in the chat box */ private final List allMessages = Lists.newArrayList(); /** * List of the ChatLines currently drawn */ private final List trimmedMessages = Lists.newArrayList(); private int chatScrollbarPos; private boolean newMessageSinceScroll; private final List messageDeletionQueue = new ArrayList(); public ChatComponent(Minecraft minecraft) { this.minecraft = minecraft; this.recentChat.addAll(minecraft.commandHistory().history()); } public void tick() { if (!this.messageDeletionQueue.isEmpty()) { this.processMessageDeletionQueue(); } } public void render(GuiGraphics guiGraphics, int tickCount, int mouseX, int mouseY, boolean focused) { if (!this.isChatHidden()) { int i = this.getLinesPerPage(); int j = this.trimmedMessages.size(); if (j > 0) { ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("chat"); float f = (float)this.getScale(); int k = Mth.ceil(this.getWidth() / f); int l = guiGraphics.guiHeight(); guiGraphics.pose().pushPose(); guiGraphics.pose().scale(f, f, 1.0F); guiGraphics.pose().translate(4.0F, 0.0F, 0.0F); int m = Mth.floor((l - 40) / f); int n = this.getMessageEndIndexAt(this.screenToChatX(mouseX), this.screenToChatY(mouseY)); double d = this.minecraft.options.chatOpacity().get() * 0.9 + 0.1; double e = this.minecraft.options.textBackgroundOpacity().get(); double g = this.minecraft.options.chatLineSpacing().get(); int o = this.getLineHeight(); int p = (int)Math.round(-8.0 * (g + 1.0) + 4.0 * g); int q = 0; for (int r = 0; r + this.chatScrollbarPos < this.trimmedMessages.size() && r < i; r++) { int s = r + this.chatScrollbarPos; Line line = (Line)this.trimmedMessages.get(s); if (line != null) { int t = tickCount - line.addedTime(); if (t < 200 || focused) { double h = focused ? 1.0 : getTimeFactor(t); int u = (int)(255.0 * h * d); int v = (int)(255.0 * h * e); q++; if (u > 3) { int w = 0; int x = m - r * o; int y = x + p; guiGraphics.fill(-4, x - o, 0 + k + 4 + 4, x, v << 24); GuiMessageTag guiMessageTag = line.tag(); if (guiMessageTag != null) { int z = guiMessageTag.indicatorColor() | u << 24; guiGraphics.fill(-4, x - o, -2, x, z); if (s == n && guiMessageTag.icon() != null) { int aa = this.getTagIconLeft(line); int ab = y + 9; this.drawTagIcon(guiGraphics, aa, ab, guiMessageTag.icon()); } } guiGraphics.pose().pushPose(); guiGraphics.pose().translate(0.0F, 0.0F, 50.0F); guiGraphics.drawString(this.minecraft.font, line.content(), 0, y, ARGB.color(u, -1)); guiGraphics.pose().popPose(); } } } } long ac = this.minecraft.getChatListener().queueSize(); if (ac > 0L) { int ad = (int)(128.0 * d); int t = (int)(255.0 * e); guiGraphics.pose().pushPose(); guiGraphics.pose().translate(0.0F, (float)m, 0.0F); guiGraphics.fill(-2, 0, k + 4, 9, t << 24); guiGraphics.pose().translate(0.0F, 0.0F, 50.0F); guiGraphics.drawString(this.minecraft.font, Component.translatable("chat.queue", ac), 0, 1, 16777215 + (ad << 24)); guiGraphics.pose().popPose(); } if (focused) { int ad = this.getLineHeight(); int t = j * ad; int ae = q * ad; int af = this.chatScrollbarPos * ae / j - m; int u = ae * ae / t; if (t != ae) { int v = af > 0 ? 170 : 96; int w = this.newMessageSinceScroll ? 13382451 : 3355562; int x = k + 4; guiGraphics.fill(x, -af, x + 2, -af - u, 100, w + (v << 24)); guiGraphics.fill(x + 2, -af, x + 1, -af - u, 100, 13421772 + (v << 24)); } } guiGraphics.pose().popPose(); profilerFiller.pop(); } } } private void drawTagIcon(GuiGraphics guiGraphics, int left, int bottom, Icon tagIcon) { int i = bottom - tagIcon.height - 1; tagIcon.draw(guiGraphics, left, i); } private int getTagIconLeft(Line line) { return this.minecraft.font.width(line.content()) + 4; } private boolean isChatHidden() { return this.minecraft.options.chatVisibility().get() == ChatVisiblity.HIDDEN; } private static double getTimeFactor(int counter) { double d = counter / 200.0; d = 1.0 - d; d *= 10.0; d = Mth.clamp(d, 0.0, 1.0); return d * d; } /** * Clears the chat. * * @param clearSentMsgHistory Whether to clear the user's sent message history */ public void clearMessages(boolean clearSentMsgHistory) { this.minecraft.getChatListener().clearQueue(); this.messageDeletionQueue.clear(); this.trimmedMessages.clear(); this.allMessages.clear(); if (clearSentMsgHistory) { this.recentChat.clear(); this.recentChat.addAll(this.minecraft.commandHistory().history()); } } public void addMessage(Component chatComponent) { this.addMessage(chatComponent, null, this.minecraft.isSingleplayer() ? GuiMessageTag.systemSinglePlayer() : GuiMessageTag.system()); } public void addMessage(Component chatComponent, @Nullable MessageSignature headerSignature, @Nullable GuiMessageTag tag) { GuiMessage guiMessage = new GuiMessage(this.minecraft.gui.getGuiTicks(), chatComponent, headerSignature, tag); this.logChatMessage(guiMessage); this.addMessageToDisplayQueue(guiMessage); this.addMessageToQueue(guiMessage); } private void logChatMessage(GuiMessage message) { String string = message.content().getString().replaceAll("\r", "\\\\r").replaceAll("\n", "\\\\n"); String string2 = Optionull.map(message.tag(), GuiMessageTag::logTag); if (string2 != null) { LOGGER.info("[{}] [CHAT] {}", string2, string); } else { LOGGER.info("[CHAT] {}", string); } } private void addMessageToDisplayQueue(GuiMessage message) { int i = Mth.floor(this.getWidth() / this.getScale()); Icon icon = message.icon(); if (icon != null) { i -= icon.width + 4 + 2; } List list = ComponentRenderUtils.wrapComponents(message.content(), i, this.minecraft.font); boolean bl = this.isChatFocused(); for (int j = 0; j < list.size(); j++) { FormattedCharSequence formattedCharSequence = (FormattedCharSequence)list.get(j); if (bl && this.chatScrollbarPos > 0) { this.newMessageSinceScroll = true; this.scrollChat(1); } boolean bl2 = j == list.size() - 1; this.trimmedMessages.add(0, new Line(message.addedTime(), formattedCharSequence, message.tag(), bl2)); } while (this.trimmedMessages.size() > 100) { this.trimmedMessages.remove(this.trimmedMessages.size() - 1); } } private void addMessageToQueue(GuiMessage message) { this.allMessages.add(0, message); while (this.allMessages.size() > 100) { this.allMessages.remove(this.allMessages.size() - 1); } } private void processMessageDeletionQueue() { int i = this.minecraft.gui.getGuiTicks(); this.messageDeletionQueue .removeIf( delayedMessageDeletion -> i >= delayedMessageDeletion.deletableAfter() ? this.deleteMessageOrDelay(delayedMessageDeletion.signature()) == null : false ); } public void deleteMessage(MessageSignature messageSignature) { ChatComponent.DelayedMessageDeletion delayedMessageDeletion = this.deleteMessageOrDelay(messageSignature); if (delayedMessageDeletion != null) { this.messageDeletionQueue.add(delayedMessageDeletion); } } @Nullable private ChatComponent.DelayedMessageDeletion deleteMessageOrDelay(MessageSignature messageSignature) { int i = this.minecraft.gui.getGuiTicks(); ListIterator listIterator = this.allMessages.listIterator(); while (listIterator.hasNext()) { GuiMessage guiMessage = (GuiMessage)listIterator.next(); if (messageSignature.equals(guiMessage.signature())) { int j = guiMessage.addedTime() + 60; if (i >= j) { listIterator.set(this.createDeletedMarker(guiMessage)); this.refreshTrimmedMessages(); return null; } return new ChatComponent.DelayedMessageDeletion(messageSignature, j); } } return null; } private GuiMessage createDeletedMarker(GuiMessage message) { return new GuiMessage(message.addedTime(), DELETED_CHAT_MESSAGE, null, GuiMessageTag.system()); } public void rescaleChat() { this.resetChatScroll(); this.refreshTrimmedMessages(); } private void refreshTrimmedMessages() { this.trimmedMessages.clear(); for (GuiMessage guiMessage : Lists.reverse(this.allMessages)) { this.addMessageToDisplayQueue(guiMessage); } } public ArrayListDeque getRecentChat() { return this.recentChat; } /** * Adds this string to the list of sent messages, for recall using the up/down arrow keys */ public void addRecentChat(String message) { if (!message.equals(this.recentChat.peekLast())) { if (this.recentChat.size() >= 100) { this.recentChat.removeFirst(); } this.recentChat.addLast(message); } if (message.startsWith("/")) { this.minecraft.commandHistory().addCommand(message); } } /** * Resets the chat scroll (executed when the GUI is closed, among others) */ public void resetChatScroll() { this.chatScrollbarPos = 0; this.newMessageSinceScroll = false; } public void scrollChat(int posInc) { this.chatScrollbarPos += posInc; int i = this.trimmedMessages.size(); if (this.chatScrollbarPos > i - this.getLinesPerPage()) { this.chatScrollbarPos = i - this.getLinesPerPage(); } if (this.chatScrollbarPos <= 0) { this.chatScrollbarPos = 0; this.newMessageSinceScroll = false; } } public boolean handleChatQueueClicked(double mouseX, double mouseY) { if (this.isChatFocused() && !this.minecraft.options.hideGui && !this.isChatHidden()) { ChatListener chatListener = this.minecraft.getChatListener(); if (chatListener.queueSize() == 0L) { return false; } else { double d = mouseX - 2.0; double e = this.minecraft.getWindow().getGuiScaledHeight() - mouseY - 40.0; if (d <= Mth.floor(this.getWidth() / this.getScale()) && e < 0.0 && e > Mth.floor(-9.0 * this.getScale())) { chatListener.acceptNextDelayedMessage(); return true; } else { return false; } } } else { return false; } } @Nullable public Style getClickedComponentStyleAt(double mouseX, double mouseY) { double d = this.screenToChatX(mouseX); double e = this.screenToChatY(mouseY); int i = this.getMessageLineIndexAt(d, e); if (i >= 0 && i < this.trimmedMessages.size()) { Line line = (Line)this.trimmedMessages.get(i); return this.minecraft.font.getSplitter().componentStyleAtWidth(line.content(), Mth.floor(d)); } else { return null; } } @Nullable public GuiMessageTag getMessageTagAt(double mouseX, double mouseY) { double d = this.screenToChatX(mouseX); double e = this.screenToChatY(mouseY); int i = this.getMessageEndIndexAt(d, e); if (i >= 0 && i < this.trimmedMessages.size()) { Line line = (Line)this.trimmedMessages.get(i); GuiMessageTag guiMessageTag = line.tag(); if (guiMessageTag != null && this.hasSelectedMessageTag(d, line, guiMessageTag)) { return guiMessageTag; } } return null; } private boolean hasSelectedMessageTag(double x, Line line, GuiMessageTag tag) { if (x < 0.0) { return true; } else { Icon icon = tag.icon(); if (icon == null) { return false; } else { int i = this.getTagIconLeft(line); int j = i + icon.width; return x >= i && x <= j; } } } private double screenToChatX(double x) { return x / this.getScale() - 4.0; } private double screenToChatY(double y) { double d = this.minecraft.getWindow().getGuiScaledHeight() - y - 40.0; return d / (this.getScale() * this.getLineHeight()); } private int getMessageEndIndexAt(double mouseX, double mouseY) { int i = this.getMessageLineIndexAt(mouseX, mouseY); if (i == -1) { return -1; } else { while (i >= 0) { if (((Line)this.trimmedMessages.get(i)).endOfEntry()) { return i; } i--; } return i; } } private int getMessageLineIndexAt(double mouseX, double mouseY) { if (this.isChatFocused() && !this.isChatHidden()) { if (!(mouseX < -4.0) && !(mouseX > Mth.floor(this.getWidth() / this.getScale()))) { int i = Math.min(this.getLinesPerPage(), this.trimmedMessages.size()); if (mouseY >= 0.0 && mouseY < i) { int j = Mth.floor(mouseY + this.chatScrollbarPos); if (j >= 0 && j < this.trimmedMessages.size()) { return j; } } return -1; } else { return -1; } } else { return -1; } } /** * Returns {@code true} if the chat GUI is open */ public boolean isChatFocused() { return this.minecraft.screen instanceof ChatScreen; } public int getWidth() { return getWidth(this.minecraft.options.chatWidth().get()); } public int getHeight() { return getHeight(this.isChatFocused() ? this.minecraft.options.chatHeightFocused().get() : this.minecraft.options.chatHeightUnfocused().get()); } public double getScale() { return this.minecraft.options.chatScale().get(); } public static int getWidth(double width) { int i = 320; int j = 40; return Mth.floor(width * 280.0 + 40.0); } public static int getHeight(double height) { int i = 180; int j = 20; return Mth.floor(height * 160.0 + 20.0); } public static double defaultUnfocusedPct() { int i = 180; int j = 20; return 70.0 / (getHeight(1.0) - 20); } public int getLinesPerPage() { return this.getHeight() / this.getLineHeight(); } private int getLineHeight() { return (int)(9.0 * (this.minecraft.options.chatLineSpacing().get() + 1.0)); } public ChatComponent.State storeState() { return new ChatComponent.State(List.copyOf(this.allMessages), List.copyOf(this.recentChat), List.copyOf(this.messageDeletionQueue)); } public void restoreState(ChatComponent.State state) { this.recentChat.clear(); this.recentChat.addAll(state.history); this.messageDeletionQueue.clear(); this.messageDeletionQueue.addAll(state.delayedMessageDeletions); this.allMessages.clear(); this.allMessages.addAll(state.messages); this.refreshTrimmedMessages(); } @Environment(EnvType.CLIENT) record DelayedMessageDeletion(MessageSignature signature, int deletableAfter) { } @Environment(EnvType.CLIENT) public static class State { final List messages; final List history; final List delayedMessageDeletions; public State(List messages, List history, List delayedMessageDeletions) { this.messages = messages; this.history = history; this.delayedMessageDeletions = delayedMessageDeletions; } } }