2025-11-24 22:52:51 +03:00

669 lines
26 KiB
Java

/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* com.google.common.collect.Lists
* com.mojang.logging.LogUtils
* org.joml.Matrix3x2f
* org.joml.Matrix3x2fc
* org.joml.Vector2f
* org.jspecify.annotations.Nullable
* org.slf4j.Logger
*/
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 java.util.Optional;
import java.util.function.Consumer;
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.gui.ActiveTextCollector;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.TextAlignment;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.HoverEvent;
import net.minecraft.network.chat.MessageSignature;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.Style;
import net.minecraft.resources.Identifier;
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.joml.Matrix3x2f;
import org.joml.Matrix3x2fc;
import org.joml.Vector2f;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
public class ChatComponent {
private static final Logger LOGGER = LogUtils.getLogger();
private static final int MAX_CHAT_HISTORY = 100;
private static final int MESSAGE_INDENT = 4;
private static final int BOTTOM_MARGIN = 40;
private static final int TOOLTIP_MAX_WIDTH = 210;
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);
public static final int MESSAGE_BOTTOM_TO_MESSAGE_TOP = 8;
public static final Identifier QUEUE_EXPAND_ID = Identifier.withDefaultNamespace("internal/expand_chat_queue");
private static final Style QUEUE_EXPAND_TEXT_STYLE = Style.EMPTY.withClickEvent(new ClickEvent.Custom(QUEUE_EXPAND_ID, Optional.empty())).withHoverEvent(new HoverEvent.ShowText(Component.translatable("chat.queue.tooltip")));
private final Minecraft minecraft;
private final ArrayListDeque<String> recentChat = new ArrayListDeque(100);
private final List<GuiMessage> allMessages = Lists.newArrayList();
private final List<GuiMessage.Line> trimmedMessages = Lists.newArrayList();
private int chatScrollbarPos;
private boolean newMessageSinceScroll;
private @Nullable Draft latestDraft;
private @Nullable ChatScreen preservedScreen;
private final List<DelayedMessageDeletion> messageDeletionQueue = new ArrayList<DelayedMessageDeletion>();
public ChatComponent(Minecraft minecraft) {
this.minecraft = minecraft;
this.recentChat.addAll(minecraft.commandHistory().history());
}
public void tick() {
if (!this.messageDeletionQueue.isEmpty()) {
this.processMessageDeletionQueue();
}
}
private int forEachLine(AlphaCalculator alphaCalculator, LineConsumer lineConsumer) {
int perPage = this.getLinesPerPage();
int count = 0;
for (int i = Math.min(this.trimmedMessages.size() - this.chatScrollbarPos, perPage) - 1; i >= 0; --i) {
int messageIndex = i + this.chatScrollbarPos;
GuiMessage.Line message = this.trimmedMessages.get(messageIndex);
float alpha = alphaCalculator.calculate(message);
if (!(alpha > 1.0E-5f)) continue;
++count;
lineConsumer.accept(message, i, alpha);
}
return count;
}
public void render(GuiGraphics graphics, Font font, int ticks, int mouseX, int mouseY, boolean isChatting) {
graphics.pose().pushMatrix();
this.render(isChatting ? new DrawingFocusedGraphicsAccess(graphics, font, mouseX, mouseY) : new DrawingBackgroundGraphicsAccess(graphics), graphics.guiHeight(), ticks, isChatting);
graphics.pose().popMatrix();
}
public void captureClickableText(ActiveTextCollector activeTextCollector, int screenHeight, int ticks, boolean isChatting) {
this.render(new ClickableTextOnlyGraphicsAccess(activeTextCollector), screenHeight, ticks, isChatting);
}
private void render(final ChatGraphicsAccess graphics, int screenHeight, int ticks, boolean isChatting) {
if (this.isChatHidden()) {
return;
}
int total = this.trimmedMessages.size();
if (total <= 0) {
return;
}
ProfilerFiller profiler = Profiler.get();
profiler.push("chat");
float scale = (float)this.getScale();
int maxWidth = Mth.ceil((float)this.getWidth() / scale);
final int chatBottom = Mth.floor((float)(screenHeight - 40) / scale);
final float textOpacity = this.minecraft.options.chatOpacity().get().floatValue() * 0.9f + 0.1f;
float backgroundOpacity = this.minecraft.options.textBackgroundOpacity().get().floatValue();
final int messageHeight = this.minecraft.font.lineHeight;
int messageBottomToMessageTop = 8;
double chatLineSpacing = this.minecraft.options.chatLineSpacing().get();
final int entryHeight = (int)((double)messageHeight * (chatLineSpacing + 1.0));
final int entryBottomToMessageY = (int)Math.round(8.0 * (chatLineSpacing + 1.0) - 4.0 * chatLineSpacing);
long queueSize = this.minecraft.getChatListener().queueSize();
AlphaCalculator alphaCalculator = isChatting ? AlphaCalculator.FULLY_VISIBLE : AlphaCalculator.timeBased(ticks);
graphics.updatePose(pose -> {
pose.scale(scale, scale);
pose.translate(4.0f, 0.0f);
});
this.forEachLine(alphaCalculator, (line, lineIndex, alpha) -> {
int entryBottom = chatBottom - lineIndex * entryHeight;
int entryTop = entryBottom - entryHeight;
graphics.fill(-4, entryTop, maxWidth + 4 + 4, entryBottom, ARGB.black(alpha * backgroundOpacity));
});
if (queueSize > 0L) {
graphics.fill(-2, chatBottom, maxWidth + 4, chatBottom + messageHeight, ARGB.black(backgroundOpacity));
}
int count = this.forEachLine(alphaCalculator, new LineConsumer(){
boolean hoveredOverCurrentMessage;
@Override
public void accept(GuiMessage.Line line, int lineIndex, float alpha) {
boolean forceIconRendering;
int entryBottom = chatBottom - lineIndex * entryHeight;
int entryTop = entryBottom - entryHeight;
int textTop = entryBottom - entryBottomToMessageY;
boolean hoveredOverCurrentLine = graphics.handleMessage(textTop, alpha * textOpacity, line.content());
this.hoveredOverCurrentMessage |= hoveredOverCurrentLine;
if (line.endOfEntry()) {
forceIconRendering = this.hoveredOverCurrentMessage;
this.hoveredOverCurrentMessage = false;
} else {
forceIconRendering = false;
}
GuiMessageTag tag = line.tag();
if (tag != null) {
graphics.handleTag(-4, entryTop, -2, entryBottom, alpha * textOpacity, tag);
if (tag.icon() != null) {
int iconLeft = line.getTagIconLeft(ChatComponent.this.minecraft.font);
int textBottom = textTop + messageHeight;
graphics.handleTagIcon(iconLeft, textBottom, forceIconRendering, tag, tag.icon());
}
}
}
});
if (queueSize > 0L) {
int queueLineBottom = chatBottom + messageHeight;
MutableComponent queueMessage = Component.translatable("chat.queue", queueSize).setStyle(QUEUE_EXPAND_TEXT_STYLE);
graphics.handleMessage(queueLineBottom - 8, 0.5f * textOpacity, queueMessage.getVisualOrderText());
}
if (isChatting) {
int virtualHeight = total * entryHeight;
int chatHeight = count * entryHeight;
int y = this.chatScrollbarPos * chatHeight / total - chatBottom;
int height = chatHeight * chatHeight / virtualHeight;
if (virtualHeight != chatHeight) {
int alpha2 = y > 0 ? 170 : 96;
int color = this.newMessageSinceScroll ? 0xCC3333 : 0x3333AA;
int scrollBarStartX = maxWidth + 4;
graphics.fill(scrollBarStartX, -y, scrollBarStartX + 2, -y - height, ARGB.color(alpha2, color));
graphics.fill(scrollBarStartX + 2, -y, scrollBarStartX + 1, -y - height, ARGB.color(alpha2, 0xCCCCCC));
}
}
profiler.pop();
}
private boolean isChatHidden() {
return this.minecraft.options.chatVisibility().get() == ChatVisiblity.HIDDEN;
}
public void clearMessages(boolean history) {
this.minecraft.getChatListener().flushQueue();
this.messageDeletionQueue.clear();
this.trimmedMessages.clear();
this.allMessages.clear();
if (history) {
this.recentChat.clear();
this.recentChat.addAll(this.minecraft.commandHistory().history());
}
}
public void addMessage(Component message) {
this.addMessage(message, null, this.minecraft.isSingleplayer() ? GuiMessageTag.systemSinglePlayer() : GuiMessageTag.system());
}
public void addMessage(Component contents, @Nullable MessageSignature signature, @Nullable GuiMessageTag tag) {
GuiMessage message = new GuiMessage(this.minecraft.gui.getGuiTicks(), contents, signature, tag);
this.logChatMessage(message);
this.addMessageToDisplayQueue(message);
this.addMessageToQueue(message);
}
private void logChatMessage(GuiMessage message) {
String messageString = message.content().getString().replaceAll("\r", "\\\\r").replaceAll("\n", "\\\\n");
String logTag = Optionull.map(message.tag(), GuiMessageTag::logTag);
if (logTag != null) {
LOGGER.info("[{}] [CHAT] {}", (Object)logTag, (Object)messageString);
} else {
LOGGER.info("[CHAT] {}", (Object)messageString);
}
}
private void addMessageToDisplayQueue(GuiMessage message) {
int maxWidth = Mth.floor((double)this.getWidth() / this.getScale());
List<FormattedCharSequence> lines = message.splitLines(this.minecraft.font, maxWidth);
boolean chatting = this.isChatFocused();
for (int i = 0; i < lines.size(); ++i) {
FormattedCharSequence line = lines.get(i);
if (chatting && this.chatScrollbarPos > 0) {
this.newMessageSinceScroll = true;
this.scrollChat(1);
}
boolean endOfEntry = i == lines.size() - 1;
this.trimmedMessages.addFirst(new GuiMessage.Line(message.addedTime(), line, message.tag(), endOfEntry));
}
while (this.trimmedMessages.size() > 100) {
this.trimmedMessages.removeLast();
}
}
private void addMessageToQueue(GuiMessage message) {
this.allMessages.addFirst(message);
while (this.allMessages.size() > 100) {
this.allMessages.removeLast();
}
}
private void processMessageDeletionQueue() {
int time = this.minecraft.gui.getGuiTicks();
this.messageDeletionQueue.removeIf(entry -> {
if (time >= entry.deletableAfter()) {
return this.deleteMessageOrDelay(entry.signature()) == null;
}
return false;
});
}
public void deleteMessage(MessageSignature signature) {
DelayedMessageDeletion delayedMessage = this.deleteMessageOrDelay(signature);
if (delayedMessage != null) {
this.messageDeletionQueue.add(delayedMessage);
}
}
private @Nullable DelayedMessageDeletion deleteMessageOrDelay(MessageSignature signature) {
int time = this.minecraft.gui.getGuiTicks();
ListIterator<GuiMessage> iterator = this.allMessages.listIterator();
while (iterator.hasNext()) {
GuiMessage message = iterator.next();
if (!signature.equals(message.signature())) continue;
int deletableAfter = message.addedTime() + 60;
if (time >= deletableAfter) {
iterator.set(this.createDeletedMarker(message));
this.refreshTrimmedMessages();
return null;
}
return new DelayedMessageDeletion(signature, deletableAfter);
}
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 message : Lists.reverse(this.allMessages)) {
this.addMessageToDisplayQueue(message);
}
}
public ArrayListDeque<String> getRecentChat() {
return this.recentChat;
}
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);
}
}
public void resetChatScroll() {
this.chatScrollbarPos = 0;
this.newMessageSinceScroll = false;
}
public void scrollChat(int dir) {
this.chatScrollbarPos += dir;
int max = this.trimmedMessages.size();
if (this.chatScrollbarPos > max - this.getLinesPerPage()) {
this.chatScrollbarPos = max - this.getLinesPerPage();
}
if (this.chatScrollbarPos <= 0) {
this.chatScrollbarPos = 0;
this.newMessageSinceScroll = false;
}
}
public boolean isChatFocused() {
return this.minecraft.screen instanceof ChatScreen;
}
private int getWidth() {
return ChatComponent.getWidth(this.minecraft.options.chatWidth().get());
}
private int getHeight() {
return ChatComponent.getHeight(this.isChatFocused() ? this.minecraft.options.chatHeightFocused().get() : this.minecraft.options.chatHeightUnfocused().get());
}
private double getScale() {
return this.minecraft.options.chatScale().get();
}
public static int getWidth(double pct) {
int max = 320;
int min = 40;
return Mth.floor(pct * 280.0 + 40.0);
}
public static int getHeight(double pct) {
int max = 180;
int min = 20;
return Mth.floor(pct * 160.0 + 20.0);
}
public static double defaultUnfocusedPct() {
int max = 180;
int min = 20;
return 70.0 / (double)(ChatComponent.getHeight(1.0) - 20);
}
public int getLinesPerPage() {
return this.getHeight() / this.getLineHeight();
}
private int getLineHeight() {
return (int)((double)this.minecraft.font.lineHeight * (this.minecraft.options.chatLineSpacing().get() + 1.0));
}
public void saveAsDraft(String text) {
boolean isCommand = text.startsWith("/");
this.latestDraft = new Draft(text, isCommand ? ChatMethod.COMMAND : ChatMethod.MESSAGE);
}
public void discardDraft() {
this.latestDraft = null;
}
public <T extends ChatScreen> T createScreen(ChatMethod chatMethod, ChatScreen.ChatConstructor<T> chat) {
if (this.latestDraft != null && chatMethod.isDraftRestorable(this.latestDraft)) {
return chat.create(this.latestDraft.text(), true);
}
return chat.create(chatMethod.prefix(), false);
}
public void openScreen(ChatMethod chatMethod, ChatScreen.ChatConstructor<?> chat) {
this.minecraft.setScreen((Screen)this.createScreen(chatMethod, chat));
}
public void preserveCurrentChatScreen() {
Screen screen = this.minecraft.screen;
if (screen instanceof ChatScreen) {
ChatScreen chatScreen;
this.preservedScreen = chatScreen = (ChatScreen)screen;
}
}
public @Nullable ChatScreen restoreChatScreen() {
ChatScreen restoredScreen = this.preservedScreen;
this.preservedScreen = null;
return restoredScreen;
}
public State storeState() {
return new State(List.copyOf(this.allMessages), List.copyOf(this.recentChat), List.copyOf(this.messageDeletionQueue));
}
public void restoreState(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();
}
@FunctionalInterface
private static interface AlphaCalculator {
public static final AlphaCalculator FULLY_VISIBLE = message -> 1.0f;
public static AlphaCalculator timeBased(int currentTickTime) {
return message -> {
int tickDelta = currentTickTime - message.addedTime();
double t = (double)tickDelta / 200.0;
t = 1.0 - t;
t *= 10.0;
t = Mth.clamp(t, 0.0, 1.0);
t *= t;
return (float)t;
};
}
public float calculate(GuiMessage.Line var1);
}
@FunctionalInterface
private static interface LineConsumer {
public void accept(GuiMessage.Line var1, int var2, float var3);
}
private static class DrawingFocusedGraphicsAccess
implements ChatGraphicsAccess,
Consumer<Style> {
private final GuiGraphics graphics;
private final Font font;
private final ActiveTextCollector textRenderer;
private ActiveTextCollector.Parameters parameters;
private final int globalMouseX;
private final int globalMouseY;
private final Vector2f localMousePos = new Vector2f();
private @Nullable Style hoveredStyle;
public DrawingFocusedGraphicsAccess(GuiGraphics graphics, Font font, int mouseX, int mouseY) {
this.graphics = graphics;
this.font = font;
this.textRenderer = graphics.textRenderer(GuiGraphics.HoveredTextEffects.TOOLTIP_AND_CURSOR, this);
this.globalMouseX = mouseX;
this.globalMouseY = mouseY;
this.parameters = this.textRenderer.defaultParameters();
this.updateLocalMousePos();
}
private void updateLocalMousePos() {
this.graphics.pose().invert(new Matrix3x2f()).transformPosition((float)this.globalMouseX, (float)this.globalMouseY, this.localMousePos);
}
@Override
public void updatePose(Consumer<Matrix3x2f> updater) {
updater.accept((Matrix3x2f)this.graphics.pose());
this.parameters = this.parameters.withPose((Matrix3x2fc)new Matrix3x2f((Matrix3x2fc)this.graphics.pose()));
this.updateLocalMousePos();
}
@Override
public void fill(int x0, int y0, int x1, int y1, int color) {
this.graphics.fill(x0, y0, x1, y1, color);
}
@Override
public void accept(Style style) {
this.hoveredStyle = style;
}
@Override
public boolean handleMessage(int textTop, float opacity, FormattedCharSequence message) {
this.hoveredStyle = null;
this.textRenderer.accept(TextAlignment.LEFT, 0, textTop, this.parameters.withOpacity(opacity), message);
return this.hoveredStyle != null;
}
private boolean isMouseOver(int left, int top, int right, int bottom) {
return ActiveTextCollector.isPointInRectangle(this.localMousePos.x, this.localMousePos.y, left, top, right, bottom);
}
@Override
public void handleTag(int x0, int y0, int x1, int y1, float opacity, GuiMessageTag tag) {
int indicatorColor = ARGB.color(opacity, tag.indicatorColor());
this.graphics.fill(x0, y0, x1, y1, indicatorColor);
if (this.isMouseOver(x0, y0, x1, y1)) {
this.showTooltip(tag);
}
}
@Override
public void handleTagIcon(int left, int bottom, boolean forceVisible, GuiMessageTag tag, GuiMessageTag.Icon icon) {
int top = bottom - icon.height - 1;
int right = left + icon.width;
boolean isMouseOver = this.isMouseOver(left, top, right, bottom);
if (isMouseOver) {
this.showTooltip(tag);
}
if (forceVisible || isMouseOver) {
icon.draw(this.graphics, left, top);
}
}
private void showTooltip(GuiMessageTag tag) {
if (tag.text() != null) {
this.graphics.setTooltipForNextFrame(this.font, this.font.split(tag.text(), 210), this.globalMouseX, this.globalMouseY);
}
}
}
private static class DrawingBackgroundGraphicsAccess
implements ChatGraphicsAccess {
private final GuiGraphics graphics;
private final ActiveTextCollector textRenderer;
private ActiveTextCollector.Parameters parameters;
public DrawingBackgroundGraphicsAccess(GuiGraphics graphics) {
this.graphics = graphics;
this.textRenderer = graphics.textRenderer(GuiGraphics.HoveredTextEffects.NONE, null);
this.parameters = this.textRenderer.defaultParameters();
}
@Override
public void updatePose(Consumer<Matrix3x2f> updater) {
updater.accept((Matrix3x2f)this.graphics.pose());
this.parameters = this.parameters.withPose((Matrix3x2fc)new Matrix3x2f((Matrix3x2fc)this.graphics.pose()));
}
@Override
public void fill(int x0, int y0, int x1, int y1, int color) {
this.graphics.fill(x0, y0, x1, y1, color);
}
@Override
public boolean handleMessage(int textTop, float opacity, FormattedCharSequence message) {
this.textRenderer.accept(TextAlignment.LEFT, 0, textTop, this.parameters.withOpacity(opacity), message);
return false;
}
@Override
public void handleTag(int x0, int y0, int x1, int y1, float opacity, GuiMessageTag tag) {
int indicatorColor = ARGB.color(opacity, tag.indicatorColor());
this.graphics.fill(x0, y0, x1, y1, indicatorColor);
}
@Override
public void handleTagIcon(int left, int bottom, boolean forceVisible, GuiMessageTag tag, GuiMessageTag.Icon icon) {
}
}
public static interface ChatGraphicsAccess {
public void updatePose(Consumer<Matrix3x2f> var1);
public void fill(int var1, int var2, int var3, int var4, int var5);
public boolean handleMessage(int var1, float var2, FormattedCharSequence var3);
public void handleTag(int var1, int var2, int var3, int var4, float var5, GuiMessageTag var6);
public void handleTagIcon(int var1, int var2, boolean var3, GuiMessageTag var4, GuiMessageTag.Icon var5);
}
private static class ClickableTextOnlyGraphicsAccess
implements ChatGraphicsAccess {
private final ActiveTextCollector output;
public ClickableTextOnlyGraphicsAccess(ActiveTextCollector output) {
this.output = output;
}
@Override
public void updatePose(Consumer<Matrix3x2f> updater) {
ActiveTextCollector.Parameters defaultParameters = this.output.defaultParameters();
Matrix3x2f newPose = new Matrix3x2f(defaultParameters.pose());
updater.accept(newPose);
this.output.defaultParameters(defaultParameters.withPose((Matrix3x2fc)newPose));
}
@Override
public void fill(int x0, int y0, int x1, int y1, int color) {
}
@Override
public boolean handleMessage(int textTop, float opacity, FormattedCharSequence message) {
this.output.accept(TextAlignment.LEFT, 0, textTop, message);
return false;
}
@Override
public void handleTag(int x0, int y0, int x1, int y1, float opacity, GuiMessageTag tag) {
}
@Override
public void handleTagIcon(int left, int bottom, boolean forceVisible, GuiMessageTag tag, GuiMessageTag.Icon icon) {
}
}
private record DelayedMessageDeletion(MessageSignature signature, int deletableAfter) {
}
public record Draft(String text, ChatMethod chatMethod) {
}
public static enum ChatMethod {
MESSAGE(""){
@Override
public boolean isDraftRestorable(Draft draft) {
return true;
}
}
,
COMMAND("/"){
@Override
public boolean isDraftRestorable(Draft draft) {
return this == draft.chatMethod;
}
};
private final String prefix;
private ChatMethod(String prefix) {
this.prefix = prefix;
}
public String prefix() {
return this.prefix;
}
public abstract boolean isDraftRestorable(Draft var1);
}
public static class State {
private final List<GuiMessage> messages;
private final List<String> history;
private final List<DelayedMessageDeletion> delayedMessageDeletions;
public State(List<GuiMessage> messages, List<String> history, List<DelayedMessageDeletion> delayedMessageDeletions) {
this.messages = messages;
this.history = history;
this.delayedMessageDeletions = delayedMessageDeletions;
}
}
}