Repository navigation
Fix spectator control valve menus 修复旁观者控制阀菜单 - #6084
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 条 ⚠️ 权限面 + 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:
- NeoForge 的
openMenu(provider)委托到openMenu(provider, (Consumer) null),writeCustomData后extraData.length == 0⇒ 不发AdvancedOpenScreenPayload,退回普通ClientboundOpenScreenPacket; - 客户端
MenuScreens.create走type.create(containerId, inventory),而IContainerFactory的默认实现就是create(id, inv, null)⇒ buf 为 null; 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 已有,属正常差异不是漂移 |
🧪 建议验证
- 原始复现:旁观模式右键控制阀 → 界面正常打开、不再
Network Protocol Error,且滑条/输入框显示的是该阀门真实流速(非 2000 默认值); - 旁观者写入(对应上方
⚠️ ):旁观者拖滑条 / 改输入框 / 放过滤器 → 确认按预期被拒绝或允许(取决于补不补 spectator 闸门); - 旁观者实时性:让生存玩家改同一阀门流速与过滤器,旁观者界面应随 BE 同步刷新;
- 非旁观者回归:生存/创造打开不受影响;
ControlValveInitPacket仍会二次设值,确认两次设值结果一致、无闪烁; - 窗口缩放:改 GUI 缩放触发
init()重跑,应从 BE 重新播种而不是回落到默认 2000; - 红石锁定态:给阀门通电后打开,滑条与输入框应为不可编辑(
isLocked()读 blockstatePOWERED,与本次改动无关,属回归确认); - 与 #6080 的合并顺序:分别单测「只合 #6084」「再合 #6080」,确认两类机器(阀门 / 其余)都不再断连。
fix(menu): 修复旁观者打开控制阀时断开连接
在控制阀菜单初始化数据中发送方块位置
打开界面时读取已同步的流速与过滤配置
fixed [Bug] 旁观模式下右键点击控制阀因网络协议错误而退出 #6059