强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

PaperMC 插件开发完全指南 / 第 11 章:数据包

第 11 章:数据包

学习 Minecraft 网络协议基础,掌握 ProtocolLib 和数据包监听/注入技术。


11.1 数据包基础

Minecraft 客户端和服务端通过**数据包(Packet)**通信。理解数据包是实现高级功能(如自定义物品栏、修改粒子、拦截聊天等)的关键。

通信流程

客户端 ←→ 网络层 ←→ 数据包编解码 ←→ Bukkit 事件/处理

数据包方向

方向说明示例
CLIENT → SERVER客户端发送玩家移动、聊天、点击
SERVER → CLIENT服务端发送方块更新、实体生成、聊天消息

常见数据包类型

包名方向说明
ClientboundChatPacketS→C聊天消息
ClientboundSetActionBarTextPacketS→C动作栏文字
ClientboundContainerSetSlotPacketS→C物品栏更新
ClientboundAddEntityPacketS→C实体生成
ClientboundTeleportEntityPacketS→C实体传送
ServerboundChatPacketC→S玩家聊天
ServerboundMovePacketC→S玩家移动

11.2 ProtocolLib 简介

ProtocolLib 是最常用的 Bukkit 数据包操作库,它封装了 NMS(net.minecraft.server)的复杂性。

添加依赖

<!-- pom.xml -->
<repository>
    <id>dmulloy2-repo</id>
    <url>https://repo.dmulloy2.net/repository/public/</url>
</repository>

<dependency>
    <groupId>com.comphenix.protocol</groupId>
    <artifactId>ProtocolLib</artifactId>
    <version>5.3.0</version>
    <scope>provided</scope>
</dependency>

在 plugin.yml 中声明依赖

depend:
  - ProtocolLib

11.3 监听数据包

基本监听

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.*;

public class PacketListener {

    private final ProtocolManager protocolManager;

    public PacketListener(MyPlugin plugin) {
        this.protocolManager = ProtocolLibrary.getProtocolManager();

        // 监听客户端发送的聊天包
        protocolManager.addPacketListener(
            new PacketAdapter(plugin, PacketType.Play.Client.CHAT) {
                @Override
                public void onPacketReceiving(PacketEvent event) {
                    // 获取玩家
                    Player player = event.getPlayer();

                    // 获取聊天内容
                    String message = event.getPacket().getStrings().read(0);

                    // 日志记录
                    plugin.getLogger().info("[Chat] " + player.getName() + ": " + message);
                }
            }
        );
    }
}

监听服务端发送的包

// 监听服务端发送的聊天消息包
protocolManager.addPacketListener(
    new PacketAdapter(plugin,
        PacketType.Play.Server.SYSTEM_CHAT_MESSAGE) {
        @Override
        public void onPacketSending(PacketEvent event) {
            Player player = event.getPlayer();

            // 读取消息内容
            var packet = event.getPacket();
            // Paper 的消息内容可能使用 Component 或 String

            // 修改消息(例如添加前缀)
            // packet.getStrings().modify(0, original -> "[PREFIX] " + original);
        }
    }
);

11.4 拦截与取消数据包

取消数据包

protocolManager.addPacketListener(
    new PacketAdapter(plugin, PacketType.Play.Client.CHAT) {
        @Override
        public void onPacketReceiving(PacketEvent event) {
            String message = event.getPacket().getStrings().read(0);

            // 禁止发送包含敏感词的消息
            if (message.contains("广告")) {
                event.setCancelled(true); // 取消数据包
                event.getPlayer().sendMessage("§c消息包含敏感内容!");
            }
        }
    }
);

延迟处理数据包

protocolManager.addPacketListener(
    new PacketAdapter(plugin, PacketType.Play.Client.CHAT) {
        @Override
        public void onPacketReceiving(PacketEvent event) {
            // 异步延迟处理
            event.setReadOnly(false); // 允许修改

            Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
                // 异步验证(如数据库检查)
                boolean allowed = checkMessage(event.getPlayer(), message);
                if (!allowed) {
                    // 在异步线程中取消
                    event.setCancelled(true);
                }
            });
        }
    }
);

11.5 修改数据包内容

修改字符串字段

protocolManager.addPacketListener(
    new PacketAdapter(plugin, PacketType.Play.Server.CHAT_MESSAGE) {
        @Override
        public void onPacketSending(PacketEvent event) {
            var packet = event.getPacket();

            // 读取原始消息
            String original = packet.getStrings().read(0);

            // 修改消息(如敏感词替换)
            String modified = original.replace("敏感词", "***");

            // 写回数据包
            packet.getStrings().write(0, modified);
        }
    }
);

修改整数字段

// 修改实体生成包中的实体类型
protocolManager.addPacketListener(
    new PacketAdapter(plugin, PacketType.Play.Server.SPAWN_ENTITY) {
        @Override
        public void onPacketSending(PacketEvent event) {
            var packet = event.getPacket();

            // 读取实体类型
            int entityId = packet.getIntegers().read(0);

            // 修改元数据
            // ...
        }
    }
);

11.6 发送自定义数据包

发送标题包

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.wrappers.WrappedChatComponent;

public class PacketSender {

    /**
     * 发送自定义标题
     */
    public static void sendTitle(Player player, String title, String subtitle,
                                  int fadeIn, int stay, int fadeOut) {
        ProtocolManager manager = ProtocolLibrary.getProtocolManager();

        // 发送淡入淡出时间
        PacketContainer times = new PacketContainer(
            PacketType.Play.Server.SET_TITLE_TIME
        );
        times.getIntegers().write(0, fadeIn);
        times.getIntegers().write(1, stay);
        times.getIntegers().write(2, fadeOut);

        // 发送标题文字
        PacketContainer titlePacket = new PacketContainer(
            PacketType.Play.Server.SET_TITLE_TEXT
        );
        titlePacket.getChatComponents().write(0,
            WrappedChatComponent.fromJson("{\"text\":\"" + title + "\"}"));

        // 发送副标题
        PacketContainer subtitlePacket = new PacketContainer(
            PacketType.Play.Server.SET_SUBTITLE_TEXT
        );
        subtitlePacket.getChatComponents().write(0,
            WrappedChatComponent.fromJson("{\"text\":\"" + subtitle + "\"}"));

        try {
            manager.sendServerPacket(player, times);
            manager.sendServerPacket(player, titlePacket);
            manager.sendServerPacket(player, subtitlePacket);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

11.7 包装器(Wrappers)

ProtocolLib 提供了多种包装器简化数据包操作:

常用包装器

包装器用途
WrappedGameProfile玩家 Profile
WrappedBlockData方块数据
WrappedChatComponent聊天组件
WrappedDataWatcher实体元数据
WrappedSignedProperty签名属性

修改实体元数据

protocolManager.addPacketListener(
    new PacketAdapter(plugin, PacketType.Play.Server.ENTITY_METADATA) {
        @Override
        public void onPacketSending(PacketEvent event) {
            var packet = event.getPacket();

            // 获取实体 ID
            int entityId = packet.getIntegers().read(0);

            // 获取元数据
            List<WrappedDataValue> dataValues = packet.getDataValueCollectionModifier()
                .read(0);

            // 修改元数据(如自定义名称)
            for (WrappedDataValue value : dataValues) {
                if (value.getIndex() == 2) { // 自定义名称
                    // 修改名称
                }
            }
        }
    }
);

11.8 ProtocolLib vs Paper 原生 API

Paper 提供了一些原生的数据包操作能力,但 ProtocolLib 仍然是最完整的解决方案。

对比

特性ProtocolLibPaper 原生
包监听完整支持有限支持
包修改完整支持有限
包发送完整支持部分支持
依赖需要额外安装无额外依赖
版本兼容自动适配跟随 Paper 版本
性能优秀略好

Paper 原生监听

// Paper 的原生方式(有限功能)
import com.destroystokyo.paper.event.player.PlayerArmorChangeEvent;

@EventHandler
public void onArmorChange(PlayerArmorChangeEvent event) {
    // Paper 原生事件,不需要 ProtocolLib
}

11.9 业务场景:自定义 Tab 列表

public class TabListManager {

    private final ProtocolManager protocolManager;

    public TabListManager(MyPlugin plugin) {
        this.protocolManager = ProtocolLibrary.getProtocolManager();
    }

    /**
     * 发送自定义 Tab 列表头部和底部
     */
    public void sendTabHeaderFooter(Player player, String header, String footer) {
        PacketContainer packet = new PacketContainer(
            PacketType.Play.Server.PLAYER_LIST_HEADER_FOOTER
        );

        packet.getChatComponents().write(0,
            WrappedChatComponent.fromJson(
                "{\"text\":\"" + header + "\"}"));
        packet.getChatComponents().write(1,
            WrappedChatComponent.fromJson(
                "{\"text\":\"" + footer + "\"}"));

        try {
            protocolManager.sendServerPacket(player, packet);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

11.10 常见问题排查

问题原因解决方案
数据包事件不触发ProtocolLib 未安装确认 depend 声明
读取字段失败版本更新导致字段变化更新 ProtocolLib
修改数据包报错只读模式调用 event.setReadOnly(false)
异步访问数据包线程安全问题使用 scheduleSync
数据包字段索引错误版本不匹配检查包结构文档

11.11 扩展阅读


11.12 本章小结

要点内容
数据包客户端与服务端通信的基本单位
ProtocolLib最强大的数据包操作库
监听PacketAdapter + PacketType
修改packet.getXxx().write(index, value)
发送protocolManager.sendServerPacket()
安全异步访问需要特殊处理

下一章: 第 12 章:数据库集成 — 学习 SQLite、MySQL 和 Redis 的集成方法。