Skip to content

Fix spectator menu previews 修复旁观者菜单预览 - #6081

Merged
PigeonNian merged 1 commit into
Anvil-Dev:dev/1.21/1.6from
WhereisFff:dev/1.21/fix-6055-spectator-menus
Nov 13, 2026
Merged

PigeonNian merged 1 commit into
Anvil-Dev:dev/1.21/1.6from
WhereisFff:dev/1.21/fix-6055-spectator-menus

Conversation

@WhereisFff

Copy link
Copy Markdown
Contributor

fix(menu): 修复旁观者菜单预览

- 补齐脉冲发生器、高级比较器和特斯拉塔的旁观者菜单入口
- 通过菜单初始化同步创造发电机功率与特斯拉塔过滤配置
Copilot AI balanced review requested due to automatic review settings November 12, 2026 19:33

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.

审查结论:REQUEST_CHANGES(1 处必须补、1 处建议本轮补齐)

方向正确,而且实现手段是唯一正确的那条:1.21.1 的旁观者右键不走 useWithoutItem,它在 ServerPlayerGameMode.useItemOn 里被提前拦下(字节码 34-73:gameModeForPlayer == SPECTATOR → BlockState.getMenuProvider(level,pos) → 非 null 则 ServerPlayer.openMenu(provider) 并返回 CONSUME)。所以「让旁观者能开预览」的落点只能是 getMenuProvider,而 BlockBehaviour.getMenuProvider 默认返回 null——高级比较器 / 脉冲发生器继承 HorizontalDirectionalBlock、特斯拉塔继承多方块基类,三者都拿不到默认实现,PR 补这三处覆写是必需的,不是可选优化。writeClientSideData 的字段顺序也与菜单构造器逐一对齐(BlockPos[+NBT] / 仅 VarInt),✅。

但「开放入口」这一步同时开放了写路径,而这一层没有配套门禁;另有两条同链路的 extraData 缺口仍在,其中一条在旁观者路径上会直接让客户端在构造菜单时抛异常。

⚠️ 审查范围说明:webhook 只提供 patch diff,且本 PR 的 API 计 11 文件 / +39 −8 与所给 diff 的文件数一致,diff 覆盖完整;但仓库其余文件、运行时行为与多人实机场景未在本次覆盖范围内,结论基于 head fb3e98b 的完整源码树(codeload 拉取)静态核对。CI:build / checkstyle 均 success。


🔴(阻塞)旁观者现在能把「预览」变成「改机器」,三个 C2S 更新包没有任何 isSpectator 门

本 PR 之前,旁观者对这三台机器右键拿不到菜单(getMenuProvider 为 null → PASS),因此下列 handler 对旁观者不可达;本 PR 之后全部可达,而它们只校验「当前菜单类型」,不校验玩家是否处于旁观模式。原版对旁观者的保护恰恰只做在 handleContainerClick / handleContainerButtonClick(槽位点击、菜单按钮),自定义 payload 不在原版保护范围内——这正是需要自己补的地方。

新开放的菜单 handler 旁观者可造成的写入
高级比较器 network/AdvancedComparatorUpdatePacket.java handleOnServer,仅有 hasContainerOpen() + instanceof AdvancedComparatorMenu setCompareMode / setOutputInvert / setRedstoneControl / setHighLimit / setLowLimit / setInputtingSignal 全部直写 BE
脉冲发生器 network/PulseGeneratorUpdatePacket.java setWaitingTime / setSignalDuration / setOutputMode / setStartMode + syncToClient()
特斯拉塔 network/TeslaAddFilterPacket.java / TeslaRemoveFilterPacket.java(menu.addFilter / menu.removeFilter 落到 BE 的 allowList) 增删他人特斯拉塔的过滤条目

修法一行,加在各 handleOnServer 入口(与 createMenu 无关,不要试图回到 createMenu 里挡,那会把预览一起挡掉):

if (player.isSpectator()) return;

注意 AdvancedComparatorUpdatePacket 已有的 if (!player.hasContainerOpen()) return; 不能替代这一条——旁观者此刻的 containerMenu 正是刚打开的那个菜单,hasContainerOpen() 为 true。同理 PulseGeneratorUpdatePacket 连 hasContainerOpen() 都没有。

顺带:CreativeGeneratorBlockEntity 的 SliderUpdatePacket(callback.onValueChange(this.value) → setPower)也是同类形态,但它在 1.21 线本来就可达(CreativeGeneratorBlock extends BetterBaseEntityBlock,BaseEntityBlock.getMenuProvider 默认返回实现了 MenuProvider 的 BE),属既有缺口,不记在本 PR 账上,可一并补。


⚠️ 两条 writeClientSideData 缺口仍在,其中一条在旁观者路径上必然抛异常

MenuProvider 实现者里,只有两个没有写客户端数据,而它们的菜单构造器无条件读该缓冲:

BE writeClientSideData 对应菜单读取
block/entity/StructureScannerBlockEntity.java 缺失 StructureScannerMenu:Objects.requireNonNull(level.getBlockEntity(extraData.readBlockPos()))
block/entity/fluid/ControlValveBlockEntity.java 缺失 ControlValveMenu:BlockPos pos = buf.readBlockPos();

失效链(逐环节可证):

  1. 旁观者右键 → 走 vanilla 分支 → player.openMenu(provider)(1 参,不带 BlockPos consumer);
  2. ServerPlayer.openMenu 用 FriendlyByteBufUtil.writeCustomData(buf -> { provider.writeClientSideData(...); consumer?.accept(buf); }) 组装额外数据;本 BE 没有覆写 ⇒ 走 IMenuProviderExtension 默认空实现 ⇒ 长度 0;
  3. 长度 0 ⇒ 不发 AdvancedOpenScreenPayload,改发普通 ClientboundOpenScreenPacket;
  4. 客户端 MenuScreens.ScreenConstructor.fromPacket 调 2 参 MenuType.create(containerId, inventory);MenuType 的 NeoForge 补丁把 4 参 create 委托给 IContainerFactory,而 IContainerFactory.create(int, Inventory) 的默认实现是 create(id, inv, **null**)(javap 实证:aconst_null 作第 3 实参);
  5. 菜单构造器在 null 缓冲上调 readBlockPos() ⇒ 抛异常,客户端在打开界面时崩/断线。

定位口径:这两台机器的普通(非旁观)打开路径都显式带了 pos(ControlValveBlock.java:184 serverPlayer.openMenu(be, pos)、StructureScannerBlock.java:158 ModMenuTypes.open(serverPlayer, menuProvider, pos)),所以平时不暴露;一旦改走不带 consumer 的 openMenu(provider) 就必炸。按 git log/base 树核对,这两处在 1.21 线属于既有缺口,不是本 PR 引入;但本 PR 的立论就是「旁观者也能开预览」且 issue 正文明确要求「建议排查所有带 UI 的方块」,这两处正好落在同一机制上,补齐成本各一行:

@Override
public void writeClientSideData(AbstractContainerMenu menu, RegistryFriendlyByteBuf buffer) {
    buffer.writeBlockPos(this.getBlockPos());
}

(若 26.1 线另有同源 PR,也请同步核对这两台机器——26.1 上结构扫描仪的 createMenu 旁观者守卫已被移除,写端不补就是新增回归,而不是既有缺口。)


💡 建议(不阻塞)

  1. 特斯拉塔过滤表走了三条通道:TeslaTowerBlockEntity.writeClientSideData(本 PR 新增)、TeslaTowerBlock.useWithoutItem 里仍然保留的 PacketDistributor.sendToPlayer(sp, TeslaFilterSyncPacket.create(...))、以及 TeslaTowerScreen.init() 新增的 handleSync(List.copyOf(menu.getBlockEntity().getAllowList()));而 TeslaTowerScreen.handleSync 末尾还会再调 this.menu.handleSync(filters) → blockEntity.handleSync(...) 第三次写同一份列表。三条都是幂等的,功能上不出错,但属「补丁叠补丁」信号:建议只保留 writeClientSideData 一条(它随开界面同包、时序最可靠),删掉 useWithoutItem 里的那次发包与 init() 里的兜底同步。若确实需要 init() 兜底(客户端 BE 尚未同步时 getAllowList() 为空),请写明原因,否则后人无法判断哪条是权威通道。
  2. SliderInitPacket 现在与 writeClientSideData 重复:功率已随菜单初始化下发并由 SliderScreen.init() 自播(slider.setValue + value.setValue),CreativeGeneratorBlock 之后的 PacketDistributor.sendToPlayer(serverPlayer, new SliderInitPacket(entity.getPower())) 全树仅此一处发送,可删;保留则建议注释说明保留理由。
  3. ModMenuTypes.open(player, provider) 这个重载现在是"完全依赖 BE 自己写 BlockPos"的隐式契约:本 PR 把 TeslaTowerBlock / CreativeGeneratorBlock 都切到了这个 2 参版本,AdvancedComparatorBlock / PulseGeneratorBlock 早已如此。任何后续给这些方块换 BE、或新增实现 MenuProvider 的机器只要漏写 writeClientSideData,就会静默退化成上面那条 NPE 链路,且编译期、CI 全绿。建议给该重载的 javadoc 写明「provider 必须自行写 BlockPos」,或保留 open(player, provider, pos) 统一带坐标(重复字节无害,缺失字节会崩),把安全性做成默认。

📋 声称验证表

PR 描述声称 核对结果
「补齐脉冲发生器、高级比较器和特斯拉塔的旁观者菜单入口」 ✅ 三处 getMenuProvider 齐全且语义正确;特斯拉塔用 getMainPartPos(pos, state) 解析主部件是必要的(多方块只有主部件挂 BE),比较器/脉冲为单方块直接查 getBlockEntity(pos) 亦正确
「通过菜单初始化同步创造发电机功率」 ✅ writeClientSideData → writeVarInt 与 SliderMenu 4 参构造器 readVarInt 对齐;ModMenuTypes.SLIDER 已切到 4 参工厂(否则 IContainerFactory 不生效);SliderScreen.init 在 setResponder 之前赋值,不会触发多余的 SliderUpdatePacket ✅
「同步特斯拉塔过滤配置」 ✅ 时序正确(writeBlockPos 后 STREAM_CODEC.encode,菜单侧先 readBlockPos 再 decode);⚠️ 但有上述三通道冗余
fixed #6055 ⚠️ 部分兑现。issue 报告的症状是旁观者右键溜槽显示「无法打开(旁观模式)」,根因是 26.1 线各 BE createMenu 里的 player.isSpectator() → null;本 PR 针对 1.21 线,而 1.21 线的这些守卫本就不存在(全树 block/entity/ 已无 createMenu 旁观者守卫),所以 1.21 上的溜槽/批量加工/收集器旁观者预览在本 PR 前后行为相同——真正被本 PR 改变的是那三台机器的可达性。另外 issue 要求「排查所有带 UI 的方块」,结构扫描仪与控制阀仍未补齐(见 ⚠️)。建议描述里写明「本 PR 只补 1.21 线上缺失 getMenuProvider 的三台机器」,避免与 26.1 线同源 PR 的叙述混淆

🧪 建议实机验证

  1. 旁观者右键:高级比较器 / 脉冲发生器 / 特斯拉塔 → 界面正常打开,且比较器阈值、脉冲时长、过滤列表显示值与服务端一致;
  2. 旁观者尝试触发写入:改比较器阈值、改脉冲时长、增删特斯拉塔过滤项 → 服务端应拒绝(补门禁后重测);
  3. 旁观者右键结构扫描仪、控制阀 → 补 writeClientSideData 前预期崩溃(用于确认失效链),补齐后应正常打开;
  4. 生存玩家正常打开上述 5 台机器,确认 writeClientSideData 改造未破坏原有参数同步(比较器的 NBT 配置、创造发电机功率、特斯拉塔过滤表);
  5. 窗口缩放(触发 SliderScreen.init() 重跑)后创造发电机功率输入框与滑块仍与打开时一致。

@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 33c58b1 into Anvil-Dev:dev/1.21/1.6 Nov 13, 2026
2 checks passed
@WhereisFff
WhereisFff deleted the dev/1.21/fix-6055-spectator-menus 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] 旁观模式下 右键溜槽显示“无法打开:战利品未生成” 而非打开ui

4 participants