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 的集成方法。