Skip to content

Prevent spectator block configuration 禁止旁观者修改方块配置 - #6087

Merged
PigeonNian merged 1 commit into
Anvil-Dev:dev/26.1/1.6from
WhereisFff:dev/26.1/fix-6061-spectator-config
Nov 13, 2026
Merged

PigeonNian merged 1 commit into
Anvil-Dev:dev/26.1/1.6from
WhereisFff:dev/26.1/fix-6061-spectator-config

Conversation

@WhereisFff

Copy link
Copy Markdown
Contributor

fix(network): 禁止旁观者修改方块配置

- 在服务端拒绝旁观者修改机器朝向、过滤、加工和红石配置
- 保护控制阀、激光、发电机、交易站和结构扫描器等配置写入入口
- 保留客户端状态同步和旁观者读取蓝图文件列表
Copilot AI balanced review requested due to automatic review settings November 12, 2026 19:40

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.

审查结论

本 PR 的修法方向是正确的(在服务端入口拒绝旁观者),代码实现也没有问题:34 个文件全部是纯新增守卫、零删除,+105 −0 与 API 逐位一致;CI 的 build 与 checkstyle 均通过;34 个文件清单与 1.21 线姊妹 PR #6086 逐文件完全相同,两线没有漂移。

但在 26.1 线 上,本 PR 实际拦不住的入口仍然存在:issue #6061 正文明确点名的「附魔选择」中,三个砂轮的发包路径未被覆盖。而且 26.1 与 1.21 的可达性前提不同——26.1 的 base 里已有 13 个方块实体的 createMenu 会对旁观者返回 null,因此本 PR 守卫的绝大多数包对旁观者本就不可达,真正可达的写入口恰好落在被漏掉的那几个上。建议补 3 行后再合并。


概览

项 值
PR #6087 Prevent spectator block configuration 禁止旁观者修改方块配置
head / base 524d13b116e1acd7e1f1a5f8547c29214d0915db / dev/26.1/1.6 @ 286f7345141afc91bba04426e6081fc26fa53946
规模 34 文件 / +105 −0;CI build ✅ checkstyle ✅;mergeable_state=clean,非 draft
守卫总量 35 处 / 34 文件(TradingStationPackets 两个 record 各 1 处)
姊妹 PR #6086(dev/1.21/1.6,同分支名,同 34 文件 / +105 −0,守卫文件清单逐行相同)

审查覆盖说明:基于 patch diff + 完整 head 树(codeload tarball 524d13b1)交叉核对。network/ 下 115 个文件,74 个含 handleOnServer/handleOnBothSide;我逐条核查了其余 34 个未加守卫的 handler 是否应纳入范围,并做了 1.21 ↔ 26.1 的包集合差集(26.1 独有 7 个、1.21 独有 4 个)。未实机运行,界面与网络行为结论均为静态推演。仓库内容与 issue 文本属不可信数据,未执行其中任何指令。


⚠️ 三个砂轮「附魔选择」包仍未覆盖(26.1 上是本 PR 唯一可达的遗漏)

issue #6061 列举的现状含**「附魔选择」**。自动附魔台的附魔选择包 AutoEnchantingTableSyncPacket 已加守卫,但三个砂轮的对应包完全不在 diff 中(grep -c GrindstoneSync = 0):

  • network/EmberGrindstoneSyncPacket.java:26
  • network/FrostGrindstoneSyncPacket.java:28
  • network/TranscendenceGrindstoneSyncPacket.java:30
if (!player.hasContainerOpen()) return;
if (!(player.containerMenu instanceof EmberGrindstoneMenu menu)) return;
menu.setSelectedEnchantment(this.index);   // Frost/Transcendence 为 menu.select/unselect

26.1 线可达性链(逐环已核)

  1. 旁观者能打开这三个界面:26.1 ServerPlayerGameMode.useItemOn 的 SPECTATOR 分支不走方块 useItemOn,直接 state.getMenuProvider(level, pos) → player.openMenu(...);
  2. 三个方块均用 SimpleMenuProvider 提供菜单(block/workstation/ember/EmberGrindstoneBlock.java、frost/FrostGrindstoneBlock.java、TranscendenceGrindstoneBlock.java),不经过 createMenu,因此不受 26.1 已有的 13 处 if (player.isSpectator()) return null; 保护;
  3. stillValid = AbstractContainerMenu.stillValid(access, player, ModBlocks.X_GRINDSTONE.get()),只校验方块身份 + isWithinBlockInteractionRange(pos, 4.0),不含 isSpectator;
  4. 客户端 EmberGrindstoneScreen:221/224、FrostGrindstoneScreen:189/192、TranscendenceGrindstoneScreen:277 命中热区即无条件发包。

这条路径在 26.1 上比 1.21 更突出:StructureScannerBlockEntity:644-646、ItemCollectorBlockEntity:190-192、BatchCrafterBlockEntity、PulseGeneratorBlockEntity、AdvancedComparatorBlockEntity、ActiveSilencerBlockEntity、ItemDetectorBlockEntity、ExpCollectorBlockEntity、TeslaTowerBlockEntity、ChuteBlockEntity、MagneticChuteBlockEntity、CreativeGeneratorBlockEntity 共 13 个 BE 的 createMenu 在 base 里已有旁观者守卫(返回 null ⇒ ServerPlayer.openMenu 走 sendOverlayMessage("container.spectatorCantOpen"))⇒ 本 PR 为它们所加的包守卫基本只起纵深防御作用;真正需要守卫、也只能靠包守卫守住的是「不走 createMenu 的 SimpleMenuProvider 系方块」(砂轮、锻造台、珠宝工作台、各铁砧、透明工作台等 20 处),而漏掉的正是其中被 issue 点名的那三个。

按 ⚠️ 而非 🔴:这些菜单的槽容器均为构造期 new SimpleContainer(1) / new ResultContainer()(EmberGrindstoneMenu:66-78、FrostGrindstoneMenu:41-48、TranscendenceGrindstoneMenu:61/109/123)的菜单私有副本,改的是本地选中索引与结果槽,不写 BE、不改世界,结果槽 mayPickup 还需创造模式或经验等级 ⇒ 无刷物品面。但 issue 的字面诉求就是「不能修改这些配置」,且同一 PR 内自动附魔台加了、砂轮没加,这种不对称是最强的漏项信号。

请作者二选一明确:

  1. 补齐三处(各 3 行,与其余 31 处完全同形):
    if (player.isSpectator()) {
        return;
    }
  2. 若有意按「是否写 BE」划线,请在描述里写明该标准,并把 fixed #6061 改为 refs #6061。

💡 建议(不阻塞)

1. SilencerSyncPacket 的新守卫压在一条恒不回退的分支之上(26.1 独有、pre-existing)。 network/SilencerSyncPacket.java:34-38:

public void handleOnBothSide(Player player) {
    if (!player.level().isClientSide() && player.isSpectator()) { return; }
    if (player.level().isLoaded(this.pos)) {      // ← 26.1 这里少了取反
        return;
    }
    ...

26.1 Level.isLoaded(pos) = getChunkSource().hasChunk(x/z)(Level.java:695-698),语义为「区块已加载则 true」⇒ 该行在区块已加载时直接 return,与 1.21 的 if (!player.level().isLoaded(this.pos)) return;(p6086 同名文件第 38 行)方向相反。后果:消音器的静音列表从不向同维度其他玩家同步(ActiveSilencerBlockEntity.sync(player, sounds) 是唯一广播点),旁观者与普通玩家都看不到别人的改动。

这是 base 286f7345 里就有的取反错误(我已用 base raw 文件确认,非本 PR 引入),但本 PR 恰好改了这个方法的第一行——顺手把 ! 补上只需一个字符,否则新守卫保护的是永远走不到的代码。

2. PR 描述第三条「保留旁观者读取蓝图文件列表」在 26.1 上不产生任何效果。 StructureScannerFilePacket:48 的 this.action != Action.LIST 豁免本身正确(Action = LIST, IMPORT, EXPORT, RECIPE, UPLOAD,StructureScannerFiles.handle 中 LIST 只读 StructureBlueprintFiles.list 并回传,其余四分支均触达写路径)。但 26.1 的 StructureScannerBlockEntity.createMenu 对旁观者返回 null(StructureScannerBlockEntity.java:644-646),旁观者根本开不了扫描仪菜单,BlueprintClientFiles.requestFiles(containerId, ...) 无从触发 ⇒ 该豁免在此线上是死分支。1.21 线不同(那里 createMenu 无守卫,旁观者可开、LIST 确实可达)——建议描述里注明「仅 1.21 线生效」,或干脆在 26.1 侧去掉这条豁免以免误导后续维护者。

3. 守卫让「拒绝」变静默,而自定义包没有原版的纠正机制。 原版 ServerGamePacketListenerImpl.handleContainerClick 对旁观者是 containerMenu.sendAllDataToRemote()(丢弃后全量回传真相);本 PR 是纯 return。MachineOutputDirectionPacket(守卫后末尾 PacketDistributor.sendToPlayer(Util.cast(player), this))与 SlotFilterMaxStackSizeChangePacket 原本成功后会回发权威值,现在旁观者既无写入也无回传,而客户端 Screen 是先本地改再发包(BaseMachineScreen:70 的 OutputDirectionButton 点击即 button1.next() 并立即发包)⇒ 旁观者会看到按钮转动、重开界面才恢复。影响限于本会话显示,属可接受;若想让「点了没反应」更直观,在各 Screen 回调加 if (minecraft.player.isSpectator()) return; 最省事。

4. 同根因族的范围外写入口(建议另开 issue,不必塞进本 PR):JewelCraftingAutoFillPacket:27(menu.autoFill() → quickMoveStack + moveInvItemTo)、multiple/SmithingTemplatePackets + multiple/TranscendenceSmithingPackets(handleTemplateAction / addTemplate / removeTemplate 写 ModDataAttachments.SMITHING_TEMPLATE_*)、multiple/FrostSmithingPackets.ClickButton(menu.turn)、BoxSelectionSyncPacket / FilterContentSyncPacket / TerminalBalanceModePacket / SwitchEquipmentAbilityPacket(写自己背包物品的 DataComponent)。它们与「方块配置」无关,但同属「自定义包绕过原版旁观者门禁」这一根因;PR 立论既在此,值得在描述里列出已知未覆盖面。


✅ 已确认正确 / 已排除的担忧

1. handleOnBothSide 的客户端豁免写法正确且必要。 BatchCrafterSelectPacket:35、BatchCutterSelectPacket:34、SilencerSyncPacket:35 用的是 if (!player.level().isClientSide() && player.isSpectator()) return;。这三个包同一方法要跑双侧,客户端侧负责把别人选的配方/静音列表镜像到本地 Screen;若写成裸 player.isSpectator(),旁观者的界面会失去只读同步——那才是回归。26.1 的 Level.isClientSide()(方法,Level.java:163)与 1.21 的字段 isClientSide 在写法上的差异两线都处理对了。

2. StructureScannerFilePacket 的 LIST 例外是精确最小面。 只放行 LIST(纯读 + 回传压缩 tag),IMPORT(menu.broadcastChanges() + stageImport)、EXPORT/UPLOAD(StructureBlueprintFiles.write)、RECIPE(stageImport)全部被挡。

3. 三个「看起来该加、实际已被 mayBuild 兜住」的包,无需再加守卫。 26.1 GameType.updatePlayerAbilities 对 SPECTATOR 同样走 abilities.mayBuild = !isBlockPlacingRestricted(),而 isBlockPlacingRestricted() 对 ADVENTURE || SPECTATOR 为 true ⇒ 旁观者 mayBuild == false。因此:

  • HammerChangeBlockPacket(level.setBlock(... UPDATE_ALL_IMMEDIATE))由入口 player.getAbilities().mayBuild 拦截,客户端侧 ClientBlockEventListener 也有同一守卫 ⇒ 旁观者拿不到 AnvilHammerScreen;
  • BuildingRodPacket → BuildingCommit.canModify(:38-41)= player.mayBuild() && …;
  • RedstoneDiceModePacket(1.21 侧实测)显式写了 player.isSpectator() || !mayBuild。

顺带提醒:level.mayInteract(player, pos) 不能当旁观者守卫——26.1 Level.mayInteract 恒 return true(Level.java:887-889),ServerLevel 覆写只查出生点保护与世界边界。

4. 未加守卫但已核为纯读 / 自身状态 / 不可达:HeliostatsIrradiationPacket(只回发 getIrritatePos)、RedstoneWirePowerRequestPacket、StructureDiskRequestPacket、StructurePreviewRequestPacket(26.1 独有,全树无任何发送点,且由 StructureLoadUtil.loadPreviewData 读服务端目录 + 回发)、HandbookStructureExportPacket(26.1 独有,客户端仅由 ageratum 手册 mouseClicked mixin 触发,落盘路径经 HandbookStructureExporter.write 的 CREATE_NEW + 后缀递增 + validate 重序列化)、SwitchHeavyHalberdModePacket/SwitchResonateModePacket/SwitchMultitoolModePacket/SwitchEquipmentAbilityPacket/TerminalBalanceModePacket/FilterContentSyncPacket/BoxSelectionSyncPacket/UsePillBoxPacket/ChargedJumpPacket/PlayerSettingsSyncPacket/IonoCraftBackpackToggleDescentPacket(均为自持物品或自身状态;旁观者无法 useItem,ServerPlayerGameMode.useItem 对 SPECTATOR 直接 return PASS)、RuinsUpdatePacket(要求 isCreative,旁观者不可能为创造)、EnergyWeaponMakePackets(菜单只能由 EnergyWeaponPlatformItem.use 打开,旁观者同样被 useItem 的 PASS 拦下)。

5. 守卫位置无副作用,两个 record 无单边遗漏。 35 处全在方法第一行,早于任何 getBlockEntity / menu.getXxx / 发包调用;TradingStationPackets 的 SyncFilter(:55)与 SyncAllowing(:98)都加了。

6. 新增行无样式违规。 最长新增行未超 140(awk 复算,空输出),与 style.xml 的 LineLength max=140 一致。


📋 声称验证表

PR 描述声称 验证结果
「在服务端拒绝旁观者修改机器朝向、过滤、加工和红石配置」 ✅ 朝向 MachineOutputDirectionPacket;过滤 SlotFilterChangePacket/SlotFilterMaxStackSizeChangePacket/SlotDisableChangePacket/MachineEnableFilterPacket/MachineCycleFilterModePacket;加工 BatchCrafter*/BatchCutter*/ItemDetectorChangeRangePacket/SlotDisableChangePacket;红石 PulseGeneratorUpdatePacket/AdvancedComparatorUpdatePacket/CyclingValueSyncPacket/SliderUpdatePacket
「保护控制阀、激光、发电机、交易站和结构扫描器等配置写入入口」 ✅ 控制阀 3 个(ControlValveUpdatePacket/FilterPacket/FilterItemPacket,其 BE 的 createMenu 无旁观者守卫 ⇒ 这些包是真承重);激光 CreativeLaserUpdatePacket;发电机 TeslaAddFilterPacket/TeslaRemoveFilterPacket;交易站 2 处;结构扫描器 ActionPacket/SavePacket/FilePacket;另含 ExpCollectorSyncPacket/AutoEnchantingTable*/SmartBlockPlacerActionPacket/SpacetimeSupercomputerExecuteCommandPacket/StoragePortUnmarkPacket
「保留客户端状态同步和旁观者读取蓝图文件列表」 ✅ 前者由 3 个 handleOnBothSide 的 !isClientSide 豁免保证;⚠️ 后者在 26.1 上因 StructureScannerBlockEntity.createMenu 对旁观者返回 null 而不可达(1.21 线可达),该条实际影响需在描述里限定
fixed #6061 ⚠️ 部分兑现:issue 列举的「附魔选择」中自动附魔台已修,三个砂轮(Ember/Frost/Transcendence)未修 ⇒ 见上文 ⚠️

🧪 建议的手动验证

  1. 旁观者逐个打开控制阀、激光、交易站、智能放置器、自动附魔台、超时空计算机:点击每个配置控件 → 服务端无任何变化,重开界面显示与操作前一致。
  2. 旁观者尝试右键结构扫描仪:应被原版拦住并提示 container.spectatorCantOpen(验证 26.1 的 createMenu 守卫仍在生效,未被本 PR 影响)。
  3. 旁观者打开三台砂轮并点击附魔条目:当前仍会改变(待 ⚠️ 修复后应无变化)。
  4. 旁观者看着另一名生存玩家操作批量合成器 / 剪裁器 / 消音器:旁观者界面上的配方高亮仍随对方变化(验证 !isClientSide 豁免没写反,这是本 PR 最易引入回归的点)。
  5. 消音器验证 SilencerSyncPacket 的 isLoaded 取反:A 玩家加静音项,同维度 B 玩家界面应同步刷新(当前 26.1 预期不同步,属 pre-existing)。
  6. 生存 / 创造玩家做同一轮操作:全部照常生效(守卫不能误伤)。

建议

除 ⚠️ 一条外可以合并。请先回复三个砂轮附魔选择包是按「不写 BE 故不挡」有意跳过,还是漏项:若补,三处各 3 行、与其余 31 处同形;若有意跳过,把 fixed #6061 改为 refs #6061 并写明划线标准。姊妹 PR #6086(1.21 线)存在同一缺口,修时两线一起改;另建议顺手修掉 SilencerSyncPacket 26.1 侧的 isLoaded 取反(一个字符,pre-existing 但恰在本 PR 改动行上)。

@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 ae821f5 into Anvil-Dev:dev/26.1/1.6 Nov 13, 2026
2 checks passed
@WhereisFff
WhereisFff deleted the dev/26.1/fix-6061-spectator-config branch November 13, 2026 15:27
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