You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:解决ProtocolLib及反射修改玩家Nametag的现存问题

解决Minecraft Bukkit服务器玩家Nametag修改的兼容问题

过去一年尝试通过反射和ProtocolLib实现类似Hypixel的玩家Nametag修改效果,但现有方案均存在缺陷,以下是问题分析及解决方案:

现有实现方案代码

ProtocolLib方案

public static void changePlayerNameTag(Player player, String newName) {
    // Update nametag for other players
    ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(EssentialsNicks.getPlugin(), PacketType.Play.Server.PLAYER_INFO) {
        @Override
        public void onPacketSending(PacketEvent event) {
            if (event.getPacket().getPlayerInfoAction().read(0) != EnumWrappers.PlayerInfoAction.ADD_PLAYER) return;
            List<PlayerInfoData> newPlayerInfoDataList = new ArrayList<PlayerInfoData>();
            List<PlayerInfoData> playerInfoDataList = event.getPacket().getPlayerInfoDataLists().read(0);
            for (PlayerInfoData playerInfoData : playerInfoDataList) {
                if (playerInfoData == null || playerInfoData.getProfile() == null || Bukkit.getPlayer(playerInfoData.getProfile().getUUID()) == null) { //Unknown Player
                    newPlayerInfoDataList.add(playerInfoData);
                    continue;
                }
                // Check if the player's UUID matches the specific player's UUID
                if (!playerInfoData.getProfile().getUUID().equals(player.getUniqueId())) {
                    newPlayerInfoDataList.add(playerInfoData);
                    continue;
                }
                WrappedGameProfile profile = playerInfoData.getProfile();
                // Create a new profile with the desired name and the original profile's properties (including the skin)
                WrappedGameProfile newProfile = new WrappedGameProfile(profile.getUUID(), ChatColor.translateAlternateColorCodes('&', newName));
                newProfile.getProperties().putAll(profile.getProperties());
                PlayerInfoData newPlayerInfoData = new PlayerInfoData(newProfile, playerInfoData.getLatency(), playerInfoData.getGameMode(), playerInfoData.getDisplayName());
                newPlayerInfoDataList.add(newPlayerInfoData);
            }
            event.getPacket().getPlayerInfoDataLists().write(0, newPlayerInfoDataList);
        }
    });

    Bukkit.getOnlinePlayers().forEach(onlinePlayer -> {
        protocolManager.sendServerPacket(onlinePlayer, packet);
        onlinePlayer.hidePlayer(player);
        onlinePlayer.showPlayer(player);
    });
}

反射方案

public static void changeName(String name, Player player) {
    try {
        Method getHandle = player.getClass().getMethod("getHandle");
        Object entityPlayer = getHandle.invoke(player);
        Class<?> entityHuman = entityPlayer.getClass().getSuperclass();
        Field gameProfileField;
        int majVersion = Integer.parseInt(Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3].replaceAll("(v|R[0-9]+)", "").split("_")[0]);
        int minVersion = Integer.parseInt(Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3].replaceAll("(v|R[0-9]+)", "").split("_")[1]);
        if (majVersion >= 1 && minVersion >= 9) {
            gameProfileField = entityHuman.getDeclaredField("bS");
        } else {
            gameProfileField = entityHuman.getDeclaredField("bH");
        }
        gameProfileField.setAccessible(true);
        gameProfileField.set(entityPlayer, new GameProfile(player.getUniqueId(), name));
        for (Player players : Bukkit.getOnlinePlayers()) {
            players.hidePlayer(player);
            players.showPlayer(player);
        }
    } catch (NoSuchMethodException | SecurityException | InvocationTargetException | NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
        e.printStackTrace();
    }
}

现有方案的问题

  • 共同问题:玩家自身客户端无法看到修改后的Nametag
  • ProtocolLib方案问题:修改后的Nametag与队伍(Teams)功能不兼容
  • 反射方案问题:修改服务器内玩家名称,导致Minecraft原生命令及其他插件命令失效,TAB补全无法识别修改后的名称

解决方案

1. 修复ProtocolLib的队伍兼容问题

Minecraft的队伍名称显示由SCOREBOARD_TEAM数据包控制,需拦截该数据包并替换队伍中目标玩家的名称:

首先维护一个存储玩家自定义名称的全局映射:

private static final Map<UUID, String> CUSTOM_NAMES = new ConcurrentHashMap<>();

添加队伍数据包监听:

public static void setupTeamPacketListener() {
    ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(EssentialsNicks.getPlugin(), PacketType.Play.Server.SCOREBOARD_TEAM) {
        @Override
        public void onPacketSending(PacketEvent event) {
            PacketContainer packet = event.getPacket();
            EnumWrappers.ScoreboardTeamMode mode = packet.getScoreboardTeamModes().read(0);
            
            // 仅处理添加/更新队员的操作
            if (mode != EnumWrappers.ScoreboardTeamMode.ADD_PLAYERS && mode != EnumWrappers.ScoreboardTeamMode.UPDATE_PLAYERS) {
                return;
            }
            
            List<String> players = packet.getStringLists().read(0);
            List<String> modifiedPlayers = new ArrayList<>();
            for (String playerName : players) {
                Player player = Bukkit.getPlayerExact(playerName);
                if (player != null && CUSTOM_NAMES.containsKey(player.getUniqueId())) {
                    modifiedPlayers.add(CUSTOM_NAMES.get(player.getUniqueId()));
                } else {
                    modifiedPlayers.add(playerName);
                }
            }
            packet.getStringLists().write(0, modifiedPlayers);
        }
    });
}

2. 修复玩家自身客户端显示问题

玩家自身看不到修改后的Nametag,是因为客户端未处理自己的PLAYER_INFO和ENTITY_METADATA数据包,需单独发送更新:

修改原changePlayerNameTag方法,加入自身更新逻辑:

public static void changePlayerNameTag(Player player, String newName) {
    String formattedName = ChatColor.translateAlternateColorCodes('&', newName);
    CUSTOM_NAMES.put(player.getUniqueId(), formattedName);
    ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();

    // 拦截其他玩家的PlayerInfo数据包
    protocolManager.addPacketListener(new PacketAdapter(EssentialsNicks.getPlugin(), PacketType.Play.Server.PLAYER_INFO) {
        @Override
        public void onPacketSending(PacketEvent event) {
            EnumWrappers.PlayerInfoAction action = event.getPacket().getPlayerInfoAction().read(0);
            // 处理添加玩家和更新显示名称两种动作
            if (action != EnumWrappers.PlayerInfoAction.ADD_PLAYER && action != EnumWrappers.PlayerInfoAction.UPDATE_DISPLAY_NAME) {
                return;
            }

            List<PlayerInfoData> newPlayerInfoDataList = new ArrayList<>();
            List<PlayerInfoData> playerInfoDataList = event.getPacket().getPlayerInfoDataLists().read(0);
            for (PlayerInfoData playerInfoData : playerInfoDataList) {
                if (playerInfoData == null || playerInfoData.getProfile() == null || Bukkit.getPlayer(playerInfoData.getProfile().getUUID()) == null) {
                    newPlayerInfoDataList.add(playerInfoData);
                    continue;
                }
                if (!playerInfoData.getProfile().getUUID().equals(player.getUniqueId())) {
                    newPlayerInfoDataList.add(playerInfoData);
                    continue;
                }
                WrappedGameProfile profile = playerInfoData.getProfile();
                WrappedGameProfile newProfile = new WrappedGameProfile(profile.getUUID(), formattedName);
                newProfile.getProperties().putAll(profile.getProperties());
                PlayerInfoData newPlayerInfoData = new PlayerInfoData(
                        newProfile,
                        playerInfoData.getLatency(),
                        playerInfoData.getGameMode(),
                        WrappedChatComponent.fromText(formattedName)
                );
                newPlayerInfoDataList.add(newPlayerInfoData);
            }
            event.getPacket().getPlayerInfoDataLists().write(0, newPlayerInfoDataList);
        }
    });

    // 更新玩家自身的显示
    updateSelfNametag(player, formattedName, protocolManager);

    // 刷新其他玩家的视图
    Bukkit.getOnlinePlayers().forEach(onlinePlayer -> {
        onlinePlayer.hidePlayer(player);
        onlinePlayer.showPlayer(player);
    });
}

private static void updateSelfNametag(Player player, String formattedName, ProtocolManager protocolManager) {
    // 发送UPDATE_DISPLAY_NAME类型的PlayerInfo数据包
    PacketContainer playerInfoPacket = protocolManager.createPacket(PacketType.Play.Server.PLAYER_INFO);
    playerInfoPacket.getPlayerInfoAction().write(0, EnumWrappers.PlayerInfoAction.UPDATE_DISPLAY_NAME);
    WrappedGameProfile profile = WrappedGameProfile.fromPlayer(player);
    WrappedGameProfile newProfile = new WrappedGameProfile(profile.getUUID(), formattedName);
    newProfile.getProperties().putAll(profile.getProperties());
    PlayerInfoData data = new PlayerInfoData(
            newProfile,
            0,
            EnumWrappers.NativeGameMode.fromBukkit(player.getGameMode()),
            WrappedChatComponent.fromText(formattedName)
    );
    playerInfoPacket.getPlayerInfoDataLists().write(0, Collections.singletonList(data));
    try {
        protocolManager.sendServerPacket(player, playerInfoPacket);
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }

    // 更新实体元数据中的CustomName(版本适配:1.8索引为10,1.12+为2)
    int customNameIndex = getCustomNameMetadataIndex();
    WrappedDataWatcher.WrappedDataWatcherObject customNameWatcher = new WrappedDataWatcher.WrappedDataWatcherObject(
            customNameIndex,
            WrappedDataWatcher.Registry.getChatComponentSerializer(true)
    );
    WrappedDataWatcher dataWatcher = WrappedDataWatcher.getEntityWatcher(player);
    dataWatcher.setObject(customNameWatcher, WrappedChatComponent.fromText(formattedName));

    PacketContainer metadataPacket = protocolManager.createPacket(PacketType.Play.Server.ENTITY_METADATA);
    metadataPacket.getIntegers().write(0, player.getEntityId());
    metadataPacket.getWatchableCollectionModifier().write(0, dataWatcher.getWatchableObjects());
    try {
        protocolManager.sendServerPacket(player, metadataPacket);
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

// 版本适配获取CustomName元数据索引
private static int getCustomNameMetadataIndex() {
    String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
    if (version.contains("1_8")) {
        return 10;
    } else {
        return 2; // 1.12+版本
    }
}

额外注意事项

  • 需在插件加载时调用setupTeamPacketListener()初始化队伍监听
  • 玩家退出服务器时,需从CUSTOM_NAMES中移除对应的UUID条目,避免内存泄漏
  • 元数据索引的版本适配可根据实际服务器版本扩展更多分支

内容的提问来源于stack exchange,提问作者Ludgerr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 23:17:33