Repository navigation
Fix spectator control valve menus 修复旁观者控制阀菜单 - #6083
PigeonNian merged 1 commit into
Conversation
- 在控制阀菜单初始化数据中发送方块位置 - 打开界面时读取已同步的流速与过滤配置 - 初始化数值时阻止客户端回写配置
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing. Review the Copilot code review billing settings.
Gugle2308
left a comment
There was a problem hiding this comment.
审查结论:APPROVE(0 个阻塞项,1 条同代码路径的缺口建议本轮一并补上)
崩溃日志已定位到同一地址,修复措施也落在同一位置:ControlValveMenu 是 IContainerFactory 通过四参数构造器创建的,会执行 buf.readBlockPos()。当服务端走 openMenu(provider) 且没有传入 BlockPos 消费者、BE 也没有实现 writeClientSideData 时,额外数据长度为 0,客户端收到的是普通 ClientboundOpenScreenPacket,随后 MenuScreens.ScreenConstructor.fromPacket 调用二参数 MenuType.create,而 IContainerFactory.create(int, Inventory) 默认把 null 作为 buf 传下去,于是在 ControlValveMenu.java:44 抛出 NPE。补上 writeClientSideData 让额外数据非空后,改走 NeoForge 的 AdvancedOpenScreenPayload,readBlockPos 就能拿到值,断连问题消失。✅ 解析方向与失效链完全一致。
范围说明:webhook 只提供 patch diff;本 PR 为 2 文件 / +18 −0,与 API 元数据一致,diff 覆盖完整。结论基于 head 871dcc7 的源码树(codeload 拉取)与 base 0ea57be7 对照。未覆盖:实机运行、多人场景,以及同一 bug 家族中其他方块的完整排查。CI:build ✅ / checkstyle ✅。目标分支 dev/1.21/1.6,issue #6059 报告版本 1.21.1 / snapshot-2863,版本对得上。
⚠️ 同一路径上仍有一个未修的方块:结构扫描仪会以完全相同的方式崩溃
StructureScannerBlockEntity 同样实现了 MenuProvider,同样没有覆写 writeClientSideData,其 StructureScannerMenu 的四参数构造器同样无条件调用 extraData.readBlockPos()(用 Objects.requireNonNull 包裹)。整个 1.21 分支的 block/entity/ 和 item/ 目录中,它是唯一一个没有覆写 writeClientSideData 却读取客户端缓冲的 MenuProvider 实现者:
NO-WCSD: block/entity/StructureScannerBlockEntity.java
它的打开路径是 StructureScannerBlock.java:156-159:
var menuProvider = state.getMenuProvider(level, pos);
if (menuProvider != null) {
ModMenuTypes.open(serverPlayer, menuProvider, pos); // 带 pos ⇒ 平时不暴露
}也就是说,生存玩家正常打开界面时正常(pos 由 openMenu(provider, BlockPos) 消费者写入),但任何走 openMenu(provider) 单参数(也就是本 PR 修的那条路径,包括旁观者经 ServerPlayerGameMode.useItemOn 的 SPECTATOR 分支进入)都会重现同一个 NPE。本 PR 的立论正是「补上 writeClientSideData 就能修好这类断连」,那么这台机器不补就等于保留同一条崩溃链;issue #6059 正文没有点名它,但根因完全同源,建议顺手加上(新增 3 行 + 1 行 import,与 ControlValveBlockEntity 写法完全相同)。
💡 updatingValueBox 这个重新进入守卫在姊妹 PR 中缺失(跨版本一致性)
26.1 线的对应修复 #6084 已存在(dev/26.1/fix-6059-control-valve-menu,2 文件),但那份 diff 里 updatingValueBox 出现次数是 0。而 26.1 的 ControlValveScreen.init() 末尾紧跟一句 this.setValue(this.value)(1.21 版没有这一句)。两份 diff 在此处的语义已经不对等:26.1 版既缺少守卫,又在 init() 里重复调用了 setValue;如果那边确实有 setValue → valueBox.setValue → responder 这条链路,就会出现「开界面即回写 ControlValveUpdatePacket」的情况。这正是本 PR 第三条 bullet「初始化数值时阻止客户端回写配置」声称要解决的问题,1.21 线做到了,26.1 线没有做到;建议在那个 PR 上同样补 updatingValueBox 字段与 onValueInput 守卫(或者至少核对一次 26.1 的 responder 是否真的会被触发)。本 PR 本体无此问题。
✅ 几处已核对、确实没问题的点
- 重新进入守卫的写法是正确的:
setValue里先置updatingValueBox = true,再调valueBox.setValue(...),最后复位;EditBox.setValue会在setResponder之后同步触发 responder,这个字段恰好覆盖了那段同步执行。而且setValue只在init()(字段尚未被响应者使用)和ControlValveInitPacket.handleOnClient里被调用,两处都不会与用户输入交错,因此不存在「守卫没复位就抛异常,导致后续输入全被吞」的风险(Integer.toString与EditBox.setValue都不抛异常)。 - 从 BE 读取状态在客户端是可靠的:
ControlValveBlockEntity.getUpdateTag已经把MaxRate/Facing/Filters/FilterItem全部写全(saveAdditional与它逐键一致),loadAdditional侧也对MaxRate做了Mth.clamp(..., 0, MAX_RATE);1.21.1 的getUpdateTag → saveAdditional走同一个扁平 tag,不存在「客户端 BE 归零」的 26.1 特有问题。因此init()里用getMaxRate()/getFilter(0)/getFilterItem()播种,读到的就是服务端的真实值。 getFilter(0)不会越界:getFilter自带index < 0 || index >= filters.size()守卫并返回FluidStack.EMPTY,FILTER_SLOT_COUNT恒 ≥ 1,无需额外判空。setFilterItem会做.copyWithCount(1)并重置filterFluids,与ControlValveFilterItemPacket.handleOnClient走同一条初始化路径,两种初始化来源(包和init())结果一致,不会出现「先按init()画、再按包重画」的图标跳变。- 这次播种不会误发 C2S 包:
setValue → slider.setValue(无回调,setValueWithUpdate才有回调)、setFilter/setFilterItem都是纯本地赋值,init()里没有任何PacketDistributor.sendToServer,不会因为开界面而给服务端发送多余的ControlValveUpdatePacket。 - 数量设置与档位对齐:
FluidRateSlider.MAX = 2000与ControlValveBlockEntity.MAX_RATE = 2000一致,clampSnap只做STEP = 50吸附、不改变上限,getMaxRate()值域被 BE 侧限制在[0, 2000]内,播种不会出现滑条位置与服务端值不一致。
🧪 建议验证
- 原始复现:在旁观模式下右键控制阀,界面应正常打开,不再出现
Network Protocol Error;确认显示的流速与过滤图标与服务端一致; - 红石锁定状态对比:给阀门通电后打开,确认
render()走locked分支把滑条与输入框压成 0、且输入框不可编辑(isLocked()读的是客户端 blockstate 的POWERED,与 BE 字段无关,不受本次改动影响); - 窗口缩放:缩放窗口会重跑
init(),随后应从 BE 重新播种,而不是清空输入框(旧代码固定写成setValue("")); - 生存玩家回归:确认非旁观模式下打开仍走
openMenu(be, pos),且ControlValveInitPacket随后把值再设一遍,结果与init()播种值相同、无闪烁; - 结构扫描仪(如果本轮补上):旁观右键打开不再断连;如果不补,只需确认它当前仍是已知缺口,而不是本次改动引入的回归。
fix(menu): 修复旁观者打开控制阀时断开连接
在控制阀菜单初始化数据中发送方块位置
打开界面时读取已同步的流速与过滤配置
初始化数值时阻止客户端回写配置
fixed [Bug] 旁观模式下右键点击控制阀因网络协议错误而退出 #6059