/* * Decompiled with CFR 0.152. * * Could not load the following classes: * com.mojang.authlib.GameProfile * it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap * org.jspecify.annotations.Nullable */ package net.minecraft.client.gui.components; import com.mojang.authlib.GameProfile; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import net.minecraft.ChatFormatting; import net.minecraft.Optionull; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.PlayerFaceRenderer; import net.minecraft.client.multiplayer.PlayerInfo; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.renderer.entity.player.AvatarRenderer; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentUtils; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.numbers.NumberFormat; import net.minecraft.network.chat.numbers.StyledFormat; import net.minecraft.resources.Identifier; import net.minecraft.util.ARGB; import net.minecraft.util.FormattedCharSequence; import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.GameType; import net.minecraft.world.scores.Objective; import net.minecraft.world.scores.PlayerTeam; import net.minecraft.world.scores.ReadOnlyScoreInfo; import net.minecraft.world.scores.ScoreHolder; import net.minecraft.world.scores.Scoreboard; import net.minecraft.world.scores.criteria.ObjectiveCriteria; import org.jspecify.annotations.Nullable; public class PlayerTabOverlay { private static final Identifier PING_UNKNOWN_SPRITE = Identifier.withDefaultNamespace("icon/ping_unknown"); private static final Identifier PING_1_SPRITE = Identifier.withDefaultNamespace("icon/ping_1"); private static final Identifier PING_2_SPRITE = Identifier.withDefaultNamespace("icon/ping_2"); private static final Identifier PING_3_SPRITE = Identifier.withDefaultNamespace("icon/ping_3"); private static final Identifier PING_4_SPRITE = Identifier.withDefaultNamespace("icon/ping_4"); private static final Identifier PING_5_SPRITE = Identifier.withDefaultNamespace("icon/ping_5"); private static final Identifier HEART_CONTAINER_BLINKING_SPRITE = Identifier.withDefaultNamespace("hud/heart/container_blinking"); private static final Identifier HEART_CONTAINER_SPRITE = Identifier.withDefaultNamespace("hud/heart/container"); private static final Identifier HEART_FULL_BLINKING_SPRITE = Identifier.withDefaultNamespace("hud/heart/full_blinking"); private static final Identifier HEART_HALF_BLINKING_SPRITE = Identifier.withDefaultNamespace("hud/heart/half_blinking"); private static final Identifier HEART_ABSORBING_FULL_BLINKING_SPRITE = Identifier.withDefaultNamespace("hud/heart/absorbing_full_blinking"); private static final Identifier HEART_FULL_SPRITE = Identifier.withDefaultNamespace("hud/heart/full"); private static final Identifier HEART_ABSORBING_HALF_BLINKING_SPRITE = Identifier.withDefaultNamespace("hud/heart/absorbing_half_blinking"); private static final Identifier HEART_HALF_SPRITE = Identifier.withDefaultNamespace("hud/heart/half"); private static final Comparator PLAYER_COMPARATOR = Comparator.comparingInt(p -> -p.getTabListOrder()).thenComparingInt(p -> p.getGameMode() == GameType.SPECTATOR ? 1 : 0).thenComparing(p -> Optionull.mapOrDefault(p.getTeam(), PlayerTeam::getName, "")).thenComparing(p -> p.getProfile().name(), String::compareToIgnoreCase); public static final int MAX_ROWS_PER_COL = 20; private final Minecraft minecraft; private final Gui gui; private @Nullable Component footer; private @Nullable Component header; private boolean visible; private final Map healthStates = new Object2ObjectOpenHashMap(); public PlayerTabOverlay(Minecraft minecraft, Gui gui) { this.minecraft = minecraft; this.gui = gui; } public Component getNameForDisplay(PlayerInfo info) { if (info.getTabListDisplayName() != null) { return this.decorateName(info, info.getTabListDisplayName().copy()); } return this.decorateName(info, PlayerTeam.formatNameForTeam(info.getTeam(), Component.literal(info.getProfile().name()))); } private Component decorateName(PlayerInfo info, MutableComponent name) { return info.getGameMode() == GameType.SPECTATOR ? name.withStyle(ChatFormatting.ITALIC) : name; } public void setVisible(boolean visible) { if (this.visible != visible) { this.healthStates.clear(); this.visible = visible; if (visible) { MutableComponent players = ComponentUtils.formatList(this.getPlayerInfos(), Component.literal(", "), this::getNameForDisplay); this.minecraft.getNarrator().saySystemNow(Component.translatable("multiplayer.player.list.narration", players)); } } } private List getPlayerInfos() { return this.minecraft.player.connection.getListedOnlinePlayers().stream().sorted(PLAYER_COMPARATOR).limit(80L).toList(); } public void render(GuiGraphics graphics, int screenWidth, Scoreboard scoreboard, @Nullable Objective displayObjective) { boolean showHead; int slots; List playerInfos = this.getPlayerInfos(); ArrayList entriesToDisplay = new ArrayList(playerInfos.size()); int spacerWidth = this.minecraft.font.width(" "); int maxNameWidth = 0; int maxScoreWidth = 0; for (PlayerInfo info : playerInfos) { Component playerName = this.getNameForDisplay(info); maxNameWidth = Math.max(maxNameWidth, this.minecraft.font.width(playerName)); int playerScore = 0; MutableComponent formattedPlayerScore = null; int playerScoreWidth = 0; if (displayObjective != null) { ScoreHolder scoreHolder = ScoreHolder.fromGameProfile(info.getProfile()); ReadOnlyScoreInfo scoreInfo = scoreboard.getPlayerScoreInfo(scoreHolder, displayObjective); if (scoreInfo != null) { playerScore = scoreInfo.value(); } if (displayObjective.getRenderType() != ObjectiveCriteria.RenderType.HEARTS) { NumberFormat objectiveDefaultFormat = displayObjective.numberFormatOrDefault(StyledFormat.PLAYER_LIST_DEFAULT); formattedPlayerScore = ReadOnlyScoreInfo.safeFormatValue(scoreInfo, objectiveDefaultFormat); playerScoreWidth = this.minecraft.font.width(formattedPlayerScore); maxScoreWidth = Math.max(maxScoreWidth, playerScoreWidth > 0 ? spacerWidth + playerScoreWidth : 0); } } entriesToDisplay.add(new ScoreDisplayEntry(playerName, playerScore, formattedPlayerScore, playerScoreWidth)); } if (!this.healthStates.isEmpty()) { Set playerIds = playerInfos.stream().map(player -> player.getProfile().id()).collect(Collectors.toSet()); this.healthStates.keySet().removeIf(id -> !playerIds.contains(id)); } int rows = slots = playerInfos.size(); int cols = 1; while (rows > 20) { rows = (slots + ++cols - 1) / cols; } boolean bl = showHead = this.minecraft.isLocalServer() || this.minecraft.getConnection().getConnection().isEncrypted(); int widthForScore = displayObjective != null ? (displayObjective.getRenderType() == ObjectiveCriteria.RenderType.HEARTS ? 90 : maxScoreWidth) : 0; int slotWidth = Math.min(cols * ((showHead ? 9 : 0) + maxNameWidth + widthForScore + 13), screenWidth - 50) / cols; int xxo = screenWidth / 2 - (slotWidth * cols + (cols - 1) * 5) / 2; int yyo = 10; int maxLineWidth = slotWidth * cols + (cols - 1) * 5; List headerLines = null; if (this.header != null) { headerLines = this.minecraft.font.split(this.header, screenWidth - 50); for (FormattedCharSequence formattedCharSequence : headerLines) { maxLineWidth = Math.max(maxLineWidth, this.minecraft.font.width(formattedCharSequence)); } } List footerLines = null; if (this.footer != null) { footerLines = this.minecraft.font.split(this.footer, screenWidth - 50); for (FormattedCharSequence line : footerLines) { maxLineWidth = Math.max(maxLineWidth, this.minecraft.font.width(line)); } } if (headerLines != null) { graphics.fill(screenWidth / 2 - maxLineWidth / 2 - 1, yyo - 1, screenWidth / 2 + maxLineWidth / 2 + 1, yyo + headerLines.size() * this.minecraft.font.lineHeight, Integer.MIN_VALUE); for (FormattedCharSequence line : headerLines) { int lineWidth = this.minecraft.font.width(line); graphics.drawString(this.minecraft.font, line, screenWidth / 2 - lineWidth / 2, yyo, -1); yyo += this.minecraft.font.lineHeight; } ++yyo; } graphics.fill(screenWidth / 2 - maxLineWidth / 2 - 1, yyo - 1, screenWidth / 2 + maxLineWidth / 2 + 1, yyo + rows * 9, Integer.MIN_VALUE); int n = this.minecraft.options.getBackgroundColor(0x20FFFFFF); for (int i = 0; i < slots; ++i) { int left; int right; int col = i / rows; int row = i % rows; int xo = xxo + col * slotWidth + col * 5; int yo = yyo + row * 9; graphics.fill(xo, yo, xo + slotWidth, yo + 8, n); if (i >= playerInfos.size()) continue; PlayerInfo info = playerInfos.get(i); ScoreDisplayEntry displayInfo = (ScoreDisplayEntry)entriesToDisplay.get(i); GameProfile profile = info.getProfile(); if (showHead) { Player playerByUUID = this.minecraft.level.getPlayerByUUID(profile.id()); boolean flip = playerByUUID != null && AvatarRenderer.isPlayerUpsideDown(playerByUUID); PlayerFaceRenderer.draw(graphics, info.getSkin().body().texturePath(), xo, yo, 8, info.showHat(), flip, -1); xo += 9; } graphics.drawString(this.minecraft.font, displayInfo.name, xo, yo, info.getGameMode() == GameType.SPECTATOR ? -1862270977 : -1); if (displayObjective != null && info.getGameMode() != GameType.SPECTATOR && (right = (left = xo + maxNameWidth + 1) + widthForScore) - left > 5) { this.renderTablistScore(displayObjective, yo, displayInfo, left, right, profile.id(), graphics); } this.renderPingIcon(graphics, slotWidth, xo - (showHead ? 9 : 0), yo, info); } if (footerLines != null) { graphics.fill(screenWidth / 2 - maxLineWidth / 2 - 1, (yyo += rows * 9 + 1) - 1, screenWidth / 2 + maxLineWidth / 2 + 1, yyo + footerLines.size() * this.minecraft.font.lineHeight, Integer.MIN_VALUE); for (FormattedCharSequence line : footerLines) { int lineWidth = this.minecraft.font.width(line); graphics.drawString(this.minecraft.font, line, screenWidth / 2 - lineWidth / 2, yyo, -1); yyo += this.minecraft.font.lineHeight; } } } protected void renderPingIcon(GuiGraphics graphics, int slotWidth, int xo, int yo, PlayerInfo info) { Identifier sprite = info.getLatency() < 0 ? PING_UNKNOWN_SPRITE : (info.getLatency() < 150 ? PING_5_SPRITE : (info.getLatency() < 300 ? PING_4_SPRITE : (info.getLatency() < 600 ? PING_3_SPRITE : (info.getLatency() < 1000 ? PING_2_SPRITE : PING_1_SPRITE)))); graphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, xo + slotWidth - 11, yo, 10, 8); } private void renderTablistScore(Objective displayObjective, int yo, ScoreDisplayEntry entry, int left, int right, UUID profileId, GuiGraphics graphics) { if (displayObjective.getRenderType() == ObjectiveCriteria.RenderType.HEARTS) { this.renderTablistHearts(yo, left, right, profileId, graphics, entry.score); } else if (entry.formattedScore != null) { graphics.drawString(this.minecraft.font, entry.formattedScore, right - entry.scoreWidth, yo, -1); } } private void renderTablistHearts(int yo, int left, int right, UUID profileId, GuiGraphics graphics, int score) { int heart; HealthState health = this.healthStates.computeIfAbsent(profileId, id -> new HealthState(score)); health.update(score, this.gui.getGuiTicks()); int fullHearts = Mth.positiveCeilDiv(Math.max(score, health.displayedValue()), 2); int heartsToRender = Math.max(score, Math.max(health.displayedValue(), 20)) / 2; boolean blink = health.isBlinking(this.gui.getGuiTicks()); if (fullHearts <= 0) { return; } int widthPerHeart = Mth.floor(Math.min((float)(right - left - 4) / (float)heartsToRender, 9.0f)); if (widthPerHeart <= 3) { float pct = Mth.clamp((float)score / 20.0f, 0.0f, 1.0f); int color = (int)((1.0f - pct) * 255.0f) << 16 | (int)(pct * 255.0f) << 8; float hearts = (float)score / 2.0f; MutableComponent hpText = Component.translatable("multiplayer.player.list.hp", Float.valueOf(hearts)); MutableComponent text = right - this.minecraft.font.width(hpText) >= left ? hpText : Component.literal(Float.toString(hearts)); graphics.drawString(this.minecraft.font, text, (right + left - this.minecraft.font.width(text)) / 2, yo, ARGB.opaque(color)); return; } Identifier sprite = blink ? HEART_CONTAINER_BLINKING_SPRITE : HEART_CONTAINER_SPRITE; for (heart = fullHearts; heart < heartsToRender; ++heart) { graphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, left + heart * widthPerHeart, yo, 9, 9); } for (heart = 0; heart < fullHearts; ++heart) { graphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, left + heart * widthPerHeart, yo, 9, 9); if (blink) { if (heart * 2 + 1 < health.displayedValue()) { graphics.blitSprite(RenderPipelines.GUI_TEXTURED, HEART_FULL_BLINKING_SPRITE, left + heart * widthPerHeart, yo, 9, 9); } if (heart * 2 + 1 == health.displayedValue()) { graphics.blitSprite(RenderPipelines.GUI_TEXTURED, HEART_HALF_BLINKING_SPRITE, left + heart * widthPerHeart, yo, 9, 9); } } if (heart * 2 + 1 < score) { graphics.blitSprite(RenderPipelines.GUI_TEXTURED, heart >= 10 ? HEART_ABSORBING_FULL_BLINKING_SPRITE : HEART_FULL_SPRITE, left + heart * widthPerHeart, yo, 9, 9); } if (heart * 2 + 1 != score) continue; graphics.blitSprite(RenderPipelines.GUI_TEXTURED, heart >= 10 ? HEART_ABSORBING_HALF_BLINKING_SPRITE : HEART_HALF_SPRITE, left + heart * widthPerHeart, yo, 9, 9); } } public void setFooter(@Nullable Component footer) { this.footer = footer; } public void setHeader(@Nullable Component header) { this.header = header; } public void reset() { this.header = null; this.footer = null; } private record ScoreDisplayEntry(Component name, int score, @Nullable Component formattedScore, int scoreWidth) { } private static class HealthState { private static final long DISPLAY_UPDATE_DELAY = 20L; private static final long DECREASE_BLINK_DURATION = 20L; private static final long INCREASE_BLINK_DURATION = 10L; private int lastValue; private int displayedValue; private long lastUpdateTick; private long blinkUntilTick; public HealthState(int value) { this.displayedValue = value; this.lastValue = value; } public void update(int value, long tick) { if (value != this.lastValue) { long blinkDuration = value < this.lastValue ? 20L : 10L; this.blinkUntilTick = tick + blinkDuration; this.lastValue = value; this.lastUpdateTick = tick; } if (tick - this.lastUpdateTick > 20L) { this.displayedValue = value; } } public int displayedValue() { return this.displayedValue; } public boolean isBlinking(long tick) { return this.blinkUntilTick > tick && (this.blinkUntilTick - tick) % 6L >= 3L; } } }