Skip to content

Improve crab trap underwater placement behavior and preview 优化蟹笼在水下放置的手感与预览 - #4965

Merged
WhereisFff merged 2 commits into
Anvil-Dev:dev/1.21/1.6from
PigeonNian:crab/1.21/1.6
Sep 20, 2026
Merged

WhereisFff merged 2 commits into
Anvil-Dev:dev/1.21/1.6from
PigeonNian:crab/1.21/1.6

Conversation

@PigeonNian

Copy link
Copy Markdown
Contributor

No description provided.

- 调整 LargeBlockPlacePreviewEventListener 以支持 PlaceInWaterBlockItem 在非水下时贴水面放置
- 使用流体射线重新计算水面放置落点,避免依赖 mc.hitResult
- 增加尝试多个放置候选点的逻辑,保证预览和实际放置位置一致
- 修改 PlaceInWaterBlockItem,水下时走普通方块放置逻辑,水面时使用流体射线定位放置点
- 提供 surfaceCandidates 方法列举水面放置的所有可能落点上下文
- 简化渲染代码中空检查,提升代码可读性与安全性
- 修复 currentPos 判断逻辑,避免空指针异常
@PigeonNian PigeonNian changed the title 优化了蟹笼在水下放置时的手感和预览 Improved the feel and visibility of the crab trap when submerged. 优化了蟹笼在水下放置时的手感和预览 Sep 20, 2026
@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4965

操作: opened(state=open, mergeable=true)
范围: 2 个文件(2 Java,0 新增,0 删除)/ 184 行 diff(+77 / −26)
分支: PigeonNian:crab/1.21/1.6dev/1.21/1.6(已核对 head sha c6731d8 为远端分支最新 tip)


🔴 关键问题(必须修复)

1. LargeBlockPlacePreviewEventListener.java:538currentPos 必然 NPE,放置预览会整体失效

private static BlockPos currentPos = null;   // L72
...
private static void validateCanRender(ItemStack item, BlockItem blockItem, BlockPos pos) {
    ...
    if (!currentPos.equals(pos)) {           // L538 ← currentPos 恒为 null
        currentPos = null;
        failBoundCooldown = 0;
    }
}

本 PR 顺手删掉的那两行,恰恰是全类中唯一给 currentPos 赋非空值的地方

-        if (currentPos == null) {
-            currentPos = pos;              // ← 唯一赋值点,被删除
-        } else if (!currentPos.equals(pos)) {
+        if (!currentPos.equals(pos)) {
             currentPos = null;
             failBoundCooldown = 0;
         }

我已在目标分支上交叉验证:currentPosrvhead4965 中仅出现 3 次(L72 声明、L538 比较、L539 赋 null),没有任何其他赋值(其他类的同名变量是无关的局部变量)。因此:

  • 首次进入 validateCanRender() 即抛 NPE;且由于永远得不到非空值,每帧都抛
  • 调用链是活的:renderGhost(RenderLevelStageEvent)updatePreview() → L196 validateCanRender(...);而 CRAB_TRAP 确实在 ModBlockTags.PLACEMENT_PREVIEW 里(BlockTagLoader),所以只要手持蟹笼/滑槽等任意可预览方块就会命中。NPE 抛在 renderEntries.add(...) 之前,渲染列表已被 clear()预览 100% 不可见;异常从渲染阶段事件处理器抛出,轻则每帧报错刷屏,重则客户端崩溃。

建议修法(保持原意的同时消除 NPE,改动方向反过来比较更安全):

if (!pos.equals(currentPos)) {
    currentPos = pos.immutable();
    failBoundCooldown = 0;
}

(原逻辑本意就是「预览目标位置变化时重置 failBoundCooldown」,这样写语义等价且不依赖 null 哨兵。撤销该 hunk 恢复原状也可以,但原 null 翻转写法本身就是可读性陷阱。)

备注:这一处与本 PR 的主题(水下放置手感/预览)无关,看起来是同 commit 里的顺手清理,很容易被漏测——建议单独拆出去或补回。


⚠️ 需要确认的行为变更

2. PlaceInWaterBlockItem.java:37-44 — 水下 useOn() 改为放行普通放置,属于对外行为变更,请确认无其他调用方依赖原来的「恒 PASS」语义

git grep 显示 PlaceInWaterBlockItem 仅被 ModBlocks.CRAB_TRAP.item(PlaceInWaterBlockItem::new))和预览监听器引用,useOn() 也没有被任何 mod 内部代码直接调用(BlockPlacementPicking 里的 stack.useOn(...) 路径在本分支已无调用方),所以当前没有破坏点。但 use() 里加的 player.isUnderWater() 守卫是这条链能自洽的关键——它挡住了「useOn 返回 FAIL/PASS 后回落到 use()」的二次放置。两处条件都取自 wasEyeInWater,保持一致性,✅ 设计正确。


💡 建议(非阻塞)

3. PlaceInWaterBlockItem.java:57-67surfaceCandidates(level, ...)level 参数没有用于构造 UseOnContext

public static List<UseOnContext> surfaceCandidates(Level level, Player player, InteractionHand hand) {
    BlockHitResult fluidHit = Item.getPlayerPOVHitResult(level, player, ...); // 用了 level
    return List.of(
        new UseOnContext(player, hand, ...),   // 3 参构造器内部用 player.level()

射线打在 level 上,上下文却建在 player.level() 上。两处调用点(use()、预览)传的都等于 player.level(),今天无实际影响;但签名暗示 level 具备语义,一旦将来有跨维度/FakePlayer 复用就会出现「射线在一个世界、落点在另一个世界」。建议显式化:new UseOnContext(level, player, hand, player.getItemInHand(hand), hit)

4. 预览判定与真实放置仍是近似等价(已有模式,仅提示)

客户端用 !BuildingRodService.singlePlacement(candidate).isEmpty() 判定,服务端用 super.useOn(candidate).indicateItemUse()singleAttempt() 内已对齐 updatePlacementContext + canPlace() + canPlace(context, state)(含 isUnobstructed 玩家碰撞箱判定),所以候选顺序、遮挡语义都对得上 ✅;残余差异只在 placeBlock()/setBlock 阶段(预览不模拟),与既有的非水面预览路径同一量级,可不处理。

5. 首帧临界竞态(极低优先级)isUnderWater() 在客户端取自本地预测位置、服务端取自权威位置,入水/出水那一两帧可能出现「预览按普通放置显示、实际走水流射线」的错位。属固有近似,记录即可。

6. PR 描述为空:建议补一段说明「改动动机 + 水下/水面两种落点来源的差异 + 手动验证场景」,方便后续追溯。


🟢 看起来不错

  • 把两个候选落点的生成逻辑抽成 surfaceCandidates() 并由放置 use() 与客户端预览共享,从根上消除了「预览在此、实际在彼」的来源,方向正确;客户端候选循环 break 条件与服务端 indicateItemUse() 同序同判 ✅。
  • 客户端预览在「两个候选都放不下」时直接 return,不再画出放不下的幽灵方块——与 use() 返回 PASS 的实际行为一致 ✅。
  • 水下改走普通 super.useOn():与 useOnplayer != null 空值防护、use() 的对应守卫构成闭环,javadoc 里对 blocksBuilding 阻塞成因的说明也准确 ✅。
  • renderMissingAmplifierGhosts 里删除的两处 if (level != null)真正的冗余清理:L311-315 已有 if (level == null) { ...; return; },且两个被调方形参均为非空 Level ✅。
  • 删除的 Item / ClipContext import 已确认在 LargeBlockPlacePreviewEventListener 中无残留引用(全文件无 ClipContext、无 Item. 限定调用),不会编译失败 ✅。
  • 新增行最长 134 字符 < style.xmlLineLength max=140,checkstyle 不触线 ✅。

📋 声称验证表

声称 状态 对应实现
优化水下放置手感 PlaceInWaterBlockItem.useOn() 水下走 super.useOn()use() 水下 early-return PASS
优化水下预览 ✅(但当前被 NPE 阻断) surfaceCandidates() + 预览端候选循环 / 水下退化为普通方块预览
预览与实际落点一致 ✅ 逻辑一致 共享 surfaceCandidates() + 单侧 break 条件对齐
validateCanRender()currentPos NPE 使预览完全不可用(回归,非声称内容)

结论: REQUEST_CHANGES — 改动思路正确、抽取得干净,但 LargeBlockPlacePreviewEventListener.validateCanRender()currentPos 的必然 NPE 是硬回归:所有可预览方块(含本 PR 的主角蟹笼)的放置预览都会失效/抛异常,修掉后即可合并。

🧪 测试建议

被测目标 推荐场景 优先级
validateCanRender() currentPos == null 起步连续调用两帧(不得抛 NPE);同位置不重置 failBoundCooldown,位置变化则重置 🔴
PlaceInWaterBlockItem.surfaceCandidates() 射线 MISS → 返回空列表;命中 → 恰好 2 个候选且顺序为「命中格 → 沿朝向偏移格」 🟡
use() / useOn() 水下分支 水下 use() 返回 PASS 且不产生放置;水下 useOn() 委托 super(可用假上下文断言不返回 PASS) 🟡
端到端手动验证 ①水下对实心方块右键;②水下对开阔水面右键;③水面(非水下)右键;④两个候选都放不下时不显示预览 🟡

关于 PR 标题: 当前标题为 Improved the feel and visibility of the crab trap when submerged. 优化了蟹笼在水下放置时的手感和预览——英文部分带多余句号,且 "visibility" 表述不准确(本次改的是放置预览,不是可见度)。我尝试用 gh pr edit 规范化,但 gh auth status 显示 The token in /opt/data/home/.config/gh/hosts.yml is invalid(Active account 存在但 token 失效),按流程未尝试导出 token,故未自动修改。建议改为:

Improve crab trap underwater placement behavior and preview 优化蟹笼在水下放置的手感与预览

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

Diff fully analyzed and cross-verified against the target branch. Here is the review:


代码审查摘要 — PR #4965

操作: edited(open,未合并)
范围: 2 个文件(2 Java,0 新增,0 删除)/ 184 行 diff(+77 / -26)/ 6 hunks
目标分支: dev/1.21/1.6(base 56ce7b8b = 当前分支 tip,与远端一致,无落后)

🔴 关键问题(阻塞合并)

1. client/event/LargeBlockPlacePreviewEventListener.java L538 — 必现 NPE(客户端崩溃)

// head
if (!currentPos.equals(pos)) {   // ← currentPos 只可能是 null
    currentPos = null;
    failBoundCooldown = 0;
}

currentPos 是 L72 声明的静态字段 private static BlockPos currentPos = null;,而在 head 版本中全文件仅出现在 72 / 538 / 539 三处grep -n currentPos 验证):它被赋值的唯一形式就是 currentPos = null再也没有任何地方写入 pos。因此 currentPos.equals(pos)第一次调用就抛 NPE。

if (currentPos == null) {
    currentPos = pos;
} else if (!currentPos.equals(pos)) {
    currentPos = null;
    failBoundCooldown = 0;
}

⚠️ 警告

2. 越界改动:两处与本 PR 主题无关的"还原保护"(commit message:「简化渲染代码中空检查」)

  • renderMissingAmplifierGhosts L330-336 删除 if (level != null)本身是安全的(L311-312 已有 Level level = mc.level; if (level == null) { ...; return; },且该 local 后续未再赋值),属于等价清理,但同样是越界的。
  • L538 的 currentPos 改动(上条 🔴)已构成回归。这两处都不是蟹笼/水面预览相关,建议从本 PR 剥离,或单独一个 commit 说明动机;当前 PR 描述(仅两行)完全没提这两处改动。

3. 行为变更需在描述中写明:水下彻底放弃"对水放置"

水下(player.isUnderWater())现在 use() 直接 pass,而 其他 BlockItemuse() 里仍会走 Fluid.SOURCE_ONLY 流体射线(对水源右键可放置)。也就是说水下拿蟹笼只能对着实体方块右键,对着水体右键不再有任何反应。
理由成立(流体射线命中的是紧贴自己那格水,落点被自身 blocksBuilding 阻挡,vanilla BlockItem.canPlace → isUnobstructed 会拒绝),但"泡在海里想往水面/水体里放蟹笼"这个最常见场景从此只能瞄海底。若这是预期就请在描述里写清;若想两者兼顾,可在 surfaceCandidates 里剔除"包含玩家自身的那格水"而不是关掉整条水面放置路径。

💡 建议

  • PlaceInWaterBlockItem.java L80 — 类内调用自有静态方法写成 PlaceInWaterBlockItem.surfaceCandidates(...),而 L88 同样调用却直接写 super;统一成不加类名前缀更简洁。
  • LargeBlockPlacePreviewEventListener.java L181 边界 — 水下改走 else 分支后,mc.hitResult 为 MISS 时可能落到 BlockPlacementPicking.findAirPlacementHit 回退并渲染鬼影,而此路径下客户端发的是 UseItem(非 UseItemOn)→ use() 水下直接 pass → 有鬼影却无法放置。触发需要 mc.hitResultplayer.pick(range,1,false) 结果不一致(视野边界/其他模组改动 hitResult),属窄边界,非阻塞,但可顺手在 else 前加一句 !player.isUnderWater() 短路。
  • 新增 surfaceCandidates 的 Javadoc(含 {@link ClipContext.Fluid#SOURCE_ONLY})在 1.21 分支可正常解析;Item/ClipContext 两个 import 在预览文件里确实已无引用(已 grep 全文件确认),删除正确,未引入多余 import。

🟢 看起来不错

  • 核心设计好: 把"水面落点候选"抽成 PlaceInWaterBlockItem.surfaceCandidates(),由放置侧 use() 与客户端预览共用同一列表与同一顺序,这是消除"预览在此、实际在彼"的正确做法——base 的预览只看候选 1,实际放置会退到候选 2,本改动真正修掉了这个不一致。
  • use() 的重构与 base 严格等价:MISS 时都返回 passInteractionResultHolder.pass(stack)new InteractionResultHolder<>(PASS, stack))、非 MISS 时"首个 indicateItemUse() 即停",与原来"第一次失败再试第二次"结果一致;blockHitResult2.missfluidHit.getType() == MISS 在 1.21 语义等价。
  • _null 安全:useOncontext.getPlayer() == null 做了判空(发射器 DirectionalPlaceContext 场景行为保持不变,仍 PASS),surfaceCandidates 返回非空 List,无空返回歧义。
  • 预览侧 3 参 new UseOnContext(player, hand, hit)(从手持物取 item)与 hand/item 的选取逻辑一致,等价于原先显式传 item 的 5 参构造。
  • BuildingRodService.singlePlacement → singleAttemptcanPlace() + getStateForPlacement + item.canPlace)判定链一致,候选筛选与实际放置谓词同源,可信度高;预览侧不会因 BuildingRodObstructions.reject 产生副作用(仅 ServerPlayer 触发)。

📋 声称验证表

PR 声称 状态 对应实现
优化蟹笼水下放置手感(水下走普通方块放置) PlaceInWaterBlockItem.useOn L37-44 + use L74-78 早退
水面落点预览与实际放置一致(多候选逐个尝试) surfaceCandidates + 预览 L156-171 循环 + use L79-85
简化渲染代码空检查"提升安全性" renderMissingAmplifierGhosts 等价但越界;validateCanRender 引入必现 NPE
修复 currentPos 判断"避免空指针" 反向:删掉唯一的 null 保护

结论: REQUEST_CHANGES — ①validateCanRendercurrentPos.equals(pos) 必现 NPE,任何手持可预览方块(含 crab_trap)的客户端一帧内崩溃,必须还原 base 写法;②建议把两处与本主题无关的空检查改动从本 PR 剥离(其中 L538 那处即为 ①)。水面/水下候选逻辑本身方向正确、抽象干净,修掉 ① 后即可合并。

🧪 测试建议

被测目标 推荐场景 优先级
validateCanRender() 首次构造(currentPos == null)即调用一次,断言不抛 NPE;落点原位/换位交替各一轮,断言 failBoundCooldown 不被每帧清零 🔴
PlaceInWaterBlockItem.surfaceCandidates() 射线 MISS → 返回空列表;命中 → 长度 2 且第 2 项 = getBlockPos().relative(player.getDirection()) 🟡
use() 水下 → PASS(不消耗、不放置);非水下候选 1 成功 → 只尝试 1 次;候选 1 失败候选 2 成功 → 使用候选 2 🟡
端到端(手动) 水面上/眼睛入水 × 瞄水/瞄实体方块/瞄空气 六宫格,比对鬼影位置与实际落点 🔴

备注:gh auth status 显示 /opt/data/home/.config/gh/hosts.yml 中 token 已失效(Active account: Gugle2308),故未执行标题修改。当前标题内容准确、英中齐全,若想更精确可改为:Improve crab trap underwater placement and water-surface placement preview 优化蟹笼水下放置与水面放置预览(本次因 gh 未认证未自动修改)。


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'style-refactor-pr-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Skill 'removed-guard-regression-review' created. · Skill updated

@PigeonNian PigeonNian changed the title Improved the feel and visibility of the crab trap when submerged. 优化了蟹笼在水下放置时的手感和预览 Improve crab trap underwater placement behavior and preview 优化蟹笼在水下放置的手感与预览 Sep 20, 2026
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /tmp && curl -sL "https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4965" -o pr.json; python3 -c "
import json
d=json.load(open('pr.json'))
print('state:',d.get('state'),'| merged:',d.get('me...

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.

@Gugle2308

Copy link
Copy Markdown
Collaborator

我已获取并核对 diff、目标分支与 PR head 的完整源码。以下是审查结论。


代码审查摘要 — PR #4965

操作: edited(重新核对全部声称)
范围: 2 个文件 / 184 行 diff(+77 / −26 / 1 commit)
版本检测: 1.21.x 常规 feature PR(跳过 26.1 API 迁移健康度检查)
标题: 已含英文 + 中文,无需 gh pr edit 修改

🔴 关键问题(必须修复,否则会崩溃)

1. LargeBlockPlacePreviewEventListener.validateCanRender() — 删除初始化后 currentPos 恒为 null,稳定触发 NPE

// L72  private static BlockPos currentPos = null;      ← 字段初始值就是 null

// L531-541(修改后)
private static void validateCanRender(ItemStack item, BlockItem blockItem, BlockPos pos) {
    ...
    if (!currentPos.equals(pos)) {      // ← currentPos 恒为 null → NPE
        currentPos = null;
        failBoundCooldown = 0;
    }
}

被删掉的 if (currentPos == null) { currentPos = pos; } 正是这个字段唯一被写入非 null 值的地方——全文件 currentPos 只出现在 3 处(声明、本行的读、= null),所以改动后它永远是 null,第一次进入预览渲染即 NullPointerExceptionupdatePreview() 每帧调用,渲染线程崩溃 → 客户端崩游戏)。

  • 取证:本地 26.1 分支参考实现(dev/26.1 的同一方法)保留了原逻辑,且字段声明为 private static @Nullable BlockPos currentPos;——@nullable 本身就是在说明「这里必须做 null 检查」,删除该守卫属于回归。
  • 附带语义损失:该方法原意是「预览落点变化时重置 failBoundCooldown」,改动后该逻辑既不再生效、也无法生效。

建议修复(恢复原状即可):

        if (currentPos == null) {
            currentPos = pos;
        } else if (!currentPos.equals(pos)) {
            currentPos = null;
            failBoundCooldown = 0;
        }

⚠️ 警告

2. 预览与实放的可行性判定是两套并行实现,javadoc 的「完全一致」表述超出代码保证

  • 预览侧用 BuildingRodService.singlePlacement()(内部 singleAttemptupdatePlacementContext / canPlace() / getStateForPlacement / item.canPlace / canModify / canBeReplaced);
  • 实放侧是 super.useOn(candidate) + indicateItemUse()BlockItem.useOn 的全部失败分支)。
  • 两者在多数情况下同结果,但 singleAttempt 额外要求 canModify(区域保护),也不建模 placeBlock 的其余失败路径 → 仍可能出现「预览无 / 实放有」或反过来的偏差。
  • PlaceInWaterBlockItem 的 javadoc 写的是「顺序与实际放置完全一致」「否则会『预览在此、实际在彼』」——建议把措辞降级为「按 use() 相同的顺序逐个尝试」,或(更彻底)把「此格能否放下」抽成一个共享谓词供两侧调用。

💡 建议(非阻塞)

  • cells == null / useContext = null 哨兵可读性List<Cell> cells = null; + if (cells == null) { cells = singlePlacement(useContext); } 把「尚未计算」和「计算结果」混在一个变量里。建议抽成私有小方法(如 private static PlacementResolve resolve(...))返回一个记录,或用一个 boolean/Optional 表达,减少读者对 null 状态的推理成本(项目 AGENTS.md 也要求 prefer self-explanatory code / 仅用 JSpecify 表达可空)。
  • surfaceCandidates 命名与归属:它是「水面放置候选落点」,名字里的 surface 容易被读成「地面/表面」。可考虑 waterSurfaceCandidates,或搬到 PlacementInteractions/BuildingRodService 这类工具类,避免客户端事件依赖具体物品类的静态方法(当前仅 2 处调用,纯风格问题)。

🟢 看起来不错

  • 删除的两处 if (level != null) 是真正的死代码清理renderMissingAmplifierGhosts 在 L300 取 mc.level 后,L301-304 已 if (level == null) { ...; return; },后续 level 必然非 null,删除安全(与 validateCanRender 的情况性质完全不同)。
  • 预览顺序修复:原实现只看流体射线命中格(候选 在REI渲染方块(初步测试) #1)就渲染,而 use() 在放不下时会沿玩家朝向再试一格 → 修复了长期存在的「预览在第一格、实际落到第二格」。改进方向正确。
  • 服务端/客户端水下判定对称use()player.isUnderWater() 提前返回 PASS,useOn() 水下走 super.useOn,客户端预览用同一 !player.isUnderWater() 分支降级为普通方块预览——三处一致。
  • use() 行为等价性核对通过:候选为空(MISS)时返回 new InteractionResultHolder<>(PASS, stack),与旧 InteractionResultHolder.pass(stack) 等价;indicateItemUse() 时才 break,与旧「第一条不消费才试第二条」语义相同;仍使用 super.useOn 绕过自身 useOn 的 PASS 覆写,未引入递归。
  • import 清理无残留:head 版不再出现裸 Item. / ClipContext,删除 net.minecraft.world.item.Itemnet.minecraft.world.level.ClipContext 后无未使用 import,编译无隐患。
  • 影响面与标题一致PlaceInWaterBlockItem 在目标分支仅被 CRAB_TRAP 一处使用(ModBlocks L2229),标题限定「蟹笼」准确。

📋 声称验证表

声称 状态 对应文件 / 依据
优化水下放置手感(水下按普通方块放置) PlaceInWaterBlockItem.useOn() 水下 super.useOn + use() 水下提前 PASS
只有陆上保留贴水面放置 use()isUnderWater() 守卫 + surfaceCandidates 不变
优化预览(与实放同顺序) ⚠️ LargeBlockPlacePreviewEventListener.updatePreview() 按候选逐个判定,方向正确但判定谓词与实放不同源(见 ⚠️2)
顺带清理冗余 null 守卫 ⚠️ renderMissingAmplifier* 两处 ✅ 真冗余;validateCanRender 一处 ❌ 误删(→ 🔴1)

🧪 测试建议

被测目标 推荐场景 优先级
validateCanRender() 连续两次以不同 pos 调用(含首次 currentPos == null)→ 断言不抛 NPE、currentPos 被正确赋值 🔴
PlaceInWaterBlockItem.use() 玩家在水下右键空气 → 返回 PASS 且无放置;水上 MISS → PASS;候选 #1 放不下时落点切到 #2 且结果一致 🔴
PlaceInWaterBlockItem.surfaceCandidates() 射线 MISS → 空列表;命中 → 恰好 2 个上下文且第 2 个沿 player.getDirection() 偏移 🟡
预览/实放一致性 同一 LocalPlayer 状态下预览 cells 首格 == 实放落点(含水下退化为普通放置分支) 🟡
useOn() context.getPlayer() == null(非玩家路径)→ 仍返回 PASS,不误放置 🟢

结论: REQUEST_CHANGES — 方向正确、清理与预览一致性改进都很好,但 validateCanRender() 的 NPE 是必崩的回归,恢复那一行 null 初始化(或改用 Objects.equals + 保留赋值)后即可合并。已核对 PR 标题(英文 + 中文齐全),未作修改。


由 Hermes Agent 审查

- 将 currentPos 标记为可为空,避免未初始化时的空指针错误
- 在比较之前增加 null 判断,防止空指针异常导致程序崩溃
- 优化 currentPos 赋值逻辑,确保正确更新位置状态
- 解决 failBoundCooldown 状态未正确重置的问题
@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'removed-guard-regression-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

curl -s "https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4965" | python3 -c "import sys,json; d=json.load(sys.stdin); print('head_sha',d['head']['sha']); print('head_repo',d['head']['repo']['f...

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4965 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4965 (https://getvet.sh); 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.

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4965

操作: synchronize
范围: 2 个文件 (2 Java, 0 新增 / 0 删除) / 183 行 diff (+78 / −24,与 API changed_files=2, additions=78, deletions=24 一致,无截断)
分支: crab/1.21/1.6dev/1.21/1.6(1.21 常规 feature PR,非 26.1 迁移,跳过 API 迁移健康度检查)

本次审查拉取了 head(7596753)与目标分支的完整源码做交叉验证,不只是读 diff。

✅ 关键点验证(结论:实现正确)

验证项 结果
use() 水下二次触发 ✅ 水下 useOn 返回 super.useOn(consuming),且 use() 内新增 player.isUnderWater() → PASS 守卫,正好堵住「useOn 已放置 + use() 再走流体射线」的双重放置
预览 ⇄ 实际候选顺序 ✅ 预览 for (candidate : surfaceCandidates(...)) 首个可放置即 break,与 use()for → super.useOn → indicateItemUse() → break 同序;这正是标题里「预览」的修复点
候选构造等价性 ✅ 预览水分支改用 new UseOnContext(player, hand, hit)hand 由 main/off-hand 判定(第 139-144 行),解析出的 stack 与原 5 参构造传入的 item 相同
删除的 if (level != null) 守卫 ✅ 安全。renderMissingAmplifierGhosts 第 313-316 行已有 if (level == null) {…return;} 早退(目标分支同样存在),后续使用处 level 必非空,属真冗余清理
删除的 import Item / import ClipContext ✅ head 文件中已无 Item. / ClipContext 引用,无编译风险
@Nullable 用法 javax.annotation.Nullable 符合目标分支 AGENTS.md(package-info 声明的 javax 非空默认)+ 该文件第 64 行原有 import;currentPos 确实会在第 542 行被置 null
分支可达性 crab_trapplacement_preview 标签内(generated/resources/.../placement_preview.json 第 24 行),且 PlaceInWaterBlockItem 全仓库仅 CRAB_TRAP 一个使用者(ModBlocks.java:2225-2229),改动的预览分支真的会生效
diff 卫生 ✅ 6 个 hunk 全有内容,无 ghost 文件、无 trailing whitespace、无 TODO、无 No newline

⚠️ 警告

  • 水下放置的可见行为变化需在 PR 描述中说明 — 改动后「眼睛入水」时不再走水面流体射线,蟹笼只能按普通方块放置(贴着看得到的实体方块/水格)。对习惯潜到水下往水面放笼子的玩家是明显手感变化(这正是本 PR 的意图,且预览已同步),但 PR 描述为 None,建议补一段 changelog 说明。
  • Javadoc 措辞与判定条件不一致 — 类注释写「玩家自身泡在水里」,实际判定是 player.isUnderWater()(= wasEyeInWater按眼睛是否浸水,不是碰撞箱)。踩水时头露出水面仍走水面放置;这点恰好是玩家最容易感知的边界,建议把注释改成「眼睛入水」以免后续维护者误解。

💡 建议

  • LargeBlockPlacePreviewEventListener.java:154PlacementInteractions.allowsPlacement(new UseOnContext(player, hand, hit)) 用的是 mc.hitResult。对水分支而言,准星拾取用 Fluid.NONE(水面不产生 mc.hitResult),这里的 hit 通常是 MISS 或水后面的那个方块(水底、水后的箱子等),与真实落点无关;既然水分支此刻已算出精确 candidate,改成对候选 candidate 判定(或仅在水分支跳过该前置检查)可让优先级判定与实际放置完全同源。属既有问题,本次改到这一区域顺手修更好。
  • 预览可放置性判据与实际放置路径的耦合 — 预览用 BuildingRodService.singlePlacement(),实际放置走 BlockItem.useOn → place();对普通 BlockItem 二者等价(singleAttempt 就是 updatePlacementContext / canPlace / getStateForPlacement / canPlace 的复刻,CrabTrapBlock 现为普通 Block implements SimpleWaterloggedBlock,✅ 无差异)。但 singlePlacementSimpleMultiPartBlockItem / FlexibleMultiPartBlockItem 会额外按 getMaxOffsetDistance(face) 位移后再试一次,super.useOn 不做这次重试 —— 一旦将来有多方块物品复用 PlaceInWaterBlockItem,候选 1 会因位移重试而被判定「可放置」,实际却会落到候选 2,重新引入「预览在此、实际在彼」。建议在 surfaceCandidates/调用处加一行约束注释(或断言方块非 multipart)。
  • use() 里的哨兵写法可读性useContext = null + 循环内赋值 + if (useContext == null) return; 表达的是「一个都没找到」,用 List<BuildingRodService.Cell> found = null / boolean found 或抽一个私有 helper 会让意图更直白;cells == null 兼作「尚未计算」也是同类写法,非阻塞。
  • 范围蔓延 — 删掉的两处 if (level != null)currentPos= null@Nullable 与本 PR 主题(蟹笼)无关。虽然经核对是安全的等价清理,但按仓库 AGENTS.md「Make the smallest change / 不要扩大范围」,建议拆成独立 commit(或独立 PR),便于日后 git blame 归因。

📋 声称验证表

标题声称 状态 对应实现
优化水下放置手感 PlaceInWaterBlockItem.useOn 水下分支 + use() 水下守卫
优化预览(与放置一致) LargeBlockPlacePreviewEventListener 水分支按 surfaceCandidates 顺序取首个可放置
落点来源差异(流体射线 vs 玩家自身阻挡)的分析 注释所述与 BlockItem.canPlace → isUnobstructedblocksBuilding)行为一致

结论: APPROVE — 未发现阻塞性缺陷。逻辑等价性、预览与放置同序、删除守卫的安全性均已用完整源码核实;上面的 ⚠️ 是文档/描述层面的补全,💡 是非阻塞的可维护性改进。标题已准确反映改动(水下行为 + 预览),本次未修改标题。

🧪 手动验证建议

场景 预期
岸上看向水面放蟹笼 预览落点 == 实际落点
水面首格被方块挡住 预览自动落到玩家朝向 +1 格,实际一致(本次修复点)
眼睛入水,右键水底实体方块 按普通放置(含水、WATERLOGGED=true),预览一致
眼睛入水,右键空气 无预览、无放置(use() 返回 PASS)
踩水(头露出水面) 仍走水面流体射线放置
主手空/非方块、副手蟹笼 预览与放置均使用副手(hand=OFF_HAND

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

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

@WhereisFff
WhereisFff merged commit 8aba6d8 into Anvil-Dev:dev/1.21/1.6 Sep 20, 2026
2 checks passed
@Gugle2308

Copy link
Copy Markdown
Collaborator

审查已跳过 — 该 webhook 的 action 为 closed(PR #4965 已关闭)。

按审查约束第 1 条(closed / labeled → 停止,不发布评论),本次不执行代码审查、不输出审查意见,也不修改 PR 标题。

如需对已合并/已关闭的改动做回溯审查,请重新触发一次 opened / synchronize / edited 事件。

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.

3 participants