求助:解决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
相关产品推荐
相关产品推荐

