313 lines
17 KiB
Java
313 lines
17 KiB
Java
/*
|
|
* 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<PlayerInfo> 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<UUID, HealthState> 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<PlayerInfo> 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<PlayerInfo> playerInfos = this.getPlayerInfos();
|
|
ArrayList<ScoreDisplayEntry> entriesToDisplay = new ArrayList<ScoreDisplayEntry>(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<FormattedCharSequence> 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<FormattedCharSequence> 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;
|
|
}
|
|
}
|
|
}
|
|
|