建筑服救火记录

《桌游工艺》

北京时间当日(8 月 4 日)凌晨 0 时左右,有其他选手发现建筑服客户端无法启动。

经查,问题系由《桌游工艺》模组的 Mixin 注入失败引发。事故细节如下:

  • 该模组在其 Mixin 配置文件中声明的 Refmap 文件名为 mixins.mod-refmap.json。

  • 该模组的源工程的build.gradle 中,MixinGradle 插件所配置的 Refmap 文件名为 mixins.mod.refmap.json。这导致最终构建产物中打包的 Refmap 文件名为 mixins.mod.refmap.json,与 Mixin 配置文件中的声明不一致。

  • Refmap 全称 Reference Map,可理解为「对照表」,作用是「令 Mixin 在混淆名不一样的环境中,仍可在 Refmap 的帮助下找到正确的注入点」。

  • 在 Refmap 文件名不一致的情况下,Mixin 无法读取该模组的 Refmap,并在日志中给出警告:Reference map 'mixins.mod-refmap.json' for mixins.mod.json could not be read. If this is a development environment you can ignore this message

  • Mixin 注入的时机为目标类载入 JVM 之前。因此,当目标类触发加载时,Mixin 无法找到正确的注入点,同时没有配置允许该注入失败,因此 Mixin 只能选择抛出异常,引发游戏无法启动。

  • 进一步检查发现,《桌游工艺》模组的最新测试版构建已经修复该问题,但提升至展示服的版本仍然存在上述问题。

  • 由于该模组声明的 Mixin 均为客户端专有,因此该问题仅会导致客户端无法启动,建筑服务器本身不受影响,也因此龙井的 DSLT 未能发现该问题(客户端专有 Mixin 在服务器上不会生效)。

考虑到该问题此前已在测试服上发现,并已及时通知当事团队问题所在并请求修复,在此对该团队提升存在问题的版本到生产环境(即建筑服)这一行为提出批评。

自 2023 年 7 月初龙井系统启动以来,茶馆员工已通过公告等途径多次反复强调,模组应通过测试服等途径确认不会引发严重问题后,再提升至建筑服。反复强调这一点是为了避免模组在建筑服上出错进而影响其他人的展馆建设进度。

同时,茶馆员工 3TUSK 愿在此分享一些他对此次事故的一些个人见解:

  • 对该模组使用的 Mixin 进行进一步分析可发现,《桌游工艺》的目标是希望能控制「第一人称视角下的手臂及手持物品」的渲染,也即 ItemInHandRenderer#renderArmWithItem 的行为;具体表现为其在注入该方法时使用的目标选择器为 HEAD。值得注意的是,Forge 在该方法在原版中仅有的两处调用点均插入了回调(透过事件总线发布 RenderHandEvent,表现为调用 ForgeHooksClient.renderSpecificFirstPersonHand),换言之使用 Forge 事件可以达成一样的效果而无需 Mixin。

    • 然而,《桌游工艺》同时还需要访问一个私有方法 ItemInHandRenderer#renderPlayerArm,不过此问题亦可通过 Access Transformer 等其他途径解决。

tin_tea_tech

北京时间当日(8 月 2 日)下午 16 时左右,有茶馆员工发现此前在测试服上发现的一个问题同时出现在了建筑服上,该问题会导致玩家无法进入建筑服。虽然茶馆员工此前已知晓该问题,但因其他事务未能及时调查。

经调查,最终确认为 tin_tea_tech 模组问题:tin_tea_tech 的至少一个网络包处理器引用了 Minecraft#player 字段,而该字段的不当使用最终导致该模组尝试在物理服务器端加载 LocalPlayer 这一客户端专有类,进而报错。

在此对该团队未经全面测试即提升版本到生产环境(即建筑服)这一行为表示遗憾。

茶馆员工 3TUSK 愿在此分享一些他的个人经验:

  • 一般来说,若网络包处理器不得不引用客户端专有类,通常的做法是将网络包处理器的实现单拆出来放到一个单独的类中,然后包处理器本身只将必须的数据作为实参传入。这样可以有效避免意外触发类加载。

  • 虽然没有基于源码的分析,但一般认为(谁?),类似 Player p = Minecraft.getInstance().player 这种涉及到「将 LocalPlayer 向上转型为 Player」的写法会引发 JVM 进行进一步类型检查,从而导致 JVM 试图加载 LocalPlayer 类,进而引发报错。而 LocalPlayer p = Minecraft.getInstance().player 则因为变量类型和字段类型一致可以避免此类问题。若有源码分析可支持此说法,或有反例存在,请联系 3TUSK 进行进一步讨论。

Convivium

建筑服在北京时间昨日(8 月 1 日)?时左右出现了「在进入建筑服后,试图打开创造模式物品栏时,客户端崩溃」的问题。经检查(以及后续验证),服务器不受此问题影响。

经调查,崩溃目前确认为 Convivium 模组对 ClientboundUpdateRecipesPacket 的处理流程理解有误引起的。具体分析如下:

  • Convivium 通过订阅 Forge 提供的 RecipeUpdatedEvent,在玩家与服务器建立连接后,获取来自服务器的合成表。

  • RecipeUpdatedEvent 的发布时机是原版处理 ClientboundUpdateRecipesPacket 时的结尾。其处理流程如下:

    • (原版固有)替换原版 RecipeManager 内全部合成表为来自服务器的合成表。

    • (原版固有)重载玩家的配方书(Recipe Book)。

    • (原版固有)根据重载完成的配方书,重建正确的配方的搜索树(Search Tree)。

    • (Forge 追加)发布 RecipeUpdatedEvent。

  • Convivium 同时还订阅了 ItemTooltipEvent,用以动态追加工具提示(Tooltip)。

  • 原版在重建配方的搜索树时,会获取每一个配方的输出物品的完整工具提示文本,用以建立全文本索引。此过程涉及到 ItemStack.getTooltipLines,而 Forge 则在该方法结尾的 return 语句之前,追加了 ItemTooltipEvent 事件发布,供模组开发者使用。

  • Convivium 注册的 ItemTooltipEvent 监听器试图遍历所有已知的 TasteRecipe(完整包名:com.khjxiaogu.convivium.data.recipes.TasteRecipe)。

    • 该监听器位于 ClientEvents#addTooltip,于摘要为 4799aef6a345 的提交中引入。

    • 在问题修复前,Convivium 假定,缓存有全部 TasteRecipe 实例的 TasteRecipe#recipes 字段此时已不为 null。

    • 而事实上,若玩家打开游戏后直接进入服务器,在 Convivium 收到 RecipeUpdatedEvent之前,Convivium 会因为原版重建搜索树(见上文关于 RecipeUpdatedEvent 发布时机的说明),而先收到 ItemTooltipEvent,此时 TasteRecipe#recipes 字段实际上仍为 null。

  • Convivium 注册的 ItemTooltipEvent 监听器因为试图遍历 null 值而引发 NullPointerException,导致配方搜索树重建时抛出异常,进而导致 ClientboundUpdateRecipesPacket 在 RecipeUpdatedEvent 发布之前抛出异常。

  • 虽然 ClientboundUpdateRecipesPacket 未能正确处理完毕,但此时玩家依然可以进入服务器。

  • 玩家试图按 E 打开创造模式物品栏。

  • 玩家打开创造模式物品栏时,Minecraft 会调用 CreativeModeTabs#buildAllTabContents 构建出所有创造物品栏标签页中的物品。此过程涉及到了 Convivium 的 BeverageItem#fillItemCategory,而该方法又进一步调用了 ContainingRecipe#reverseFluidType 以填充正确的 NBT 数据。

  • 在问题修复前,ContainingRecipe#reverseFluidType 会遍历 ContainingRecipe#recipes 字段以提供正确的返回值。和上文中 TasteRecipe#recipes 类似,因为 RecipeUpdatedEvent 未能发布,ContainingRecipe#recipes值此时为 null,最终引发客户端崩溃。

目前,该模组已暂时移出建筑服,等待该问题得到正确修复,并在测试服务器确认后,再安装回建筑服。虽然建筑服并未受波及,但仍在此对崩溃一事表示遗憾。

本次事件:

  • 再次彰显了「在测试服上先行测试,然后再提升版本至建筑服」之必要。

  • 凸显了对模组内容进行进一步测试之必要,而不只是仅仅确认「游戏可正常启动至主界面」。

    • 当然,若模组无玩法上内容,可能启动到游戏主界面也足够了,例如 iChun 的 Ding 模组,但这样的案例终究是少数。

    • 茶馆员工 3TUSK 愿在此分享他的个人经验:

      • 准备一常驻的测试用存档,其中应有大量此前测试时积累下的各种测试用摆设。

      • 游戏启动后,直接进入旧存档。此过程可暴露潜在的数据迁移问题。

      • 若无旧存档可用,亦可使用一全新存档。为节约时间,建议使用虚空超平坦。在新存档创建成功并进入游戏后,不要按 WASD 移动,先打开创造模式物品栏,取出一信标,然后打掉出生点正下方方块,把信标填进去,并在其正下方铺一层 3 * 3 铁块以激活信标。

      • 若你的模组有新追加的创造物品栏,尝试打开新追加的创造物品栏。

  • 展示了 ItemTooltipEvent 背后的复杂机制,及原版和 Forge 隐藏的众多 Fail-fast 假设。(Fail fast 可理解为「快速失效」,其含义是「在一套系统发现问题时,哪怕问题看起来并不影响系统正常工作,也仍然会迅速引发整套系统停转,以避免问题进一步扩大」。)自原版游戏于 1.12 引入配方书后,ItemTooltipEvent 的发布时机越发复杂,且牵涉众多。若你订阅了 ItemTooltipEvent,请务必确保你的监听器其不会抛出任何异常!

同时,茶馆员工 3TUSK 愿在此重提他基于阅读并分析崩溃报告,以及现实中工作经历,总结来的一些经验:

  • 崩溃报告不会说谎,但崩溃报告一般不会揭示问题的根源。

    • 也因此,虽然并不多见,但崩溃报告所展示的问题,和最终调查总结出的问题有可能大相径庭。

      • 然而,对崩溃报告的分析仍然可为我们提供足够多的线索。

      • 也因此,在遇到崩溃时,请务必不要惊慌,保持冷静,并相信崩溃报告所给出的信息是真实的。

      • 在缺少线索的情况下,调查崩溃报告的起点一般是栈帧(Stack trace)中直接暴露出的模组。若栈帧中没有暴露出任何模组,则起点是直接查阅 Minecraft 本体的反编译代码。因此,负责建筑服运维茶馆员工在发现崩溃的第一时间,也会按照这个顺序尝试联系相关人士,或查阅是否已有同类报告。

    • 「起初,没有人在意那一行配置文件的消失,抑或是那一行不起眼的代码优化。[中间忘了。] 直到问题与每一个人息息相关。」

    • 你可以询问 ZekerZhayard 是否能给你讲一些有趣的故事:

      • 用户名里有 asm 三个字时,Forge 无法启动

      • 游戏根目录 .minecraft 放在磁盘根目录时,Forge 无法启动

      • 未完待续

  • 一切所谓「常识」在足够罕见的崩溃报告前都将不复存在,惟数学与逻辑永恒。

    • 模组开发中切忌过于仰仗「常识」。虽然有时候很有用。

  • 过去的 Bug 造成的损失已无法挽回,我们眼下能做的,只有尽快修复以止损,并全面复盘,以期我们从中汲取的经验教训,可令后人免于重蹈覆辙。

    • 「情绪激动无助于问题解决。我知道这很难,我上周也因为现实工作中遇到的沟通不畅差点情绪失控,但若只是无能狂怒,我恐怕会因为由此产生的骨牌效应而错过最终的 deadline 吧。」——3TUSK

    • 考虑到 3TUSK 本人与大部分报名者及大部分茶馆员工之间存在有十余个小时的时差,在发生崩溃时,若时间上不允许展开进一步调查(例如,已是深夜),可将已知信息交给其他时区的在线人士(例如 3TUSK),请求接力调查。

感谢本届参赛选手 AR 在调查过程中给予的帮助。

Noise Redstone

建筑服在北京时间今日(8 月 1 日)上午 11 时左右发生了一次崩溃,经调查发现是由 Noise Redstone 模组在 Block.updateShape(SRG 名 m_7417)返回 null 导致的,而此方法不能返回 null。

该模组现已暂时移除出建筑服。直至该模组在测试服中确认不会引发崩溃之前,茶馆员工不会允许其出现在建筑服上。

TeaCon 在 TeaCon 2021 正式引入「测试服」概念的动机是为建筑服提供一个缓冲,让各位报名者可以放心测试模组,而不必担心影响到他人搭建展馆的进度。在 TeaCon 2021 和 2022 中,测试服也不负众望,为我们拦下了不少「疑难杂症」。

希望各位引以为戒,并善用测试服,同时不要提升未经测试的版本到建筑服。

若有模组反复导致建筑服崩溃,进而导致建筑服建造进度受影响,对导致崩溃的模组的开发团队的处理就将不会是公告这么简单了。