NoVanillaLog 开发笔记
GitHub开源链接
记录从 v1.2.0 到 v1.2.8 的完整踩坑过程。
v1.2.0 — 初始版本
LogFilter:Log4j 过滤器,拦截控制台日志SystemChatFilter:监听AsyncChatEvent,拦截游戏内聊天- 问题:
AsyncChatEvent只拦截玩家发送的聊天消息,不拦截服务器→客户端的系统消息(如 “Applied effect Resistance to mozz”)
v1.2.1 — 尝试 ProtocolLib
思路:用 ProtocolLib 监听 PacketType.Play.Server.SYSTEM_CHAT 数据包。
问题:
- ProtocolLib 的
StructureModifier.read(int, Class)方法签名不匹配,编译失败 - 修复后发现
read(0)返回的是 NMS 的Component(net.minecraft.network.chat.Component),不是 Adventure 的Component raw instanceof Component(Adventure)永远为false,导致消息未被拦截
教训:ProtocolLib 读取的字段是原始 NMS 类型,不是 Paper remap 后的 Adventure 类型。
v1.2.2 — 修复 ProtocolLib 类型问题
修复:去掉 instanceof 检查,直接用 raw.toString() 做文本匹配。
结果:编译通过,但部署后 onPacketSending 根本没被调用。
原因:ProtocolLib 的 ByteBuddy 注入系统在 Java 26 上失败了。日志中有 ByteArrayClassLoader 异常堆栈,ProtocolLib 内部的类注入机制不兼容 Java 26。
教训:ProtocolLib 依赖 ByteBuddy 做运行时类生成,高版本 Java 上可能不兼容。
v1.2.3 — 改用 Paper 原生 Netty 注入
思路:完全去掉 ProtocolLib,用 Paper 的 ChannelInitializeListenerHolder API 直接注入 Netty 通道处理器。
实现:
ChannelInitializeListenerHolder.addListener(key, channel -> ...)— 拦截新连接CraftPlayer.getHandle().connection.connection.channel— 获取已在线玩家的 Netty 通道ChannelOutboundHandlerAdapter.write()— 拦截出站数据包
问题:
paper-mojangapi依赖不存在,编译失败- 改用纯反射调用
ChannelInitializeListenerHolder,不需要 NMS 编译依赖 - Netty 类不在 Paper API 的编译路径中,需要单独添加
netty-transport依赖
教训:Paper API 不包含 Netty 编译依赖,需要手动添加。ChannelInitializeListenerHolder 是 Paper 的 server-side API,需要用反射调用。
v1.2.4 — 大小写敏感问题
问题:配置里写的 applied(小写)匹配不到 Applied(首字母大写)。String.contains() 区分大小写。
修复:三个过滤层全部改成 text.toLowerCase().contains(pattern.toLowerCase())。
v1.2.5 — Reload 时旧处理器残留
问题:/novanillalog reload 时旧的 Netty 处理器没有从已连接玩家的通道中移除,导致新旧处理器冲突。
修复:在 unregister() 中遍历所有在线玩家,从他们的通道中移除处理器。
v1.2.6 ~ v1.2.7 — 调试阶段
问题:effect 和 particle 能拦截,但 Applied 和 Summoned 不行。
排查:加了详细调试日志,发现两种数据包类型:
| 类型 | toString() 输出 | 匹配 “Applied” |
|---|---|---|
AdventureComponent |
io.papermc.paper.adventure.AdventureComponent@97a4b1de(对象引用) |
❌ |
MutableComponent |
translation{key='commands.damage.success', ...}(翻译键结构) |
❌ |
根本原因:
AdventureComponent的toString()返回的是对象引用,不是文本内容MutableComponent的toString()返回的是翻译键结构,不是显示的文本(如 “Applied 5.0 damage”)
v1.2.8 — 最终修复
修复:
AdventureComponent:通过反射调用PlainTextComponentSerializer.plainText().serialize(component)获取纯文本MutableComponent:在配置中同时添加翻译键模式(commands.effect、commands.summon、commands.damage、commands.particle)
结果:两种格式的数据包都能被正确拦截。
关键教训
- AsyncChatEvent 不拦截服务器→客户端消息 — 只拦截玩家聊天
- ProtocolLib 在高版本 Java 上可能不兼容 — ByteBuddy 注入失败
- NMS Component ≠ Adventure Component — Paper remap 不影响所有地方
- toString() 不等于显示文本 — AdventureComponent 返回对象引用,MutableComponent 返回翻译键结构
- String.contains() 区分大小写 — 过滤器应该不区分大小写
- Reload 时要清理旧资源 — Netty 处理器残留会导致冲突
- Paper API 不包含所有编译依赖 — Netty、NMS 需要单独处理
最终架构
1 | 原版指令输出 |
零外部依赖,仅使用 Paper API + Netty(服务端自带)。