Skip to content

Add concrete fluid behavior. 添加水泥流体行为 - #4982

Open
PigeonNian wants to merge 2 commits into
Anvil-Dev:dev/1.21/1.6from
PigeonNian:cemet/1.21/1.6
Open

PigeonNian wants to merge 2 commits into
Anvil-Dev:dev/1.21/1.6from
PigeonNian:cemet/1.21/1.6

Conversation

@PigeonNian

Copy link
Copy Markdown
Contributor

- 新增 CementFluid 流体类型,支持多彩色彩,通过 Color 进行区分
- 实现水泥流体的源头方块和流动方块,继承自 BaseFlowingFluid
- 随机刻逻辑中支持源头沉降,如果下方为空则整体下移
- 添加水泥凝固机制,源头有概率与周围两层流动水泥一同凝固成混凝土方块
- 识别重复随机刻派发,避免源头多次重生导致数量异常增加
- 实现触碰糖块、粘液块与蜂蜜块时凝固或下移行为的特殊逻辑
- 优化宽广区域内的向下寻路算法,限定单次搜索上限,不影响服务器效率
- 在 ModFluids 注册新水泥流体,替换旧的基类实例化为 CementFluid 实例
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /opt/data/workspace && P=/opt/data/skills/github/github-code-review/scripts/mc_class_probe.py; for J in /tmp/minecraft-client-1.21.1.jar /tmp/minecraft-server-1.21.1.jar; do echo "=== $J ($(stat -c...

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 marked this pull request as draft September 21, 2026 11:59
@Gugle2308

Copy link
Copy Markdown
Collaborator

All verification done. Here is the review (posted as the reply):


代码审查摘要 — PR #4982「Add concrete fluid behavior 添加水泥流体行为」

操作: opened
范围: 2 个文件(2 Java,1 新增)/ 332 行 diff / +301 −3
提交: CementFluid.java(新增,293 行)+ ModFluids.java(source/flowing 注册改用新类)

✅ 核心诊断已用 1.21.1 原版字节码核实(不是推测)

作者注释里"同一格每刻被派发两次随机刻"的判断完全正确,逐条证据(Mojang 1.21.1 client_mappings + 混淆客户端字节码):

关系 混淆类/方法 字节码证据
ServerLevel#tickChunk 双派发 aqu.a(Ldvi;I)V 同时 BlockState.randomTick(ServerLevel,BlockPos,RandomSource)×1 FluidState.randomTick(Level,BlockPos,RandomSource)×1,外加 FluidState.isRandomlyTicking()×1
LiquidBlock#randomTick 转交流体 dko.b(Ldtc;Laqu;Ljd;Layw;)V BlockState.getFluidState()FluidState.randomTick(...)
LiquidBlock#isRandomlyTicking 委托流体 dko.d_(Ldtc;)Z state.getFluidState().isRandomlyTicking()(所以注册里不加 .randomTicks() 也会被选中)
覆写可见性 Fluid.randomTick(...) / Fluid.isRandomlyTicking() 均为 protected → 本 PR 的 protected 覆写签名合法;FluidState.getAmount() 为 public

所以:液态路径(液态混凝土方块)与流体状态路径会各掷一次,不去重时凝固概率确实是 1−0.75²≈43.75%,去重方向正确。实时状态守卫(CementFluid.java:75-78)也正确消除了 stale snapshot 导致的源头增殖。

🔴 关键问题

未发现阻塞性缺陷(编译面:覆写签名/可见性、getAmountBuiltInRegistries.BLOCK.get(ResourceLocation)javax.annotation.Nullable(本分支 AGENTS.md/CONTRIBUTING 规定用此注解,package-info.java 已声明默认非空)均核对通过)。

⚠️ 警告

  1. isDuplicateDispatch 把可变的"上一刻位置/维度"状态挂在注册表单例上(CementFluid.java:56-63,120-129),并且强引用 Level

    • CementFluid.SourceBuiltInRegistries.FLUID 常驻单例,lastTickLevel永久强引用一个 ServerLevel:单人退出世界后该 ServerLevel(含已加载区块/实体)无法被回收,直到下一个世界里有源头被随机刻覆盖 → 典型的静态引用泄漏。
    • 建议改成不持有 Level 值对象:private ResourceKey<Level> lastTickDimension; + 比较 this.lastTickDimension == level.dimension()(维度 key 是值对象,不会泄漏)。
    • 另外该去重依赖"方块路径与流体状态路径紧邻且同格"这一原版实现细节;它是正确的,但属于脆弱假设,值得在注释里写明"依赖 LiquidBlock.randomTick 转发 + tickChunk 二次派发"。
    • 可选的无状态替代:把行为放进自定义 LiquidBlock 子类(覆写 isRandomlyTicking→true、randomTick→自身逻辑且不 super),同时让流体 isRandomlyTicking() 保持 false,则每刻只有一次派发,去重状态整块可以删掉。代价是要动 ModBlocksLiquidBlock 的注册。
  2. concrete() 违反非空契约(CementFluid.java:231-241)。
    dev.dubhe.anvilcraft.fluidpackage-info.java@MethodsReturnNonnullByDefault,而 concrete()BuiltInRegistries.BLOCK.get(...) 返回 null 时会返回 null(字段本身标了 @Nullable),随后 this.concrete().defaultBlockState() 会 NPE;若注册表是 defaulted 语义则相反——缺失颜色会被静默替换成 AIR(源头凭空消失)。当前 Color 恰好是原版 16 色且 getSerializedName()white_concretepink_concrete 一一对应,所以暂时不会触发;但建议按本分支 AGENTS.md「先检查 @nullable 再解引用」:给 concrete()@Nullable 并在 solidify 里 null/air 早退(或抛明确异常),避免以后新增颜色时出现静默行为。

  3. MAX_SETTLE_SEARCH = 64 与最大铺开半径 7 的边界算术对不上(CementFluid.java:53,168-174)。
    搜索预算按"入队的同色连通格数"计。以源头为中心的菱形水面上,曼哈顿半径 r 内格数约 1+4+…+4r:r=5 → 61(≤64,正常);r=6 → 85、r=7 → 113(超预算直接返回 null)。而原版源头在平地上最多铺到半径 7,落差落点又恰在池子最外圈,于是"宽台面(落差距源头 ≥6 格)"时源头找不到下一层而原地凝固,窄台面时却会跟着水沉降——同一机制在两个尺度上表现不一致。建议确认这是否符合预期(若不符合,抬高上限或改为"先向下探 1 格、失败再横向 BFS")。

💡 建议

  1. 水平位移距离findLowerFlow 返回的是 BFS 意义上最近的下一层流动格,水平距离可达铺开半径(最多约 7 格),moveDown 于是让源头一次随机刻"瞬移"到几格外的落水点。若意图只是"顺着自己淌出的那股水就近下沉",建议限制 BFS 层数/距离(例如 ≤2),否则请确认这个瞬移符合 [TODO] 水泥流体行为 #4972 的描述。
  2. 每刻分配:一次随机刻会 new ObjectOpenHashSet + ArrayDequefindLowerFlow)以及 solidify 里再 new 两个 Set。量有上界、可接受,但在大面积水泥上仍是可回收的开销,后续可考虑复用/基本类型数组(非阻塞)。
  3. 文档未更新src/main/resources/assets/anvilcraft/ageratum/{zh_cn,en_us}/002_material/005_cement.md 目前只写了"用来合成混凝土",没有世界行为(沉降、约 0.25/随机刻凝固为对应颜色混凝土、糖/粘液/蜂蜜三种交互)。新机制建议补进 ageratum 文档。
  4. 节奏自检:该机制纯随机刻驱动。默认 randomTickSpeed=3、单格选中概率约 3/4096(约 68 s 一次),期望 4 次命中才凝固 → 单个源头期望约 4~5 分钟才硬化。若设计预期是"几秒内变硬",需要改用计划刻/计数器。
  5. 交叉系统已确认无冲突(好消息):cement_dyeing / cement_staining 走的是 SolidLiquidRecipe(铁砧+锅)路径、作用于锅内容物,不是世界里的 LiquidBlock,因此世界内自凝固不会破坏这些配方。

🟢 看起来不错

  • 双派发/快照问题定位精准,且用实时状态 + 同刻去重双重守卫,修复方向正确、注释解释了原因与 43.75% 的推导。
  • 环 1/环 2 = amount 7/6 == LEVEL 1/2,与 Source.getAmount()=8Flowing.getAmount()=state.getValue(LEVEL) 语义自洽;Color 16 色名与 vanilla 混凝土名严格对应。
  • 搜索只走同色连通水体、只扫 sourceY/targetY 两层、访问量有常数上界,符合"随机刻常数工作量"的设计目标;visited 用新 BlockPos 去重正确,lastTickPos 做了 immutable()
  • Source.isRandomlyTicking() 只在源头返回 true,流动部分不参与随机刻(且 FlowingFluid 确实未覆写该方法是 false)——与注释一致。
  • 风格合规:style.xmlLineLength=140(新增最长 132)、CustomImportOrder(三方 / java+javax 分组、组间空行、组内字母序)、allowNoEmptyLineBetweenFields=true、逗号 EOL 等均满足;未新增 @SuppressWarnings
  • 无敏感信息、无调试残留、无 TODO。

📋 声称验证表

声称 状态 依据
resolved #4972 ⚠️ 无法离线核验 本环境 gh token 失效、GitHub API 401,无法读取 issue 的验收细则;请确认"糖块只挡凝固 / 粘液只挡下移 / 蜂蜜两者都挡"这三条与 issue 描述一致(尤其"糖块阻止凝固"的直觉相反方向)
水泥流体世界行为 CementFluid.java 新增沉降 + 双层凝固 + 三方块交互,ModFluids 注册已切换
源头不再成倍增殖 实时状态守卫 + 同刻去重,已按原版派发链核实

结论: COMMENT(建议合并前处理 ⚠️1/⚠️2/⚠️3) — 无编译级或必然运行的缺陷,逻辑方向正确;主要待办是去重状态别强引用 Levelconcrete() 的非空契约,以及确认 64 格搜索上限在宽台面上的表现是否符合设计。

标题建议(本次因 gh 未认证未自动修改,gh auth status = token invalid):
Add world behavior for cement fluid (source settling + solidify into concrete) 添加水泥流体的世界行为(源头沉降与凝固为混凝土)
现标题 "Add concrete fluid behavior" 的英文部分有歧义(易读成"新增一种混凝土流体",而实际是"给水泥流体加世界行为")。

🧪 测试建议

被测目标 推荐测试场景 优先级
CementFluid.randomTick 同刻双派发下凝固概率≈25%(统计 10k 次)、移动后第二次派发不生成新源头 🔴
findLowerFlow 半径 5 台面成功下沉 / 半径 6~7 台面触发 MAX_SETTLE_SEARCH 的行为是否符合预期;金字塔交错水流不会串到隔壁水股 🟡
solidify 环 1/环 2 精确命中 amount 7/6;两个相邻内圈格指向同一外圈格时用 Set 去重(不重复写块、不重复触发更新) 🟡
三方块交互 糖/粘液/蜂蜜分别只阻止其对应行为;蜂蜡边界(对角不算接触) 🟡
CementFluid.Source/Flowing getAmount/isSource/默认 LEVEL=7createLegacyBlock() 一致,桶拾取仍正常 🟢

由 Hermes Agent 审查(diff: patch-diff.githubusercontent.com 匿名获取,md5 8646530947f39ac43262d49525ddab18;原版 API 用 Mojang 1.21.1 mappings + 客户端字节码离线核对)

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/style-check-ci-gate-verification.md in skill 'offline-pr-evidence-verification' (1 replacement). · Patched SKILL.md in skill 'offline-pr-evidence-verification' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4982「Add concrete fluid behavior 添加水泥流体行为」

操作: converted_to_draft(草稿阶段审查 → 仅列阻塞/严重项,省略风格建议)
HEAD: 34e81502(与 opened 阶段那次审查的提交同一 SHA:本地 git fetch PigeonNian/AnvilCraft cemet/1.21/1.6 得到的 head 与 API head.sha 一致;diff md5 8646530947f39ac43262d49525ddab18 未变)→ 之前那份 review 里的待办全部仍未处理
CI: build ✅、checkstyle ✅(两个 check run 均 success,故本分支 AGENTS.md 约定的 javax.annotation.Nullable 用法与 style.xml 门禁均无异议)
范围: 2 文件(1 新增 Java + 注册切换)/ +299 −2

🔴 关键问题

无阻塞性缺陷:覆写签名与可见性(Fluid.randomTick / Fluid.isRandomlyTicking 均为 protected)、getAmount/isSource 语义、Flowing 构造里的 registerDefaultState(LEVEL=7)(与 BaseFlowingFluid.Flowing 一致)均已用 1.21.1 官方 mappings + 混淆客户端字节码核对通过。

⚠️ 转 ready 前需要处理(3 项沿袭 + 1 项更正)

1【本次新发现】SOLIDIFY_AMOUNT_RING_1/2 = 7/6#4972 的「level6 和 level7」存在数字空间歧义**,两种读法选的是相反的两圈。**
我逐字节核对了 1.21.1 的方块↔流体 level 映射(这不是推测,是构造器与 getFluidState 的字节码):

  • LiquidBlock.<init> 构造 stateCacheadd(fluid.getSource(false)) → 循环 i=0..7fluid.getFlowing(8-i, false) → 末尾加 fluid.getFlowing(8, true)
  • LiquidBlock.getFluidState(state) = stateCache.get(min(blockLevel, 8))
  • 方块 level 0 = 源头、1 ↔ 流体 LEVEL 7、2 ↔ LEVEL 6、…、7 ↔ LEVEL 1、8 = 下落(与 getLegacyLevel = 8-min(amount,8)+(FALLING?8:0) 互为逆映射,往返自洽);
  • FlowingFluid.getNewLiquid:新 level = 邻居最大 amount(=源头 8) − getDropOff(=1) = 7 ⇒ amount 7 确实是紧贴源头的那一圈 ✅(与本 PR javadoc「换算成方块状态即 LEVEL 的 1 与 2」完全吻合)。

问题在于:#4972 原文写的是「该源头附近 level6 和 level7 的流动部分」。若作者说的 level 是流体 LEVEL(amount),那 7/6 就是紧贴源头的两圈 = 本 PR 实现(合理,且「附近」措辞支持这一读法);若说的是方块状态 level(F3 里看到的 [level=6]/[level=7]),对应 amount 是 2 和 1,即最外圈——本 PR 就会凝到池子边缘而非源头周围。issue 里的示意图应该能直接判定,建议合并前与 issue 作者确认一句,并在 PR 描述里明确「level 指流体 LEVEL(amount)」,否则后续维护者按方块 level 复核会得出相反结论。

2 去重状态把可变 Level 挂在注册表单例上(沿袭,未处理)lastTickLevel/lastTickPos/lastTickTimeCementFluid.Source 实例字段,而 Source 是 BuiltInRegistries.FLUID 常驻单例:单人退出世界后旧 ServerLevel(含其区块/实体)会被强引用,直到新世界里某个水泥源头被随机刻覆盖。改为不持有 Level 对象(ResourceKey<Level>/level.dimension() 比较)即可,改动很小。同时建议在注释里写明该去重依赖LiquidBlock.randomTick 转发 FluidState.randomTick + tickChunk 同格二次派发」这一原版实现细节(我已复核两条路径确实存在:ServerLevel.tickChunk 同一次采样的 BlockState.randomTickFluidState.randomTick 紧邻调用;LiquidBlock.isRandomlyTicking 委托 state.getFluidState().isRandomlyTicking()LiquidBlock.randomTickFluidState.randomTick)。概率推导 1−0.75²≈43.75% 成立,去重方向正确。

3 MAX_SETTLE_SEARCH = 64 与平地最大铺开半径 7 的算术对不上(沿袭,未处理) — 复算:BFS 入队上限 64 按「同色连通格数」计,平地上以源为中心的菱形格数 1+2r(r+1):r=5 → 61(≤64),r=6 → 85、r=7 → 113(超预算直接 return null)。而落点恰在池子最外圈,于是台面宽度 ≥6 格时源头找不到下一层而原地凝固,窄台面却会正常沉降——同一机制在两个尺度上表现相反。请确认是否符合设计;若要一致,抬高上限(≥128)或改成「先探正下方 1 格,失败再横向 BFS」更贴近「就近下沉」的意图。

4【对上次意见的更正】concrete() 的风险不是 NPE,而是静默变成空气(且当前安全) — 我核对了 DefaultedMappedRegistry.get(ResourceLocation) 的字节码:v = super.get(name); return v == null ? this.defaultValue.value() : v;,即缺失键返回注册表默认值(对 BLOCK 就是 air),永不返回 null。所以不会 NPE,真正的风险是:若将来 Color 增加一个没有 vanilla 混凝土对应的颜色,solidify 会把源头直接换成空气(水泥凭空消失,且无任何日志)。当前 16 色 white_concretepink_concreteColor.getSerializedName() 一一对应,当前不会触发;仍建议加显式校验(解析到 AIR 时放弃凝固或抛异常),并顺带把字段/javadoc 与「实际永不返回 null」对齐。

💡 建议(非阻塞)

  • 时序自检:纯随机刻驱动,默认 randomTickSpeed=3 下单个源头约 68 s 命中一次、期望 4 次命中才凝固 ⇒ 约 4~5 分钟才硬化一次;若设计预期是「几秒」,需要改用计划刻/计数器。
  • 文档ageratum/{zh_cn,en_us}/…/005_cement.md 目前只写「用来合成混凝土」,未提世界行为(沉降、≈0.25/随机刻凝固、糖/粘液/蜂蜜交互),新机制建议补上。

🟢 看起来不错

  • 双派发/旧快照诊断准确(非猜测,字节码可复现);「实时状态守卫 + 同刻去重」双重防护方向正确,current.isSource() 早退同时消除了 stale snapshot 导致的源头增殖。
  • 环 1/环 2 = amount 7/6 就是紧贴源头的两圈(已用 LiquidBlock.<init>+getNewLiquid 字节码核实,与 javadoc 一致);Color 16 色名与 vanilla 混凝土名严格对应。
  • 只让源头参与随机刻(流动部分 isRandomlyTicking() 保持 false)、BFS 只扫 sourceY/targetY 两层且有常数上界、visited 去重与 immutable() 处理正确 —— 满足 [TODO] 水泥流体行为 #4972「不能产生大量同 gt 运算」的要求。
  • 未新增 @SuppressWarnings、无调试残留、无 TODO、无敏感信息;CI 双绿。

📋 声称验证表

声称 状态 依据
resolved #4972 ⚠️ 待确认一处 已核到 issue 正文:源头沉降 / 25% 源头+两圈凝固为原版混凝土 / 糖只挡凝固、粘液只挡下移、蜂蜜两者都挡 / 性能要求 —— 实现逐条对应 ✅;唯「level6 和 level7」的数字空间需按上面 ⚠️1 与作者确认(离线可读到 issue,无需 token)
添加水泥流体世界行为 CementFluid.java(新增 293 行)+ ModFluids 注册切换到 CementFluid.Source/Flowing
源头不再成倍增殖 实时状态守卫 + 同刻去重(派发链已按原版字节码核实)

结论: COMMENT(建议保持 Draft 直到 ⚠️1 与 issue 作者对齐、⚠️2/⚠️3 处理完成) — 无编译级或必然运行缺陷,机制方向正确且注释自证;主要待办是澄清 level 数字空间、去重状态别强引用 Level、确认 64 格上限在宽台面上的表现。

标题建议(本次 gh auth status = token invalid,无法自动修改,故未执行 gh pr edit):
Add world behavior for cement fluid (source settling + solidify into concrete) 添加水泥流体的世界行为(源头沉降与凝固为混凝土)
现标题英文部分「Add concrete fluid behavior」有歧义,容易读成「新增一种混凝土流体」,实际是「给水泥流体新增世界行为」。

🧪 测试建议

被测目标 推荐场景 优先级
CementFluid.randomTick 同刻双派发下凝固概率≈25%(1 万次统计);沉降后第二次派发不再生成源头 🔴
findLowerFlow 台面半径 5 成功沉降 vs 半径 6~7 触发 MAX_SETTLE_SEARCH 的行为确认;金字塔交错水流不串到隔壁水股 🟡
solidify 精确命中 amount 7/6 两圈;相邻内圈格指向同一外圈格时 Set 去重(不重复写块/触发更新) 🟡
三方块交互 糖/粘液/蜂蜜各只阻止其对应行为;对角不算接触 🟡

由 Hermes Agent 审查(diff: patch-diff.githubusercontent.com 匿名获取,md5 8646530947f39ac43262d49525ddab18;原版行为用 Mojang 1.21.1 client_mappings + 混淆客户端字节码离线核对:tickChunk/LiquidBlock/FlowingFluid/DefaultedMappedRegistry

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/head-sha-freshness-and-prior-review-verification.md in skill 'github-webhook-pr-review' (1 replacement). · Patched SKILL.md in skill 'minecraft-modding' (1 replacement).

- 改为计划刻驱动水泥源头逐格下沉,避免源头在随机刻中重复复制
- 源头下沉时优先挪动最近的源头,保持源头数量稳定不变
- 限定搜索方向为四个水平方向加正上方,避免够到无关水体
- 调整凝固概率从 0.25 降至 0.1,防止过快凝固
- 只认实时流体状态,防止快照导致的重复操作或错判
- 添加判断接触糖块、粘液块和蜂蜜块时的特殊处理,控制下移及凝固行为
- 实现重复随机刻派发识别,避免同一刻多次凝固判定
- 规范水泥颜色判断,确保源头与流动部分正确区分和处理
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

which unzip javap java python3 2>&1; echo "---"; ls /usr/lib/jvm 2>/dev/null; echo "--- test jar mapping ---"; cd /tmp && python3 -c "1" 2>&1 | head -2

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

分析完成。以下是审查意见:


代码审查摘要 — PR #4982

操作: synchronize
范围: 2 个文件(1 新增 / 1 修改)/ 406 行 diff

  • src/main/java/dev/dubhe/anvilcraft/fluid/CementFluid.java(新增,367 行)
  • 🔧 src/main/java/dev/dubhe/anvilcraft/init/block/ModFluids.java(+6/-2:1 行 import + 两处 BaseFlowingFluid.Source/FlowingCementFluid.Source/Flowing

无删除文件、无 ghost 文件、无 EOF 缺换行、无 TODO/调试残留。增量构成本 PR 全部内容,无越界改动。

⚠️ 本环境无 JVM/gradle,无法执行 compileJava。以下 API 存在性与行为结论通过 Mojang 官方 client_mappings(1.21.1)+ 反混淆客户端字节码 + NeoForge 21.1.238 universal jar 逐项核对得出,请在本地补跑 ./gradlew compileJava --console=plain 与实机验证。


🟢 核对通过(含证据)

1. 内嵌 Source/FlowingBaseFlowingFluid.Source/Flowing 的等价替换(本 PR 最关键的结构性检查)
核对 NeoForge 21.1.238 BaseFlowingFluid 的类结构:

覆写的成员
BaseFlowingFluid$Source <init> / getAmount(=8) / isSource(=true)
BaseFlowingFluid$Flowing <init>(registerDefaultState LEVEL=7) / createFluidStateDefinition(add LEVEL) / getAmount(getValue(LEVEL)) / isSource(=false)

CementFluid.Source/Flowing 覆写的成员与之逐一对应BaseFlowingFluid 自身已实现 getSlopeFindDistance/getDropOff/createLegacyBlock/getTickDelay/getBucket/getPickupSound 等,因此 CementFluid extends BaseFlowingFluid 可直接实例化且无遗漏覆写 ✅。另核对全仓:BaseFlowingFluidMeltGemFluidModFluids 引用,无 BaseFlowingFluid.Source.class / instanceof BaseFlowingFluid 之类会因换类而失效的判定。

2. 用到的 API 在 1.21.1 全部存在FlowingFluid.spread(Level,BlockPos,FluidState)(protected)、getNewLiquid(Level,BlockPos,BlockState)canSpreadTo(BlockGetter,…)Level 实现 BlockGetter ✅)、Fluid.randomTick(…)FluidState.createLegacyBlock()Level.setBlockAndUpdateFluid.LEVEL 常量继承自 FlowingFluid ✅。

3. 覆写 randomTick 不调 super 无行为损失:1.21.1 中 Fluid.randomTick(...)空实现epd.b(...) 仅一条 return),且 FlowingFluid 完全不覆写 randomTick/isRandomlyTicking。所以不存在「丢掉原版流动衰减」的问题。

4. Source.isRandomlyTicking() → true 是必需的Fluid.isRandomlyTicking() 默认 falseepd.i()Z = 03 ac = iconst_0; ireturn),改动前水泥源头一个随机刻都收不到,不覆写则凝固逻辑永不触发 ✅。

5. isDuplicateDispatch 的去重前提是真实的,不是过度防御 —— 这条值得保留并写清楚,我一开始也怀疑它多余,字节码核对后确认成立:

  • LiquidBlock.isRandomlyTicking(state) = state.getFluidState().isRandomlyTicking()dko.d_ = 2b b6 00 92 b6 00 a6 ac);
  • LiquidBlock.randomTick(...) = state.getFluidState().randomTick(level, pos, random)dko.b(…) = getFluidState → randomTick → return);
  • ServerLevel.tickChunk 每格同刻BlockState.randomTick FluidState.randomTickaqu.a(Ldvi;I)V 调用表同时含 dtc.b(...) x1epe.b(...) x1)。

⇒ 本 PR 把 isRandomlyTicking 打开后,每个水泥源头方块也变成随机刻方块,同一格同一 gt 会被派发两次随机刻,不去重则概率变成 1-(1-p)²。注释里的说法是对的 ✅。

6. 源头是「挪」不是「复制」,落点确实生成真源头FlowingFluid.getLegacyLevel(state)state.isSource() ? 0 : …epc.e(Lepe;)I 开头 2a b6 00 db 99 00 05 03 ac),BaseFlowingFluid.createLegacyBlock 走它 → 源头 createLegacyBlock() 得到 LEVEL=0LiquidBlock(真源头)✅。另外 transferSourceDown 的两处 setBlockAndUpdate 触发的都只是排程onPlace/neighborChangedscheduleTick),不会同刻重入 spread,因此「先放后清」的短暂重复不会被观察到;源头被挪走时 return true 跳过 super.spread 也正确避免了用陈旧 state 把源头在原地重建出来。

7. 颜色映射完整Color.getSerializedName() = whitepink,16 色对应原版 <color>_concrete 全部存在,且 ConcreteRecipeLoader.java:50 已是同款写法 ✅。

8. 与 issue #4972 的交互约定一致:粘液块→不下移、糖块/蜂蜜块→不凝固、蜂蜜块→两者都不,Direction.values() 六向判定 ✅。凝固环常量 7/6(FluidState amount)= 方块 LEVEL 1/2 = 紧贴源头的两圈,与 issue「附近 level6 和 level7 的流动部分」意图一致,javadoc 的换算说明也正确 ✅。


⚠️ 需要确认或修复

1. 凝固概率与 issue 规格不符:SOLIDIFY_CHANCE = 0.1F vs issue 的 25%
issue #4972 原文:「源头被随机刻选中且无法向下移动时,25% 概率源头和该源头附近 level6 和 level7 的流动部分都凝固」。去重后每刻有效概率即 10%。单格被随机刻命中的期望约 1365 gt(~68 s),对应期望凝固时间约 11 分钟(25% 时约 4.5 分钟)——差距是玩家可感知的。若是有意调低,请在 PR 描述里说明理由;否则建议改为 0.25F

2. isDuplicateDispatch 用注册期单例缓存了 Level 强引用
lastTickLevel 存在 Fluid 单例上,实例与进程同寿命 → 会一直强引用「最后一次被随机刻的 Level」。集成服务器退世界后,那个 ServerLevel(连同地图/世界数据)无法释放,直到水泥在另一个 Level 里被 tick 到。虽然最多只滞留 1 个对象,但整个 ServerLevel 不算小事。建议改用 level.dimension()ResourceKey<Level>,不会持有 Level)或 WeakReference<Level> 参与比较。

3. setBlockAndUpdate(target, …) 的返回值被忽略 → 极端情况下水泥凭空消失

level.setBlockAndUpdate(target, sourceState.createLegacyBlock());
level.setBlockAndUpdate(sourcePos, Blocks.AIR.defaultBlockState());   // 无条件清空

pos.below() 落在世界底部(y = minBuildHeight - 1)或目标区块未加载时,写入会静默失败(返回 false),但下一行仍把源头抹成空气——源头就此丢失。建议:

if (!level.setBlockAndUpdate(target, sourceState.createLegacyBlock())) {
    return false;
}

(并顺带确认 canFlowDown 在世界底部的行为,避免「能下流但其实写不进去」的组合。)

4. 触发节奏与 issue 的性能约束需要给个交代
issue 明确要求「触发条件以随机刻为例,也可以写成别的方式,但基本时间间隔要差不多,且不能产生大量同 gt 运算」。本实现改在 spread(计划刻,getTickDelay=5 gt)里触发:

  • 下沉节奏远快于随机刻(源头基本以 1 格 / 5 gt 跟随水流下落,随机刻方案约 1 格 / 68 s)——是否符合预期?
  • 每个「能向下淌」的流动格都会在同 gt 各跑一次 findNearestSource(最多 64 次出队 × 5 方向 + 每次 ObjectOpenHashSet/ArrayDeque 分配)。静止池子廉价(canFlowDown 为假即短路),但大面积斜坡上一次性倒满水泥时,同 gt 的 BFS 次数可能很可观,正是 issue 想避免的情形。建议补一次实测(大平台/金字塔场景下的 MSPT 或 BFS 调用次数),或把触发限制为「每 gt 每个流体最多转移一次」(可复用去重字段)。

💡 建议(非阻塞)

  • 注释准确性Source.isRandomlyTicking() 的注释说「流动部分即便被随机刻选中也不做任何事,让它参与只会白白增加随机刻负载」——实际上流动部分根本收不到随机刻Fluid 默认 false,FlowingFluid 不覆写,LiquidBlock 也只是委托给流体),!current.isSource() 守卫是纯防御性的。建议改成「流动部分不参与随机刻,此处守卫仅作防御」。
  • 把双派发机制写进注释randomTick 里的「原版对同一格会连续派发两次随机刻」建议展开为 LiquidBlock.randomTick → FluidState.randomTick + tickChunk 自身再派发一次——否则后续读者很容易把 isDuplicateDispatch 当冗余删掉,概率随即翻倍。
  • 下落柱(falling, amount == 8)不在凝固范围内isFlowOfAmount 只匹配 7/6,若出现「源头被粘液块/蜂蜜块挡住不能下移、但流动部分仍向下淌」的组合,凝固后那条下落柱会留下仍在流动的水泥。是否期望一并凝固?确认一下即可。
  • concrete() 的静默回退BuiltInRegistries.BLOCK 是 DefaultedRegistry(缺省 air),get() 对未知 id 不会返回 null 而是返回空气 → 字段上的 @Nullable + null 判断是误导性的,且映射出错时会静默把水泥替换成空气。虽然 Color 是闭集 16 值、当前不可能发生,仍建议用 getOptional(...) 或断言非 Blocks.AIR;另可考虑与 ConcreteRecipeLoader 共用一份映射辅助方法。
  • 行宽:最长新增行 139 字符,style.xmlLineLength 上限 140——能过,但 CJK 文本几乎没余量了。
  • PR 描述resolved #4972 不会自动关闭 issue(issue 目前仍 open),建议改成 Fixes #4972
  • 标题:水泥是 cement,凝固产物才是 concrete,现标题的英文部分不准确;建议改为
    Add cement fluid world behavior 添加水泥流体在世界中的行为
    (本次 gh auth status 显示 token 已失效,未自动修改,请在本地执行 gh pr edit 4982 --title "…"。)

📋 声称验证表(对照 issue #4972

issue 要求 状态 对应实现
把最近的「高一格源头」挪到流动部分已到达的更低格 ⚠️ 机制符合,但触发源改为计划刻(issue 允许替代方案,节奏/性能需确认) transferSourceDown + findNearestSource(BFS,含 Direction.UP,限 64 格、纵向 ±1)
25% 概率凝固 ⚠️ 实现为 10% SOLIDIFY_CHANCE = 0.1F
源头 + 附近 level6/level7 流动部分一起凝固为对应颜色混凝土 solidify + isFlowOfAmount(7/6),先取内圈再扩外圈、Set 去重
接触糖块的源头不凝固、也不引发凝固 isSolidifyBlockedSUGAR_BLOCK
接触粘液块的源头不移动走 isMoveBlockedSLIME_BLOCK
接触蜂蜜块的源头两者都不 两个守卫均含 HONEY_BLOCK
避免性能问题、不产生大量同 gt 运算 ⚠️ 需实测(见 ⚠️4)

🧪 测试建议

被测目标 推荐场景 优先级
transferSourceDown / findNearestSource 阶梯、金字塔、垂直落差:源头逐格下沉;全程源头数量恒为 1(反复倒/收,统计源头格数) 🔴
isDuplicateDispatch 固定随机源,统计 N 次随机刻的实际凝固比例应为 ~10%(若去重失效会变成 ~19%,可直接暴露双派发) 🔴
solidify 定格验证:源头 + 两圈流动部分同时变混凝土;内圈/外圈 Set 去重(相邻内圈共享外圈格不会被重复写) 🔴
边界 世界底部 y=minBuildHeight 处倒水泥(setBlockAndUpdate 失败路径);区块边界/未加载区块 🟡
三色阻挡 糖块 / 粘液块 / 蜂蜜块分别贴源头:不凝固 / 不下移 / 两者都不 🟡
异色相邻 两种颜色水泥相邻流动:源头落点已有异色流动部分时会被覆盖(当前只检查了 isSource()),确认是否符合预期 🟡
性能 大型平台/斜坡一次性倒满 16 色水泥,观察 MSPT 与同 gt BFS 次数 🟡

结论: COMMENT — 核心机制(源头下移、双派发去重、两圈凝固、颜色映射、Source/Flowing 等价替换)经字节码与 mappings 逐项核对成立且实现干净,注释质量也高于平均水准。合并前请明确两点:(1) 凝固概率 10% 是否有意偏离 issue 的 25%;(2) 计划刻触发带来的下沉节奏与同 gt BFS 负载是否满足 issue 的性能约束(最好附实测)。另外建议顺手修掉 Level 强引用与忽略 setBlockAndUpdate 返回值这两处。

由 Hermes Agent 审查


📎 附带说明(不属本 PR):本次把两条可复用核对手法写进了 github-code-review 技能——新增 scripts/mc_method_bytecode_dump.py(免 JVM 读方法体字节码)与 references/fluid-random-tick-double-dispatch.mdLiquidBlock ↔ FluidState 双派发机制、NeoForge BaseFlowingFluid 等价替换核对表),下次遇到自定义流体 PR 不用再重推一遍。

@PigeonNian
PigeonNian marked this pull request as ready for review September 21, 2026 14:50
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

curl -sL "https://patch-diff.githubusercontent.com/raw/Anvil-Dev/AnvilCraft/pull/4982.diff" -o /tmp/pr4982b.diff; md5sum /tmp/pr4982.diff /tmp/pr4982b.diff; echo "--- issue 4972 body ---"; curl -sL "h...

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/issues/4972 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilCraft/issues/4972 (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

All checks done. Here is the review.


代码审查摘要 — PR #4982

操作: ready_for_review
范围: 2 个文件 / +373 −2(与 API 元数据 changed_files=2, additions=373, deletions=2 完全一致,diff 完整无截断)

  • 新增 fluid/CementFluid.java(367 行,abstract CementFluid + 内嵌 Source / Flowing
  • init/block/ModFluids.java:把 BaseFlowingFluid.Source/Flowing 换成 CementFluid.Source/Flowing(+1 import)

结论先行:未发现编译错误、崩溃路径或物品/方块丢失缺陷;实现与 TODO #4972 的规格逐条对应。 有 2 处与规格的数值/节奏偏差、1 处 NeoForge 事件约定、1 处单例状态问题建议确认(均非阻塞)。

✅ 已交叉核对通过(附证据)

检查项 结论 证据
调用的 MC API 可访问性 ✅ 全部 protected,子类可调 1.21.1 client_mappings + 客户端 jar 字节码探测:FlowingFluid.canSpreadTo(BlockGetter,…,Fluid)ZgetNewLiquid(Level,BlockPos,BlockState)spread(Level,BlockPos,FluidState)protected
覆写可见性 ✅ 与父类一致,不会「降低访问权限」编译失败 Fluid.randomTick(...)Fluid.isRandomlyTicking() 在 1.21.1 均为 protectedepd 字节码),PR 用 protected 正确
Source/Flowing 覆写集合 ✅ 等价替换 与 NeoForge BaseFlowingFluid$SourcegetAmount/isSource)、$FlowingregisterDefaultState(LEVEL,7)/createFluidStateDefinition/getAmount/isSource)一致,无遗漏
随机刻去重是否必要 ✅ 确实必要,不是过度防御 水泥方块是原版 LiquidBlockModBlocks:4490),其 isRandomlyTicking/randomTick 委托给流体 ⇒ isRandomlyTicking()=true 时同格同 gt 被派发 2 次,不去重则概率变 1-(1-p)^2
环常量 7 / 6 是否正确 ✅ 正确,且注释换算口径准确 getLegacyLevel(state) = isSource ? 0 : 8 - min(8, amount)(字节码 epc.e(Lepe;)I 实测)⇒ amount 7 ↔ 方块 LEVEL 1(紧贴源头)、6 ↔ LEVEL 2;createCementProperties 未改 levelDecreasePerBlock(默认 1)与 slopeFindDistance(默认 4),故两圈确实存在
混凝土解析 ✅ 16 色全部命中 Color 的 16 个序列名与原版混凝土同名;仓库既有 ConcreteRecipeLoader:49-50 用同一 BuiltInRegistries.*.get(ResourceLocation.withDefaultNamespace("%s_concrete")) 写法
空值注解约定 ✅ 符合本分支 dev/1.21/1.6 的 AGENTS.md 明确要求 javax.annotation.Nullable,分支内 328 个文件在用、jspecify 0 个
炼药锅安全性 ✅ 不会被新逻辑改写/吞掉 CementCauldronBlock/BetterAbstractCauldronBlock/Layered4LevelCauldronBlock 均未覆写 getFluidState;NeoForge 的 AbstractCauldronBlock 补丁只动 capabilities,CauldronFluidContent 不提供流体状态 ⇒ 锅位不会返回水泥源头 FluidStateisSameCement 直接否决
规格对应(#4972 ✅ 见下表

⚠️ 建议确认

  1. 凝固概率与规格不一致(CementFluid.java:39 — 规格原文:「源头被随机刻选中且无法向下移动时,25% 概率源头和该源头附近 level6 和 level7 的流动部分都凝固成对应颜色的原版混凝土」,而 SOLIDIFY_CHANCE = 0.1F10%。若是有意的平衡下调,建议在 javadoc/PR 描述里说明;否则应改回 0.25。(环的两圈口径按流体 amount 7/6 实现,与规格「附近 level6/7」自洽,这点没问题。)
  2. 下沉节奏远快于规格示例 — 规格以随机刻为例并注明「也可以写成别的方式,但基本时间间隔要差不多」。随机刻下的期望间隔约 4096/3 ≈ 1365 gt/格(≈68 s),本实现走计划刻、每格 getTickDelay() 默认 5 gt ≈ 0.25 s,快约 270 倍。javadoc 已解释为何选计划刻(避免同 gt 复制源头),但这是玩家可见的手感差异,建议请维护者确认这是预期效果。(规格要求的「不能大量同 gt 运算」这点实现是满足的。)
  3. solidify() 未触发 EventHooks.fireFluidPlaceBlockEventCementFluid.java:269 起) — 流体→实心方块的转换在本仓库既有约定是包一层该事件:MeltGemFluid:33(流体→陶瓦)与 ModFluids:399(熔宝石→方块)都这么做。水泥→混凝土同理,依赖该事件做区域保护/防破坏的模组会漏掉这次放置。建议 level.setBlockAndUpdate(pos, EventHooks.fireFluidPlaceBlockEvent(level, pos, pos, concreteState))(含两圈)。
  4. 单例上的 Level 强引用(CementFluid.java:69-72lastTickLevel/lastTickPos/lastTickTime 存在注册期单例(每种颜色各一份)上,去重逻辑本身正确(原版两次派发是同格背靠背,故「最后一次」判据够用),但 lastTickLevel 会强引用最后 tick 过的 Level:集成服务器退世界后该世界对象(含区块)无法回收。建议比对 level.dimension() 或改用 WeakReference<Level>;另外这些字段无同步(原版单服务器线程下无碍,只在并行区块 tick 类模组下会退化为概率翻倍)。

💡 建议

  • concrete() 可能返回 nullCementFluid.java:307-316):当前 16 色都能命中,但将来若 Color 新增无对应原版混凝土的值,会在 defaultBlockState() 处 NPE。可加一次空值兜底(或直接在注册期建表)。
  • transferSourceDown 中硬编码 Blocks.AIR:127-128):与原版 FlowingFluid.tick 流体耗尽时的写法一致,作为防御可在置空前确认该格方块确为水泥的 LiquidBlock
  • 指南未同步ageratum/{zh_cn,en_us}/002_material/005_cement.md 目前只写「获取 / 合成混凝土」,新增的世界行为(源头逐格下沉、无法下移时凝固)值得补一段。

📋 规格核对表(TODO #4972

规格条目 状态 实现位置
源头被选中且流动部分已到更低一格 → 把最近的高一格源头挪下来 spreadtransferSourceDown + findNearestSource(BFS 含 Direction.UP、`
无法下移时按概率凝固(源头 + 附近 level6/7 流动部分 → 对应色原版混凝土) ⚠️ 概率 10% ≠ 25% randomTicksolidifySOLIDIFY_AMOUNT_RING_1/2 = 7/6
源头只「挪」不「复制」,数量守恒 落点已是同色源头则放弃并源;原格 setBlockAndUpdate(..., AIR)
接触糖块的源头不凝固、也不引发凝固 isSolidifyBlockedSUGAR_BLOCK
接触粘液块的源头不移走 isMoveBlockedSLIME_BLOCK
接触蜂蜜块的源头既不凝固也不移走 同时出现在两个判定里
避免大量同 gt 运算/性能问题 每格每 tick 最多一次下沉;BFS 只入队水泥格,受 MAX_SOURCE_SEARCH = 64 兜底
触发方式可不同,但间隔要差不多 ⚠️ 见上第 2 条 计划刻 5 gt/格

🧪 测试建议

被测目标 推荐场景 优先级
CementFluid.randomTick 同一 gt 同格两次派发只掷一次骰(去重);跨维度/跨世界切换后去重不误判 🔴
transferSourceDown 阶梯/金字塔边缘由流动格触发;落差直落(源头在正上方);落点已有同色源头(不并源、总数不变);源头相邻糖/粘液/蜂蜜时行为分别符合规格 🔴
solidify 内圈 amount=7、外圈 amount=6 两圈都转换;相邻两个内圈格指向同一外圈格时只写一次;非本色的流动水泥不被误转 🟡
findNearestSource 直落场景必须命中(回归「完全不触发」);超出 MAX_SOURCE_SEARCH 返回 null 且不移动 🟡
concrete() 16 个 Color 全部解析到 minecraft:<color>_concrete;结果被缓存(只查一次注册表) 🟢

标题建议

PR 英文标题写成了 "Add concrete fluid behavior",但本 PR 加的是水泥(cement,类名 CementFluid)的流体行为,concrete(混凝土)是它的产物——中文标题「添加水泥流体行为」是对的。建议改为:Add cement fluid behavior 添加水泥流体行为。本次 gh auth status 显示 token 已失效(The token in /opt/data/home/.config/gh/hosts.yml is invalid),故未自动修改。

结论: COMMENT — 无阻塞性缺陷,逻辑与规格逐条对应且关键常量(环 7/6、isLegacyLevel 换算、去重必要性)经字节码/mappings 实测无误;建议先与维护者确认 ⚠️1(10% vs 25%)与 ⚠️2(下沉节奏)是否为预期,并补上 ⚠️3 的事件触发后合并。


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Skill 'custom-fluid-behavior-pr-review' created. · Patched SKILL.md in skill 'gas-system-pr-review' (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.

[TODO] 水泥流体行为

2 participants