diff --git a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts index bcea9954..cca8484a 100644 --- a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts @@ -12,20 +12,32 @@ java { repositories { mavenCentral() - maven("https://repo.pgm.fyi/snapshots") // Sportpaper & other pgm-specific stuff - maven("https://repo.papermc.io/repository/maven-public/") // PaperMC repo - maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") // Spigot repo - maven("https://repo.aikar.co/content/groups/aikar/") // aikar repo + maven("https://repo.pgm.fyi/snapshots") // SportPaper & other PGM-specific stuff + maven("https://repo.papermc.io/repository/maven-public/") // Paper builds & paperweight plugin + maven("https://repo.aikar.co/content/groups/aikar/") // Aikar repo + maven("https://repo.codemc.io/repository/maven-releases/") // PacketEvents + exclusiveContent { + forRepository { + maven("https://jitpack.io") + } + filter { + includeGroup("com.github.OvercastCommunity.adventure-platform") + includeGroup("com.github.MinusKube") + } + } mavenLocal() // Local last } dependencies { api("com.zaxxer:HikariCP:2.4.1") { isTransitive = false } - api("fr.minuskube.inv:smart-invs:1.2.7") { isTransitive = false } + // Latest SmartInvs commit + api("com.github.MinusKube:SmartInvs:9c9dbbee16") { isTransitive = false } api("redis.clients:jedis:3.5.1") api("net.kyori:adventure-api:4.26.1") api("net.kyori:adventure-text-serializer-plain:4.26.1") - api("net.kyori:adventure-platform-bukkit:4.4.1") + // adventure-platform fork with ViaVersion and 1.21.11+ fixes + // https://github.com/OvercastCommunity/adventure-platform + api("com.github.OvercastCommunity.adventure-platform:adventure-platform-bukkit:04de657e85") api("org.reflections:reflections:0.10.2") // Annotations @@ -37,7 +49,7 @@ dependencies { compileOnly("tc.oc.pgm:util:0.16-SNAPSHOT") compileOnly("tc.oc.occ:Environment:1.0.0-SNAPSHOT") compileOnly("org.incendo:cloud-annotations:2.0.0") - compileOnly("net.dmulloy2:ProtocolLib:5.4.0") + compileOnly("com.github.retrooper:packetevents-spigot:2.12.0") // Paper and SportPaper include these (or equivalents) compileOnly("it.unimi.dsi:fastutil:8.5.15") diff --git a/core/src/main/java/dev/pgm/community/Community.java b/core/src/main/java/dev/pgm/community/Community.java index 61d056ba..02a1f7a0 100644 --- a/core/src/main/java/dev/pgm/community/Community.java +++ b/core/src/main/java/dev/pgm/community/Community.java @@ -1,5 +1,7 @@ package dev.pgm.community; +import static dev.pgm.community.nick.identity.PlayerIdentity.PLAYER_IDENTITY; + import dev.pgm.community.commands.graph.CommunityCommandGraph; import dev.pgm.community.events.CommunityEvent; import dev.pgm.community.feature.FeatureManager; @@ -51,8 +53,11 @@ public void onEnable() { } catch (Throwable t) { getLogger().log(Level.SEVERE, "Failed to initialize Community platform", t); getServer().getPluginManager().disablePlugin(this); + return; } + Platform.MANIFEST.onEnable(this); + this.setupConfig(); getLogger().info(dev.pgm.community.database.DatabaseExecutor.describeBackend()); this.setupFeatures(); @@ -60,7 +65,9 @@ public void onEnable() { @Override public void onDisable() { - features.disable(); + Platform.MANIFEST.onDisable(); + if (features != null) features.disable(); + PLAYER_IDENTITY.clearAll(); dev.pgm.community.database.DatabaseExecutor.shutdown(); } @@ -97,7 +104,6 @@ private void setupTranslations() { } public void registerListener(Listener listener) { - Platform.MANIFEST.onEnable(this); getServer().getPluginManager().registerEvents(listener, this); } diff --git a/core/src/main/java/dev/pgm/community/nick/feature/PGMNickIntegration.java b/core/src/main/java/dev/pgm/community/nick/feature/PGMNickIntegration.java index 827353d7..190c94dd 100644 --- a/core/src/main/java/dev/pgm/community/nick/feature/PGMNickIntegration.java +++ b/core/src/main/java/dev/pgm/community/nick/feature/PGMNickIntegration.java @@ -1,5 +1,6 @@ package dev.pgm.community.nick.feature; +import static dev.pgm.community.nick.identity.PlayerIdentity.PLAYER_IDENTITY; import static net.kyori.adventure.text.Component.text; import dev.pgm.community.utils.PGMUtils; @@ -10,11 +11,13 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; import tc.oc.pgm.api.PGM; import tc.oc.pgm.api.integration.Integration; import tc.oc.pgm.api.integration.NickIntegration; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.util.skin.Skin; public class PGMNickIntegration implements NickIntegration { @@ -36,6 +39,11 @@ public String getNick(Player player) { return nick.getOnlineNick(player.getUniqueId()); } + @Override + public Skin getPlayerSkin(@NonNull Player player, Player viewer) { + return PLAYER_IDENTITY.getSkin(player, viewer); + } + public void cancelTask() { hotbarTask.cancel(true); } diff --git a/core/src/main/java/dev/pgm/community/nick/feature/types/NickFeatureCore.java b/core/src/main/java/dev/pgm/community/nick/feature/types/NickFeatureCore.java index 627859c3..b7ec4160 100644 --- a/core/src/main/java/dev/pgm/community/nick/feature/types/NickFeatureCore.java +++ b/core/src/main/java/dev/pgm/community/nick/feature/types/NickFeatureCore.java @@ -86,9 +86,21 @@ public SkinManager getSkinManager() { @Override public void enable() { super.enable(); + skins.enable(); integrate(); } + @Override + public void disable() { + if (pgmNicks != null) { + pgmNicks.cancelTask(); + pgmNicks = null; + } + + skins.disable(); + super.disable(); + } + private void integrate() { if (isPGMEnabled()) { pgmNicks = new PGMNickIntegration(this); @@ -183,12 +195,12 @@ public void onJoin(PlayerJoinEvent event) { .thenAcceptAsync(name -> this.setNick(player.getUniqueId(), name) .thenAcceptAsync(success -> { if (success) { - nickedPlayers.put(player.getUniqueId(), nick.getName()); + nickedPlayers.put(player.getUniqueId(), name); Audience.get(player) .sendWarning(text( "You had no nickname, so a random one has been assigned", NamedTextColor.GREEN)); - sendLoginNotification(player, nick.getName(), true); + sendLoginNotification(player, name, true); } })); } diff --git a/core/src/main/java/dev/pgm/community/nick/identity/PlayerIdentity.java b/core/src/main/java/dev/pgm/community/nick/identity/PlayerIdentity.java new file mode 100644 index 00000000..d1fbd5ed --- /dev/null +++ b/core/src/main/java/dev/pgm/community/nick/identity/PlayerIdentity.java @@ -0,0 +1,216 @@ +package dev.pgm.community.nick.identity; + +import static dev.pgm.community.util.PlayerUtils.PLAYER_UTILS; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.UUID; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import tc.oc.pgm.util.skin.Skin; + +@NullMarked +public final class PlayerIdentity { + public static final int MAX_NICK_LENGTH = 16; + public static final PlayerIdentity PLAYER_IDENTITY = new PlayerIdentity(); + + private final Map> playerSkins = new HashMap<>(); + private final Map> playerNames = new HashMap<>(); + private final Map> playerDisplayNames = new HashMap<>(); + private final Map>> viewerTeamEntries = new HashMap<>(); + private final Map> viewerVisibleNames = new HashMap<>(); + + private PlayerIdentity() {} + + public synchronized void set( + Player player, + Player viewer, + @Nullable String displayName, + @Nullable String nick, + @Nullable Skin skin) { + validateNick(nick); + + UUID playerId = player.getUniqueId(); + UUID viewerId = viewer.getUniqueId(); + + set(playerSkins, playerId, viewerId, skin); + set(playerNames, playerId, viewerId, nick); + set(playerDisplayNames, playerId, viewerId, displayName); + setVisibleName(viewerId, player.getName(), getName(player, viewer)); + } + + private static void set( + Map> identities, UUID playerId, UUID viewerId, @Nullable T value) { + if (value == null) { + Map viewers = identities.get(playerId); + if (viewers == null) return; + + viewers.remove(viewerId); + if (viewers.isEmpty()) identities.remove(playerId); + return; + } + + identities.computeIfAbsent(playerId, k -> new HashMap<>()).put(viewerId, value); + } + + public static void validateNick(@Nullable String name) { + if (name != null && name.length() > MAX_NICK_LENGTH) { + throw new IllegalArgumentException( + "Player nick names are limited to " + MAX_NICK_LENGTH + " characters in length"); + } + } + + public synchronized void clearPlayer(UUID playerId, String realName) { + playerSkins.remove(playerId); + playerNames.remove(playerId); + playerDisplayNames.remove(playerId); + viewerVisibleNames.values().removeIf(visibleNames -> { + visibleNames.remove(realName); + return visibleNames.isEmpty(); + }); + } + + public synchronized void clearViewer(UUID viewerId) { + clearViewer(playerSkins, viewerId); + clearViewer(playerNames, viewerId); + clearViewer(playerDisplayNames, viewerId); + viewerTeamEntries.remove(viewerId); + viewerVisibleNames.remove(viewerId); + } + + private static void clearViewer(Map> identities, UUID viewerId) { + identities.values().removeIf(viewers -> { + viewers.remove(viewerId); + return viewers.isEmpty(); + }); + } + + public synchronized void clearAll() { + playerSkins.clear(); + playerNames.clear(); + playerDisplayNames.clear(); + viewerTeamEntries.clear(); + viewerVisibleNames.clear(); + } + + public synchronized @Nullable String getVisibleName(UUID viewerId, String realName) { + Map visibleNames = viewerVisibleNames.get(viewerId); + return visibleNames == null ? null : visibleNames.get(realName); + } + + private void setVisibleName(UUID viewerId, String realName, String visibleName) { + if (realName.equals(visibleName)) { + clearVisibleName(viewerId, realName); + return; + } + + viewerVisibleNames.computeIfAbsent(viewerId, k -> new HashMap<>()).put(realName, visibleName); + } + + private void clearVisibleName(UUID viewerId, String realName) { + Map visibleNames = viewerVisibleNames.get(viewerId); + if (visibleNames == null) return; + + visibleNames.remove(realName); + if (visibleNames.isEmpty()) viewerVisibleNames.remove(viewerId); + } + + public synchronized @Nullable String getTeamName(UUID viewerId, String entry) { + Map> teamEntries = viewerTeamEntries.get(viewerId); + if (teamEntries == null) return null; + + for (Map.Entry> team : teamEntries.entrySet()) { + if (team.getValue().contains(entry)) return team.getKey(); + } + + return null; + } + + public synchronized boolean hasTeamEntry(UUID viewerId, String teamName, String entry) { + Map> teamEntries = viewerTeamEntries.get(viewerId); + if (teamEntries == null) return false; + + HashSet entries = teamEntries.get(teamName); + return entries != null && entries.contains(entry); + } + + public synchronized void createTeam(UUID viewerId, String teamName, Collection entries) { + removeTeam(viewerId, teamName); + addTeamEntries(viewerId, teamName, entries); + } + + public synchronized void removeTeam(UUID viewerId, String teamName) { + Map> teamEntries = viewerTeamEntries.get(viewerId); + if (teamEntries == null) return; + + teamEntries.remove(teamName); + if (teamEntries.isEmpty()) viewerTeamEntries.remove(viewerId); + } + + public synchronized void addTeamEntries( + UUID viewerId, String teamName, Collection entries) { + if (entries.isEmpty()) return; + + Map> teamEntries = + viewerTeamEntries.computeIfAbsent(viewerId, k -> new HashMap<>()); + teamEntries.values().forEach(team -> team.removeAll(entries)); + teamEntries.values().removeIf(Collection::isEmpty); + teamEntries.computeIfAbsent(teamName, k -> new HashSet<>()).addAll(entries); + } + + public synchronized void removeTeamEntries( + UUID viewerId, String teamName, Collection entries) { + Map> teamEntries = viewerTeamEntries.get(viewerId); + if (teamEntries == null) return; + + HashSet team = teamEntries.get(teamName); + if (team == null) return; + + team.removeAll(entries); + if (team.isEmpty()) teamEntries.remove(teamName); + if (teamEntries.isEmpty()) viewerTeamEntries.remove(viewerId); + } + + public synchronized String getDisplayName(Player player, Player viewer) { + String displayName = get(playerDisplayNames, player.getUniqueId(), viewer.getUniqueId()); + if (displayName != null) return displayName; + + return player.getDisplayName(); + } + + public synchronized boolean hasDisplayName(Player player, Player viewer) { + return get(playerDisplayNames, player.getUniqueId(), viewer.getUniqueId()) != null; + } + + public synchronized String getName(Player player, Player viewer) { + String name = get(playerNames, player.getUniqueId(), viewer.getUniqueId()); + if (name != null) return name; + + return player.getName(); + } + + public synchronized boolean hasName(Player player, Player viewer) { + return get(playerNames, player.getUniqueId(), viewer.getUniqueId()) != null; + } + + public synchronized Skin getSkin(Player player, Player viewer) { + Skin skin = get(playerSkins, player.getUniqueId(), viewer.getUniqueId()); + if (skin != null && !skin.isEmpty()) return skin; + + return PLAYER_UTILS.getPlayerSkin(player); + } + + public synchronized boolean hasSkin(Player player, Player viewer) { + Skin skin = get(playerSkins, player.getUniqueId(), viewer.getUniqueId()); + return skin != null && !skin.isEmpty(); + } + + private static @Nullable T get( + Map> identities, UUID playerId, UUID viewerId) { + Map viewers = identities.get(playerId); + return viewers == null ? null : viewers.get(viewerId); + } +} diff --git a/core/src/main/java/dev/pgm/community/nick/skin/SkinCache.java b/core/src/main/java/dev/pgm/community/nick/skin/SkinCache.java index 432884c8..0c62492a 100644 --- a/core/src/main/java/dev/pgm/community/nick/skin/SkinCache.java +++ b/core/src/main/java/dev/pgm/community/nick/skin/SkinCache.java @@ -1,5 +1,6 @@ package dev.pgm.community.nick.skin; +import static dev.pgm.community.nick.identity.PlayerIdentity.PLAYER_IDENTITY; import static dev.pgm.community.util.PlayerUtils.PLAYER_UTILS; import com.google.common.cache.Cache; @@ -67,6 +68,15 @@ public void onPlayerQuit(PlayerQuitEvent event) { if (canUseSkin(player)) { offlineSkins.put(player.getUniqueId(), PLAYER_UTILS.getPlayerSkin(player)); } + + PLAYER_IDENTITY.clearViewer(player.getUniqueId()); + UUID playerId = player.getUniqueId(); + String playerName = player.getName(); + Bukkit.getScheduler().runTask(Community.get(), () -> { + if (Bukkit.getPlayer(playerId) == null) { + PLAYER_IDENTITY.clearPlayer(playerId, playerName); + } + }); } @EventHandler(priority = EventPriority.LOW) @@ -79,9 +89,6 @@ public void refreshNamesOnLogin(PlayerJoinEvent event) { refreshPlayer(event.getPlayer()); } - // SPORTPAPER STUFF - TODO: Add alternative method and check if server is running SportPaper to - // enable - private void refreshAllViewers(Player player) { Bukkit.getOnlinePlayers().forEach(viewer -> refreshFakeName(player, viewer)); } @@ -109,7 +116,6 @@ private void refreshSelfView(Player viewer) { Bukkit.getOnlinePlayers().forEach(other -> refreshFakeName(other, viewer)); } - // TODO: Figure out how to use without SPORTPAPER API private void refreshFakeName(Player player, Player viewer) { boolean nicked = Integration.getNick(player) != null; boolean areFriends = Integration.isFriend(player, viewer); @@ -121,6 +127,8 @@ private void refreshFakeName(Player player, Player viewer) { if (nicked && !canSeeRealName) { String nick = Integration.getNick(player); MatchPlayer matchPlayer = PGM.get().getMatchManager().getPlayer(player); + if (matchPlayer == null) return; + String displayName = PGM.get() .getNameDecorationRegistry() .getDecoratedName(player, matchPlayer.getParty().getColor()); diff --git a/core/src/main/java/dev/pgm/community/nick/skin/SkinManager.java b/core/src/main/java/dev/pgm/community/nick/skin/SkinManager.java index a2cbb56b..099cb29e 100644 --- a/core/src/main/java/dev/pgm/community/nick/skin/SkinManager.java +++ b/core/src/main/java/dev/pgm/community/nick/skin/SkinManager.java @@ -1,19 +1,39 @@ package dev.pgm.community.nick.skin; +import static dev.pgm.community.nick.identity.PlayerIdentity.PLAYER_IDENTITY; + import dev.pgm.community.Community; import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; import tc.oc.pgm.util.skin.Skin; public class SkinManager { private final SkinCache cache; + private boolean registered; public SkinManager() { this.cache = new SkinCache(); + enable(); + } + + public void enable() { + if (registered) return; + Community.get().registerListener(cache); + registered = true; } public void setSkin(Player player, Skin skin) { cache.onSkinRefresh(player, skin); } + + public void disable() { + if (registered) { + HandlerList.unregisterAll(cache); + registered = false; + } + + PLAYER_IDENTITY.clearAll(); + } } diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index b15c74c2..a8f7008c 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -5,4 +5,4 @@ main: ${mainClass} version: ${version} (git-${commitHash}) website: ${url} author: ${author} -softdepend: [Database, PGM, Environment] +softdepend: [Database, PGM, Environment, packetevents] diff --git a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/ModernPlatform.java b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/ModernPlatform.java index 99572f99..dde1d5fa 100644 --- a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/ModernPlatform.java +++ b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/ModernPlatform.java @@ -5,12 +5,29 @@ import dev.pgm.community.util.Platform; import dev.pgm.community.util.Supports; +import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; @Supports(value = PAPER, minVersion = "1.21.11", priority = HIGHEST) public class ModernPlatform implements Platform.Manifest { + private PacketManipulations packetManipulations; + @Override public void onEnable(Plugin plugin) { - new PacketManipulations(plugin); + if (!plugin.getServer().getPluginManager().isPluginEnabled("packetevents")) { + Bukkit.getServer().getPluginManager().disablePlugin(plugin); + throw new IllegalStateException( + "PacketEvents is not installed, and is required for Community modern version support"); + } + + packetManipulations = new PacketManipulations(); + } + + @Override + public void onDisable() { + if (packetManipulations != null) { + packetManipulations.unregister(); + packetManipulations = null; + } } } diff --git a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/ModernPlayerUtils.java b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/ModernPlayerUtils.java index 483014bc..470e75f1 100644 --- a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/ModernPlayerUtils.java +++ b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/ModernPlayerUtils.java @@ -1,17 +1,20 @@ package dev.pgm.community.platform.modern; +import static dev.pgm.community.nick.identity.PlayerIdentity.PLAYER_IDENTITY; import static dev.pgm.community.util.Supports.Variant.PAPER; import com.destroystokyo.paper.profile.CraftPlayerProfile; import com.destroystokyo.paper.profile.PlayerProfile; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams.TeamMode; import dev.pgm.community.util.PlayerUtils; import dev.pgm.community.util.Supports; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +import java.util.List; import java.util.UUID; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.Material; @@ -21,6 +24,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.profile.PlayerTextures; +import org.jspecify.annotations.NonNull; import tc.oc.pgm.platform.modern.util.Skins; import tc.oc.pgm.util.skin.Skin; @@ -32,59 +36,44 @@ public Skin getPlayerSkin(Player player) { return Skins.fromProfile(craftPlayer.getProfile()); } - private final Map> playerSkins = new HashMap<>(); - private final Map> playerNames = new HashMap<>(); - private final Map> playerDisplayNames = new HashMap<>(); - @Override public void setFakeNameAndSkin( Player player, Player viewer, String displayName, String nick, Skin skin) { - playerSkins - .computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()) - .put(viewer.getUniqueId(), skin); - playerNames - .computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()) - .put(viewer.getUniqueId(), nick); - playerDisplayNames - .computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()) - .put(viewer.getUniqueId(), displayName); - } + String oldName = PLAYER_IDENTITY.getName(player, viewer); - @Override - public String getPlayerDisplayName(Player player, Player viewer) { - if (playerDisplayNames.containsKey(player.getUniqueId())) { - Map uuidStringMap = playerDisplayNames.get(player.getUniqueId()); - String displayName = uuidStringMap.get(viewer.getUniqueId()); - if (displayName != null) return displayName; - } + PLAYER_IDENTITY.set(player, viewer, displayName, nick, skin); - return LegacyComponentSerializer.legacySection().serialize(player.displayName()); - } + String newName = PLAYER_IDENTITY.getName(player, viewer); - @Override - public String getPlayerName(Player player, Player viewer) { - if (playerNames.containsKey(player.getUniqueId())) { - Map uuidStringMap = playerNames.get(player.getUniqueId()); - String name = uuidStringMap.get(viewer.getUniqueId()); - if (name != null) return name; + if (!oldName.equals(newName)) { + updateTeamEntry(viewer, oldName, newName); } - - return player.getName(); } - @Override - public Skin getPlayerSkin(Player player, Player viewer) { - if (playerSkins.containsKey(player.getUniqueId())) { - Map uuidSkinMap = playerSkins.get(player.getUniqueId()); - Skin skin = uuidSkinMap.get(viewer.getUniqueId()); - if (skin != null) return skin; - } + private void updateTeamEntry(@NonNull Player viewer, String oldName, String newName) { + UUID viewerId = viewer.getUniqueId(); + String teamName = PLAYER_IDENTITY.getTeamName(viewerId, oldName); + if (teamName == null) return; + + sendTeamPacketSilently(viewer, teamName, TeamMode.REMOVE_ENTITIES, List.of(oldName)); + PLAYER_IDENTITY.removeTeamEntries(viewerId, teamName, List.of(oldName)); + + sendTeamPacketSilently(viewer, teamName, TeamMode.ADD_ENTITIES, List.of(newName)); + PLAYER_IDENTITY.addTeamEntries(viewerId, teamName, List.of(newName)); + } - return getPlayerSkin(player); + private void sendTeamPacketSilently( + Player viewer, String teamName, TeamMode mode, java.util.Collection names) { + PacketEvents.getAPI() + .getPlayerManager() + .sendPacketSilently( + viewer, + new WrapperPlayServerTeams( + teamName, mode, (WrapperPlayServerTeams.ScoreBoardTeamInfo) null, names)); } @Override - public ItemStack customSkull(String url, String displayName, String... lore) { + public ItemStack customSkull(@NonNull String url, String displayName, String... lore) { ItemStack head = new ItemStack(Material.PLAYER_HEAD); if (url.isEmpty()) { return head; diff --git a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/PacketManipulations.java b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/PacketManipulations.java index ca26b38f..0c3f04fb 100644 --- a/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/PacketManipulations.java +++ b/platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/PacketManipulations.java @@ -1,81 +1,165 @@ package dev.pgm.community.platform.modern; -import static dev.pgm.community.util.PlayerUtils.PLAYER_UTILS; - -import com.comphenix.protocol.PacketType; -import com.comphenix.protocol.events.ListenerPriority; -import com.comphenix.protocol.events.PacketEvent; -import com.comphenix.protocol.wrappers.EnumWrappers; -import com.comphenix.protocol.wrappers.PlayerInfoData; -import com.comphenix.protocol.wrappers.WrappedChatComponent; -import com.comphenix.protocol.wrappers.WrappedGameProfile; -import com.comphenix.protocol.wrappers.WrappedSignedProperty; +import static dev.pgm.community.nick.identity.PlayerIdentity.PLAYER_IDENTITY; + +import com.github.retrooper.packetevents.event.PacketListenerCommon; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.TextureProperty; +import com.github.retrooper.packetevents.protocol.player.UserProfile; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPlayerInfoUpdate; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams.TeamMode; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.apache.commons.lang3.StringUtils; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import tc.oc.pgm.platform.modern.packets.PacketSender; -import tc.oc.pgm.platform.modern.util.Packets; +import org.jspecify.annotations.NonNull; +import tc.oc.pgm.util.nms.packets.PacketEventsUtil; import tc.oc.pgm.util.skin.Skin; -public class PacketManipulations implements PacketSender { +public class PacketManipulations { + private final PacketListenerCommon listener; + + public PacketManipulations() { + this.listener = PacketEventsUtil.registerSend( + PacketListenerPriority.LOWEST, + Map.of( + PacketType.Play.Server.PLAYER_INFO_UPDATE, + this::handlePlayerInfo, + PacketType.Play.Server.TEAMS, + this::handleScoreboardTeams)); + } - public PacketManipulations(Plugin plugin) { - Packets.register( - plugin, - ListenerPriority.LOWEST, - Map.of(PacketType.Play.Server.PLAYER_INFO, this::handlePlayerInfo)); + public void unregister() { + PacketEventsUtil.unregister(listener); } - private void handlePlayerInfo(PacketEvent event) { + private void handlePlayerInfo(@NonNull PacketSendEvent event) { Player viewer = event.getPlayer(); + var wrapper = new WrapperPlayServerPlayerInfoUpdate(event); - Set actions = - event.getPacket().getPlayerInfoActions().read(0); - boolean hasAddPlayer = actions.contains(EnumWrappers.PlayerInfoAction.ADD_PLAYER); + var actions = wrapper.getActions(); + boolean hasAddPlayer = actions.contains(WrapperPlayServerPlayerInfoUpdate.Action.ADD_PLAYER); boolean hasUpdateDisplayName = - actions.contains(EnumWrappers.PlayerInfoAction.UPDATE_DISPLAY_NAME); + actions.contains(WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_DISPLAY_NAME); if (!hasAddPlayer && !hasUpdateDisplayName) return; - List infoList = event.getPacket().getPlayerInfoDataLists().read(0); - for (int i = 0; i < infoList.size(); i++) { - PlayerInfoData playerInfoData = infoList.get(i); - if (playerInfoData == null) continue; - - UUID playerId = playerInfoData.getProfileId(); - Player player = Bukkit.getPlayer(playerId); - if (player == null || player.equals(viewer) || !player.isOnline()) continue; - - String playerDisplayName = PLAYER_UTILS.getPlayerDisplayName(player, viewer); - String playerName = PLAYER_UTILS.getPlayerName(player, viewer); - - if (StringUtils.isBlank(playerName) || StringUtils.isBlank(playerDisplayName)) continue; - - WrappedGameProfile wrappedGameProfile = playerInfoData.getProfile().withName(playerName); - if (hasAddPlayer) { - Skin playerSkin = PLAYER_UTILS.getPlayerSkin(player, viewer); - wrappedGameProfile - .getProperties() - .put( - "textures", - new WrappedSignedProperty( - "textures", playerSkin.getData(), playerSkin.getSignature())); + boolean modified = false; + var entries = wrapper.getEntries(); + for (var entry : entries) { + Player player = Bukkit.getPlayer(entry.getProfileId()); + if (player == null || player.equals(viewer)) continue; + + boolean hasName = PLAYER_IDENTITY.hasName(player, viewer); + boolean hasSkin = PLAYER_IDENTITY.hasSkin(player, viewer); + boolean hasDisplayName = PLAYER_IDENTITY.hasDisplayName(player, viewer); + + if (!hasName && !hasSkin && !hasDisplayName) continue; + + if (hasAddPlayer && (hasName || hasSkin)) { + UserProfile profile = entry.getGameProfile(); + if (hasName) { + String playerName = PLAYER_IDENTITY.getName(player, viewer); + if (StringUtils.isBlank(playerName)) continue; + + profile.setName(playerName); + } + + if (hasSkin) { + Skin skin = PLAYER_IDENTITY.getSkin(player, viewer); + profile.getTextureProperties().clear(); + profile + .getTextureProperties() + .add(new TextureProperty("textures", skin.getData(), skin.getSignature())); + } + + entry.setGameProfile(profile); + } + + if (hasUpdateDisplayName && hasDisplayName) { + String playerDisplayName = PLAYER_IDENTITY.getDisplayName(player, viewer); + if (StringUtils.isBlank(playerDisplayName)) continue; + + entry.setDisplayName( + LegacyComponentSerializer.legacySection().deserialize(playerDisplayName)); + } + + modified = true; + } + + if (modified) event.markForReEncode(true); + } + + private void handleScoreboardTeams(@NonNull PacketSendEvent event) { + Player viewer = event.getPlayer(); + UUID viewerId = viewer.getUniqueId(); + var wrapper = new WrapperPlayServerTeams(event); + TeamMode mode = wrapper.getTeamMode(); + String teamName = wrapper.getTeamName(); + + List players = new ArrayList<>(wrapper.getPlayers()); + if (players.isEmpty()) { + trackTeamPacket(viewerId, teamName, mode, players); + return; + } + + boolean modified = false; + for (int i = 0; i < players.size(); i++) { + String entry = players.get(i); + String visibleName = PLAYER_IDENTITY.getVisibleName(viewerId, entry); + if (mode == TeamMode.REMOVE_ENTITIES) { + if (!StringUtils.isBlank(visibleName) + && PLAYER_IDENTITY.hasTeamEntry(viewerId, teamName, visibleName)) { + if (!visibleName.equals(entry)) { + players.set(i, visibleName); + modified = true; + } + continue; + } + + if (PLAYER_IDENTITY.hasTeamEntry(viewerId, teamName, entry)) continue; + + players.remove(i--); + modified = true; + continue; } - infoList.set( - i, - new PlayerInfoData( - playerId, - playerInfoData.getLatency(), - playerInfoData.isListed(), - playerInfoData.getGameMode(), - wrappedGameProfile, - WrappedChatComponent.fromLegacyText(playerDisplayName))); + if (visibleName == null || visibleName.equals(entry)) continue; + + players.set(i, visibleName); + modified = true; + } + + if (mode == TeamMode.REMOVE_ENTITIES && players.isEmpty()) { + event.setCancelled(true); + return; + } + + if (modified) { + wrapper.setPlayers(players); + event.markForReEncode(true); + } + + trackTeamPacket(viewerId, teamName, mode, players); + } + + private void trackTeamPacket( + UUID viewerId, String teamName, TeamMode mode, List players) { + switch (mode) { + case CREATE -> PLAYER_IDENTITY.createTeam(viewerId, teamName, players); + case REMOVE -> PLAYER_IDENTITY.removeTeam(viewerId, teamName); + case ADD_ENTITIES -> PLAYER_IDENTITY.addTeamEntries(viewerId, teamName, players); + case REMOVE_ENTITIES -> PLAYER_IDENTITY.removeTeamEntries(viewerId, teamName, players); + case UPDATE -> { + // Team metadata update; membership is unchanged. + } } } } diff --git a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/SpPlatform.java b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/SpPlatform.java index ffe9a911..2dbe1010 100644 --- a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/SpPlatform.java +++ b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/SpPlatform.java @@ -5,10 +5,6 @@ import dev.pgm.community.util.Platform; import dev.pgm.community.util.Supports; -import org.bukkit.plugin.Plugin; @Supports(value = SPORTPAPER, priority = HIGHEST) -public class SpPlatform implements Platform.Manifest { - @Override - public void onEnable(Plugin plugin) {} -} +public class SpPlatform implements Platform.Manifest {} diff --git a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/SpPlayerUtils.java b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/SpPlayerUtils.java index 2e9bc6d4..7bd21b01 100644 --- a/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/SpPlayerUtils.java +++ b/platform/platform-sportpaper/src/main/java/dev/pgm/community/platform/sportpaper/SpPlayerUtils.java @@ -1,5 +1,6 @@ package dev.pgm.community.platform.sportpaper; +import static dev.pgm.community.nick.identity.PlayerIdentity.PLAYER_IDENTITY; import static dev.pgm.community.util.Supports.Variant.SPORTPAPER; import com.mojang.authlib.GameProfile; @@ -19,6 +20,8 @@ import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import tc.oc.pgm.platform.sportpaper.utils.Skins; import tc.oc.pgm.util.bukkit.BukkitUtils; import tc.oc.pgm.util.skin.Skin; @@ -35,6 +38,7 @@ public Skin getPlayerSkin(Player player) { @Override public void setFakeNameAndSkin( Player player, Player viewer, String displayName, String nick, Skin skin) { + PLAYER_IDENTITY.set(player, viewer, displayName, nick, skin); var fakeSkin = skin == null ? null : new org.bukkit.Skin(skin.getData(), skin.getSignature()); player.setFakeDisplayName(viewer, displayName); @@ -42,23 +46,7 @@ public void setFakeNameAndSkin( } @Override - public String getPlayerDisplayName(Player player, Player viewer) { - return player.getDisplayName(viewer); - } - - @Override - public String getPlayerName(Player player, Player viewer) { - return player.getName(viewer); - } - - @Override - public Skin getPlayerSkin(Player player, Player viewer) { - org.bukkit.Skin skin = player.getSkin(viewer); - return new Skin(skin.getData(), skin.getSignature()); - } - - @Override - public ItemStack customSkull(String url, String displayName, String... lore) { + public ItemStack customSkull(@NonNull String url, String displayName, String... lore) { ItemStack head = new ItemStack(Material.SKULL_ITEM); head.setDurability((short) SkullType.PLAYER.ordinal()); if (url.isEmpty()) { @@ -82,7 +70,7 @@ public ItemStack customSkull(String url, String displayName, String... lore) { return head; } - private static GameProfile createGameProfile(String url) { + private static @Nullable GameProfile createGameProfile(String url) { GameProfile profile = new GameProfile(UUID.randomUUID(), null); PropertyMap propertyMap = profile.getProperties(); if (propertyMap == null) { diff --git a/util/src/main/java/dev/pgm/community/util/Platform.java b/util/src/main/java/dev/pgm/community/util/Platform.java index b922044f..28e886d3 100644 --- a/util/src/main/java/dev/pgm/community/util/Platform.java +++ b/util/src/main/java/dev/pgm/community/util/Platform.java @@ -4,9 +4,10 @@ import dev.pgm.community.util.Supports.Variant; import java.util.Arrays; +import java.util.regex.Pattern; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import org.reflections.Reflections; import org.reflections.util.ClasspathHelper; import org.reflections.util.ConfigurationBuilder; @@ -20,20 +21,32 @@ public abstract class Platform { .addUrls(ClasspathHelper.forPackage("dev.pgm.community", Platform.class.getClassLoader())) .forPackage("dev.pgm.community.platform") .setScanners(TypesAnnotated)); + private static final Pattern VERSION_MATCHER = + Pattern.compile("(\\d{1,2})\\.(\\d{1,2})\\.(\\d{1,2})"); public static final Version MINECRAFT_VERSION; public static final Variant VARIANT; + private static Version parseServerVersion(final @NonNull String versionName) { + var matcher = VERSION_MATCHER.matcher(versionName); + return matcher.find() + ? new Version( + Integer.parseInt(matcher.group(1)), + Integer.parseInt(matcher.group(2)), + Integer.parseInt(matcher.group(3))) + : new Version(0, 0, 0); + } + static { var sv = Bukkit.getServer(); - MINECRAFT_VERSION = TextParser.parseVersion(sv.getBukkitVersion().split("-")[0]); + MINECRAFT_VERSION = parseServerVersion(sv.getBukkitVersion()); VARIANT = Arrays.stream(Supports.Variant.values()) .filter(v -> v.matcher.test(sv)) .findFirst() .orElse(null); } - public static final @NotNull Manifest MANIFEST = get(Manifest.class); + public static final @NonNull Manifest MANIFEST = get(Manifest.class); /** * Do a minimum sanity-check of the platform's viability and early-load some codepaths @@ -44,7 +57,7 @@ public static void init() throws Throwable { Effects.EFFECTS.dummy(); } - public static @NotNull T get(Class clazz) { + public static @NonNull T get(Class clazz) { return (T) Platform.getBestSupported(clazz); } @@ -61,10 +74,15 @@ private static Object getBestSupported(Class parent) { Supports[] supportList = clazz.getDeclaredAnnotationsByType(Supports.class); for (Supports sup : supportList) { if (VARIANT != sup.value()) continue; - if (!sup.minVersion().isEmpty() - && MINECRAFT_VERSION.isOlderThan(TextParser.parseVersion(sup.minVersion()))) continue; - if (!sup.maxVersion().isEmpty() - && TextParser.parseVersion(sup.maxVersion()).isOlderThan(MINECRAFT_VERSION)) continue; + if (!sup.minVersion().isEmpty()) { + Version min = TextParser.parseVersion(sup.minVersion()); + if (MINECRAFT_VERSION.isOlderThan(min)) continue; + } + + if (!sup.maxVersion().isEmpty()) { + Version max = TextParser.parseVersion(sup.maxVersion()); + if (max.isOlderThan(MINECRAFT_VERSION)) continue; + } if (priority == null || priority.compareTo(sup.priority()) < 0) { priority = sup.priority(); @@ -80,6 +98,8 @@ private static Object getBestSupported(Class parent) { } public interface Manifest { - void onEnable(Plugin plugin); + default void onEnable(Plugin plugin) {} + + default void onDisable() {} } } diff --git a/util/src/main/java/dev/pgm/community/util/PlayerUtils.java b/util/src/main/java/dev/pgm/community/util/PlayerUtils.java index 99c3679e..89951181 100644 --- a/util/src/main/java/dev/pgm/community/util/PlayerUtils.java +++ b/util/src/main/java/dev/pgm/community/util/PlayerUtils.java @@ -2,6 +2,7 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NonNull; import tc.oc.pgm.util.skin.Skin; public interface PlayerUtils { @@ -11,11 +12,5 @@ public interface PlayerUtils { void setFakeNameAndSkin(Player player, Player viewer, String displayName, String nick, Skin skin); - String getPlayerDisplayName(Player player, Player viewer); - - String getPlayerName(Player player, Player viewer); - - Skin getPlayerSkin(Player player, Player viewer); - - ItemStack customSkull(String url, String displayName, String... lore); + ItemStack customSkull(@NonNull String url, String displayName, String... lore); }