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 javax.annotation.Nullable; 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.multiplayer.PlayerInfo; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.entity.LivingEntityRenderer; 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.ResourceLocation; 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 net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @OnlyIn(Dist.CLIENT) public class PlayerTabOverlay { private static final ResourceLocation PING_UNKNOWN_SPRITE = ResourceLocation.withDefaultNamespace("icon/ping_unknown"); private static final ResourceLocation PING_1_SPRITE = ResourceLocation.withDefaultNamespace("icon/ping_1"); private static final ResourceLocation PING_2_SPRITE = ResourceLocation.withDefaultNamespace("icon/ping_2"); private static final ResourceLocation PING_3_SPRITE = ResourceLocation.withDefaultNamespace("icon/ping_3"); private static final ResourceLocation PING_4_SPRITE = ResourceLocation.withDefaultNamespace("icon/ping_4"); private static final ResourceLocation PING_5_SPRITE = ResourceLocation.withDefaultNamespace("icon/ping_5"); private static final ResourceLocation HEART_CONTAINER_BLINKING_SPRITE = ResourceLocation.withDefaultNamespace("hud/heart/container_blinking"); private static final ResourceLocation HEART_CONTAINER_SPRITE = ResourceLocation.withDefaultNamespace("hud/heart/container"); private static final ResourceLocation HEART_FULL_BLINKING_SPRITE = ResourceLocation.withDefaultNamespace("hud/heart/full_blinking"); private static final ResourceLocation HEART_HALF_BLINKING_SPRITE = ResourceLocation.withDefaultNamespace("hud/heart/half_blinking"); private static final ResourceLocation HEART_ABSORBING_FULL_BLINKING_SPRITE = ResourceLocation.withDefaultNamespace("hud/heart/absorbing_full_blinking"); private static final ResourceLocation HEART_FULL_SPRITE = ResourceLocation.withDefaultNamespace("hud/heart/full"); private static final ResourceLocation HEART_ABSORBING_HALF_BLINKING_SPRITE = ResourceLocation.withDefaultNamespace("hud/heart/absorbing_half_blinking"); private static final ResourceLocation HEART_HALF_SPRITE = ResourceLocation.withDefaultNamespace("hud/heart/half"); private static final Comparator PLAYER_COMPARATOR = Comparator.comparingInt(p_357670_ -> -p_357670_.getTabListOrder()) .thenComparingInt(p_253306_ -> p_253306_.getGameMode() == GameType.SPECTATOR ? 1 : 0) .thenComparing(p_269613_ -> Optionull.mapOrDefault(p_269613_.getTeam(), PlayerTeam::getName, "")) .thenComparing(p_253305_ -> p_253305_.getProfile().getName(), String::compareToIgnoreCase); public static final int MAX_ROWS_PER_COL = 20; private final Minecraft minecraft; private final Gui gui; @Nullable private Component footer; @Nullable private Component header; private boolean visible; private final Map healthStates = new Object2ObjectOpenHashMap<>(); public PlayerTabOverlay(Minecraft p_94527_, Gui p_94528_) { this.minecraft = p_94527_; this.gui = p_94528_; } public Component getNameForDisplay(PlayerInfo p_94550_) { return p_94550_.getTabListDisplayName() != null ? this.decorateName(p_94550_, p_94550_.getTabListDisplayName().copy()) : this.decorateName(p_94550_, PlayerTeam.formatNameForTeam(p_94550_.getTeam(), Component.literal(p_94550_.getProfile().getName()))); } private Component decorateName(PlayerInfo p_94552_, MutableComponent p_94553_) { return p_94552_.getGameMode() == GameType.SPECTATOR ? p_94553_.withStyle(ChatFormatting.ITALIC) : p_94553_; } public void setVisible(boolean p_94557_) { if (this.visible != p_94557_) { this.healthStates.clear(); this.visible = p_94557_; if (p_94557_) { Component component = ComponentUtils.formatList(this.getPlayerInfos(), Component.literal(", "), this::getNameForDisplay); this.minecraft.getNarrator().sayNow(Component.translatable("multiplayer.player.list.narration", component)); } } } private List getPlayerInfos() { return this.minecraft.player.connection.getListedOnlinePlayers().stream().sorted(PLAYER_COMPARATOR).limit(80L).toList(); } public void render(GuiGraphics p_281484_, int p_283602_, Scoreboard p_282338_, @Nullable Objective p_282369_) { List list = this.getPlayerInfos(); List list1 = new ArrayList<>(list.size()); int i = this.minecraft.font.width(" "); int j = 0; int k = 0; for (PlayerInfo playerinfo : list) { Component component = this.getNameForDisplay(playerinfo); j = Math.max(j, this.minecraft.font.width(component)); int l = 0; Component component1 = null; int i1 = 0; if (p_282369_ != null) { ScoreHolder scoreholder = ScoreHolder.fromGameProfile(playerinfo.getProfile()); ReadOnlyScoreInfo readonlyscoreinfo = p_282338_.getPlayerScoreInfo(scoreholder, p_282369_); if (readonlyscoreinfo != null) { l = readonlyscoreinfo.value(); } if (p_282369_.getRenderType() != ObjectiveCriteria.RenderType.HEARTS) { NumberFormat numberformat = p_282369_.numberFormatOrDefault(StyledFormat.PLAYER_LIST_DEFAULT); component1 = ReadOnlyScoreInfo.safeFormatValue(readonlyscoreinfo, numberformat); i1 = this.minecraft.font.width(component1); k = Math.max(k, i1 > 0 ? i + i1 : 0); } } list1.add(new PlayerTabOverlay.ScoreDisplayEntry(component, l, component1, i1)); } if (!this.healthStates.isEmpty()) { Set set = list.stream().map(p_250472_ -> p_250472_.getProfile().getId()).collect(Collectors.toSet()); this.healthStates.keySet().removeIf(p_248583_ -> !set.contains(p_248583_)); } int j2 = list.size(); int k2 = j2; int l2; for (l2 = 1; k2 > 20; k2 = (j2 + l2 - 1) / l2) { l2++; } boolean flag1 = this.minecraft.isLocalServer() || this.minecraft.getConnection().getConnection().isEncrypted(); int i3; if (p_282369_ != null) { if (p_282369_.getRenderType() == ObjectiveCriteria.RenderType.HEARTS) { i3 = 90; } else { i3 = k; } } else { i3 = 0; } int j3 = Math.min(l2 * ((flag1 ? 9 : 0) + j + i3 + 13), p_283602_ - 50) / l2; int k3 = p_283602_ / 2 - (j3 * l2 + (l2 - 1) * 5) / 2; int l3 = 10; int i4 = j3 * l2 + (l2 - 1) * 5; List list2 = null; if (this.header != null) { list2 = this.minecraft.font.split(this.header, p_283602_ - 50); for (FormattedCharSequence formattedcharsequence : list2) { i4 = Math.max(i4, this.minecraft.font.width(formattedcharsequence)); } } List list3 = null; if (this.footer != null) { list3 = this.minecraft.font.split(this.footer, p_283602_ - 50); for (FormattedCharSequence formattedcharsequence1 : list3) { i4 = Math.max(i4, this.minecraft.font.width(formattedcharsequence1)); } } if (list2 != null) { p_281484_.fill(p_283602_ / 2 - i4 / 2 - 1, l3 - 1, p_283602_ / 2 + i4 / 2 + 1, l3 + list2.size() * 9, Integer.MIN_VALUE); for (FormattedCharSequence formattedcharsequence2 : list2) { int j1 = this.minecraft.font.width(formattedcharsequence2); p_281484_.drawString(this.minecraft.font, formattedcharsequence2, p_283602_ / 2 - j1 / 2, l3, -1); l3 += 9; } l3++; } p_281484_.fill(p_283602_ / 2 - i4 / 2 - 1, l3 - 1, p_283602_ / 2 + i4 / 2 + 1, l3 + k2 * 9, Integer.MIN_VALUE); int j4 = this.minecraft.options.getBackgroundColor(553648127); for (int k4 = 0; k4 < j2; k4++) { int l4 = k4 / k2; int k1 = k4 % k2; int l1 = k3 + l4 * j3 + l4 * 5; int i2 = l3 + k1 * 9; p_281484_.fill(l1, i2, l1 + j3, i2 + 8, j4); if (k4 < list.size()) { PlayerInfo playerinfo1 = list.get(k4); PlayerTabOverlay.ScoreDisplayEntry playertaboverlay$scoredisplayentry = list1.get(k4); GameProfile gameprofile = playerinfo1.getProfile(); if (flag1) { Player player = this.minecraft.level.getPlayerByUUID(gameprofile.getId()); boolean flag = player != null && LivingEntityRenderer.isEntityUpsideDown(player); PlayerFaceRenderer.draw(p_281484_, playerinfo1.getSkin().texture(), l1, i2, 8, playerinfo1.showHat(), flag, -1); l1 += 9; } p_281484_.drawString( this.minecraft.font, playertaboverlay$scoredisplayentry.name, l1, i2, playerinfo1.getGameMode() == GameType.SPECTATOR ? -1862270977 : -1 ); if (p_282369_ != null && playerinfo1.getGameMode() != GameType.SPECTATOR) { int j5 = l1 + j + 1; int k5 = j5 + i3; if (k5 - j5 > 5) { this.renderTablistScore(p_282369_, i2, playertaboverlay$scoredisplayentry, j5, k5, gameprofile.getId(), p_281484_); } } this.renderPingIcon(p_281484_, j3, l1 - (flag1 ? 9 : 0), i2, playerinfo1); } } if (list3 != null) { l3 += k2 * 9 + 1; p_281484_.fill(p_283602_ / 2 - i4 / 2 - 1, l3 - 1, p_283602_ / 2 + i4 / 2 + 1, l3 + list3.size() * 9, Integer.MIN_VALUE); for (FormattedCharSequence formattedcharsequence3 : list3) { int i5 = this.minecraft.font.width(formattedcharsequence3); p_281484_.drawString(this.minecraft.font, formattedcharsequence3, p_283602_ / 2 - i5 / 2, l3, -1); l3 += 9; } } } protected void renderPingIcon(GuiGraphics p_283286_, int p_281809_, int p_282801_, int p_282223_, PlayerInfo p_282986_) { ResourceLocation resourcelocation; if (p_282986_.getLatency() < 0) { resourcelocation = PING_UNKNOWN_SPRITE; } else if (p_282986_.getLatency() < 150) { resourcelocation = PING_5_SPRITE; } else if (p_282986_.getLatency() < 300) { resourcelocation = PING_4_SPRITE; } else if (p_282986_.getLatency() < 600) { resourcelocation = PING_3_SPRITE; } else if (p_282986_.getLatency() < 1000) { resourcelocation = PING_2_SPRITE; } else { resourcelocation = PING_1_SPRITE; } p_283286_.pose().pushPose(); p_283286_.pose().translate(0.0F, 0.0F, 100.0F); p_283286_.blitSprite(RenderType::guiTextured, resourcelocation, p_282801_ + p_281809_ - 11, p_282223_, 10, 8); p_283286_.pose().popPose(); } private void renderTablistScore( Objective p_283381_, int p_282557_, PlayerTabOverlay.ScoreDisplayEntry p_312058_, int p_283533_, int p_281254_, UUID p_283099_, GuiGraphics p_282280_ ) { if (p_283381_.getRenderType() == ObjectiveCriteria.RenderType.HEARTS) { this.renderTablistHearts(p_282557_, p_283533_, p_281254_, p_283099_, p_282280_, p_312058_.score); } else if (p_312058_.formattedScore != null) { p_282280_.drawString(this.minecraft.font, p_312058_.formattedScore, p_281254_ - p_312058_.scoreWidth, p_282557_, 16777215); } } private void renderTablistHearts(int p_282904_, int p_283173_, int p_282149_, UUID p_283348_, GuiGraphics p_281723_, int p_281354_) { PlayerTabOverlay.HealthState playertaboverlay$healthstate = this.healthStates .computeIfAbsent(p_283348_, p_249546_ -> new PlayerTabOverlay.HealthState(p_281354_)); playertaboverlay$healthstate.update(p_281354_, (long)this.gui.getGuiTicks()); int i = Mth.positiveCeilDiv(Math.max(p_281354_, playertaboverlay$healthstate.displayedValue()), 2); int j = Math.max(p_281354_, Math.max(playertaboverlay$healthstate.displayedValue(), 20)) / 2; boolean flag = playertaboverlay$healthstate.isBlinking((long)this.gui.getGuiTicks()); if (i > 0) { int k = Mth.floor(Math.min((float)(p_282149_ - p_283173_ - 4) / (float)j, 9.0F)); if (k <= 3) { float f1 = Mth.clamp((float)p_281354_ / 20.0F, 0.0F, 1.0F); int j1 = (int)((1.0F - f1) * 255.0F) << 16 | (int)(f1 * 255.0F) << 8; float f = (float)p_281354_ / 2.0F; Component component = Component.translatable("multiplayer.player.list.hp", f); Component component1; if (p_282149_ - this.minecraft.font.width(component) >= p_283173_) { component1 = component; } else { component1 = Component.literal(Float.toString(f)); } p_281723_.drawString( this.minecraft.font, component1, (p_282149_ + p_283173_ - this.minecraft.font.width(component1)) / 2, p_282904_, j1 ); } else { ResourceLocation resourcelocation = flag ? HEART_CONTAINER_BLINKING_SPRITE : HEART_CONTAINER_SPRITE; for (int l = i; l < j; l++) { p_281723_.blitSprite(RenderType::guiTextured, resourcelocation, p_283173_ + l * k, p_282904_, 9, 9); } for (int i1 = 0; i1 < i; i1++) { p_281723_.blitSprite(RenderType::guiTextured, resourcelocation, p_283173_ + i1 * k, p_282904_, 9, 9); if (flag) { if (i1 * 2 + 1 < playertaboverlay$healthstate.displayedValue()) { p_281723_.blitSprite(RenderType::guiTextured, HEART_FULL_BLINKING_SPRITE, p_283173_ + i1 * k, p_282904_, 9, 9); } if (i1 * 2 + 1 == playertaboverlay$healthstate.displayedValue()) { p_281723_.blitSprite(RenderType::guiTextured, HEART_HALF_BLINKING_SPRITE, p_283173_ + i1 * k, p_282904_, 9, 9); } } if (i1 * 2 + 1 < p_281354_) { p_281723_.blitSprite(RenderType::guiTextured, i1 >= 10 ? HEART_ABSORBING_FULL_BLINKING_SPRITE : HEART_FULL_SPRITE, p_283173_ + i1 * k, p_282904_, 9, 9); } if (i1 * 2 + 1 == p_281354_) { p_281723_.blitSprite(RenderType::guiTextured, i1 >= 10 ? HEART_ABSORBING_HALF_BLINKING_SPRITE : HEART_HALF_SPRITE, p_283173_ + i1 * k, p_282904_, 9, 9); } } } } } public void setFooter(@Nullable Component p_94555_) { this.footer = p_94555_; } public void setHeader(@Nullable Component p_94559_) { this.header = p_94559_; } public void reset() { this.header = null; this.footer = null; } @OnlyIn(Dist.CLIENT) 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 p_250562_) { this.displayedValue = p_250562_; this.lastValue = p_250562_; } public void update(int p_251066_, long p_251460_) { if (p_251066_ != this.lastValue) { long i = p_251066_ < this.lastValue ? 20L : 10L; this.blinkUntilTick = p_251460_ + i; this.lastValue = p_251066_; this.lastUpdateTick = p_251460_; } if (p_251460_ - this.lastUpdateTick > 20L) { this.displayedValue = p_251066_; } } public int displayedValue() { return this.displayedValue; } public boolean isBlinking(long p_251847_) { return this.blinkUntilTick > p_251847_ && (this.blinkUntilTick - p_251847_) % 6L >= 3L; } } @OnlyIn(Dist.CLIENT) static record ScoreDisplayEntry(Component name, int score, @Nullable Component formattedScore, int scoreWidth) { } }