From 32a00750d2f1dd537112cf31d3463872053d9e8d Mon Sep 17 00:00:00 2001 From: Pablete1234 Date: Fri, 25 Nov 2022 06:48:21 +0100 Subject: [PATCH] Fixes & cache username rendering Signed-off-by: Pablete1234 --- .../pgm/api/player/MatchPlayerResolver.java | 7 + .../oc/pgm/api/player/MatchPlayerState.java | 16 ++ .../oc/pgm/api/player/ParticipantState.java | 2 + .../main/java/tc/oc/pgm/db/UsernameImpl.java | 5 +- .../tc/oc/pgm/destroyable/Destroyable.java | 3 +- core/src/main/java/tc/oc/pgm/ffa/Tribute.java | 11 +- .../inventory/ViewInventoryMatchModule.java | 2 +- .../oc/pgm/listeners/AntiGriefListener.java | 25 ++- .../java/tc/oc/pgm/match/MatchPlayerImpl.java | 4 +- .../tc/oc/pgm/match/MatchPlayerStateImpl.java | 108 ++++++++----- .../tc/oc/pgm/match/ParticipantStateImpl.java | 3 +- .../ConfigDecorationProvider.java | 7 +- .../NameDecorationRegistryImpl.java | 4 +- .../java/tc/oc/pgm/spawns/states/Dead.java | 4 +- .../java/tc/oc/pgm/stats/PlayerStats.java | 10 +- .../tc/oc/pgm/stats/StatsMatchModule.java | 78 +++------- .../stats/menu/items/PlayerStatsMenuItem.java | 19 ++- .../stats/menu/items/TeamStatsMenuItem.java | 14 +- .../tc/oc/pgm/tablist/MatchTabManager.java | 4 +- .../java/tc/oc/pgm/util/PlayerComponent.java | 142 ------------------ .../oc/pgm/util/player/PlayerComponent.java | 107 +++++++++++++ .../tc/oc/pgm/util/player/PlayerData.java | 138 +++++++++++++++++ .../pgm/util/player/PlayerRelationship.java | 51 +++++++ .../tc/oc/pgm/util/player/PlayerRenderer.java | 134 +++++++++++++++++ .../oc/pgm/util/tablist/PlayerTabEntry.java | 9 +- .../tc/oc/pgm/util/text/NumberComponent.java | 38 +++++ .../tc/oc/pgm/util/text/TextTranslations.java | 14 +- .../oc/pgm/util/text/NumberComponentTest.java | 68 +++++++++ 28 files changed, 714 insertions(+), 313 deletions(-) delete mode 100644 core/src/main/java/tc/oc/pgm/util/PlayerComponent.java create mode 100644 core/src/main/java/tc/oc/pgm/util/player/PlayerComponent.java create mode 100644 core/src/main/java/tc/oc/pgm/util/player/PlayerData.java create mode 100644 core/src/main/java/tc/oc/pgm/util/player/PlayerRelationship.java create mode 100644 core/src/main/java/tc/oc/pgm/util/player/PlayerRenderer.java create mode 100644 util/src/main/java/tc/oc/pgm/util/text/NumberComponent.java create mode 100644 util/src/test/java/tc/oc/pgm/util/text/NumberComponentTest.java diff --git a/core/src/main/java/tc/oc/pgm/api/player/MatchPlayerResolver.java b/core/src/main/java/tc/oc/pgm/api/player/MatchPlayerResolver.java index 583d70a607..fb169aad96 100644 --- a/core/src/main/java/tc/oc/pgm/api/player/MatchPlayerResolver.java +++ b/core/src/main/java/tc/oc/pgm/api/player/MatchPlayerResolver.java @@ -30,6 +30,13 @@ default MatchPlayer getPlayer(@Nullable UUID playerId) { return playerId == null ? null : getPlayer(Bukkit.getPlayer(playerId)); } + @Nullable + default MatchPlayerState getPlayerState(@Nullable UUID playerId) { + if (playerId == null) return null; + MatchPlayer matchPlayer = getPlayer(playerId); + return matchPlayer == null ? null : matchPlayer.getState(); + } + @Nullable default MatchPlayerState getPlayerState(@Nullable Player player) { if (player == null) return null; diff --git a/core/src/main/java/tc/oc/pgm/api/player/MatchPlayerState.java b/core/src/main/java/tc/oc/pgm/api/player/MatchPlayerState.java index 9eb78d14d1..e453dcb854 100644 --- a/core/src/main/java/tc/oc/pgm/api/player/MatchPlayerState.java +++ b/core/src/main/java/tc/oc/pgm/api/player/MatchPlayerState.java @@ -3,6 +3,8 @@ import java.util.Optional; import java.util.UUID; import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.party.Party; import tc.oc.pgm.util.Audience; @@ -18,6 +20,7 @@ public interface MatchPlayerState extends Audience, Named { * * @return The {@link Match}. */ + @NotNull Match getMatch(); /** @@ -25,6 +28,7 @@ public interface MatchPlayerState extends Audience, Named { * * @return The {@link Party}. */ + @NotNull Party getParty(); /** @@ -32,6 +36,7 @@ public interface MatchPlayerState extends Audience, Named { * * @return The unique identifier. */ + @NotNull UUID getId(); /** @@ -39,8 +44,19 @@ public interface MatchPlayerState extends Audience, Named { * * @return The last known {@link Location}. */ + @NotNull Location getLocation(); + /** @return if the player is currently dead */ + boolean isDead(); + + /** @return if the player is vanished */ + boolean isVanished(); + + /** @return the players' current nick */ + @Nullable + String getNick(); + /** * Get the current {@link tc.oc.pgm.api.player.MatchPlayer} if they are online and their {@link * Party} is the same. diff --git a/core/src/main/java/tc/oc/pgm/api/player/ParticipantState.java b/core/src/main/java/tc/oc/pgm/api/player/ParticipantState.java index 2fab38903c..ae981a0a12 100644 --- a/core/src/main/java/tc/oc/pgm/api/player/ParticipantState.java +++ b/core/src/main/java/tc/oc/pgm/api/player/ParticipantState.java @@ -1,10 +1,12 @@ package tc.oc.pgm.api.player; +import org.jetbrains.annotations.NotNull; import tc.oc.pgm.api.party.Competitor; /** A {@link MatchPlayerState} that exclusively represents a {@link Competitor}. */ public interface ParticipantState extends MatchPlayerState { @Override + @NotNull Competitor getParty(); } diff --git a/core/src/main/java/tc/oc/pgm/db/UsernameImpl.java b/core/src/main/java/tc/oc/pgm/db/UsernameImpl.java index 4113847c52..189ddc5197 100644 --- a/core/src/main/java/tc/oc/pgm/db/UsernameImpl.java +++ b/core/src/main/java/tc/oc/pgm/db/UsernameImpl.java @@ -1,14 +1,13 @@ package tc.oc.pgm.db; import static tc.oc.pgm.util.Assert.assertNotNull; -import static tc.oc.pgm.util.PlayerComponent.player; +import static tc.oc.pgm.util.player.PlayerComponent.player; import java.util.UUID; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.player.Username; -import tc.oc.pgm.util.PlayerComponent; import tc.oc.pgm.util.UsernameResolver; import tc.oc.pgm.util.named.NameStyle; @@ -35,7 +34,7 @@ public String getNameLegacy() { @Override public Component getName(NameStyle style) { - return name == null ? PlayerComponent.UNKNOWN : player(Bukkit.getPlayer(id), name, style); + return player(Bukkit.getPlayer(id), name, style); } @Override diff --git a/core/src/main/java/tc/oc/pgm/destroyable/Destroyable.java b/core/src/main/java/tc/oc/pgm/destroyable/Destroyable.java index af29e4457b..3d021ae34e 100644 --- a/core/src/main/java/tc/oc/pgm/destroyable/Destroyable.java +++ b/core/src/main/java/tc/oc/pgm/destroyable/Destroyable.java @@ -573,8 +573,7 @@ public boolean isCompleted(Competitor team) { return this.contributions; } - Map playerDamage = - new DefaultMapAdapter<>(new HashMap(), 0); + Map playerDamage = new DefaultMapAdapter<>(new HashMap<>(), 0); int totalDamage = 0; for (DestroyableHealthChange change : this.events) { diff --git a/core/src/main/java/tc/oc/pgm/ffa/Tribute.java b/core/src/main/java/tc/oc/pgm/ffa/Tribute.java index 60c70e5f22..a05ba8d904 100644 --- a/core/src/main/java/tc/oc/pgm/ffa/Tribute.java +++ b/core/src/main/java/tc/oc/pgm/ffa/Tribute.java @@ -1,8 +1,7 @@ package tc.oc.pgm.ffa; -import static net.kyori.adventure.text.Component.*; import static tc.oc.pgm.util.Assert.assertNotNull; -import static tc.oc.pgm.util.PlayerComponent.player; +import static tc.oc.pgm.util.player.PlayerComponent.player; import java.util.Collection; import java.util.Collections; @@ -64,7 +63,7 @@ public Tribute(final MatchPlayer player, final @Nullable ChatColor color) { this.chatColor = color == null ? ChatColor.YELLOW : color; this.color = BukkitUtils.colorOf(this.chatColor); this.dyeColor = BukkitUtils.chatColorToDyeColor(this.chatColor); - this.textColor = TextFormatter.convert(color); + this.textColor = TextFormatter.convert(chatColor); this.query = new PartyQuery(null, this); } @@ -110,7 +109,7 @@ public TextColor getTextColor() { @Override public Component getName(final NameStyle style) { - return player(player != null ? player.getBukkit() : null, style); + return player(player, style); } @Override @@ -120,7 +119,7 @@ public String getNameLegacy() { @Override public Component getChatPrefix() { - return empty(); + return Component.empty(); } @Override @@ -158,7 +157,7 @@ private void checkPlayer(final UUID playerId) { public void addPlayer(final MatchPlayer player) { checkPlayer(assertNotNull(player).getId()); this.player = player; - this.players = Collections.unmodifiableList(Collections.singletonList(player)); + this.players = Collections.singletonList(player); } @Override diff --git a/core/src/main/java/tc/oc/pgm/inventory/ViewInventoryMatchModule.java b/core/src/main/java/tc/oc/pgm/inventory/ViewInventoryMatchModule.java index 29c48a84e0..c5d1de6860 100644 --- a/core/src/main/java/tc/oc/pgm/inventory/ViewInventoryMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/inventory/ViewInventoryMatchModule.java @@ -1,6 +1,6 @@ package tc.oc.pgm.inventory; -import static tc.oc.pgm.util.PlayerComponent.player; +import static tc.oc.pgm.util.player.PlayerComponent.player; import com.google.common.collect.Lists; import com.google.common.collect.Maps; diff --git a/core/src/main/java/tc/oc/pgm/listeners/AntiGriefListener.java b/core/src/main/java/tc/oc/pgm/listeners/AntiGriefListener.java index a80fa173c0..2fa6342470 100644 --- a/core/src/main/java/tc/oc/pgm/listeners/AntiGriefListener.java +++ b/core/src/main/java/tc/oc/pgm/listeners/AntiGriefListener.java @@ -2,14 +2,14 @@ import static net.kyori.adventure.key.Key.key; import static net.kyori.adventure.sound.Sound.sound; -import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; -import static tc.oc.pgm.util.PlayerComponent.player; +import static tc.oc.pgm.util.player.PlayerComponent.player; import java.util.ArrayList; import java.util.Collections; import java.util.List; import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.ChatColor; import org.bukkit.Location; @@ -94,40 +94,33 @@ private void participantDefuse(Player player, Entity entity) { this.notifyDefuse( clicker, entity, - ChatColor.RED - + TextTranslations.translate( - "moderation.defuse.player", - clicker.getBukkit(), - player(owner.getBukkit(), NameStyle.FANCY).color(NamedTextColor.RED))); + translatable("moderation.defuse.player", NamedTextColor.RED, owner.getName())); ChatDispatcher.broadcastAdminChatMessage( translatable( "moderation.defuse.alert.player", NamedTextColor.GRAY, - clicker.getName(NameStyle.FANCY), - owner.getName(NameStyle.FANCY), + clicker.getName(), + owner.getName(), MinecraftComponent.entity(entity.getType()).color(NamedTextColor.DARK_RED)), clicker.getMatch()); } else { this.notifyDefuse( - clicker, - entity, - ChatColor.RED - + TextTranslations.translate("moderation.defuse.world", clicker.getBukkit())); + clicker, entity, translatable("moderation.defuse.world", NamedTextColor.RED)); ChatDispatcher.broadcastAdminChatMessage( translatable( "moderation.defuse.alert.world", NamedTextColor.GRAY, - clicker.getName(NameStyle.FANCY), + clicker.getName(), MinecraftComponent.entity(entity.getType()).color(NamedTextColor.DARK_RED)), clicker.getMatch()); } } } - private void notifyDefuse(MatchPlayer clicker, Entity entity, String message) { - clicker.sendMessage(text(message)); + private void notifyDefuse(MatchPlayer clicker, Entity entity, Component message) { + clicker.sendMessage(message); clicker .getMatch() .playSound( diff --git a/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java index 95cbb6cdcc..3b27bf8196 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java @@ -1,7 +1,7 @@ package tc.oc.pgm.match; import static tc.oc.pgm.util.Assert.assertNotNull; -import static tc.oc.pgm.util.PlayerComponent.player; +import static tc.oc.pgm.util.player.PlayerComponent.player; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -413,7 +413,7 @@ public World getWorld() { @Override public Component getName(NameStyle style) { - return player(getBukkit(), style); + return player(this, style); } @Override diff --git a/core/src/main/java/tc/oc/pgm/match/MatchPlayerStateImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchPlayerStateImpl.java index 46b6be77a4..d03d715e5a 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchPlayerStateImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchPlayerStateImpl.java @@ -1,7 +1,7 @@ package tc.oc.pgm.match; import static tc.oc.pgm.util.Assert.assertNotNull; -import static tc.oc.pgm.util.PlayerComponent.player; +import static tc.oc.pgm.util.player.PlayerComponent.player; import java.util.Optional; import java.util.UUID; @@ -9,6 +9,8 @@ import org.bukkit.Location; import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.integration.Integration; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.player.MatchPlayer; @@ -17,39 +19,49 @@ import tc.oc.pgm.util.named.NameStyle; public class MatchPlayerStateImpl implements MatchPlayerState { - private final Match match; - private final String username; - private final UUID uuid; - private final Party party; - private final Vector location; - private final Audience audience; - - protected MatchPlayerStateImpl(MatchPlayer player) { + private final @NotNull Match match; + private final @NotNull String username; + private final @NotNull UUID uuid; + private final @NotNull Party party; + // Excluded from equals/hashcode + private final @NotNull Vector location; + private final @NotNull Audience audience; + + private final boolean dead; + + private final boolean vanished; + private final @Nullable String nick; + + protected MatchPlayerStateImpl(@NotNull MatchPlayer player) { this.match = assertNotNull(player).getMatch(); - this.username = player.getBukkit().getName(); - this.uuid = player.getId(); + this.username = assertNotNull(player.getBukkit().getName()); + this.uuid = assertNotNull(player.getId()); this.party = assertNotNull(player.getParty()); - this.location = player.getBukkit().getLocation().toVector(); - this.audience = getPlayer().isPresent() ? getPlayer().get() : Audience.empty(); + this.location = assertNotNull(player.getBukkit().getLocation().toVector()); + this.audience = player; + + this.dead = player.isDead(); + this.vanished = Integration.isVanished(player.getBukkit()); + this.nick = Integration.getNick(player.getBukkit()); } @Override - public Match getMatch() { + public @NotNull Match getMatch() { return match; } @Override - public Party getParty() { + public @NotNull Party getParty() { return party; } @Override - public UUID getId() { + public @NotNull UUID getId() { return uuid; } @Override - public Location getLocation() { + public @NotNull Location getLocation() { return location.toLocation(match.getWorld()); } @@ -60,8 +72,7 @@ public Optional getPlayer() { @Override public Component getName(NameStyle style) { - MatchPlayer player = match.getPlayer(uuid); - return player(player != null ? player.getBukkit() : null, username, style); + return player(this, style); } @Override @@ -76,33 +87,48 @@ public Audience audience() { } @Override - public int hashCode() { - int hash = 7; - hash = 31 * hash + this.getId().hashCode(); - hash = 31 * hash + this.getParty().hashCode(); - hash = 31 * hash + this.getMatch().hashCode(); - return hash; + public boolean isDead() { + return dead; + } + + @Override + public boolean isVanished() { + return vanished; } @Override - public boolean equals(Object obj) { - if (!(obj instanceof MatchPlayerState)) return false; - final MatchPlayerState o = (MatchPlayerState) obj; - return this.getId().equals(o.getId()) - && this.getParty().equals(o.getParty()) - && this.getMatch().equals(o.getMatch()); + @Nullable + public String getNick() { + return nick; } @Override - public String toString() { - return "MatchPlayerState{id=" - + this.getId() - + ", party=" - + this.getParty().getDefaultName() - + ", match=" - + this.getMatch().getId() - + ", location=" - + this.location - + "}"; + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MatchPlayerStateImpl)) return false; + + MatchPlayerStateImpl that = (MatchPlayerStateImpl) o; + + if (isDead() != that.isDead()) return false; + if (isVanished() != that.isVanished()) return false; + if (!getMatch().equals(that.getMatch())) return false; + if (!username.equals(that.username)) return false; + if (!uuid.equals(that.uuid)) return false; + if (!getParty().equals(that.getParty())) return false; + if (!audience.equals(that.audience)) return false; + return getNick() != null ? getNick().equals(that.getNick()) : that.getNick() == null; + } + + @Override + public int hashCode() { + int result = getMatch().hashCode(); + result = 31 * result + username.hashCode(); + result = 31 * result + uuid.hashCode(); + result = 31 * result + getParty().hashCode(); + result = 31 * result + audience.hashCode(); + result = 31 * result + (isDead() ? 1 : 0); + result = 31 * result + (isVanished() ? 1 : 0); + result = 31 * result + (getNick() != null ? getNick().hashCode() : 0); + return result; } } diff --git a/core/src/main/java/tc/oc/pgm/match/ParticipantStateImpl.java b/core/src/main/java/tc/oc/pgm/match/ParticipantStateImpl.java index 7b6f1eb000..be83fc69a7 100644 --- a/core/src/main/java/tc/oc/pgm/match/ParticipantStateImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/ParticipantStateImpl.java @@ -2,6 +2,7 @@ import static tc.oc.pgm.util.Assert.assertTrue; +import org.jetbrains.annotations.NotNull; import tc.oc.pgm.api.party.Competitor; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.api.player.ParticipantState; @@ -14,7 +15,7 @@ protected ParticipantStateImpl(MatchPlayer player) { } @Override - public Competitor getParty() { + public @NotNull Competitor getParty() { return (Competitor) super.getParty(); } } diff --git a/core/src/main/java/tc/oc/pgm/namedecorations/ConfigDecorationProvider.java b/core/src/main/java/tc/oc/pgm/namedecorations/ConfigDecorationProvider.java index 71f92282c4..c263c51276 100644 --- a/core/src/main/java/tc/oc/pgm/namedecorations/ConfigDecorationProvider.java +++ b/core/src/main/java/tc/oc/pgm/namedecorations/ConfigDecorationProvider.java @@ -2,6 +2,7 @@ import static net.kyori.adventure.text.Component.text; +import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -22,16 +23,16 @@ public class ConfigDecorationProvider implements NameDecorationProvider { @Override public String getPrefix(UUID uuid) { return groups(uuid) - .filter(g -> g.getPrefix() != null) .map(Config.Group::getPrefix) + .filter(Objects::nonNull) .collect(Collectors.joining()); } @Override public String getSuffix(UUID uuid) { return groups(uuid) - .filter(g -> g.getSuffix() != null) .map(Config.Group::getSuffix) + .filter(Objects::nonNull) .collect(Collectors.joining()); } @@ -48,7 +49,7 @@ public Component getSuffixComponent(UUID uuid) { private Component generateFlair(Stream flairs, boolean prefix) { TextComponent.Builder builder = text(); flairs - .filter(p -> prefix ? p.getPrefix() != null : p.getSuffix() != null) + .filter(g -> prefix ? g.getPrefix() != null : g.getSuffix() != null) .map(Config.Group::getFlair) .forEach(flair -> builder.append(flair.getComponent(prefix))); return builder.build(); diff --git a/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java b/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java index af880c5594..41001eeeab 100644 --- a/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java +++ b/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java @@ -27,8 +27,8 @@ import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.events.PlayerJoinMatchEvent; import tc.oc.pgm.events.PlayerPartyChangeEvent; -import tc.oc.pgm.util.PlayerComponent; import tc.oc.pgm.util.named.NameDecorationProvider; +import tc.oc.pgm.util.player.PlayerComponent; import tc.oc.pgm.util.text.TextFormatter; @SuppressWarnings("UnstableApiUsage") @@ -39,7 +39,7 @@ public class NameDecorationRegistryImpl implements NameDecorationRegistry, Liste private NameDecorationProvider provider; private final LoadingCache decorationCache = CacheBuilder.newBuilder() - .expireAfterAccess(1, TimeUnit.HOURS) + .expireAfterAccess(15, TimeUnit.MINUTES) .build( new CacheLoader() { @Override diff --git a/core/src/main/java/tc/oc/pgm/spawns/states/Dead.java b/core/src/main/java/tc/oc/pgm/spawns/states/Dead.java index d560e22c6e..f2f44e3ef2 100644 --- a/core/src/main/java/tc/oc/pgm/spawns/states/Dead.java +++ b/core/src/main/java/tc/oc/pgm/spawns/states/Dead.java @@ -1,7 +1,7 @@ package tc.oc.pgm.spawns.states; import static net.kyori.adventure.text.Component.translatable; -import static tc.oc.pgm.util.PlayerComponent.player; +import static tc.oc.pgm.util.player.PlayerComponent.player; import java.util.List; import net.kyori.adventure.text.Component; @@ -123,7 +123,7 @@ protected Component getTitle(boolean spectator) { return translatable( "deathScreen.title" + (spectator ? ".spectator" : ""), NamedTextColor.RED, - player(this.player.getBukkit(), NameStyle.SIMPLE_COLOR)); + player(player, NameStyle.SIMPLE_COLOR)); } @Override diff --git a/core/src/main/java/tc/oc/pgm/stats/PlayerStats.java b/core/src/main/java/tc/oc/pgm/stats/PlayerStats.java index 748bf03d47..7eb92bd6cc 100644 --- a/core/src/main/java/tc/oc/pgm/stats/PlayerStats.java +++ b/core/src/main/java/tc/oc/pgm/stats/PlayerStats.java @@ -1,7 +1,7 @@ package tc.oc.pgm.stats; import static net.kyori.adventure.text.Component.translatable; -import static tc.oc.pgm.stats.StatsMatchModule.numberComponent; +import static tc.oc.pgm.util.text.NumberComponent.number; import java.time.Duration; import java.time.Instant; @@ -106,10 +106,10 @@ public Component getBasicStatsMessage() { return translatable( "match.stats", NamedTextColor.GRAY, - numberComponent(kills, NamedTextColor.GREEN), - numberComponent(killstreak, NamedTextColor.GREEN), - numberComponent(deaths, NamedTextColor.RED), - numberComponent(getKD(), NamedTextColor.GREEN)); + number(kills, NamedTextColor.GREEN), + number(killstreak, NamedTextColor.GREEN), + number(deaths, NamedTextColor.RED), + number(getKD(), NamedTextColor.GREEN)); } // Getters, both raw stats and some handy calculations diff --git a/core/src/main/java/tc/oc/pgm/stats/StatsMatchModule.java b/core/src/main/java/tc/oc/pgm/stats/StatsMatchModule.java index 455fb6a1f4..bb193c5436 100644 --- a/core/src/main/java/tc/oc/pgm/stats/StatsMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/stats/StatsMatchModule.java @@ -3,7 +3,8 @@ import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; import static net.kyori.adventure.text.event.HoverEvent.showText; -import static tc.oc.pgm.util.PlayerComponent.player; +import static tc.oc.pgm.util.player.PlayerComponent.player; +import static tc.oc.pgm.util.text.NumberComponent.number; import com.google.common.collect.Lists; import java.text.DecimalFormat; @@ -23,7 +24,6 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Arrow; import org.bukkit.entity.Player; @@ -80,9 +80,15 @@ public class StatsMatchModule implements MatchModule, Listener { private final int verboseItemSlot = PGM.get().getConfiguration().getVerboseItemSlot(); /** Common formats used by stats with decimals */ - public static final DecimalFormat TWO_DECIMALS = new DecimalFormat("#.##"); + public static final DecimalFormat FORMATTER = new DecimalFormat("#.00"); - public static final DecimalFormat ONE_DECIMAL = new DecimalFormat("#.#"); + public static final DecimalFormat THOUSANDS_FORMATTER = new DecimalFormat("#.00"); + + static { + THOUSANDS_FORMATTER.setMultiplier(1000); + THOUSANDS_FORMATTER.setPositiveSuffix("k"); + THOUSANDS_FORMATTER.setNegativeSuffix("k"); + } public static final Component HEART_SYMBOL = text("\u2764"); // ❤ @@ -255,7 +261,7 @@ public void onStatsDisplay(MatchStatsEvent event) { best.add( translatable( "match.stats.damage", - playerName(bestDamage.getKey()), + player(bestDamage.getKey(), NameStyle.FANCY), damageComponent(bestDamage.getValue(), NamedTextColor.GREEN))); } } @@ -276,16 +282,16 @@ public void onStatsDisplay(MatchStatsEvent event) { Component ksHover = translatable( "match.stats.killstreak.concise", - numberComponent(stats.getKillstreak(), NamedTextColor.GREEN)); + number(stats.getKillstreak(), NamedTextColor.GREEN)); viewer.sendMessage( translatable( "match.stats.own", - numberComponent(stats.getKills(), NamedTextColor.GREEN), - numberComponent(stats.getMaxKillstreak(), NamedTextColor.GREEN) + number(stats.getKills(), NamedTextColor.GREEN), + number(stats.getMaxKillstreak(), NamedTextColor.GREEN) .hoverEvent(showText(ksHover)), - numberComponent(stats.getDeaths(), NamedTextColor.RED), - numberComponent(stats.getKD(), NamedTextColor.GREEN), + number(stats.getDeaths(), NamedTextColor.RED), + number(stats.getKD(), NamedTextColor.GREEN), damageComponent(stats.getDamageDone(), NamedTextColor.GREEN))); } @@ -365,61 +371,13 @@ private Map.Entry sortStatsDouble(Map map) { Component getMessage( String messageKey, Map.Entry mapEntry, TextColor color) { return translatable( - messageKey, playerName(mapEntry.getKey()), numberComponent(mapEntry.getValue(), color)); - } - - /** - * Wraps a {@link Number} in a {@link Component} that is colored with the given {@link TextColor}. - * Rounds the number to a maximum of 2 decimals - * - *

If the number is NaN "-" is wrapped instead - * - *

If the number is >= 10000 it will be represented in the thousands (10k, 25.5k, 120.3k etc.) - * - * @param stat The number you want wrapped - * @param color The color you want the number to be - * @return a colored component wrapping the given number or "-" if NaN - */ - public static Component numberComponent(Number stat, TextColor color) { - double doubleStat = stat.doubleValue(); - boolean tenThousand = doubleStat >= 10000; - String returnValue = null; - if (Double.isNaN(doubleStat)) returnValue = "-"; // If NaN, dont try to display as a number - else if (doubleStat % 1 == 0) { // Can the given number can be displayed as an integer? - int value = stat.intValue(); - if (!tenThousand - || value % 1000 - == 0) // If the number is above 999 we also need to check if the shortened number can - // be displayed as an integer - returnValue = Integer.toString(tenThousand ? value / 1000 : value); - } - if (returnValue - == null) { // If not yet defined, display as a double with either 1 or 2 decimals - if (tenThousand) doubleStat /= 1000; - String decimals = Double.toString(doubleStat).split("\\.")[1]; - if (decimals.chars().sum() == 1 || tenThousand) returnValue = ONE_DECIMAL.format(doubleStat); - else returnValue = TWO_DECIMALS.format(doubleStat); - } - return text(returnValue + (tenThousand ? "k" : ""), color); + messageKey, player(mapEntry.getKey(), NameStyle.FANCY), number(mapEntry.getValue(), color)); } /** Formats raw damage to damage relative to the amount of hearths the player would have broken */ public static Component damageComponent(double damage, TextColor color) { - double hearts = damage / (double) 2; - - return numberComponent(hearts, color).append(HEART_SYMBOL); - } - - private Component playerName(UUID playerUUID) { - return player( // TODO: make #player take a Supplier instead of the "defName" String - Bukkit.getPlayer(playerUUID), - allPlayerStats.keySet().stream() - .filter(id -> id.equals(playerUUID)) - .findFirst() - .map(id -> PGM.get().getDatastore().getUsername(id).getNameLegacy()) - .orElse("Unknown"), - NameStyle.FANCY); + return number(hearts, color).append(HEART_SYMBOL); } private Stream getOfflinePlayersWithStats() { diff --git a/core/src/main/java/tc/oc/pgm/stats/menu/items/PlayerStatsMenuItem.java b/core/src/main/java/tc/oc/pgm/stats/menu/items/PlayerStatsMenuItem.java index fb81fc6510..d32142f12b 100644 --- a/core/src/main/java/tc/oc/pgm/stats/menu/items/PlayerStatsMenuItem.java +++ b/core/src/main/java/tc/oc/pgm/stats/menu/items/PlayerStatsMenuItem.java @@ -3,7 +3,7 @@ import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; import static tc.oc.pgm.stats.StatsMatchModule.damageComponent; -import static tc.oc.pgm.stats.StatsMatchModule.numberComponent; +import static tc.oc.pgm.util.text.NumberComponent.number; import java.time.Duration; import java.util.ArrayList; @@ -59,14 +59,14 @@ public List getLore(Player player) { translatable( "match.stats.concise", RESET, - numberComponent(stats.getKills(), NamedTextColor.GREEN), - numberComponent(stats.getDeaths(), NamedTextColor.RED), - numberComponent(stats.getKD(), NamedTextColor.GREEN)); + number(stats.getKills(), NamedTextColor.GREEN), + number(stats.getDeaths(), NamedTextColor.RED), + number(stats.getKD(), NamedTextColor.GREEN)); Component killstreakLore = translatable( "match.stats.killstreak.concise", RESET, - numberComponent(stats.getMaxKillstreak(), NamedTextColor.GREEN)); + number(stats.getMaxKillstreak(), NamedTextColor.GREEN)); Component damageDealtLore = translatable( "match.stats.damage.dealt", @@ -83,9 +83,9 @@ public List getLore(Player player) { translatable( "match.stats.bow", RESET, - numberComponent(stats.getShotsHit(), NamedTextColor.YELLOW), - numberComponent(stats.getShotsTaken(), NamedTextColor.YELLOW), - numberComponent(stats.getArrowAccuracy(), NamedTextColor.YELLOW).append(text('%'))); + number(stats.getShotsHit(), NamedTextColor.YELLOW), + number(stats.getShotsTaken(), NamedTextColor.YELLOW), + number(stats.getArrowAccuracy(), NamedTextColor.YELLOW).append(text('%'))); lore.add(TextTranslations.translateLegacy(statLore, player)); lore.add(TextTranslations.translateLegacy(killstreakLore, player)); @@ -116,8 +116,7 @@ public List getLore(Player player) { private boolean optionalStat(List lore, Number stat, String key, Player player) { if (stat.doubleValue() > 0) { lore.add(null); - Component loreComponent = - translatable(key, RESET, numberComponent(stat, NamedTextColor.AQUA)); + Component loreComponent = translatable(key, RESET, number(stat, NamedTextColor.AQUA)); lore.add(TextTranslations.translateLegacy(loreComponent, player)); return true; } diff --git a/core/src/main/java/tc/oc/pgm/stats/menu/items/TeamStatsMenuItem.java b/core/src/main/java/tc/oc/pgm/stats/menu/items/TeamStatsMenuItem.java index 7586abd501..241b2ae96b 100644 --- a/core/src/main/java/tc/oc/pgm/stats/menu/items/TeamStatsMenuItem.java +++ b/core/src/main/java/tc/oc/pgm/stats/menu/items/TeamStatsMenuItem.java @@ -3,7 +3,7 @@ import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; import static tc.oc.pgm.stats.StatsMatchModule.damageComponent; -import static tc.oc.pgm.stats.StatsMatchModule.numberComponent; +import static tc.oc.pgm.util.text.NumberComponent.number; import com.google.common.collect.Lists; import java.util.Collection; @@ -94,9 +94,9 @@ public List getLore(Player player) { translatable( "match.stats.concise", RESET, - numberComponent(stats.getTeamKills(), NamedTextColor.GREEN), - numberComponent(stats.getTeamDeaths(), NamedTextColor.RED), - numberComponent(stats.getTeamKD(), NamedTextColor.GREEN)); + number(stats.getTeamKills(), NamedTextColor.GREEN), + number(stats.getTeamDeaths(), NamedTextColor.RED), + number(stats.getTeamKD(), NamedTextColor.GREEN)); Component damageDealtLore = translatable( @@ -114,9 +114,9 @@ public List getLore(Player player) { translatable( "match.stats.bow", RESET, - numberComponent(stats.getShotsHit(), NamedTextColor.YELLOW), - numberComponent(stats.getShotsTaken(), NamedTextColor.YELLOW), - numberComponent(stats.getTeamBowAcc(), NamedTextColor.YELLOW).append(text('%'))); + number(stats.getShotsHit(), NamedTextColor.YELLOW), + number(stats.getShotsTaken(), NamedTextColor.YELLOW), + number(stats.getTeamBowAcc(), NamedTextColor.YELLOW).append(text('%'))); lore.add(TextTranslations.translateLegacy(statLore, player)); lore.add(TextTranslations.translateLegacy(damageDealtLore, player)); diff --git a/core/src/main/java/tc/oc/pgm/tablist/MatchTabManager.java b/core/src/main/java/tc/oc/pgm/tablist/MatchTabManager.java index 2779b5ce84..60488495dd 100644 --- a/core/src/main/java/tc/oc/pgm/tablist/MatchTabManager.java +++ b/core/src/main/java/tc/oc/pgm/tablist/MatchTabManager.java @@ -1,6 +1,6 @@ package tc.oc.pgm.tablist; -import static tc.oc.pgm.util.PlayerComponent.player; +import static tc.oc.pgm.util.player.PlayerComponent.player; import java.util.Map; import java.util.concurrent.Future; @@ -119,7 +119,7 @@ public MatchTabManager( PlayerTabEntry.setShowRealPing(false); } - PlayerTabEntry.setPlayerComponent((pl, viewer) -> player(pl, NameStyle.TAB).render(viewer)); + PlayerTabEntry.setPlayerComponent(pl -> player(pl, NameStyle.TAB)); } protected static TabEntry[] headerFactory(Match match) { diff --git a/core/src/main/java/tc/oc/pgm/util/PlayerComponent.java b/core/src/main/java/tc/oc/pgm/util/PlayerComponent.java deleted file mode 100644 index 3a7c410bb0..0000000000 --- a/core/src/main/java/tc/oc/pgm/util/PlayerComponent.java +++ /dev/null @@ -1,142 +0,0 @@ -package tc.oc.pgm.util; - -import static net.kyori.adventure.text.Component.text; -import static net.kyori.adventure.text.Component.translatable; -import static net.kyori.adventure.text.event.ClickEvent.runCommand; -import static net.kyori.adventure.text.event.HoverEvent.showText; - -import java.util.UUID; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextComponent; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextColor; -import net.kyori.adventure.text.format.TextDecoration; -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.Nullable; -import tc.oc.pgm.api.integration.Integration; -import tc.oc.pgm.util.bukkit.BukkitUtils; -import tc.oc.pgm.util.bukkit.MetadataUtils; -import tc.oc.pgm.util.named.NameDecorationProvider; -import tc.oc.pgm.util.named.NameStyle; -import tc.oc.pgm.util.named.NameStyle.Flag; -import tc.oc.pgm.util.text.RenderableComponent; - -/** PlayerComponent is used to format player names in a consistent manner with optional styling */ -public final class PlayerComponent implements RenderableComponent { - - public static final TextColor OFFLINE_COLOR = NamedTextColor.DARK_AQUA; - public static final TextColor DEAD_COLOR = NamedTextColor.DARK_GRAY; - - public static final Component UNKNOWN = - translatable("misc.unknown", OFFLINE_COLOR, TextDecoration.ITALIC); - public static final Component CONSOLE = translatable("misc.console", OFFLINE_COLOR); - - private final Player player; - private final String defName; - private final NameStyle style; - - private PlayerComponent(@Nullable Player player, String defName, NameStyle style) { - this.player = player; - this.defName = defName; - this.style = style; - } - - public static Component player(UUID playerId, NameStyle style) { - Player player = Bukkit.getPlayer(playerId); - return player != null ? player(player, style) : UNKNOWN; - } - - public static Component player(CommandSender sender, NameStyle style) { - return sender instanceof Player ? player((Player) sender, style) : CONSOLE; - } - - public static PlayerComponent player(Player player, NameStyle style) { - return player(player, null, style); - } - - public static PlayerComponent player(Player player, String defName, NameStyle style) { - return new PlayerComponent(player, defName, style); - } - - public Component render(CommandSender viewer) { - if (player == null && defName == null) { - return UNKNOWN; - } - - boolean online = player != null && player.isOnline(); - boolean reveal = online && Players.shouldReveal(viewer, player); - - String nick = player != null ? Integration.getNick(player) : null; - - NameDecorationProvider provider = getNameDecorations(); - - UUID uuid = online ? player.getUniqueId() : null; - - TextComponent.Builder builder = text(); - if (online && reveal && style.has(Flag.FLAIR)) { - builder.append(provider.getPrefixComponent(uuid)); - } - - String visibleName = player != null ? Players.getVisibleName(viewer, player) : defName; - - TextComponent.Builder name = text().content(visibleName); - - TextColor color = getColor(online, uuid, provider); - if (color != null) name.color(color); - - if (player == viewer && reveal && style.has(Flag.SELF)) { - name.decoration(TextDecoration.BOLD, true); - } - if (reveal && style.has(Flag.FRIEND) && Players.isFriend(viewer, player)) { - name.decoration(TextDecoration.ITALIC, true); - } - if (reveal && style.has(Flag.DISGUISE) && (nick != null || Integration.isVanished(player))) { - name.decoration(TextDecoration.STRIKETHROUGH, true); - - if (nick != null && style.has(Flag.NICKNAME)) { - name.append( - text(" " + nick, color, TextDecoration.ITALIC) - .decoration(TextDecoration.STRIKETHROUGH, false)); - } - } - - if (online && style.has(Flag.TELEPORT)) { - name.hoverEvent(showText(translatable("misc.teleportTo", NamedTextColor.GRAY, name.build()))) - .clickEvent(runCommand("/tp " + visibleName)); - } - - builder.append(name); - - if (online && reveal && style.has(Flag.FLAIR)) { - builder.append(provider.getSuffixComponent(uuid)); - } - - // Optimization: only if flairs were rendered do we need the whole builder. - return online && reveal && style.has(Flag.FLAIR) ? builder.build() : name.build(); - } - - private TextColor getColor(boolean online, UUID uuid, NameDecorationProvider provider) { - if (online && style.has(Flag.DEATH) && isDead()) { - return DEAD_COLOR; - } else if (style.has(Flag.COLOR)) { - return online ? provider.getColor(uuid) : OFFLINE_COLOR; - } - return null; - } - - private NameDecorationProvider getNameDecorations() { - if (player != null && player.hasMetadata(NameDecorationProvider.METADATA_KEY)) { - return MetadataUtils.getOptionalMetadata( - player, NameDecorationProvider.METADATA_KEY, BukkitUtils.getPlugin()) - .map(mv -> (NameDecorationProvider) mv.value()) - .orElse(NameDecorationProvider.DEFAULT); - } - return NameDecorationProvider.DEFAULT; - } - - public boolean isDead() { - return player != null && (player.hasMetadata("isDead") || player.isDead()); - } -} diff --git a/core/src/main/java/tc/oc/pgm/util/player/PlayerComponent.java b/core/src/main/java/tc/oc/pgm/util/player/PlayerComponent.java new file mode 100644 index 0000000000..b0bfdf96ac --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/util/player/PlayerComponent.java @@ -0,0 +1,107 @@ +package tc.oc.pgm.util.player; + +import static net.kyori.adventure.text.Component.translatable; + +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.api.player.MatchPlayerState; +import tc.oc.pgm.util.named.NameStyle; +import tc.oc.pgm.util.text.RenderableComponent; + +/** PlayerComponent is used to format player names in a consistent manner with optional styling */ +public final class PlayerComponent implements RenderableComponent { + + public static final TextColor OFFLINE_COLOR = NamedTextColor.DARK_AQUA; + + public static final Component UNKNOWN = + translatable("misc.unknown", OFFLINE_COLOR, TextDecoration.ITALIC); + public static final Component CONSOLE = translatable("misc.console", OFFLINE_COLOR); + public static final PlayerComponent UNKNOWN_PLAYER = + new PlayerComponent(null, new PlayerData(null, null, NameStyle.PLAIN)); + + private static final PlayerRenderer RENDERER = new PlayerRenderer(); + + // The data for player being rendered. + private final @Nullable Player player; + private final @NotNull PlayerData data; + + private Style style = Style.empty(); + + private PlayerComponent(@Nullable Player player, @NotNull PlayerData data) { + this.player = player; + this.data = data; + } + + public static Component player(@Nullable UUID playerId, @NotNull NameStyle style) { + MatchPlayer player = PGM.get().getMatchManager().getPlayer(playerId); + if (player != null) return player(player, style); + + // Fallback to resolving the user + return PGM.get().getDatastore().getUsername(playerId).getName(style); + } + + public static Component player(CommandSender sender, @NotNull NameStyle style) { + if (sender == null) return UNKNOWN_PLAYER; + if (sender instanceof Player) return player((Player) sender, style); + return CONSOLE; + } + + public static PlayerComponent player(Player player, @NotNull NameStyle style) { + if (player == null) return UNKNOWN_PLAYER; + return new PlayerComponent(player, new PlayerData(player, style)); + } + + public static Component player( + @Nullable Player player, @Nullable String username, @NotNull NameStyle style) { + if (player == null && username == null) return UNKNOWN_PLAYER; + return new PlayerComponent(player, new PlayerData(player, username, style)); + } + + public static PlayerComponent player( + @Nullable MatchPlayerState player, @NotNull NameStyle style) { + if (player == null) return UNKNOWN_PLAYER; + return new PlayerComponent(Bukkit.getPlayer(player.getId()), new PlayerData(player, style)); + } + + public static PlayerComponent player(@Nullable MatchPlayer player, @NotNull NameStyle style) { + if (player == null) return UNKNOWN_PLAYER; + return new PlayerComponent(player.getBukkit(), new PlayerData(player, style)); + } + + public Component render(CommandSender viewer) { + // Render using a specialized render, which caches results + Component rendered = RENDERER.render(data, new PlayerRelationship(player, viewer)); + + // If any specific styles were requested, add them in + if (!style.isEmpty()) return rendered.style(rendered.style().merge(style)); + return rendered; + } + + @Override + public @NotNull RenderableComponent style(@NotNull Style style) { + this.style = style; + return this; + } + + @Override + public @NotNull Style style() { + return this.style; + } + + @Override + public @NotNull RenderableComponent colorIfAbsent(@Nullable TextColor color) { + if (!data.style.has(NameStyle.Flag.COLOR)) style = style.colorIfAbsent(color); + return this; + } +} diff --git a/core/src/main/java/tc/oc/pgm/util/player/PlayerData.java b/core/src/main/java/tc/oc/pgm/util/player/PlayerData.java new file mode 100644 index 0000000000..7a374c7a47 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/util/player/PlayerData.java @@ -0,0 +1,138 @@ +package tc.oc.pgm.util.player; + +import java.util.Objects; +import java.util.UUID; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.integration.Integration; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.api.player.MatchPlayerState; +import tc.oc.pgm.util.named.NameStyle; + +class PlayerData { + public final @Nullable UUID uuid; + + public final @Nullable String name; + public final @Nullable String nick; + public final @NotNull TextColor teamColor; + public final boolean dead; + public final boolean vanish; + public final boolean online; + public final boolean conceal; // If player is disguise, pretend they are offline? + public final @NotNull NameStyle style; + + public PlayerData(@NotNull Player player, @NotNull NameStyle style) { + this.uuid = player.getUniqueId(); + this.name = player.getName(); + this.nick = Integration.getNick(player); + MatchPlayer mp = PGM.get().getMatchManager().getPlayer(player); + this.teamColor = mp == null ? PlayerComponent.OFFLINE_COLOR : mp.getParty().getTextColor(); + this.dead = mp != null && mp.isDead(); + this.vanish = Integration.isVanished(player); + this.online = player.isOnline(); + this.conceal = false; + this.style = style; + } + + public PlayerData(@NotNull MatchPlayer mp, @NotNull NameStyle style) { + this.uuid = mp.getId(); + + this.name = mp.getNameLegacy(); + this.nick = Integration.getNick(mp.getBukkit()); + this.teamColor = mp.getParty().getTextColor(); + this.dead = mp.isDead(); + this.vanish = Integration.isVanished(mp.getBukkit()); + this.online = mp.getBukkit().isOnline(); + this.conceal = false; + this.style = style; + } + + public PlayerData(@NotNull MatchPlayerState mps, @NotNull NameStyle style) { + this.uuid = mps.getId(); + + this.name = mps.getNameLegacy(); + this.nick = mps.getNick(); + this.teamColor = mps.getParty().getTextColor(); + this.dead = mps.isDead(); + this.vanish = mps.isVanished(); + this.online = true; // They were online for sure when the state was created. + this.conceal = false; + this.style = style; + } + + public PlayerData(@Nullable Player player, @Nullable String username, @NotNull NameStyle style) { + this.uuid = player != null ? player.getUniqueId() : null; + + this.name = player != null ? player.getName() : username; + this.nick = player != null ? Integration.getNick(player) : null; + // Null-check is relevant as MatchManager will be null when loading author names. + MatchPlayer mp = player != null ? PGM.get().getMatchManager().getPlayer(player) : null; + this.teamColor = mp == null ? PlayerComponent.OFFLINE_COLOR : mp.getParty().getTextColor(); + this.dead = mp != null && mp.isDead(); + this.vanish = mp != null && mp.isVanished(); + this.online = player != null && player.isOnline(); + this.conceal = true; + this.style = style; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PlayerData)) return false; + + PlayerData that = (PlayerData) o; + + if (dead != that.dead) return false; + if (vanish != that.vanish) return false; + if (online != that.online) return false; + if (conceal != that.conceal) return false; + if (!Objects.equals(uuid, that.uuid)) return false; + if (!Objects.equals(name, that.name)) return false; + if (!Objects.equals(nick, that.nick)) return false; + if (!teamColor.equals(that.teamColor)) return false; + return style == that.style; + } + + @Override + public int hashCode() { + int result = uuid != null ? uuid.hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (nick != null ? nick.hashCode() : 0); + result = 31 * result + teamColor.hashCode(); + result = 31 * result + (dead ? 1 : 0); + result = 31 * result + (vanish ? 1 : 0); + result = 31 * result + (online ? 1 : 0); + result = 31 * result + (conceal ? 1 : 0); + result = 31 * result + style.hashCode(); + return result; + } + + @Override + public String toString() { + return "PlayerData{" + + "uuid=" + + uuid + + ", name='" + + name + + '\'' + + ", nick='" + + nick + + '\'' + + ", teamColor=" + + teamColor + + ", dead=" + + dead + + ", vanish=" + + vanish + + ", online=" + + online + + ", conceal=" + + conceal + + ", style=" + + style + + '}'; + } +} diff --git a/core/src/main/java/tc/oc/pgm/util/player/PlayerRelationship.java b/core/src/main/java/tc/oc/pgm/util/player/PlayerRelationship.java new file mode 100644 index 0000000000..23d60ef3bf --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/util/player/PlayerRelationship.java @@ -0,0 +1,51 @@ +package tc.oc.pgm.util.player; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.util.Players; + +class PlayerRelationship { + public final boolean reveal; + public final boolean self; + public final boolean friend; + + public PlayerRelationship(@Nullable Player pl, @NotNull CommandSender viewer) { + this.reveal = pl != null && Players.shouldReveal(viewer, pl); + this.self = pl == viewer; + this.friend = pl != null && Players.isFriend(viewer, pl); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PlayerRelationship)) return false; + + PlayerRelationship that = (PlayerRelationship) o; + + if (reveal != that.reveal) return false; + if (self != that.self) return false; + return friend == that.friend; + } + + @Override + public int hashCode() { + int result = (reveal ? 1 : 0); + result = 31 * result + (self ? 1 : 0); + result = 31 * result + (friend ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "PlayerRelationship{" + + "reveal=" + + reveal + + ", self=" + + self + + ", friend=" + + friend + + '}'; + } +} diff --git a/core/src/main/java/tc/oc/pgm/util/player/PlayerRenderer.java b/core/src/main/java/tc/oc/pgm/util/player/PlayerRenderer.java new file mode 100644 index 0000000000..384568470a --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/util/player/PlayerRenderer.java @@ -0,0 +1,134 @@ +package tc.oc.pgm.util.player; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.Component.textOfChildren; +import static net.kyori.adventure.text.Component.translatable; +import static net.kyori.adventure.text.event.ClickEvent.runCommand; +import static net.kyori.adventure.text.event.HoverEvent.showText; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.jetbrains.annotations.NotNull; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.util.named.NameDecorationProvider; +import tc.oc.pgm.util.named.NameStyle; + +@SuppressWarnings("UnstableApiUsage") +class PlayerRenderer { + private static final TextColor DEAD_COLOR = NamedTextColor.DARK_GRAY; + private static final Style NICK_STYLE = + Style.style(TextDecoration.ITALIC).decoration(TextDecoration.STRIKETHROUGH, false); + + private final LoadingCache nameCache; + + protected PlayerRenderer() { + this.nameCache = + CacheBuilder.newBuilder() + .expireAfterAccess(15, TimeUnit.MINUTES) + .build( + new CacheLoader() { + @Override + public Component load(@NotNull PlayerCacheKey key) { + return render(key); + } + }); + } + + public Component render(PlayerData data, PlayerRelationship relation) { + return nameCache.getUnchecked(new PlayerCacheKey(data, relation)); + } + + private Component render(PlayerCacheKey key) { + PlayerData data = key.data; + PlayerRelationship relation = key.relationship; + if (data.name == null) return PlayerComponent.UNKNOWN; + + // Generic term for either nicked or vanished + boolean disguised = (data.nick != null || data.vanish); + + if (!data.online || (data.conceal && disguised && !relation.reveal)) { + return text(data.name, PlayerComponent.OFFLINE_COLOR); + } + + String plName = relation.reveal || data.nick == null ? data.name : data.nick; + UUID uuid = data.uuid; + + TextColor color = + data.style.has(NameStyle.Flag.DEATH) && data.dead + ? DEAD_COLOR + : data.style.has(NameStyle.Flag.COLOR) ? data.teamColor : null; + + TextComponent.Builder name = text().content(plName).color(color); + + if (relation.reveal && data.style.has(NameStyle.Flag.SELF) && relation.self) { + name.decoration(TextDecoration.BOLD, true); + } + if (relation.reveal && data.style.has(NameStyle.Flag.FRIEND) && relation.friend) { + name.decoration(TextDecoration.ITALIC, true); + } + if (relation.reveal && data.style.has(NameStyle.Flag.DISGUISE) && disguised) { + name.decoration(TextDecoration.STRIKETHROUGH, true); + + if (data.nick != null && data.style.has(NameStyle.Flag.NICKNAME)) { + name.append(text(" " + data.nick, NICK_STYLE)); + } + } + + if (data.style.has(NameStyle.Flag.TELEPORT)) { + name.hoverEvent(showText(translatable("misc.teleportTo", NamedTextColor.GRAY, name.build()))) + .clickEvent(runCommand("/tp " + plName)); + } + + if (relation.reveal && data.style.has(NameStyle.Flag.FLAIR)) { + NameDecorationProvider provider = PGM.get().getNameDecorationRegistry(); + return textOfChildren( + provider.getPrefixComponent(uuid), name, provider.getSuffixComponent(uuid)); + } else { + // Optimization: if flairs aren't rendered, we can eliminate one nesting step + return name.build(); + } + } + + private static class PlayerCacheKey { + public final PlayerData data; + public final PlayerRelationship relationship; + + public PlayerCacheKey(PlayerData data, PlayerRelationship relationship) { + this.data = data; + this.relationship = relationship; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PlayerCacheKey)) return false; + + PlayerCacheKey that = (PlayerCacheKey) o; + + if (!Objects.equals(data, that.data)) return false; + return Objects.equals(relationship, that.relationship); + } + + @Override + public int hashCode() { + int result = data != null ? data.hashCode() : 0; + result = 31 * result + (relationship != null ? relationship.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "PlayerCacheKey{" + "data=" + data + ", relationship=" + relationship + '}'; + } + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/tablist/PlayerTabEntry.java b/util/src/main/java/tc/oc/pgm/util/tablist/PlayerTabEntry.java index f54a71e659..86eaa05519 100644 --- a/util/src/main/java/tc/oc/pgm/util/tablist/PlayerTabEntry.java +++ b/util/src/main/java/tc/oc/pgm/util/tablist/PlayerTabEntry.java @@ -3,7 +3,7 @@ import static net.kyori.adventure.text.Component.text; import java.util.UUID; -import java.util.function.BiFunction; +import java.util.function.Function; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; import org.jetbrains.annotations.Nullable; @@ -21,10 +21,9 @@ public class PlayerTabEntry extends DynamicTabEntry { private static boolean showPing = false; - private static BiFunction playerComponent = - (p, v) -> text(p.getName()); + private static Function playerComponent = p -> text(p.getName()); - public static void setPlayerComponent(BiFunction playerComponent) { + public static void setPlayerComponent(Function playerComponent) { PlayerTabEntry.playerComponent = playerComponent; } @@ -58,7 +57,7 @@ public PlayerTabEntry(Player player) { @Override public Component getContent(TabView view) { - return playerComponent.apply(player, view.getViewer()); + return playerComponent.apply(player); } @Override diff --git a/util/src/main/java/tc/oc/pgm/util/text/NumberComponent.java b/util/src/main/java/tc/oc/pgm/util/text/NumberComponent.java new file mode 100644 index 0000000000..f2cd970162 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/text/NumberComponent.java @@ -0,0 +1,38 @@ +package tc.oc.pgm.util.text; + +import static net.kyori.adventure.text.Component.text; + +import java.text.DecimalFormat; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; + +public class NumberComponent { + + /** Common formats used by stats with decimals */ + public static final DecimalFormat FORMATTER = new DecimalFormat("0.##"); + + public static Component number(Number stat) { + double value = stat.doubleValue(); + boolean useShort = Math.abs(value) >= 10_000; + return text( + Double.isNaN(value) + ? "-" + : FORMATTER.format(useShort ? value / 1000 : value) + (useShort ? "k" : "")); + } + + /** + * Wraps a {@link Number} in a {@link Component} that is colored with the given {@link TextColor}. + * Rounds the number to a maximum of 2 decimals + * + *

If the number is NaN "-" is wrapped instead + * + *

If the number is >= 10000 it will be represented in the thousands (10k, 25.5k, 120.3k etc.) + * + * @param stat The number you want wrapped + * @param color The color you want the number to be + * @return a colored component wrapping the given number or "-" if NaN + */ + public static Component number(Number stat, TextColor color) { + return number(stat).color(color); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/text/TextTranslations.java b/util/src/main/java/tc/oc/pgm/util/text/TextTranslations.java index 49b714004f..9869ae1ea6 100644 --- a/util/src/main/java/tc/oc/pgm/util/text/TextTranslations.java +++ b/util/src/main/java/tc/oc/pgm/util/text/TextTranslations.java @@ -1,6 +1,8 @@ package tc.oc.pgm.util.text; import static net.kyori.adventure.key.Key.key; +import static net.kyori.adventure.text.Component.score; +import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; import static tc.oc.pgm.util.Assert.assertTrue; @@ -30,6 +32,7 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.pointer.Pointered; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.translation.GlobalTranslator; @@ -392,15 +395,20 @@ public static String translate(String key, @Nullable CommandSender viewer, Objec * @see #translate(Component, Pointered) for the newer text system. */ @Deprecated - public static String translate(String key, @Nullable Pointered viewer, Object... args) { + public static String translate(String key, @NotNull Pointered viewer, Object... args) { final Component text = translatable( - key, - Stream.of(args).map(String::valueOf).map(Component::text).collect(Collectors.toList())); + key, Stream.of(args).map(TextTranslations::toComponent).collect(Collectors.toList())); return LegacyComponentSerializer.legacySection().serialize(translate(text, viewer)); } + private static ComponentLike toComponent(Object obj) { + if (obj instanceof Component) return (Component) obj; + if (obj instanceof ComponentLike) return (ComponentLike) obj; + return text(String.valueOf(obj)); + } + public static String toMinecraftGson(Component component, @Nullable CommandSender viewer) { Component translated = translate(component, getPointered(viewer)); return GsonComponentSerializer.colorDownsamplingGson().serialize(translated); diff --git a/util/src/test/java/tc/oc/pgm/util/text/NumberComponentTest.java b/util/src/test/java/tc/oc/pgm/util/text/NumberComponentTest.java new file mode 100644 index 0000000000..48442df0e5 --- /dev/null +++ b/util/src/test/java/tc/oc/pgm/util/text/NumberComponentTest.java @@ -0,0 +1,68 @@ +package tc.oc.pgm.util.text; + +import static net.kyori.adventure.text.Component.text; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static tc.oc.pgm.util.text.NumberComponent.number; + +import java.util.Locale; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public final class NumberComponentTest { + + static { + Locale.setDefault(Locale.ROOT); + } + + @ParameterizedTest + @CsvSource({ + "-10, -10", + "-1000, -1000", + "-5000, -5000", + "0, 0", + "10, 10", + "1000, 1000", + "5000, 5000", + }) + void testIntegers(double value, String expected) { + assertEquals(text(expected), number(value)); + } + + @ParameterizedTest + @CsvSource({ + "-0.1, -0.1", + "-0.15, -0.15", + "-100.5, -100.5", + "-100.53, -100.53", + "-100.537, -100.54", + "0.1, 0.1", + "0.15, 0.15", + "100.5, 100.5", + "100.53, 100.53", + "100.537, 100.54", + }) + void testDecimals(double value, String expected) { + assertEquals(text(expected), number(value)); + } + + @ParameterizedTest + @CsvSource({ + "-10000, -10k", + "-10001, -10k", + "-10009, -10.01k", + "-10010, -10.01k", + "-10100, -10.1k", + "-15530, -15.53k", + "-15537, -15.54k", + "10000, 10k", + "10001, 10k", + "10009, 10.01k", + "10010, 10.01k", + "10100, 10.1k", + "15530, 15.53k", + "15537, 15.54k", + }) + void testShort(double value, String expected) { + assertEquals(text(expected), number(value)); + } +}