Repository navigation
Prevent spectator block configuration 禁止旁观者修改方块配置 - #6087
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.
审查结论
本 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:26network/FrostGrindstoneSyncPacket.java:28network/TranscendenceGrindstoneSyncPacket.java:30
if (!player.hasContainerOpen()) return;
if (!(player.containerMenu instanceof EmberGrindstoneMenu menu)) return;
menu.setSelectedEnchantment(this.index); // Frost/Transcendence 为 menu.select/unselect26.1 线可达性链(逐环已核)
- 旁观者能打开这三个界面:26.1
ServerPlayerGameMode.useItemOn的SPECTATOR分支不走方块useItemOn,直接state.getMenuProvider(level, pos)→player.openMenu(...); - 三个方块均用
SimpleMenuProvider提供菜单(block/workstation/ember/EmberGrindstoneBlock.java、frost/FrostGrindstoneBlock.java、TranscendenceGrindstoneBlock.java),不经过createMenu,因此不受 26.1 已有的 13 处if (player.isSpectator()) return null;保护; stillValid=AbstractContainerMenu.stillValid(access, player, ModBlocks.X_GRINDSTONE.get()),只校验方块身份 +isWithinBlockInteractionRange(pos, 4.0),不含isSpectator;- 客户端
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 内自动附魔台加了、砂轮没加,这种不对称是最强的漏项信号。
请作者二选一明确:
- 补齐三处(各 3 行,与其余 31 处完全同形):
if (player.isSpectator()) { return; }
- 若有意按「是否写 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 豁免保证;StructureScannerBlockEntity.createMenu 对旁观者返回 null 而不可达(1.21 线可达),该条实际影响需在描述里限定 |
fixed #6061 |
🧪 建议的手动验证
- 旁观者逐个打开控制阀、激光、交易站、智能放置器、自动附魔台、超时空计算机:点击每个配置控件 → 服务端无任何变化,重开界面显示与操作前一致。
- 旁观者尝试右键结构扫描仪:应被原版拦住并提示
container.spectatorCantOpen(验证 26.1 的createMenu守卫仍在生效,未被本 PR 影响)。 - 旁观者打开三台砂轮并点击附魔条目:当前仍会改变(待
⚠️ 修复后应无变化)。 - 旁观者看着另一名生存玩家操作批量合成器 / 剪裁器 / 消音器:旁观者界面上的配方高亮仍随对方变化(验证
!isClientSide豁免没写反,这是本 PR 最易引入回归的点)。 - 消音器验证
SilencerSyncPacket的isLoaded取反:A 玩家加静音项,同维度 B 玩家界面应同步刷新(当前 26.1 预期不同步,属 pre-existing)。 - 生存 / 创造玩家做同一轮操作:全部照常生效(守卫不能误伤)。
建议
除 fixed #6061 改为 refs #6061 并写明划线标准。姊妹 PR #6086(1.21 线)存在同一缺口,修时两线一起改;另建议顺手修掉 SilencerSyncPacket 26.1 侧的 isLoaded 取反(一个字符,pre-existing 但恰在本 PR 改动行上)。
fix(network): 禁止旁观者修改方块配置
在服务端拒绝旁观者修改机器朝向、过滤、加工和红石配置
保护控制阀、激光、发电机、交易站和结构扫描器等配置写入入口
保留客户端状态同步和旁观者读取蓝图文件列表
fixed [Bug] 旁观模式下可以修改部分方块的配置 #6061