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

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

第 11 章:数据包

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


11.1 数据包基础

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

通信流程

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

数据包方向

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

常见数据包类型

包名 方向 说明
ClientboundChatPacket S→C 聊天消息
ClientboundSetActionBarTextPacket S→C 动作栏文字
ClientboundContainerSetSlotPacket S→C 物品栏更新
ClientboundAddEntityPacket S→C 实体生成
ClientboundTeleportEntityPacket S→C 实体传送
ServerboundChatPacket C→S 玩家聊天
ServerboundMovePacket C→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 仍然是最完整的解决方案。

对比

特性 ProtocolLib Paper 原生
包监听 完整支持 有限支持
包修改 完整支持 有限
包发送 完整支持 部分支持
依赖 需要额外安装 无额外依赖
版本兼容 自动适配 跟随 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 的集成方法。