Skip to content

Fix undo after falling blocks replace double plants 修复下落方块覆盖双格植物后的撤销 - #5128

Merged
PigeonNian merged 3 commits into
Anvil-Dev:dev/1.21/1.6from
WhereisFff:codex/fix-5124-double-plant-undo
Oct 2, 2026
Merged

PigeonNian merged 3 commits into
Anvil-Dev:dev/1.21/1.6from
WhereisFff:codex/fix-5124-double-plant-undo

Conversation

@WhereisFff

Copy link
Copy Markdown
Contributor

fix(building): 补齐覆盖双格植物的下落方块撤销 (#5124)

- 记录落点双格植物的完整配对快照,恢复上下两部分并按实际资源结算。
- 撤销前检查历史植物配对完整性,避免混合时点的快照消耗材料后恢复失败。
@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #5128

操作: opened(草稿 PR,draft: true,mergeable_state: unstable,未合并)
范围: 1 个文件 / +26 −2(28 行变更)——仅 building/BuildingRegionSnapshot.java
取证: 与 patch-diff.githubusercontent.com 的 5128.diff 逐字节一致(59 行 / 3776 B,两次下载 md5 相同),changed_files=1 / additions=26 / deletions=2 对齐 ⇒ 未截断;git merge-base rvbase1216 rvhead5128 == rvbase1216(base tip 9cb2569999)⇒ 分支未落后、无夹带改动,范围干净。
判修订: base 已含 b3096d8509 Fix undo after pasted falling blocks land (#5126)(captureBlocks/addedPositions/placeFallingBlock 均已在 base)。本 PR 是在「落地格增量捕获」之上新增双格植物配对补记 + 撤销前配对校验,属该族的下一增量形态。


✅ 已核实通过(写在这里,便于判断哪些地方是安全的)

  1. 配对补记是必要且方向正确的。 BlueprintMultiblocks.forEachPart(LOWER) 会额外展开 pos.above()(HALF=UPPER)——见 BlueprintMultiblocks.java:60-62;而 checkParts 对任一未 contains() 的部件直接抛 Multipart block crosses undo bounds(BuildingRegionSnapshot.java:189-202),contains = bounds ∪ addedPositions。所以「落点替换了植物一半、另一半在区域外」必然让 resources() 抛异常 → undo_conflict → 整次撤销失败,正是本 PR 要修的现象。补记后两半都在 contains 内,resources()/restore() 都能走通。
  2. 配对判定条件写对了:otherState.is(state.getBlock()) && HALF != HALF(同种 + 对半),且补记位置复用既有 !contains && isInWorldBounds + immutable/distinct 过滤(:81-94);其余分支用 continue 保守跳过,不会误吞相邻方块。
  3. 材料不重复计费(PR 描述的「按实际资源结算」成立):BuildingUndoResources.block 的 if (!BlueprintMultiblocks.shouldRecord(state)) return; 位于 this.item(material.stack()...) 之前(BuildingUndoResources.java:126-129),而 shouldRecord 对双格只认 LOWER ⇒ 一对植物只收 1 件(materialCount(plantLOWER)=1,BuildingRodService.java:670-678)。上半格还在该 return 之前,因此也不会撞上后面的 Unknown block material 抛错。
  4. 回滚对称:placeFallingBlock 的 finally 在 !placed 时 removeCapturedBlocks(added),added 含补记的另一半,两半一起回滚。
  5. 覆盖范围没有遗漏:能被下落实体覆盖的位置必须 canBeReplaced && canSurvive(FloatingBlockEntity.java:82-84、FallingGiantAnvilEntity.java:164),而门/床/活塞头都有碰撞箱、下落实体无法占据其格 ⇒ 其余双格/多元方块(DoorBlock/BedBlock/PistonHeadBlock)不需要同样补记。只处理 DoublePlantBlock(tall grass / 大型花这类无碰撞、可替换的双格)是正确的收口。
  6. 本 PR 未改 mixin/网络/持久化,客户端零开销与 Fix undo after pasted falling blocks land 修复粘贴下落方块落地后的撤销 #5126 的结论一致。

⚠️ 新增的配对门是「全有或全无」,且捕获侧会主动造出它自己拒绝的状态

canRestore 新门(:133-140):只要快照里存在一株不完整的双格植物(被记录的那一半,在快照 original 里找不到同种、对半的另一半),就 return false → undo() 首次检查即 message(player, "blocked") return(BuildingRodUndo.java:333-336)。而 captureBlocks 的新分支(:82-92)只要落点或其上下相邻格是 DoublePlantBlock 就记下这一半,不校验配对是否完整(otherState 不匹配时只记这一半)。

也就是说:捕获侧能造出不完整配对,校验侧又把它作为硬拒条件 —— 两侧互相矛盾。举例(本届门唯一会触发的形态):落点上原本就是一「半个」双格植物(另一半此前已被别的落地/放置换掉),捕获只记到这一半,随后整段历史撤销都会被拒绝;而修复前这次撤销是可以成功的(contains(partner) 成立时不抛,只把被替换的那一半按记录状态恢复)。提示 building_rod.blocked(“放置区域有障碍或受到保护”,zh_cn.json:2738)与真实原因(快照植物配对不完整)不符,玩家除手动把落点改回快照状态外无补救路径。

建议(二选一,改动都很小):

❓ 描述里「避免消耗材料后恢复失败」这条收益我未能复现,请确认路径

undo() 的顺序是 canRestore → resources() → recovery.reserve → consume → restore()(BuildingRodUndo.java:333-368),而 resources() 里的 checkParts 在扣料前就会抛(partner 不在 contains 时 → undo_conflict,材料尚未消耗)。所以我找不到「消耗材料后恢复失败」的实路径;请提供你实测到的场景(我推测是 partner 落在 bounds 内、contains() 成立但快照态不匹配的情形)。若确为后者,则直接删除该门、让既有 checkParts 负责更简单——而且 undo_conflict(“无法安全结算变更的资源”)比 blocked 更贴近真实原因。

💡 其余


🧪 测试建议

被测目标 推荐场景 优先级
BuildingRegionSnapshot.captureBlocks 落点=植物下半/上半(另一半在区域内/区域外/已被换成非植物方块)三种组合,断言 addedPositions 与 restore() 后的世界状态 🔴
canRestore 配对门 快照仅有半边、快照配对完整、世界侧玩家已改动半边 —— 断言撤销是否应被拒 🔴
材料账目 一对双格植物 + 一次落地覆盖 → required.items 恰 1 件;recovery/required 经 cancel() 后净额正确 🟡
placeFallingBlock 回滚 placement 返回 false / 抛异常时,两半均从 addedPositions/blocks/ticks/events 移除 🟡

结论: COMMENT(草稿 PR,非阻塞式驳回) — 配对捕获与材料结算已逐条核实、方向正确;合并前建议收敛新增配对门的行为(捕获侧不产生半边,或校验侧降级为跳过该格 + 专用文案),并修正描述里的重复 claim。


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/falling-block-landing-capture-review.md in skill 'building-rod-blueprint-review' (1 replacement). · Patched SKILL.md in skill 'building-rod-blueprint-review' (1 replacement).

- 在权限检查后、资源结算前独立校验双格植物快照,保留生存与创造模式的原子拒绝。
- 使用现有资源冲突提示说明不完整或混合时点的植物快照。
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /tmp && rm -rf pr5128new && mkdir -p pr5128new/src/main/java/dev/dubhe/anvilcraft/building && cp /tmp/BRS_base.java pr5128new/src/main/java/dev/dubhe/anvilcraft/building/BuildingRegionSnapshot.java...

Reason: recursive delete

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #5128

操作: synchronize(opened→synchronize,PR 状态 = Open,未合并)
范围: 2 个文件(2 Java,0 新增,0 删除)/ 77 行 diff(+36 / −2)
目标分支: dev/1.21/1.6(本地核对 tip = 9cb2569999,其中已包含 #5126 squash commit b3096d8509「Fix undo after pasted falling blocks land」→ 本 PR 是在已合并 #5126 之上对双格植物的后续补齐,与 issue #5124 由 #5126/#5128 双 PR 关闭的现状一致)

改动实质

  1. BuildingRegionSnapshot.captureBlocks()(新 L82–93):把落点位置在「当前是双格植物半块」时,把另一半(pos.above()/below())一并补记进快照。
  2. BuildingRegionSnapshot.hasCompletePlants()(新 L148–163)+ BuildingRodUndo.undo() 前置检查(L337–340):当快照里存在「现场已变化、但快照中缺少配对半块」的植物半块时,直接 undo_conflict 返回,不进入资源结算。

已交叉验证的事实(可复现)

  • checkParts() 对「半块落在撤销范围外」的拒绝机制成立:core(lower)=pos、core(upper)=pos.below(),forEachPart(lower) 会额外产出 pos.above() → 只要配对半块不在 bounds ∪ addedPositions 内就抛 Multipart block crosses undo bounds;这正是 issue [Bug] 建筑杖粘贴的下落方块实体落地后无法撤销 #5124「落地后无法撤销、铁砧方块不还原」的成因(上半块落点、或下半块落点在区域外)。本 PR 通过补记配对半块把 contains() 补齐 → 撤销得以进行 ✅ 修复方向正确。
  • BlueprintMultiblocks.forEachPart()/core() 对 DoublePlantBlock 的上下半处理(shouldRecord(upper)=false)与新增逻辑一致,未相互矛盾。
  • 不会重复扣料:BuildingUndoResources.block() 在 if (!BlueprintMultiblocks.shouldRecord(state)) return; 之前不会产生材料需求 → 补记的上半块不计费,植物仍只按下半块收/退 1 个物品 ✅
  • 恢复顺序无风险:BuildingCommit.set() 是 LevelChunkSection.setBlockState(..., false) 裸写(无 onPlace/掉落),邻接刷新只发生在 activate() 全部方块放置之后 → 先还原上半块再还原下半块也不会互相打掉 ✅
  • 植物类型覆盖面无缺口:用仓库自身生成物取证——src/generated/.../block_placement_rules/tall_grass.json、large_fern.json、sunflower/lilac/rose_bush/peony/... 均带 half=lower/upper 规则,说明 1.21.1 的 tall_grass / large_fern 确实是 DoublePlantBlock(TallGrassBlock 在 1.21.1 是 1 格矮草,extends BushBlock)→ instanceof DoublePlantBlock 覆盖了最常见的落地植物 ✅
  • Checkstyle/风格:新增行最长 137 字符 < style.xml 的 LineLength max=140 ✅;RequireThis(this. 全限定)✅;新增 import 顺序/位置正确 ✅;无 javadoc 强制模块。

🔴 关键

无。

⚠️ 警告

  1. hasCompletePlants() 只看快照、不看现场 → 保守误拒(undo_conflict) — BuildingRegionSnapshot.java L148–163
    判据是「快照里找不到配对半块 或 配对半块状态不匹配(非同种方块/同侧)」。但配对半块不在快照里 ≠ 现场植株已损坏:只要现场 this.level.getBlockState(other) 仍是期望的另一半,restore() 就不会碰它,恢复结果是完整植株,撤销本应成功。
    典型触发:区域包围盒在 minY/maxY 边缘把一株植物切开(构造器只记到半块、另一半在框外且现场完好),而该半块又被本次建造改动过。

    • 生存模式:同一场景旧代码会被 checkParts() 的 core()/forEachPart() 抛出同样拒绝,行为无回归;
    • 创造模式:旧代码不进入 resources()(checkParts 不执行),撤销一律可进行;此检查会让创造模式出现新的拒绝路径 —— 这是本 PR 唯一的行为收紧,请确认是有意为之。
      建议(低风险改进):otherState == null 时不要直接判负,补一句现场校验,例如现场 other 位置状态等于 block.state() 的期望另一半(setValue(HALF, 反向))即视为完整;或至少给 javadoc 说明「快照不完整 ⇒ 拒绝」是有意的保守策略。
  2. 同类失败模式只修了双格植物,门/床/活塞头仍会命中 [Bug] 建筑杖粘贴的下落方块实体落地后无法撤销 #5124 的症状 — 同文件 L82–93 与 BlueprintMultiblocks.isDoubleBlock()
    isDoubleBlock() = DoorBlock || DoublePlantBlock,且 core()/forEachPart() 同样处理 BedBlock、延伸的 PistonBaseBlock、AbstractMultiPartBlock。铁砧落在区域外的门下半/上半(或床 foot/head、伸出的活塞头)时:captureBlocks 只记落点单块 → checkParts() 抛 Multipart block crosses undo bounds → 依然是「落地方块撤不掉、报 undo_conflict」。
    建议:把「补记」与「完整性检查」都改为复用 BlueprintMultiblocks.core() + forEachPart() 这一既有部件枚举(而非新增 instanceof DoublePlantBlock 特例)。这样植物/门/床/活塞/自研多部件共用一份真相,避免以后两处清单不一致。

💡 建议

  1. 复用的文案语义不符 + zh_cn 缺键 — BuildingRodUndo.java L338
    新拒绝路径复用 undo_conflict,其 en_us 文案是 "Cannot safely account for the changed resources; undo cancelled without changing the area"(描述的是资源结算冲突),而这里是「植株配对不完整」,玩家看到会误解。可考虑独立键(如 undo_incomplete_plants)。
    另外 undo_conflict/undo_missing_materials/undo_missing_containers/undo_partial 只存在于生成物 en_us/en_ud(BuildingRodLang),手工维护的 src/main/resources/assets/anvilcraft/lang/zh_cn.json 里没有(只有 nothing_to_undo/undone)→ 中文客户端会回退英文。属既有缺口,但本 PR 新增了一处可见触发点,建议顺手补 zh_cn(以及依据仓库既有流程补 weblate)。

  2. hasCompletePlants() 与 resources() 重复构造 original 映射,且名字未体现「读现场状态」 — 同文件 L135–136 与 L148–151
    两处都是 this.blocks → LinkedHashMap<BlockPos, BlockState>,建议抽 originalStates();方法名更贴近语义(如 plantsPairComplete()),并补一句与邻近方法同风格的中文 javadoc(精髓在 getBlockState(pos) == block.state() 这个「仅校验已变化半块」的提前 continue,容易被误读)。

  3. 补记范围比落点略宽(可能把区域外位置纳入撤销写域) — L82–93
    !this.contains(pos) 过滤发生在扩展之后,因此当落点本身已在区域内、而配对半块在区域外时,这个区域外位置也会进入 addedPositions/blocks:撤销时会一并 canRestore 保护并在变化时还原它。用于修补「包围盒边缘切开的植株」时是需要的,但请在 javadoc 里点明这一有意的范围外扩(或确认是否想收紧)。

🟢 看起来不错

  • 修复落点准确:补记配对半块后 contains() 补齐 → checkParts() 不再误抛,撤销能真正把落地方块归位并退料,符合 issue [Bug] 建筑杖粘贴的下落方块实体落地后无法撤销 #5124 的预期行为。
  • 前置检查放在 resources()/reserve()/consume() 之前,拒绝时不会先扣料 —— 与 PR 描述「避免混合时点的快照消耗材料后恢复失败」一致。
  • 补记/回滚对称(placeFallingBlock 失败时 removeCapturedBlocks(added) 会连配对半块一起移除,不会污染 addedPositions)。
  • 材料结算无副作用(上半块不计费,已实证);restore() 顺序安全(已实证)。
  • 无调试残留/TODO/硬编码密钥,无无关文件扩散(scope 纯净)。

📋 声称验证表

PR 声称 状态 依据
记录落点双格植物的完整配对快照 ✅ L82–93 补记 pos.above()/below(),isInWorldBounds + 同类反侧校验
恢复上下两部分并按实际资源结算 ✅ restore() 逐格还原;上半块经 shouldRecord() 不计费 → 只按下半块结算
撤销前检查配对完整性,避免先扣料再失败 ✅ hasCompletePlants() 在 resources() 前;undo_conflict 提前返回
fixed #5124 ✅(针对双格植物) 落点在区域外的半块场景已可撤销;门/床/活塞头同类场景仍拒绝(见 ⚠️2,属后续范围)

结论: APPROVE(建议按 ⚠️1/⚠️2 后续小改) — 修复方向、落点与安全检查位置均正确,未发现正确性缺陷或数据/资源风险;两条警告分别是「保守误拒的心智模型与创造模式行为收紧」与「同类多部件块未一并覆盖」,均非本 PR 的阻塞项(标题已限定 double plants)。

PR 标题「Fix undo after falling blocks replace double plants 修复下落方块覆盖双格植物后的撤销」与改动一致,无需修改(未执行 gh pr edit)。

🧪 测试建议

被测目标 推荐场景 优先级
captureBlocks() 扩展 铁砧落在区域外双格植物下半 / 上半 → 撤销后上下两半均还原、仅消耗 1 个物品 🔴
captureBlocks() + 邻居更新 落点替换下半块后上半块被 updateShape 打成空气 → 撤销恢复完整植株、退回的物料正确 🔴
hasCompletePlants() 混合时点(配对半块现场已是铁砧)→ 返回 undo_conflict 且背包物品数不变 🔴
hasCompletePlants() 误拒边界 植株被包围盒 minY/maxY 切开(配对半块在框外且现场完好)→ 期望成功撤销(当前会拒绝,见 ⚠️1) 🟡
创造模式撤销 创造下走新拒绝路径 / 走正常恢复,均不产生物料增减 🟡
多部件同类回归 门 / 床 / 延伸活塞头落在区域外 → 现行为(undo_conflict)不被本改动影响(并作为 ⚠️2 的回归基线) 🟡
世界边界 植物位于 y 边界(partner 超出世界边界)→ 不抛异常、added 无重复项 🟢

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/falling-block-landing-capture-review.md in skill 'building-rod-blueprint-review' (1 replacement). · Patched SKILL.md in skill 'building-rod-blueprint-review' (1 replacement). · Patched references/shell-only-diff-navigation.md in skill 'github-webhook-pr-review' (1 replacement).

- 仅在配对方块没有历史快照时检查现场状态,允许创造模式安全恢复区域边界的完整植物。
- 保留已有快照的配对冲突校验,不覆盖区域外方块,也不放宽生存模式的资源检查。
@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #5128

操作: synchronize(open,mergeable=true,3 commits,未合并)
范围: 2 个文件(2 Java,+38 / −2 行)
CI: build ✅ success / checkstyle ✅ success
diff 完整性: grep -c '^+'=40(含 2 行 +++ 头 ⇒ 38 新增)、^-=4(2 删除),与 API additions=38 / deletions=2 / changed_files=2 一致,无截断

🔎 根因确认(#5124 症状与代码路径吻合)

  1. BuildingRegionSnapshot.resources() → checkParts(pos, block.state(), original::get) → BlueprintMultiblocks.forEachPart()/core() 要求所有配对部件都在 contains() 内,否则抛 IllegalArgumentException("Multipart block crosses undo bounds");
  2. BuildingRodUndo.undo() 的 catch (RuntimeException) 把它统一转成 undo_conflict 并 整体 return(不扣料、不回滚);
  3. 于是玩家看到的就是 issue 里那句「撤销时不会撤销该下落方块实体变成的方块」——整场撤销被中止。

修复方向正确:captureBlocks() 把配对方块一并纳入 blocks / addedPositions,contains(part) 成立 ⇒ checkParts 不再抛;restore() 遍历 this.blocks 因此会同时还原上下两半。
时序已核对正确:placeFallingBlock() 中 captureBlocks(positions)(L142)先于 placement.getAsBoolean()(L146)执行,读到的是植物落地前的原始状态,配对判定成立。

🔴 关键问题

无。

⚠️ 警告(建议跟进,不阻塞)

1. 同一类「整场撤销被永久锁死」缺陷在门 / 床 / 活塞上仍然存在(严重度等同 #5124),而本修复只覆盖 DoublePlantBlock

checkParts() 走的是 BlueprintMultiblocks,它把 DoorBlock 明确算作 double block(isDoubleBlock() 第 51 行),并对 BedBlock、PistonBaseBlock(EXTENDED) 也做部件展开。因此落地方块覆盖这些方块的一半/一个部件时,路径完全一样:

被落点覆盖的方块 contains(part) 检查的对象 结果
门下半 HALF=LOWER pos.above()(door UPPER) 区域外且未入快照 ⇒ throw
床尾 PART=FOOT pos.relative(FACING)(HEAD) 同上
伸出活塞基座 pos.relative(FACING)(PISTON_HEAD) 同上

(captureBlocks 只对 state.getBlock() instanceof DoublePlantBlock 做配对展开,门/床/活塞都不会被补记 ⇒ resources() 仍抛异常 ⇒ 仍是 undo_conflict,且由于配对位置永远进不了 contains(),玩家无法通过任何操作补救,只能放弃这次撤销。)

建议:把「配对/部件位置推导」下沉到 BlueprintMultiblocks(新增如 partnerOf(pos, state),覆盖 door/bed/piston-head),captureBlocks 与 hasCompletePlants 共用一份实现,即可顺带覆盖这三类。
⚠️ 注意 forEachPart() 不能直接拿来用:对 HALF=UPPER 的方块 shouldRecord() 为 false,它在 accept(pos) 后直接 return,不会给出下方的配对方块;只有 HALF=LOWER 才会展开 → 所以需要的是 core() 反向推导,而不是 forEachPart()。若维护者认为应限定范围,至少建议立刻开 issue 跟踪门/床场景。

💡 建议

2. hasCompletePlants() 的净收益需要写清(避免被当成纯消息优化)
生存模式下 resources() 抛出的异常已被同一个 catch 映射成同一条 undo_conflict 消息,且扣料发生在 catch 之后的 recovery.consume(),所以该守卫在生存模式只是「提前一次、少算一遍资源」;它真正新增的作用域是创造模式(if (!undo.creative) 跳过 resources(),因此旧路径在创造模式不会做任何部件校验)。commit 3 的描述说明这是刻意的,建议在 javadoc 里点明「含创造模式」,否则读代码的人会以为与 checkParts 重复而无意义。

3. 「快照优先于现场」的判定会锁死一类可撤销的快照
hasCompletePlants() 先取 original.get(other),为空才回退现场。若快照本身记录的就是一株残缺植物(例如区域内只有下半块、上方位置在快照中是 AIR;或区域顶边切过植物且上方半块此前已被吃掉),只要该半块后来发生变化,就会 return false 把整场撤销永久禁止——而旧行为在这种「快照忠实复刻残缺世界」的情形下是能正常撤销的。建议区分「快照残缺」与「现场残缺」,或至少让提示可操作(现有文案 "Cannot safely account for the changed resources; undo cancelled without changing the area" 已经不错,但没告诉玩家该拆掉什么)。

4. 配对逻辑三处分散
captureBlocks(H86)与 hasCompletePlants(H156)各写一份 HALF→配对方向;resources() 里又构建了一份同构的 original map。建议合并为 BlueprintMultiblocks 的一个小 helper,避免三处漂移。

5. zh_cn 缺键(非本 PR 引入)
zh_cn.json 缺 message.anvilcraft.building_rod.undo_conflict / undo_missing_materials / undo_missing_containers / undo_partial(base 已在使用),中文客户端会由 en_us 兜底显示英文;可在 Weblate 补译。

🟢 看起来不错

  • 跳过条件 !(block.state().getBlock() instanceof DoublePlantBlock) || getBlockState(pos) == block.state() 的 De Morgan 展开正确;otherState == null || !otherState.is(...) || otherState.getValue(HALF) == half 靠 || 短路保证了先 is() 再 getValue(),不存在跨方块读属性的风险。
  • above()/below() 前都有 isInWorldBounds 守卫,世界高度边缘不会越界取态;BlockState 用 ==/is() 与既有 canRestore()/resources() 的惯例一致(状态实例是规范化的)。
  • expanded 用 new ArrayList<>(positions) 拷贝,兼容调用方传入的 List.of(pos) 不可变列表;distinct() + immutable() 保留原顺序语义。
  • 失败回滚路径完整:placeFallingBlock 的 finally 里 removeCapturedBlocks(added) 会同时清掉 blocks/ticks/events/addedPositions,配对位置不会残留。
  • 影响面扩展后 addedPositions 同时惠及 replaced()、blockDrops()(配对半块若因邻居更新掉落,其掉落物现在会进 derived,撤销时不会再出现「植物恢复了、地上还躺着一个掉落物」的重复)、以及 restore() 里对新增位点的事件/tick 清理,语义自洽。
  • restore() 只有 undo() 一个入口,守卫位置覆盖完整;新 import 顺序与行宽(137/135 < style.xml 的 140)合规,CI 双绿。

📋 声称验证表

声称 状态 证据
记录落点双格植物的完整配对快照 ✅ captureBlocks 展开 + otherState.is(state.getBlock()) 校验,且在 placement 之前执行
恢复上下两部分并按实际资源结算 ✅ restore() 遍历含配对位的 this.blocks;resources() 对未变化的配对位 continue,不重复扣料
撤销前检查历史植物配对完整性 ✅ hasCompletePlants() + undo() L337 独立守卫,位于权限检查后、资源结算前
避免混合时点快照消耗材料后恢复失败 ⚠️ 部分 生存模式本就在 consume() 之前中止,净新增作用域是创造模式与提前拒绝(见建议 2)
fixed #5124 ✅ 根因成立 checkParts→forEachPart 部件包含性抛出 ⇒ 撤销整体中止,与 issue 症状逐字吻合
覆盖门/床/活塞等同类多部件方块 ❌ 未覆盖 BlueprintMultiblocks.isDoubleBlock 含 DoorBlock;captureBlocks 仅处理 DoublePlantBlock(见警告 1)

结论: APPROVE(改动本身正确,建议跟进 1 项) — 修复方向、时序、序列化/结算语义均无误,#5124 根因已消除;唯一遗留是同类缺陷在门/床/活塞上仍在,泛化成本很低(一处 helper),建议本 PR 内一并覆盖或立刻开 issue 跟踪。

PR 标题已为「英文 + 中文」且准确描述根因(双格植物),无需修改。

🧪 人工验证建议(项目 src 下无 test 目录,无法加单测)

场景 期望
生存模式:蓝图含铁砧落在双格植物下半块上 → 落地后撤销 上下两半都还原,材料按实际结算,无重复掉落物
同上但落在上半块 同上(配对方向为 below())
创造模式:同场景 不再被 undo_conflict 误拒(commit 3 的现场状态回退路径)
双格植物跨区域顶边(上方半块在区域外且完好) 撤销正常
对照:落点覆盖门的一半 / 床尾 / 伸出活塞基座 目前仍会 undo_conflict 且无法补救 —— 用于确认警告 1

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'building-rod-blueprint-review' (1 replacement). · Patched references/pr-class-routing.md in skill 'github-webhook-pr-review' (1 replacement).

@WhereisFff
WhereisFff marked this pull request as ready for review October 2, 2026 08:02
Copilot AI balanced review requested due to automatic review settings October 2, 2026 08:02

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 reached their quota limit.

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /tmp && python3 -c "print(1)" 2>&1 | head -2; echo "---"; grep -o 'issue-body[^>]*' /tmp/issue5124.html | head -2; echo "=== try extracting text ==="; sed -n 's/.*<meta name="description" content="...

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@PigeonNian
PigeonNian merged commit 9e750b3 into Anvil-Dev:dev/1.21/1.6 Oct 2, 2026
2 checks passed
@Gugle2308

Copy link
Copy Markdown
Collaborator

🔔 先说明:该 PR 已经合并(本评论为事后复查)

action=ready_for_review 送达时,PR #5128 已被 squash 合并:merge commit 9e750b31cc9d9dffb43aabea12eb52113d1826ac("Fix undo after falling blocks replace double plants 修复下落方块覆盖双格植物后的撤销 (#5128)",3 commits squashed),已落入 dev/1.21/1.6 且就是该分支当前 tip(由 PigeonNian 合并,时间 2026-10-02 08:04 UTC)。

验证方式(匿名公开端点,未使用任何 token):

因此下面没有「合并前需修复」的动作项,结论按 post-merge 观察 + 可选后续项给出;也无需改标题(标题准确,且已合并)。


代码审查摘要 — PR #5128

范围: 2 个文件(2 Java / 0 新增 / 0 删除)/ 79 行 diff(+38 −2)/ 4 hunks

📋 声称验证表

声称 状态 依据
记录落点双格植物的完整配对快照、恢复上下两部分 ✅ BuildingRegionSnapshot.captureBlocks() 新增 expanded 配对扩展(+14 行);restore() 本就遍历全部 blocks 落块
按实际资源结算(不重复计材料) ✅ BuildingUndoResources.block() 在 shouldRecord(state)==false(UPPER 半)时提前 return,一对植物只计 1 个物品
撤销前检查历史植物配对完整性,避免「消耗材料后恢复失败」 ✅ BuildingRodUndo.undo() 在 resources() 之前插入 hasCompletePlants() 门禁(+4 行)
fixed #5124 ✅ issue 已 COMPLETED;#5126 覆盖「落地在区域外」主干,#5128 补齐双格植物

🟢 实现正确性(逐点核对)

  • 捕获时机正确:captureBlocks() 在 placement.getAsBoolean() 之前执行,getBlockState(pos) 拿到的是落地前的植物原状态,配对扩展读的也是同一时点 ⇒ 记录的是真实「现场原状」。
  • 去重与边界:!this.contains(pos) 过滤区域内外重复(配对半已在快照集合内时不会重复记录/重复计数),immutable().distinct() 归一化,isInWorldBounds 双重守卫 ✓。
  • 回滚对称:placeFallingBlock() 的 finally 分支 removeCapturedBlocks(added) 对 blocks/ticks/events/addedPositions 四处都清,新增的配对半块不会残留成幽灵快照 ✓。
  • 门禁位置与副作用:位于 canRestore/canRemove 之后、BuildingUndoResources 结算之前;拒绝路径零副作用(不消耗材料、不动世界、不写 HISTORY)✓,canRemove/owned() 本身无副作用,重复调用安全。
  • 短路安全:otherState == null || !otherState.is(...) || otherState.getValue(HALF) == half 保证 getValue 仅在「同一方块」时求值,不会抛属性异常/NPE ✓。
  • 不误伤既有植物:只校验「当前状态 ≠ 记录状态」的植物(this.level.getBlockState(pos) == block.state() 跳过),建造未碰过的植物不参与判定;与 e10793f 的「配对方块无快照时才看现场」语义一致 ✓。
  • 无需新 lang key:复用 message.anvilcraft.building_rod.undo_conflict(BuildingRodLang + en_us/en_ud 均已存在)✓。

💡 建议(可选,非阻塞)

  1. 配对规则已有权威实现,建议收敛 — BlueprintMultiblocks.isDoubleBlock/shouldRecord/forEachPart/core 已是仓库内「双格方块配对」的唯一权威(checkParts() 也用它)。本 PR 在捕获侧与校验侧各自内联了一份 DoublePlantBlock + HALF 手写逻辑,规则散成三处;后续可改用 BlueprintMultiblocks.forEachPart(pos, state, ...),将来若要把 DoorBlock/BedBlock 纳入配对判定也只改一处。
  2. getValue(DoublePlantBlock.HALF) 缺少 hasProperty 守卫 — 判定条件是 instanceof DoublePlantBlock,但状态来自 getBlockState 的任意模组方块。第三方模组若有 DoublePlantBlock 子类未把 HALF 放进 state definition(可编译、自行处理放置),getValue 会抛 IllegalArgumentException,而抛点位于下落方块落地路径(placeFallingBlock → captureBlocks)里,属硬崩。仓库既有代码(BlueprintMultiliblocks.isDoubleBlock)同样假设,故非本 PR 引入;新增的两处读点建议加 hasProperty 守卫或复用 helper。
  3. 配对判据 otherState.is(block.state().getBlock()) 假设上下两半是同一方块 — 原版 BIG_DRIPLEAF(上半)与 BIG_DRIPLEAF_STEM(下半)是两个不同方块,这类双格既不会进入 expanded,hasCompletePlants() 也会直接判「不完整」→ 走拒绝分支(安全,但不完整)。若将来要覆盖,需要按原版 placeAt 语义取配对方块,而不是 is(同一方块)。
  4. 硬阻断的可恢复性/提示语 — 一旦历史里存在「已变化、且配对方块既无快照、现场也不是配对半块」的植物,hasCompletePlants() 每次都返回 false,HISTORY 保留 ⇒ 该次 Ctrl+Z 永久失败,只能靠在配对方块位置补种(使现场与快照配对)或用新建造覆盖历史来解锁。而 undo_conflict 的文案是「Cannot safely account for the changed resources…」,没有告诉玩家「去 X 处补种」。建议消息带上原因/坐标,或加一条专用 key(这条也可能是作者有意为之的「原子拒绝」,仅提示可读性)。
  5. 资源侧新可达路径值得实测对齐 — 本 PR 让「落点覆盖双格植物」在生存模式下首次能走到 resources()(此前 resources() 内 checkParts() 会因 pos.above() 不在 contains 内而抛 Multipart block crosses undo bounds → 直接 undo_conflict)。现在 required 会计入 1 个植物物品(UPPER 半不计数 ✓),而落地下落方块是 Level.setBlock 静默替换、被覆盖植物是否掉落物品我无法在静态审查中确认:若现场无掉落物可抵扣 recovered,玩家会看到 undo_missing_materials(需背包里有同种植物)而非撤销成功——与 issue 里「返还消耗的物品给玩家」的预期需实测确认。

📋 合并状态说明

PR 已合并,本评论仅作 post-merge 记录;第 13 条可作为后续小重构,第 45 条建议用下面的回归用例验证一次即可。

🧪 测试建议

被测目标 推荐场景 优先级
captureBlocks() 配对扩展 铁砧落在双格植物下半(上半在蓝图范围外)→ 撤销:上下两半都恢复、铁砧被移除并返还物品 🔴
captureBlocks() 对称性 落在上半、落点在世界/区域边界、多个落点同一 tick 🟡
hasCompletePlants() 落地后手动打掉另一半 → 应 undo_conflict,且世界不被改动、材料不被消耗 🔴
hasCompletePlants() 区域边界完整植物(配对半无快照、现场完整)→ 应允许撤销(e10793f 的目标行为,防止把可用撤销误拒) 🔴
placeFallingBlock() finally 回滚 放置失败(setBlock 返回 false)→ 新增的配对半块从 blocks/ticks/events/addedPositions 干净移除 🟡
资源结算 生存模式 above 场景:玩家持有 / 不持有植物物品各一次,确认 undo_missing_materials 与成功路径的预期 🟡

结论: 已合并,无需动作 — 核心修复(捕获配对 + 门禁前置)逻辑自洽、副作用为零、资源不重复计数;上述 5 条均为 post-merge 观察与可选后续项,其中第 4、5 条建议实测确认后再决定是否跟进。


由 Hermes Agent 审查(webhook 自动投递,未执行任何 gh 评论命令)

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/merged-state-and-ci-verification.md in skill 'github-webhook-pr-review' (1 replacement). · Patched SKILL.md in skill 'building-rod-blueprint-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

PR #5128 审查

⚠️ 该 PR 已合并:由 PigeonNian 于 2026-10-02 08:04:33 UTC 合并(squash commit 9e750b31cc,现已是 dev/1.21/1.6 分支 tip)。已核对:合并后 base tip 内容与 PR head(e10793f6d8) diff 为空,即我审查的就是已落库内容;merge commit 的 check-runs 结论为 success。因此以下为 post-merge 观察,不含"请修复后合并"类动作项。

范围: 2 个文件(+38/−2),BuildingRegionSnapshot.java / BuildingRodUndo.java,单主题小 PR。
对应 issue: #5124《建筑杖粘贴的下落方块实体落地后无法撤销》(WhereisFff 提,同日 06:44 关闭)。


🟢 修复机理核实(正确)

  1. 根因链条成立:resources() → checkParts() → BlueprintMultiblocks.core()/forEachPart() 要求多方块所有部件都满足 contains()(bounds ∪ addedPositions)。单格下落方块走 FallingBlockEntityMixin(List.of(pos),只记落点),落点压碎的双格植物若配对方块在 undo bounds 之外 → forEachPart 取 pos.above() 判 contains 为 false → 抛 Multipart block crosses undo bounds → catch → undo_conflict → 整个撤销被原子拒绝(生存);创造模式则完全绕过 resources()(if (!undo.creative))而静默只还原半株。两者都表现为"落地方块留在原地撤不掉",与 issue 症状一致。修复在捕获阶段补记配对(进入 addedPositions)→ contains 成立;守卫放在 if (!undo.creative) 之外是必要的,否则创造模式这条路径无人把关。
  2. "按实际资源结算"确实成立:BuildingUndoResources.block() 第 126 行 if (!BlueprintMultiblocks.shouldRecord(state)) return; —— 双格植物的 UPPER 半不计材料,只按下半收 1 份,多记一格不会多扣物品。这是我最担心会翻车的一点,实测是对的。
  3. 状态比较沿用仓库既有约定(getBlockState(...) == block.state() 依赖 BlockState 规范实例),otherState.is(block.state().getBlock()) 之后再取 HALF 属性是安全的(is(Block) 已保证同种 DoublePlantBlock)。译键 message.anvilcraft.building_rod.undo_conflict 早已存在于 BuildingRodLang + 生成 lang,无需补 lang。
  4. 回滚路径对新增格同样生效:placeFallingBlock 的 finally → removeCapturedBlocks(added) + changedPositions.removeAll(added),配对格在放置失败时会被一起回滚,不会残留 addedPositions。

⚠️ 遗留风险(post-merge 跟进项)

A. 覆盖面只到双格植物,同类多方块没修。 BlueprintMultiblocks.isDoubleBlock 本身就含 DoorBlock || DoublePlantBlock,另有 BedBlock(FACING 配对)、PistonBaseBlock(extended → 活塞头)、LargeCakeBlock(checkParts 里单独抛 Incomplete cake crosses undo boundary)。这些类型"被下落方块压碎半件、配对在 undo bounds 之外"的失败模式与修复前一模一样:forEachPart/core 判 contains false → undo_conflict → 落地方块撤不掉。issue 的复现条件是"任意含下落铁砧实体的蓝图",所以这一族仍可能复现。建议后续把 captureBlocks 的扩展改为复用 BlueprintMultiblocks.forEachPart/core(或抽 partner(pos, state)),避免两处配对算术后继漂移。

B. 守卫在"配对有快照"时无条件信任历史状态,而本版恰好把区域外配对也纳入了快照。 已实测 BuildingCommit.set():直接 section.setBlockState(...) + chunk.removeBlockEntity(pos),前一格方块及其方块实体内容物不掉落、不退还。于是:落地压碎高花 → 玩家在区域外那半格放了装有物品的箱子 → Ctrl+Z:original.get(other) 非空 → 守卫放行 → 箱子连内容物被静默覆写,同时仍向玩家收 1 份植物材料(recovered 只抵扣材料、不退物品)。触发面是本次修复新扩大的一格(原先配对多半根本没进快照,生存会被原子拒绝、不会覆写)。建议 hasCompletePlants() 在配对有快照时也校验现场状态是否仍等于记录状态(或让 canRestore 对 addedPositions 中区域外位置做同样的状态一致性检查),不一致即 undo_conflict 而非静默覆写。

C. 拒绝是"永久性"的,且提示文案与判据不对应。 HISTORY 不清空、可重试(这点没问题),但配对格处于不可满足状态时每次 Ctrl+Z 都是同一条 Cannot safely account for the changed resources; undo cancelled without changing the area,文案描述的是"资源无法安全结算",实际判据是"植物配对不完整",玩家难以据此自救。建议补一个更贴近的提示键。

💡 建议

  • hasCompletePlants() 里构造 original(LinkedHashMap)与 resources() 中同名字段/同一逻辑重复,可抽 recordedStates() 一份语义两处共用。
  • captureBlocks 的 expanded 复制 + distinct() 去重无问题(positions 在 giant anvil 路径是 betweenClosedStream(...).map(BlockPos::immutable) 的 27 个不可变 BlockPos,单格路径是 List.of(pos)),保留现有 .map(BlockPos::immutable) 时机即可。
  • hasCompletePlants() 只遍历 this.blocks 是充分的:bounds 内所有位置在构造期已全部入快照,不存在"区域内未记录却已变化"的同株植物。

📋 声称验证表

声称 状态 依据
记录落点双格植物的完整配对快照 ✅ captureBlocks 配对扩展 → addedPositions
恢复上下两部分 ✅ 两半均在 this.blocks,restore() 逐格 set
按实际资源结算 ✅ shouldRecord(UPPER)==false ⇒ 材料只计 1 份
撤销前检查历史植物配对完整性 ⚠️ 仅 DoublePlantBlock 门/床/活塞/大蛋糕未覆盖(风险 A)
fixed #5124 ⚠️ 部分 双格植物场景已修;同类多方块仍可能"落地方块撤不掉"

🧪 验证建议(仓库无 src/test,只能游戏内验证)

场景 步骤 期望
双格植物跨界·生存 含下落铁砧实体的蓝图,落点旁/下方是区域外高花 Ctrl+Z 成功:铁砧方块消失、上下两半还原、只扣 1 份花
同上·创造 同上,创造模式 不出现"单独下半株"
混合时点 落地后手动拆掉配对方块,再 Ctrl+Z undo_conflict,区域不变
配对格被占用 落地后在配对格放箱子(含物品)再 Ctrl+Z 当前会静默覆写且内容物丢失 → 确认风险 B
门/床/活塞/蛋糕 下落方块压碎半件、另一半在区域外 目前仍 undo_conflict → 复现风险 A

结论: APPROVE(已合并) — 修复方向、原子性位置与资源结算均正确,回滚路径无回归;A、B 两点作为 post-merge 跟进建议。PR 标题准确,无需改动(且已合并,未执行 gh pr edit)。


由 Hermes Agent 审查(base 9cb2569999 → merged 9e750b31cc;head e10793f6d8)

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/merged-state-and-ci-verification.md in skill 'github-webhook-pr-review' (1 replacement). · Patched SKILL.md in skill 'pr-fix-claim-verification' (1 replacement).

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] 建筑杖粘贴的下落方块实体落地后无法撤销

4 participants