minecraft-src/net/minecraft/client/gui/components/PlayerTabOverlay.java
2025-07-04 03:15:13 +03:00

382 lines
15 KiB
Java

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.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
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.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.RenderType;
import org.jetbrains.annotations.Nullable;
@Environment(EnvType.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<PlayerInfo> PLAYER_COMPARATOR = Comparator.comparingInt(playerInfo -> -playerInfo.getTabListOrder())
.thenComparingInt(playerInfo -> playerInfo.getGameMode() == GameType.SPECTATOR ? 1 : 0)
.thenComparing(playerInfo -> Optionull.mapOrDefault(playerInfo.getTeam(), PlayerTeam::getName, ""))
.thenComparing(playerInfo -> playerInfo.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;
/**
* Weither or not the playerlist is currently being rendered
*/
private boolean visible;
private final Map<UUID, PlayerTabOverlay.HealthState> healthStates = new Object2ObjectOpenHashMap<>();
public PlayerTabOverlay(Minecraft minecraft, Gui gui) {
this.minecraft = minecraft;
this.gui = gui;
}
public Component getNameForDisplay(PlayerInfo playerInfo) {
return playerInfo.getTabListDisplayName() != null
? this.decorateName(playerInfo, playerInfo.getTabListDisplayName().copy())
: this.decorateName(playerInfo, PlayerTeam.formatNameForTeam(playerInfo.getTeam(), Component.literal(playerInfo.getProfile().getName())));
}
private Component decorateName(PlayerInfo playerInfo, MutableComponent name) {
return playerInfo.getGameMode() == GameType.SPECTATOR ? name.withStyle(ChatFormatting.ITALIC) : name;
}
/**
* Called by GuiIngame to update the information stored in the playerlist, does not actually render the list, however.
*/
public void setVisible(boolean visible) {
if (this.visible != visible) {
this.healthStates.clear();
this.visible = visible;
if (visible) {
Component component = ComponentUtils.formatList(this.getPlayerInfos(), Component.literal(", "), this::getNameForDisplay);
this.minecraft.getNarrator().sayNow(Component.translatable("multiplayer.player.list.narration", component));
}
}
}
private List<PlayerInfo> getPlayerInfos() {
return this.minecraft.player.connection.getListedOnlinePlayers().stream().sorted(PLAYER_COMPARATOR).limit(80L).toList();
}
public void render(GuiGraphics guiGraphics, int width, Scoreboard scoreboard, @Nullable Objective objective) {
List<PlayerInfo> list = this.getPlayerInfos();
List<PlayerTabOverlay.ScoreDisplayEntry> list2 = 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 component2 = null;
int m = 0;
if (objective != null) {
ScoreHolder scoreHolder = ScoreHolder.fromGameProfile(playerInfo.getProfile());
ReadOnlyScoreInfo readOnlyScoreInfo = scoreboard.getPlayerScoreInfo(scoreHolder, objective);
if (readOnlyScoreInfo != null) {
l = readOnlyScoreInfo.value();
}
if (objective.getRenderType() != RenderType.HEARTS) {
NumberFormat numberFormat = objective.numberFormatOrDefault(StyledFormat.PLAYER_LIST_DEFAULT);
component2 = ReadOnlyScoreInfo.safeFormatValue(readOnlyScoreInfo, numberFormat);
m = this.minecraft.font.width(component2);
k = Math.max(k, m > 0 ? i + m : 0);
}
}
list2.add(new PlayerTabOverlay.ScoreDisplayEntry(component, l, component2, m));
}
if (!this.healthStates.isEmpty()) {
Set<UUID> set = (Set<UUID>)list.stream().map(playerInfo -> playerInfo.getProfile().getId()).collect(Collectors.toSet());
this.healthStates.keySet().removeIf(uUID -> !set.contains(uUID));
}
int n = list.size();
int o = n;
int p;
for (p = 1; o > 20; o = (n + p - 1) / p) {
p++;
}
boolean bl = this.minecraft.isLocalServer() || this.minecraft.getConnection().getConnection().isEncrypted();
int q;
if (objective != null) {
if (objective.getRenderType() == RenderType.HEARTS) {
q = 90;
} else {
q = k;
}
} else {
q = 0;
}
int m = Math.min(p * ((bl ? 9 : 0) + j + q + 13), width - 50) / p;
int r = width / 2 - (m * p + (p - 1) * 5) / 2;
int s = 10;
int t = m * p + (p - 1) * 5;
List<FormattedCharSequence> list3 = null;
if (this.header != null) {
list3 = this.minecraft.font.split(this.header, width - 50);
for (FormattedCharSequence formattedCharSequence : list3) {
t = Math.max(t, this.minecraft.font.width(formattedCharSequence));
}
}
List<FormattedCharSequence> list4 = null;
if (this.footer != null) {
list4 = this.minecraft.font.split(this.footer, width - 50);
for (FormattedCharSequence formattedCharSequence2 : list4) {
t = Math.max(t, this.minecraft.font.width(formattedCharSequence2));
}
}
if (list3 != null) {
guiGraphics.fill(width / 2 - t / 2 - 1, s - 1, width / 2 + t / 2 + 1, s + list3.size() * 9, Integer.MIN_VALUE);
for (FormattedCharSequence formattedCharSequence2 : list3) {
int u = this.minecraft.font.width(formattedCharSequence2);
guiGraphics.drawString(this.minecraft.font, formattedCharSequence2, width / 2 - u / 2, s, -1);
s += 9;
}
s++;
}
guiGraphics.fill(width / 2 - t / 2 - 1, s - 1, width / 2 + t / 2 + 1, s + o * 9, Integer.MIN_VALUE);
int v = this.minecraft.options.getBackgroundColor(553648127);
for (int w = 0; w < n; w++) {
int u = w / o;
int x = w % o;
int y = r + u * m + u * 5;
int z = s + x * 9;
guiGraphics.fill(y, z, y + m, z + 8, v);
if (w < list.size()) {
PlayerInfo playerInfo2 = (PlayerInfo)list.get(w);
PlayerTabOverlay.ScoreDisplayEntry scoreDisplayEntry = (PlayerTabOverlay.ScoreDisplayEntry)list2.get(w);
GameProfile gameProfile = playerInfo2.getProfile();
if (bl) {
Player player = this.minecraft.level.getPlayerByUUID(gameProfile.getId());
boolean bl2 = player != null && LivingEntityRenderer.isEntityUpsideDown(player);
PlayerFaceRenderer.draw(guiGraphics, playerInfo2.getSkin().texture(), y, z, 8, playerInfo2.showHat(), bl2, -1);
y += 9;
}
guiGraphics.drawString(this.minecraft.font, scoreDisplayEntry.name, y, z, playerInfo2.getGameMode() == GameType.SPECTATOR ? -1862270977 : -1);
if (objective != null && playerInfo2.getGameMode() != GameType.SPECTATOR) {
int aa = y + j + 1;
int ab = aa + q;
if (ab - aa > 5) {
this.renderTablistScore(objective, z, scoreDisplayEntry, aa, ab, gameProfile.getId(), guiGraphics);
}
}
this.renderPingIcon(guiGraphics, m, y - (bl ? 9 : 0), z, playerInfo2);
}
}
if (list4 != null) {
s += o * 9 + 1;
guiGraphics.fill(width / 2 - t / 2 - 1, s - 1, width / 2 + t / 2 + 1, s + list4.size() * 9, Integer.MIN_VALUE);
for (FormattedCharSequence formattedCharSequence3 : list4) {
int x = this.minecraft.font.width(formattedCharSequence3);
guiGraphics.drawString(this.minecraft.font, formattedCharSequence3, width / 2 - x / 2, s, -1);
s += 9;
}
}
}
protected void renderPingIcon(GuiGraphics guiGraphics, int width, int x, int y, PlayerInfo playerInfo) {
ResourceLocation resourceLocation;
if (playerInfo.getLatency() < 0) {
resourceLocation = PING_UNKNOWN_SPRITE;
} else if (playerInfo.getLatency() < 150) {
resourceLocation = PING_5_SPRITE;
} else if (playerInfo.getLatency() < 300) {
resourceLocation = PING_4_SPRITE;
} else if (playerInfo.getLatency() < 600) {
resourceLocation = PING_3_SPRITE;
} else if (playerInfo.getLatency() < 1000) {
resourceLocation = PING_2_SPRITE;
} else {
resourceLocation = PING_1_SPRITE;
}
guiGraphics.pose().pushPose();
guiGraphics.pose().translate(0.0F, 0.0F, 100.0F);
guiGraphics.blitSprite(net.minecraft.client.renderer.RenderType::guiTextured, resourceLocation, x + width - 11, y, 10, 8);
guiGraphics.pose().popPose();
}
private void renderTablistScore(
Objective objective, int y, PlayerTabOverlay.ScoreDisplayEntry displayEntry, int minX, int maxX, UUID playerUuid, GuiGraphics guiGraphics
) {
if (objective.getRenderType() == RenderType.HEARTS) {
this.renderTablistHearts(y, minX, maxX, playerUuid, guiGraphics, displayEntry.score);
} else if (displayEntry.formattedScore != null) {
guiGraphics.drawString(this.minecraft.font, displayEntry.formattedScore, maxX - displayEntry.scoreWidth, y, 16777215);
}
}
private void renderTablistHearts(int y, int minX, int maxX, UUID playerUuid, GuiGraphics guiGraphics, int health) {
PlayerTabOverlay.HealthState healthState = (PlayerTabOverlay.HealthState)this.healthStates
.computeIfAbsent(playerUuid, uUID -> new PlayerTabOverlay.HealthState(health));
healthState.update(health, this.gui.getGuiTicks());
int i = Mth.positiveCeilDiv(Math.max(health, healthState.displayedValue()), 2);
int j = Math.max(health, Math.max(healthState.displayedValue(), 20)) / 2;
boolean bl = healthState.isBlinking(this.gui.getGuiTicks());
if (i > 0) {
int k = Mth.floor(Math.min((float)(maxX - minX - 4) / j, 9.0F));
if (k <= 3) {
float f = Mth.clamp(health / 20.0F, 0.0F, 1.0F);
int l = (int)((1.0F - f) * 255.0F) << 16 | (int)(f * 255.0F) << 8;
float g = health / 2.0F;
Component component = Component.translatable("multiplayer.player.list.hp", g);
Component component2;
if (maxX - this.minecraft.font.width(component) >= minX) {
component2 = component;
} else {
component2 = Component.literal(Float.toString(g));
}
guiGraphics.drawString(this.minecraft.font, component2, (maxX + minX - this.minecraft.font.width(component2)) / 2, y, l);
} else {
ResourceLocation resourceLocation = bl ? HEART_CONTAINER_BLINKING_SPRITE : HEART_CONTAINER_SPRITE;
for (int l = i; l < j; l++) {
guiGraphics.blitSprite(net.minecraft.client.renderer.RenderType::guiTextured, resourceLocation, minX + l * k, y, 9, 9);
}
for (int l = 0; l < i; l++) {
guiGraphics.blitSprite(net.minecraft.client.renderer.RenderType::guiTextured, resourceLocation, minX + l * k, y, 9, 9);
if (bl) {
if (l * 2 + 1 < healthState.displayedValue()) {
guiGraphics.blitSprite(net.minecraft.client.renderer.RenderType::guiTextured, HEART_FULL_BLINKING_SPRITE, minX + l * k, y, 9, 9);
}
if (l * 2 + 1 == healthState.displayedValue()) {
guiGraphics.blitSprite(net.minecraft.client.renderer.RenderType::guiTextured, HEART_HALF_BLINKING_SPRITE, minX + l * k, y, 9, 9);
}
}
if (l * 2 + 1 < health) {
guiGraphics.blitSprite(
net.minecraft.client.renderer.RenderType::guiTextured, l >= 10 ? HEART_ABSORBING_FULL_BLINKING_SPRITE : HEART_FULL_SPRITE, minX + l * k, y, 9, 9
);
}
if (l * 2 + 1 == health) {
guiGraphics.blitSprite(
net.minecraft.client.renderer.RenderType::guiTextured, l >= 10 ? HEART_ABSORBING_HALF_BLINKING_SPRITE : HEART_HALF_SPRITE, minX + l * k, y, 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;
}
@Environment(EnvType.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 displayedValue) {
this.displayedValue = displayedValue;
this.lastValue = displayedValue;
}
public void update(int value, long guiTicks) {
if (value != this.lastValue) {
long l = value < this.lastValue ? 20L : 10L;
this.blinkUntilTick = guiTicks + l;
this.lastValue = value;
this.lastUpdateTick = guiTicks;
}
if (guiTicks - this.lastUpdateTick > 20L) {
this.displayedValue = value;
}
}
public int displayedValue() {
return this.displayedValue;
}
public boolean isBlinking(long guiTicks) {
return this.blinkUntilTick > guiTicks && (this.blinkUntilTick - guiTicks) % 6L >= 3L;
}
}
@Environment(EnvType.CLIENT)
record ScoreDisplayEntry(Component name, int score, @Nullable Component formattedScore, int scoreWidth) {
}
}