Skip to content

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

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

PigeonNian merged 1 commit into
Anvil-Dev:dev/26.1/1.6from
WhereisFff:dev/26.1/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:36

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 条 ⚠️ 权限面 + 3 条 💡)

修复定位准确,而且这两处改动确实相互依赖、缺一不可——这一点比 PR 描述里呈现的更强,值得说明。

范围说明:webhook 仅提供 patch diff;本 PR 为 2 文件 / +12 −0,与 API 元数据(changed_files=2 / additions=12 / deletions=0)一致,diff 覆盖完整。结论基于 head 3cf232fb 完整源码树(codeload 拉取)与 base 286f7345 逐文件对照。未覆盖:实机运行、多人 / 旁观者实机手感。CI:build ✅ / checkstyle ✅。目标分支 dev/26.1/1.6。


✅ 失效链已完整对上,且「必须两处一起改」有硬证据

旁观者右键走的是原版 ServerPlayerGameMode.useItemOn 的 SPECTATOR 分支(26.1 反编译原文):

if (this.gameModeForPlayer == GameType.SPECTATOR) {
    MenuProvider menuProvider = state.getMenuProvider(level, pos);
    if (menuProvider != null) { player.openMenu(menuProvider); return InteractionResult.CONSUME; }
    ...

要点在于它是单参 openMenu(provider) 且提前 return CONSUME:

  1. NeoForge 的 openMenu(provider) 委托到 openMenu(provider, (Consumer) null),writeCustomData 后 extraData.length == 0 ⇒ 不发 AdvancedOpenScreenPayload,退回普通 ClientboundOpenScreenPacket;
  2. 客户端 MenuScreens.create 走 type.create(containerId, inventory),而 IContainerFactory 的默认实现就是 create(id, inv, null) ⇒ buf 为 null;
  3. ControlValveMenu 四参构造器无条件 extraData.readBlockPos()(inventory/ControlValveMenu.java:46)⇒ NPE ⇒ 断连。与 issue #6059 附带的崩溃栈(buf 为 null 的 readBlockPos)逐帧吻合。

补上 writeClientSideData(ControlValveBlockEntity.java:291-294)后 extraData.length != 0 ⇒ 走 AdvancedOpenScreenPayload ⇒ buf 非空 ⇒ 解析出 pos。✅

第二处改动不是锦上添花,而是必需品:因为 SPECTATOR 分支在 state.useItemOn 之前就 return CONSUME,ControlValveBlock.use(block/fluid/ControlValveBlock.java:196-197,那里才发 ControlValveInitPacket)对旁观者根本不会执行。也就是说旁观者的屏只能靠客户端 BE 的 update tag + 新增的 init() 播种拿到数值;没有这段播种,旁观者会看到「满流速 / 空过滤」的默认态且永远不会被纠正。PR 描述把两件事并列写,容易让人以为是两处独立修补,建议改写成「旁观者不会收到 InitPacket,故必须在 init 从客户端 BE 播种」——这个因果关系才是关键。

顺带被这次改动一并修掉的既有隐患:ControlValveInitPacket.handleOnClient 只在 screen instanceof ControlValveScreen 时生效,若包先于屏幕创建到达就被静默丢弃;此前玩家会看到默认值,现在 init() 从 BE 播种可兜住。

✅ 播种路径已逐项核对,不会误发包、不会越界

  • 不触发 C2S 回写:setValue → setValueBoxText 会置 updatingValueBox(ControlValveScreen.java:164-169),onValueInput 首行即 if (this.updatingValueBox || ...) return;(:194)⇒ responder 被吞;slider.setExactValue(FluidRateSlider.java:41-43)不带回调,setValueWithUpdate 才回调 ⇒ 打开界面不会凭空发 ControlValveUpdatePacket。这个守卫 26.1 base 里已存在(base 该文件的 setValueBoxText 已是 flag 版本),故本 PR 无需重复添加,也不存在姊妹线漂移。
  • 顺序安全:播种块插在 this.setValue(this.value) 之前(:131-136),后者用刚被赋值的 this.value 重算一次,setValue 幂等(clamp + setExactValue + getValue 同值)⇒ 不会反向覆盖播种值。若插在之后就会变成「播种被默认值冲掉」,位置是对的。
  • 数值域:ControlValveBlockEntity.MAX_RATE = 2000 与 FluidRateSlider.MAX = 2000 一致,setValue 内 Math.clamp 兜底。
  • 过滤读取不越界:getFilter(0) 自带 index < 0 || index >= filters.size() 守卫(:97-99),FILTER_SLOT_COUNT = 1。
  • 客户端 BE 数据齐全:26.1 getUpdateTag 写 MaxRate / Facing / Filters / FilterItem,loadAdditional 按同一批扁平 key 读回(input.read(FILTER_ITEM_NBT_ID, ...).orElse(EMPTY))⇒ 对称,播种读到的就是服务端现值;blockEntity == null 有 null 守卫,不会 NPE。

⚠️ 旁观者从此拥有了可写的 GUI,三个 C2S 处理器都没有 isSpectator 闸门

这是本 PR 唯一实质风险,也是「把旁观者从崩溃改成可开界面」必然带来的新面:

  • 客户端侧 render() 只按红石 isLocked() 决定可编辑性(ControlValveScreen.java:305-307:slider.active = !locked、valueBox.setEditable(!locked)),没有 spectator 维度 ⇒ 旁观者可以拖滑条、在输入框键入;
  • 服务端侧 ControlValveUpdatePacket.handleOnServer、ControlValveFilterPacket.handleOnServer、ControlValveFilterItemPacket.handleOnServer 全部只校验 player.containerMenu instanceof ControlValveMenu(+ 最后一个校验 stillValid),三者都无 player.isSpectator()(network/ 下 0 命中);
  • 原版对旁观者的容器交互是丢弃的:ServerGamePacketListenerImpl.handleContainerClick 里 if (this.player.isSpectator()) this.player.containerMenu.sendAllDataToRemote(); —— 而这三个包是自定义 C2S,完全绕开这条闸门。

净效果:旁观者(在交互距离内、界面打开时)可以修改阀门流速与过滤配置。仓库约定本就偏保守(RedstoneDiceRollPacket / SwapPocketPacket / BigRedButtonHoldPacket / HammerUsePacket / StoragePortTakeOutPacket 等 6 个包都显式拒绝旁观者)。若意图是「旁观者只读浏览」(#6080 的描述正是「允许旁观者查看」),建议在本 PR 一并补:三个 handler 加 player.isSpectator() 早退,或 Screen 侧对旁观者整体只读(slider.active = false + valueBox.setEditable(false))。不做也不算崩溃,但会让「旁观者能改机器」变成既成事实。

💡 ControlValveInitPacket 现在是第三条冗余通道,且与 #6080 的新约定不一致

同一份数据现在有三条路:writeClientSideData 写 pos → 菜单解析出客户端 BE(数据来自 BE 的 update tag)→ ControlValveInitPacket 再推一遍 maxRate/filter/filterItem。而姊妹 PR #6080(Fix spectator menu previews,24 文件,同 base 286f7345)已把同类机器的做法统一成「数据走 writeClientSideData、删掉独立 init 包」(CreativeGeneratorBlockEntity 改为 buffer.writeVarInt(this.getPower()) 并给 SliderMenu 加 power 参数、TeslaTower 同理),且 #6080 不含 ControlValve(grep -c ControlValve pr6080.diff = 0)——两 PR 是同一修复的两个互补半区,各自单独合入都安全(本 PR 不依赖 #6080,反之亦然),但风格上阀门成了唯一保留 init 包的机器。

进一步说,openMenu(be, pos) 的 pos-consumer 与 writeClientSideData 现在写的是同一个 BlockPos(共 16 字节,菜单只读前 8 字节、余下 8 字节被忽略且 buf.release() 正常);等 #6080 落地后可以简化为 serverPlayer.openMenu(be) + 删除 ControlValveInitPacket,少一条通道、少一处「包早到被丢弃」的时序依赖。

💡 未覆盖的同类机器已由 #6080 承接(不是本 PR 的缺口)

26.1 head 里 MenuProvider 共 16 个 BE,writeClientSideData 缺失且 createMenu 无旁观者守卫的还有 CelestialForgingAnvilBlockEntity(block/entity/CelestialForgingAnvilBlockEntity.java:1267-1270 只判 level == null)。但它不会崩:四处打开点全部走 ModMenuTypes.open(sp, cfaBe, pos)(block/cfa/CelestialForgingAnvilBlock.java:450、AmplifierBlock.java:406、InterfacePlaceholderBlock.java:128、InterfaceBlock.java:96),pos-consumer 保证 extraData 非空;其余 13 个缺 wcsd 的(AdvancedComparator / ItemCollector / PulseGenerator / ActiveSilencer / ItemDetector / TeslaTower / CreativeGenerator / StructureScanner 及 BaseMachine / BaseChute / BaseBatchCrafting 链)目前在 base 上都有 if (player.isSpectator()) return null; 或由 #6080 补 wcsd,故本 PR 单独合入不会留下第二个「右键即断连」的机器——这点已逐个核对,不构成遗漏。


📋 声称验证表

PR 声称 状态 证据
在控制阀菜单初始化数据中发送方块位置 ✅ ControlValveBlockEntity.java:291-294 覆写 writeClientSideData 写 getBlockPos();ControlValveMenu.java:46 正是读它
打开界面时读取已同步的流速与过滤配置 ✅ ControlValveScreen.java:131-135 从 menu.getBlockEntity() 取 getMaxRate() / getFilter(0) / getFilterItem();BE 的 getUpdateTag ↔ loadAdditional key 对称
fixed #6059(旁观者右键断连) ✅ 崩溃链已逐环复现(见上);fixed 的语义是「不再断连」,达成
(未声明)阻止初始化时客户端回写 ✅ 无需改动 26.1 base 的 updatingValueBox 守卫已存在(setValueBoxText / onValueInput)
跨线对应 ℹ️ 同标题 1.21 版为 #6083(dev/1.21/fix-6059-control-valve-menu,2 文件 +18 −0,同为 open)。两线需一起合入;1.21 侧额外补了 updatingValueBox,26.1 侧 base 已有,属正常差异不是漂移

🧪 建议验证

  1. 原始复现:旁观模式右键控制阀 → 界面正常打开、不再 Network Protocol Error,且滑条/输入框显示的是该阀门真实流速(非 2000 默认值);
  2. 旁观者写入(对应上方 ⚠️):旁观者拖滑条 / 改输入框 / 放过滤器 → 确认按预期被拒绝或允许(取决于补不补 spectator 闸门);
  3. 旁观者实时性:让生存玩家改同一阀门流速与过滤器,旁观者界面应随 BE 同步刷新;
  4. 非旁观者回归:生存/创造打开不受影响;ControlValveInitPacket 仍会二次设值,确认两次设值结果一致、无闪烁;
  5. 窗口缩放:改 GUI 缩放触发 init() 重跑,应从 BE 重新播种而不是回落到默认 2000;
  6. 红石锁定态:给阀门通电后打开,滑条与输入框应为不可编辑(isLocked() 读 blockstate POWERED,与本次改动无关,属回归确认);
  7. 与 #6080 的合并顺序:分别单测「只合 #6084」「再合 #6080」,确认两类机器(阀门 / 其余)都不再断连。

@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 ba726a5 into Anvil-Dev:dev/26.1/1.6 Nov 13, 2026
2 checks passed
@WhereisFff
WhereisFff deleted the dev/26.1/fix-6059-control-valve-menu branch November 13, 2026 15:26
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.

4 participants