Skip to content

Fix spectator control valve menus 修复旁观者控制阀菜单 - #6083

Merged
PigeonNian merged 1 commit into
Anvil-Dev:dev/1.21/1.6from
WhereisFff:dev/1.21/fix-6059-control-valve-menu
Nov 13, 2026
Merged

PigeonNian merged 1 commit into
Anvil-Dev:dev/1.21/1.6from
WhereisFff:dev/1.21/fix-6059-control-valve-menu

Conversation

@WhereisFff

Copy link
Copy Markdown
Contributor

fix(menu): 修复旁观者打开控制阀时断开连接

- 在控制阀菜单初始化数据中发送方块位置
- 打开界面时读取已同步的流速与过滤配置
- 初始化数值时阻止客户端回写配置
Copilot AI balanced review requested due to automatic review settings November 12, 2026 19:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Gugle2308 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论: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 本体无此问题。

✅ 几处已核对、确实没问题的点

  1. 重新进入守卫的写法是正确的:setValue 里先置 updatingValueBox = true,再调 valueBox.setValue(...),最后复位;EditBox.setValue 会在 setResponder 之后同步触发 responder,这个字段恰好覆盖了那段同步执行。而且 setValue 只在 init()(字段尚未被响应者使用)和 ControlValveInitPacket.handleOnClient 里被调用,两处都不会与用户输入交错,因此不存在「守卫没复位就抛异常,导致后续输入全被吞」的风险(Integer.toString 与 EditBox.setValue 都不抛异常)。
  2. 从 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() 播种,读到的就是服务端的真实值。
  3. getFilter(0) 不会越界:getFilter 自带 index < 0 || index >= filters.size() 守卫并返回 FluidStack.EMPTY,FILTER_SLOT_COUNT 恒 ≥ 1,无需额外判空。
  4. setFilterItem 会做 .copyWithCount(1) 并重置 filterFluids,与 ControlValveFilterItemPacket.handleOnClient 走同一条初始化路径,两种初始化来源(包和 init())结果一致,不会出现「先按 init() 画、再按包重画」的图标跳变。
  5. 这次播种不会误发 C2S 包:setValue → slider.setValue(无回调,setValueWithUpdate 才有回调)、setFilter / setFilterItem 都是纯本地赋值,init() 里没有任何 PacketDistributor.sendToServer,不会因为开界面而给服务端发送多余的 ControlValveUpdatePacket。
  6. 数量设置与档位对齐:FluidRateSlider.MAX = 2000 与 ControlValveBlockEntity.MAX_RATE = 2000 一致,clampSnap 只做 STEP = 50 吸附、不改变上限,getMaxRate() 值域被 BE 侧限制在 [0, 2000] 内,播种不会出现滑条位置与服务端值不一致。

🧪 建议验证

  1. 原始复现:在旁观模式下右键控制阀,界面应正常打开,不再出现 Network Protocol Error;确认显示的流速与过滤图标与服务端一致;
  2. 红石锁定状态对比:给阀门通电后打开,确认 render() 走 locked 分支把滑条与输入框压成 0、且输入框不可编辑(isLocked() 读的是客户端 blockstate 的 POWERED,与 BE 字段无关,不受本次改动影响);
  3. 窗口缩放:缩放窗口会重跑 init(),随后应从 BE 重新播种,而不是清空输入框(旧代码固定写成 setValue(""));
  4. 生存玩家回归:确认非旁观模式下打开仍走 openMenu(be, pos),且 ControlValveInitPacket 随后把值再设一遍,结果与 init() 播种值相同、无闪烁;
  5. 结构扫描仪(如果本轮补上):旁观右键打开不再断连;如果不补,只需确认它当前仍是已知缺口,而不是本次改动引入的回归。

@PigeonNian PigeonNian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@PigeonNian
PigeonNian merged commit 45609d6 into Anvil-Dev:dev/1.21/1.6 Nov 13, 2026
2 checks passed
@WhereisFff
WhereisFff deleted the dev/1.21/fix-6059-control-valve-menu branch November 13, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 旁观模式下右键点击控制阀因网络协议错误而退出

4 participants