Skip to content

Plan recipe batches within container capacity 添加受容器容量约束的配方批次规划 - #245

Merged
PigeonNian merged 1 commit into
Anvil-Dev:dev/26.2from
WhereisFff:codex/fix-5961-batches-26.2
Nov 8, 2026
Merged

PigeonNian merged 1 commit into
Anvil-Dev:dev/26.2from
WhereisFff:codex/fix-5961-batches-26.2

Conversation

@WhereisFff

Copy link
Copy Markdown
Contributor

feat(recipe): 添加受容器容量约束的批次规划

  • 基于库存快照选择能容纳全部产物的最长连续批次,支持满槽压缩
  • 保留未加工原料及可复用催化剂,阻止本轮产物再次参与输入
  • 预计算随机与组件函数结果,在库存提交成功后执行选中批次副作用
  • 补充鱼缸接入、槽位限制和外部资源校验的接口文档
  • resolved #5961

- 基于库存快照选择能容纳全部产物的最长连续批次,支持满槽压缩
- 保留未加工原料及可复用催化剂,阻止本轮产物再次参与输入
- 预计算随机与组件函数结果,在库存提交成功后执行选中批次副作用
- 补充鱼缸接入、槽位限制和外部资源校验的接口文档
@Gugle2308

Copy link
Copy Markdown

🌿 Roseau API Breaking Change Report

Module Status Breaking Changes
codec ✅ Compatible 0
collision ✅ Compatible 0
cube ✅ Compatible 0
config ✅ Compatible 0
integration ✅ Compatible 0
moveable-entity-block ✅ Compatible 0
network ✅ Compatible 0
rendering ✅ Compatible 0
space-select ✅ Compatible 0
font ✅ Compatible 0
util ✅ Compatible 0
explosion ✅ Compatible 0
rpc ✅ Compatible 0
math ✅ Compatible 0
multiblock ✅ Compatible 0
recipe ✅ Compatible 0
registrum ✅ Compatible 0
sync ✅ Compatible 0
ui ✅ Compatible 0
wheel ✅ Compatible 0
main ✅ Compatible 0

Full CSVs: see the Artifacts section of this workflow run.

@PigeonNian
PigeonNian merged commit 39898da into Anvil-Dev:dev/26.2 Nov 8, 2026
1 check passed

@Gugle2308 Gugle2308 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论:此为第 6 条版本线,算法本体与已合并版本五条线逐字节相同;但库自带的 ChooseOneOutcome 仍无法参与容量规划——choose_one 产物的配方仍会复现 #5961 的产物溢出,需在 AnvilCraft 接入前补齐;另,344 行回溯搜索零单元测试。

审查范围:dev/26.2 → head 35de5faf,5 文件 +439/−10(patch-diff 与 API 声明逐位一致)、draft=false、state=open、CI build / build 仍 in_progress、无既有评论或 review。

跨线基线核对:ItemBatchPlan.java(344 行)、IPlannedItemOutcome.java、PlannedItem.java 与已合并的 dev/1.21.11(#242)逐字节相同;SpawnItem.java 因线别 API 不同(ItemStackTemplate vs ItemStack、typeHolder()/components() vs getItemHolder()/getComponentsPatch()),plan() 主体结构一致;README.md 差异仅为既有示例的 26.2 命名(Blocks.CUT_COPPER_STAIRS.waxed()、EntityTypes.COW),新增的「容器配方批次规划」整节逐字相同。因此以下结论对全部 6 条线成立。

🔴 一、ChooseOneOutcome 绕开容量规划:#5961 的核心场景仍会复现

IPlannedItemOutcome 是"全部产物"的唯一声明入口(IPlannedItemOutcome.java:14),但库自带的 ChooseOneOutcome 未实现它:

// ChooseOneOutcome.java:26
public record ChooseOneOutcome(NumberProvider chance, List<Choice> choices)
    implements IRecipeOutcome<ChooseOneOutcome> {      // ← 不是 IPlannedItemOutcome

// :116  Choice 可持有任意 outcome,包括 SpawnItem
public record Choice(IRecipeOutcome<?> outcome, NumberProvider weight) {}

// :84   执行时直接产出
this.choices.get(i).outcome().acceptWithChance(context);

后果两条:

  1. 规划侧完全不可见 —— plan/planWithChance 只存在于 IPlannedItemOutcome,ChooseOneOutcome 既无 plan,也不暴露内部候选。按 README 收集 IPlannedItemOutcome 去规划时,这份产物不进 products,容量算少;真正执行时 InWorldRecipe.java 的 outcome.acceptWithChance(context) 照样产出 → 超出容量的部分再次掉落,与 #5961 现象一致。
  2. 调用方无法"安全拒绝" —— choose_one 不暴露内部 outcome,只能用 instanceof ChooseOneOutcome 整类排除配方,等于放弃功能。

下游正在使用(AnvilCraft 默认分支 dev/1.21/1.6 实测 ItemCrushRecipeLoader.java:94):

.chooseOne((builder) -> builder
    .choice(SpawnItem.builder().offset(0, -.6875, 0).item(ModItems.RUBY).build(), .25f)
    .choice(...).choice(...).choice(...))

建议(前者更彻底):让 ChooseOneOutcome implements IPlannedItemOutcome<ChooseOneOutcome>,plan 复用与 accept 同一套权重前缀和选出下标,再委托 choices.get(i).outcome() instanceof IPlannedItemOutcome<?> p ? p.plan(context) : List.of(),并保证 planWithChance→plan 只抽样一次;或至少在 README 明确"含 choose_one 的配方必须由调用方整体排除"。

⚠️ 二、344 行回溯搜索与公开 API 零单元测试

仓库其他模块已有测试源集(module.config / module.math(含 MathTestBootstrap)/ module.registrum / module.rendering / module.util),而 module.recipe/src/test 不存在,本 PR 新增的 344 行回溯 + 备忘录 + 容量回调无任何测试覆盖。

建议的最小用例矩阵(均可用小型假 SlotCapacity 表达):

用例 期望
单产物恰好填满全部槽位 batches() == limit
产物总量超出容器容量 batches() 收敛到最大可行前缀
shared 且输出槽全被原料占满 靠后续批次腾槽后成功(README 的"满槽压缩")
空槽对不同物品容量不同 fillEmpty 回溯找到有效布局(而非贪心失败)
take 超过匹配总量 返回 List.of() 且 remaining 不变
catalyst 跨批复用 第 2 批仍可取到同一催化剂
capacity 返回负数 IllegalArgumentException
commit 二次调用 IllegalStateException

⚠️ 三、fillEmpty 最坏复杂度与"每批重算"缺搜索预算上限

fillEmpty 逐空槽 × 逐产物 + "留空"分支回溯,靠 failed(LayoutState = (index, remaining))去重。去重键本身正确:同一 index 之后待填槽位与容量规则只依赖 remaining,容量回调按槽号取值、不依赖已填布局。但:

  • 递归深度 = 空槽数(最多 54),分支数 = 产物种类数 + 1;
  • 不同 remaining 向量数随产物种类数组合增长;
  • fit 在 plan 的每一轮批次都从头重跑,无增量复用。

最坏为 批次上限 × 组合状态数,仓库无性能门禁,CI 不会暴露。建议加可配置的节点预算(超限保守返回 null = 取更短前缀,语义安全),并在 README 注明。

💡 四、resolved #5961 是跨仓引用,且合入不等于 issue 已修复

PR 在 AnvilLib,issue #5961 在 AnvilCraft。resolved #5961 只会被解析为本仓库的 #5961,不会建立跨仓链接,应写 Anvil-Dev/AnvilCraft#5961。且该 issue 当前已是 closed,本 PR 只提供库侧 API、AnvilCraft 接入尚未发生(正文自述"库发版后由 AnvilCraft 更新依赖并接入"),建议把 resolved 改为 part of / contributes to。

💡 五、commit() 两处边界建议补文档

this.committed = true;
if (this.batches == 0) return;          // ① 不调用 inventoryCommit
inventoryCommit.accept(this);
this.effects.forEach(Runnable::run);    // ② 某条 effect 抛异常会中断后续 effect
  • ① batches == 0 时直接返回、不调用库存回调。若下游把"外部资源(燃料/流体)校验"也放在该回调里会静默跳过——README 只写了"库存回调失败会向上传递异常"。
  • ② 库存已提交后,effects 中任一条抛异常会跳过其余,留下"部分副作用已执行"的中间态。README 覆盖了"库存回调失败不执行副作用",未覆盖"副作用自身失败"。

💡 六、Batch.take/catalyst 的 count <= 0 抛异常未在 README 说明

reserve 对 count <= 0 抛 IllegalArgumentException。README 详述了"不足时返回空列表且不改变预约",但没提"非正数直接抛"。

✅ 七、已验证正确的部分

SpawnItem 重构是行为等价重构(对照 base 逐行核对):

检查点 结论
plan() 与旧 accept() 求值顺序 完全一致:getInt(count,0,99) → item.withCount(1).create().copyWithCount(count) → BlockCache + IRecipeResultOffsetBlock 修正偏移 → 逐个 IOutcomeFunction.apply
count == 0 早退 旧 return → 新 List.of() ✅
ItemCache 注册时机 旧:accept 内 putAcceptor;新:PlannedItem.accept 内同一位置 ✅
概率抽取次数 IRecipeOutcome.acceptWithChance(:47,getFloat(this.chance()))与新增 IPlannedItemOutcome.acceptWithChance 都是"先 roll chance,再 roll count",各一次 ✅
残留 import SpawnItem 已移除 ItemCache/ICacheOutput,零残留 ✅
其它 outcome 未受影响 本分支 5 个 implements IRecipeOutcome 的类中只有 SpawnItem 被改 ✅

算法完备性:fillEmpty 对每个空槽穷举"放哪个产物(取最大可放量)/ 留空"全部分支,kinds == 0 才返回 true,返回 true 时产物必然全部落位;requiredSlots/space 两处剪枝都是合法下界(remaining > 0 && space >= remaining 已保证 space > 0,故 largest > 0)。plan 允许"第 k+1 批装不下、第 k+2 批装得下"而接受前缀 1..k+2,正是满槽压缩,acceptedEffects = effects.size() 快照语义与之一致 ✅

其它正向确认:take 不足时在任何 shrink 之前返回 List.of();产物从不进入下一批的 available;shared 下 inputs()/outputs() 指向同一最终布局;Math.addExact 防合并溢出;ItemBatchPlan 文件 344 行完整、无 >140 列行、无尾随空白。org.jspecify.annotations.Nullable 在本分支已被 25 个文件使用、根 build.gradle:100 启用 @NullMarked,不构成新问题;ItemStackTemplate 全仓 17 处既有用法,withCount(1).create() 为 26.x 标准 API。

优先修复顺序

  1. §一 ChooseOneOutcome —— 不修则 #5961 对 choose_one 配方无效,下游也无法自行绕过;
  2. §三 搜索预算 —— 容器配方会频繁调用,先给上限再接入;
  3. §二 测试 —— 至少覆盖上表 8 条;
  4. §四/§五/§六 文案与边界说明。

验证边界

未编译、未运行(依赖 MC 注册表),且本 head 的 CI 仍 in_progress,请以 Actions 最终结果为准。结论基于 head 源码逐行核对 + base 侧对照 + 一处 AnvilCraft 实际调用点取证(ItemCrushRecipeLoader.java:94)。ItemBatchPlan 目前全仓无调用方,故无集成回归可查;下游接入后的实际槽位/容量语义需 AnvilCraft 侧验证。

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