diff --git a/connector/src/main/java/org/geysermc/connector/entity/Entity.java b/connector/src/main/java/org/geysermc/connector/entity/Entity.java index f8bf9693d..6dfaad337 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/Entity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/Entity.java @@ -41,7 +41,6 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import lombok.Getter; import lombok.Setter; import net.kyori.adventure.text.Component; -import org.geysermc.connector.entity.living.ArmorStandEntity; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.entity.type.EntityType; import org.geysermc.connector.network.session.GeyserSession; @@ -256,10 +255,7 @@ public class Entity { setAir((int) entityMetadata.getValue()); break; case 2: // custom name - if (entityMetadata.getValue() instanceof Component message) { - // Always translate even if it's a TextMessage since there could be translatable parameters - metadata.put(EntityData.NAMETAG, MessageTranslator.convertMessage(message, session.getLocale())); - } + setDisplayName(session, (Component) entityMetadata.getValue()); break; case 3: // is custom name visible if (!this.is(PlayerEntity.class)) @@ -310,6 +306,21 @@ public class Entity { return false; } + /** + * @return the translated string display + */ + protected String setDisplayName(GeyserSession session, Component name) { + if (name != null) { + String displayName = MessageTranslator.convertMessage(name, session.getLocale()); + metadata.put(EntityData.NAMETAG, displayName); + return displayName; + } else if (!metadata.getString(EntityData.NAMETAG).isEmpty()) { + // Clear nametag + metadata.put(EntityData.NAMETAG, ""); + } + return null; + } + /** * Set the height and width of the entity's bounding box */ diff --git a/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java index 85493660b..e248d5c63 100644 --- a/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java +++ b/connector/src/main/java/org/geysermc/connector/entity/player/PlayerEntity.java @@ -29,6 +29,7 @@ import com.github.steveice10.mc.auth.data.GameProfile; import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose; import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition; +import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.nukkitx.math.vector.Vector3f; import com.nukkitx.math.vector.Vector3i; @@ -39,9 +40,11 @@ import com.nukkitx.protocol.bedrock.data.entity.EntityData; import com.nukkitx.protocol.bedrock.data.entity.EntityFlag; import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData; import com.nukkitx.protocol.bedrock.packet.*; +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import net.kyori.adventure.text.Component; +import org.geysermc.connector.common.ChatColor; import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.LivingEntity; import org.geysermc.connector.entity.living.animal.tameable.ParrotEntity; @@ -53,6 +56,7 @@ import org.geysermc.connector.scoreboard.Score; import org.geysermc.connector.scoreboard.Team; import org.geysermc.connector.scoreboard.UpdateType; +import javax.annotation.Nullable; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -64,6 +68,9 @@ public class PlayerEntity extends LivingEntity { private String username; private boolean playerList = true; // Player is in the player list + @Getter(AccessLevel.NONE) + private String displayName; + /** * Saves the parrot currently on the player's left shoulder; otherwise null */ @@ -79,6 +86,7 @@ public class PlayerEntity extends LivingEntity { profile = gameProfile; uuid = gameProfile.getId(); username = gameProfile.getName(); + displayName = username; // For the OptionalPack, set all bits as invisible by default as this matches Java Edition behavior metadata.put(EntityData.MARK_VARIANT, 0xff); @@ -240,23 +248,6 @@ public class PlayerEntity extends LivingEntity { public void updateBedrockMetadata(EntityMetadata entityMetadata, GeyserSession session) { super.updateBedrockMetadata(entityMetadata, session); - if (entityMetadata.getId() == 2) { - String username = this.username; - Component name = (Component) entityMetadata.getValue(); - if (name != null) { - username = MessageTranslator.convertMessage(name); - } - Team team = session.getWorldCache().getScoreboard().getTeamFor(username); - if (team != null) { - String displayName = ""; - if (team.isVisibleFor(session.getPlayerEntity().getUsername())) { - displayName = MessageTranslator.toChatColor(team.getColor()) + username; - displayName = team.getCurrentData().getDisplayName(displayName); - } - metadata.put(EntityData.NAMETAG, displayName); - } - } - // Extra hearts - is not metadata but an attribute on Bedrock if (entityMetadata.getId() == 15) { UpdateAttributesPacket attributesPacket = new UpdateAttributesPacket(); @@ -319,6 +310,65 @@ public class PlayerEntity extends LivingEntity { } } + @Override + protected String setDisplayName(GeyserSession session, Component name) { + String displayName = super.setDisplayName(session, name); + this.displayName = displayName != null ? displayName : username; + // Update if we know this player has a team + updateDisplayName(session, null, false); + + return this.displayName; + } + + //todo this will become common entity logic once UUID support is implemented for them + /** + * @param useGivenTeam even if there is no team, update the username in the entity metadata anyway, and don't look for a team + */ + public void updateDisplayName(GeyserSession session, @Nullable Team team, boolean useGivenTeam) { + if (team == null && !useGivenTeam) { + // Only search for the team if we are not supposed to use the given team + // If the given team is null, this is intentional that we are being removed from the team + team = session.getWorldCache().getScoreboard().getTeamFor(username); + } + + boolean needsUpdate; + String newDisplayName = this.displayName; + if (team != null) { + if (team.isVisibleFor(session.getPlayerEntity().getUsername())) { + TeamColor color = team.getColor(); + String chatColor; + if (color == TeamColor.NONE) { + chatColor = ChatColor.RESET; + } else { + chatColor = MessageTranslator.toChatColor(color); + } + // We have to emulate what modern Java text already does for us and add the color to each section + String prefix = team.getCurrentData().getPrefix(); + String suffix = team.getCurrentData().getSuffix(); + newDisplayName = chatColor + prefix + chatColor + this.displayName + chatColor + suffix; + } else { + // The name is not visible to the session player; clear name + newDisplayName = ""; + } + needsUpdate = useGivenTeam && !newDisplayName.equals(metadata.getString(EntityData.NAMETAG, null)); + metadata.put(EntityData.NAMETAG, newDisplayName); + } else if (useGivenTeam) { + // The name has reset, if it was previously something else + needsUpdate = !newDisplayName.equals(metadata.getString(EntityData.NAMETAG)); + metadata.put(EntityData.NAMETAG, this.displayName); + } else { + needsUpdate = false; + } + + if (needsUpdate) { + // Update the metadata as it won't be updated later + SetEntityDataPacket packet = new SetEntityDataPacket(); + packet.getMetadata().put(EntityData.NAMETAG, newDisplayName); + packet.setRuntimeEntityId(geyserId); + session.sendUpstreamPacket(packet); + } + } + @Override protected void setDimensions(Pose pose) { float height; diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java index afc90fbe0..3ff547c95 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/EntityCache.java @@ -25,13 +25,16 @@ package org.geysermc.connector.network.session.cache; -import it.unimi.dsi.fastutil.longs.*; +import it.unimi.dsi.fastutil.longs.Long2LongMap; +import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.Getter; -import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.entity.Entity; +import org.geysermc.connector.entity.Tickable; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java index e7aea8909..73d4d7dfc 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java @@ -28,8 +28,6 @@ package org.geysermc.connector.network.translators.chat; import com.github.steveice10.mc.protocol.data.DefaultComponentSerializer; import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.renderer.TranslatableComponentRenderer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.legacyimpl.NBTLegacyHoverEventSerializer; @@ -38,9 +36,10 @@ import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; -import java.util.HashMap; +import java.util.EnumMap; import java.util.Locale; import java.util.Map; +import java.util.regex.Pattern; public class MessageTranslator { @@ -54,7 +53,7 @@ public class MessageTranslator { .build(); // Store team colors for player names - private static final Map TEAM_FORMATS = new HashMap<>(); + private static final Map TEAM_COLORS = new EnumMap<>(TeamColor.class); // Legacy formatting character private static final String BASE = "\u00a7"; @@ -62,11 +61,36 @@ public class MessageTranslator { // Reset character private static final String RESET = BASE + "r"; + /* Various regexes to fix formatting for Bedrock's specifications */ + private static final Pattern STRIKETHROUGH_UNDERLINE = Pattern.compile("\u00a7[mn]"); + private static final Pattern COLOR_CHARACTERS = Pattern.compile("\u00a7([0-9a-f])"); + private static final Pattern DOUBLE_RESET = Pattern.compile("\u00a7r\u00a7r"); + static { - TEAM_FORMATS.put(TeamColor.OBFUSCATED, TextDecoration.OBFUSCATED); - TEAM_FORMATS.put(TeamColor.BOLD, TextDecoration.BOLD); - TEAM_FORMATS.put(TeamColor.STRIKETHROUGH, TextDecoration.STRIKETHROUGH); - TEAM_FORMATS.put(TeamColor.ITALIC, TextDecoration.ITALIC); + TEAM_COLORS.put(TeamColor.NONE, ""); + + TEAM_COLORS.put(TeamColor.BLACK, BASE + "0"); + TEAM_COLORS.put(TeamColor.DARK_BLUE, BASE + "1"); + TEAM_COLORS.put(TeamColor.DARK_GREEN, BASE + "2"); + TEAM_COLORS.put(TeamColor.DARK_AQUA, BASE + "3"); + TEAM_COLORS.put(TeamColor.DARK_RED, BASE + "4"); + TEAM_COLORS.put(TeamColor.DARK_PURPLE, BASE + "5"); + TEAM_COLORS.put(TeamColor.GOLD, BASE + "6"); + TEAM_COLORS.put(TeamColor.GRAY, BASE + "7"); + TEAM_COLORS.put(TeamColor.DARK_GRAY, BASE + "8"); + TEAM_COLORS.put(TeamColor.BLUE, BASE + "9"); + TEAM_COLORS.put(TeamColor.GREEN, BASE + "a"); + TEAM_COLORS.put(TeamColor.AQUA, BASE + "b"); + TEAM_COLORS.put(TeamColor.RED, BASE + "c"); + TEAM_COLORS.put(TeamColor.LIGHT_PURPLE, BASE + "d"); + TEAM_COLORS.put(TeamColor.YELLOW, BASE + "e"); + TEAM_COLORS.put(TeamColor.WHITE, BASE + "f"); + + // Formats, not colors + TEAM_COLORS.put(TeamColor.OBFUSCATED, BASE + "k"); + TEAM_COLORS.put(TeamColor.BOLD, BASE + "l"); + TEAM_COLORS.put(TeamColor.STRIKETHROUGH, BASE + "m"); + TEAM_COLORS.put(TeamColor.ITALIC, BASE + "o"); // Tell MCProtocolLib to use our serializer DefaultComponentSerializer.set(GSON_SERIALIZER); @@ -88,12 +112,12 @@ public class MessageTranslator { String legacy = LegacyComponentSerializer.legacySection().serialize(message); // Strip strikethrough and underline as they are not supported on bedrock - legacy = legacy.replaceAll("\u00a7[mn]", ""); + legacy = STRIKETHROUGH_UNDERLINE.matcher(legacy).replaceAll(""); // Make color codes reset formatting like Java // See https://minecraft.gamepedia.com/Formatting_codes#Usage - legacy = legacy.replaceAll("\u00a7([0-9a-f])", "\u00a7r\u00a7$1"); - legacy = legacy.replaceAll("\u00a7r\u00a7r", "\u00a7r"); + legacy = COLOR_CHARACTERS.matcher(legacy).replaceAll("\u00a7r\u00a7$1"); + legacy = DOUBLE_RESET.matcher(legacy).replaceAll("\u00a7r"); return legacy; } catch (Exception e) { @@ -158,84 +182,6 @@ public class MessageTranslator { return GSON_SERIALIZER.serialize(component); } - /** - * Convert a {@link NamedTextColor} into a string for inserting into messages - * - * @param color {@link NamedTextColor} to convert - * @return The converted color string - */ - private static String getColor(NamedTextColor color) { - StringBuilder str = new StringBuilder(BASE); - if (color.equals(NamedTextColor.BLACK)) { - str.append("0"); - } else if (color.equals(NamedTextColor.DARK_BLUE)) { - str.append("1"); - } else if (color.equals(NamedTextColor.DARK_GREEN)) { - str.append("2"); - } else if (color.equals(NamedTextColor.DARK_AQUA)) { - str.append("3"); - } else if (color.equals(NamedTextColor.DARK_RED)) { - str.append("4"); - } else if (color.equals(NamedTextColor.DARK_PURPLE)) { - str.append("5"); - } else if (color.equals(NamedTextColor.GOLD)) { - str.append("6"); - } else if (color.equals(NamedTextColor.GRAY)) { - str.append("7"); - } else if (color.equals(NamedTextColor.DARK_GRAY)) { - str.append("8"); - } else if (color.equals(NamedTextColor.BLUE)) { - str.append("9"); - } else if (color.equals(NamedTextColor.GREEN)) { - str.append("a"); - } else if (color.equals(NamedTextColor.AQUA)) { - str.append("b"); - } else if (color.equals(NamedTextColor.RED)) { - str.append("c"); - } else if (color.equals(NamedTextColor.LIGHT_PURPLE)) { - str.append("d"); - } else if (color.equals(NamedTextColor.YELLOW)) { - str.append("e"); - } else if (color.equals(NamedTextColor.WHITE)) { - str.append("f"); - } else { - return ""; - } - - return str.toString(); - } - - /** - * Convert a {@link TextDecoration} into a string for inserting into messages - * - * @param format {@link TextDecoration} to convert - * @return The converted chat formatting string - */ - private static String getFormat(TextDecoration format) { - StringBuilder str = new StringBuilder(BASE); - switch (format) { - case OBFUSCATED: - str.append("k"); - break; - case BOLD: - str.append("l"); - break; - case STRIKETHROUGH: - str.append("m"); - break; - case UNDERLINED: - str.append("n"); - break; - case ITALIC: - str.append("o"); - break; - default: - return ""; - } - - return str.toString(); - } - /** * Convert a team color to a chat color * @@ -243,16 +189,7 @@ public class MessageTranslator { * @return The chat color character */ public static String toChatColor(TeamColor teamColor) { - if (teamColor.equals(TeamColor.NONE)) { - return ""; - } - - NamedTextColor textColor = NamedTextColor.NAMES.value(teamColor.name().toLowerCase()); - if (textColor != null) { - return getColor(textColor); - } - - return getFormat(TEAM_FORMATS.get(teamColor)); + return TEAM_COLORS.getOrDefault(teamColor, ""); } /** diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaTeamTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaTeamTranslator.java index c4b2eacd5..e309209aa 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaTeamTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/scoreboard/JavaTeamTranslator.java @@ -25,7 +25,9 @@ package org.geysermc.connector.network.translators.java.scoreboard; +import com.github.steveice10.mc.protocol.data.game.scoreboard.NameTagVisibility; import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamAction; +import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor; import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerTeamPacket; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserLogger; @@ -37,9 +39,9 @@ import org.geysermc.connector.scoreboard.Scoreboard; import org.geysermc.connector.scoreboard.ScoreboardUpdater; import org.geysermc.connector.scoreboard.Team; import org.geysermc.connector.scoreboard.UpdateType; -import org.geysermc.connector.utils.LanguageUtils; import java.util.Arrays; +import java.util.Set; @Translator(packet = ServerTeamPacket.class) public class JavaTeamTranslator extends PacketTranslator { @@ -60,48 +62,77 @@ public class JavaTeamTranslator extends PacketTranslator { Scoreboard scoreboard = session.getWorldCache().getScoreboard(); Team team = scoreboard.getTeam(packet.getTeamName()); switch (packet.getAction()) { - case CREATE -> - scoreboard.registerNewTeam(packet.getTeamName(), packet.getPlayers()) - .setName(MessageTranslator.convertMessage(packet.getDisplayName())) - .setColor(packet.getColor()) - .setNameTagVisibility(packet.getNameTagVisibility()) - .setPrefix(MessageTranslator.convertMessage(packet.getPrefix(), session.getLocale())) - .setSuffix(MessageTranslator.convertMessage(packet.getSuffix(), session.getLocale())); + case CREATE -> { + team = scoreboard.registerNewTeam(packet.getTeamName(), packet.getPlayers()) + .setName(MessageTranslator.convertMessage(packet.getDisplayName())) + .setColor(packet.getColor()) + .setNameTagVisibility(packet.getNameTagVisibility()) + .setPrefix(MessageTranslator.convertMessage(packet.getPrefix(), session.getLocale())) + .setSuffix(MessageTranslator.convertMessage(packet.getSuffix(), session.getLocale())); + + if (packet.getPlayers().length != 0) { + if ((team.getNameTagVisibility() != NameTagVisibility.ALWAYS && !team.isVisibleFor(session.getPlayerEntity().getUsername())) + || team.getColor() != TeamColor.NONE + || !team.getCurrentData().getPrefix().isEmpty() + || !team.getCurrentData().getSuffix().isEmpty()) { + // Something is here that would modify entity names + scoreboard.updateEntityNames(team, true); + } + } + } case UPDATE -> { if (team == null) { - logger.debug(LanguageUtils.getLocaleStringLog( - "geyser.network.translator.team.failed_not_registered", - packet.getAction(), packet.getTeamName() - )); + if (logger.isDebug()) { + logger.debug("Error while translating Team Packet " + packet.getAction() + + "! Scoreboard Team " + packet.getTeamName() + " is not registered." + ); + } return; } + TeamColor oldColor = team.getColor(); + NameTagVisibility oldVisibility = team.getNameTagVisibility(); + String oldPrefix = team.getCurrentData().getPrefix(); + String oldSuffix = team.getCurrentData().getSuffix(); + team.setName(MessageTranslator.convertMessage(packet.getDisplayName())) .setColor(packet.getColor()) .setNameTagVisibility(packet.getNameTagVisibility()) .setPrefix(MessageTranslator.convertMessage(packet.getPrefix(), session.getLocale())) .setSuffix(MessageTranslator.convertMessage(packet.getSuffix(), session.getLocale())) .setUpdateType(UpdateType.UPDATE); + + if (oldVisibility != team.getNameTagVisibility() + || oldColor != team.getColor() + || !oldPrefix.equals(team.getCurrentData().getPrefix()) + || !oldSuffix.equals(team.getCurrentData().getSuffix())) { + // Update entities attached to this team as something about their nameplates have changed + scoreboard.updateEntityNames(team, false); + } } case ADD_PLAYER -> { if (team == null) { - logger.debug(LanguageUtils.getLocaleStringLog( - "geyser.network.translator.team.failed_not_registered", - packet.getAction(), packet.getTeamName() - )); + if (logger.isDebug()) { + logger.debug("Error while translating Team Packet " + packet.getAction() + + "! Scoreboard Team " + packet.getTeamName() + " is not registered." + ); + } return; } - team.addEntities(packet.getPlayers()); + Set added = team.addEntities(packet.getPlayers()); + scoreboard.updateEntityNames(team, added, true); } case REMOVE_PLAYER -> { if (team == null) { - logger.debug(LanguageUtils.getLocaleStringLog( - "geyser.network.translator.team.failed_not_registered", - packet.getAction(), packet.getTeamName() - )); + if (logger.isDebug()) { + logger.debug("Error while translating Team Packet " + packet.getAction() + + "! Scoreboard Team " + packet.getTeamName() + " is not registered." + ); + } return; } - team.removeEntities(packet.getPlayers()); + Set removed = team.removeEntities(packet.getPlayers()); + scoreboard.updateEntityNames(null, removed, true); } case REMOVE -> scoreboard.removeTeam(packet.getTeamName()); } diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java index f194b97f4..3596907e7 100644 --- a/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Scoreboard.java @@ -33,10 +33,12 @@ import com.nukkitx.protocol.bedrock.packet.SetScorePacket; import lombok.Getter; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserLogger; +import org.geysermc.connector.entity.Entity; import org.geysermc.connector.entity.player.PlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; +import javax.annotation.Nullable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; @@ -127,7 +129,8 @@ public final class Scoreboard { return team; } - team = new Team(this, teamName).addEntities(players); + team = new Team(this, teamName); + team.addEntities(players); teams.put(teamName, team); return team; } @@ -155,6 +158,9 @@ public final class Scoreboard { Team remove = teams.remove(teamName); if (remove != null) { remove.setUpdateType(REMOVE); + // We need to use the direct entities list here, so #refreshSessionPlayerDisplays also updates accordingly + // With the player's lack of a team in visibility checks + updateEntityNames(remove, remove.getEntities(), true); } } @@ -326,4 +332,47 @@ public final class Scoreboard { } return null; } + + /** + * Updates the display names of all entities in a given team. + * @param teamChange the players have either joined or left the team. Used for optimizations when just the display name updated. + */ + public void updateEntityNames(Team team, boolean teamChange) { + Set names = new HashSet<>(team.getEntities()); + updateEntityNames(team, names, teamChange); + } + + /** + * Updates the display name of a set of entities within a given team. The team may also be null if the set is being removed + * from a team. + */ + public void updateEntityNames(@Nullable Team team, Set names, boolean teamChange) { + if (names.remove(session.getPlayerEntity().getUsername()) && teamChange) { + // If the player's team changed, then other entities' teams may modify their visibility based on team status + refreshSessionPlayerDisplays(); + } + if (!names.isEmpty()) { + for (Entity entity : session.getEntityCache().getEntities().values()) { + // This more complex logic is for the future to iterate over all entities, not just players + if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) { + player.updateDisplayName(session, team, true); + if (names.isEmpty()) { + break; + } + } + } + } + } + + /** + * If the team's player was refreshed, then we need to go through every entity and check... + */ + private void refreshSessionPlayerDisplays() { + for (Entity entity : session.getEntityCache().getEntities().values()) { + if (entity instanceof PlayerEntity player) { + Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername()); + player.updateDisplayName(session, playerTeam, true); + } + } + } } diff --git a/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java b/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java index 41891ee5b..b5aef31d0 100644 --- a/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java +++ b/connector/src/main/java/org/geysermc/connector/scoreboard/Team.java @@ -33,8 +33,7 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; -import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; import java.util.Set; @Getter @@ -43,7 +42,7 @@ public final class Team { private final Scoreboard scoreboard; private final String id; - @Getter(AccessLevel.NONE) + @Getter(AccessLevel.PACKAGE) private final Set entities; @Setter private NameTagVisibility nameTagVisibility; @Setter private TeamColor color; @@ -60,16 +59,16 @@ public final class Team { entities = new ObjectOpenHashSet<>(); } - public Team addEntities(String... names) { - List added = new ArrayList<>(); + public Set addEntities(String... names) { + Set added = new HashSet<>(); for (String name : names) { if (entities.add(name)) { added.add(name); } } - if (added.size() == 0) { - return this; + if (added.isEmpty()) { + return added; } // we don't have to change the updateType, // because the scores itself need updating, not the team @@ -81,13 +80,21 @@ public final class Team { } } } - return this; + + return added; } - public void removeEntities(String... names) { + /** + * @return all removed entities from this team + */ + public Set removeEntities(String... names) { + Set removed = new HashSet<>(); for (String name : names) { - entities.remove(name); + if (entities.remove(name)) { + removed.add(name); + } } + return removed; } public boolean hasEntity(String name) { @@ -172,7 +179,11 @@ public final class Team { public boolean isVisibleFor(String entity) { return switch (nameTagVisibility) { - case HIDE_FOR_OTHER_TEAMS -> hasEntity(entity); + case HIDE_FOR_OTHER_TEAMS -> { + // Player must be in a team in order for HIDE_FOR_OTHER_TEAMS to be triggered + Team team = scoreboard.getTeamFor(entity); + yield team == null || team == this; + } case HIDE_FOR_OWN_TEAM -> !hasEntity(entity); case ALWAYS -> true; case NEVER -> false;