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 的 Componentnet.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 — 调试阶段

问题effectparticle 能拦截,但 AppliedSummoned 不行。

排查:加了详细调试日志,发现两种数据包类型:

类型 toString() 输出 匹配 “Applied”
AdventureComponent io.papermc.paper.adventure.AdventureComponent@97a4b1de(对象引用)
MutableComponent translation{key='commands.damage.success', ...}(翻译键结构)

根本原因

  1. AdventureComponenttoString() 返回的是对象引用,不是文本内容
  2. MutableComponenttoString() 返回的是翻译键结构,不是显示的文本(如 “Applied 5.0 damage”)

v1.2.8 — 最终修复

修复

  1. AdventureComponent:通过反射调用 PlainTextComponentSerializer.plainText().serialize(component) 获取纯文本
  2. MutableComponent:在配置中同时添加翻译键模式(commands.effectcommands.summoncommands.damagecommands.particle

结果:两种格式的数据包都能被正确拦截。


关键教训

  1. AsyncChatEvent 不拦截服务器→客户端消息 — 只拦截玩家聊天
  2. ProtocolLib 在高版本 Java 上可能不兼容 — ByteBuddy 注入失败
  3. NMS Component ≠ Adventure Component — Paper remap 不影响所有地方
  4. toString() 不等于显示文本 — AdventureComponent 返回对象引用,MutableComponent 返回翻译键结构
  5. String.contains() 区分大小写 — 过滤器应该不区分大小写
  6. Reload 时要清理旧资源 — Netty 处理器残留会导致冲突
  7. Paper API 不包含所有编译依赖 — Netty、NMS 需要单独处理

最终架构

1
2
3
4
5
6
7
8
9
原版指令输出

├─→ 控制台 / log 文件 → Log4j LogFilter (AbstractFilter)
│ 大小写不敏感匹配

└─→ 游戏内数据包 → Netty ChannelOutboundHandlerAdapter
通过 ChannelInitializeListenerHolder 注入
匹配 AdventureComponent 纯文本 + NMS 翻译键
大小写不敏感

零外部依赖,仅使用 Paper API + Netty(服务端自带)。