Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions module.recipe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,29 @@ InWorldRecipeBuilder.incompatible(trigger)
消耗直接移除实体,不触发伤害、死亡或击杀成就。开启物品掉落时,生物使用原生掉落生成流程(包含装备及 LivingDropsEvent),非生物保留其原有移除掉落,例如容器内容物;不会伪造玩家击杀条件。经验为生物经验奖励在无攻击者条件下乘倍率并向下取整,非生物为零,物品和经验可分别开启。奖励遵守 mob/entity drops 游戏规则,默认也会抑制容器移除时的自动掉落。

实体生成支持乘客 NBT,类型和根实体落点由配方决定,UUID 总是重新生成。未指定 NBT 的生物经过原生生成初始化。提交前失效的实体会使提交失败;沿用现有提交失败语义,不尝试回滚已经完成的其他世界修改。

## 容器配方批次规划

`ItemBatchPlan` 在库存快照上选取容量合法的最长连续批次。使用 `separate` 表示独立输入和输出,使用 `shared` 表示原料与产物共用槽位。规划会检查直到原料不足或达到调用方指定的批次上限,所以满槽压缩能够通过后续批次的累计消耗腾出产物槽位。

```java
ItemBatchPlan plan = ItemBatchPlan.shared(snapshot, maxBatches, (slot, stack) -> slotLimit, batch -> {
if (batch.take(stack -> stack.is(Items.IRON_INGOT), 9).isEmpty()) {
return false;
}
batch.produce(new ItemStack(Items.IRON_BLOCK));
batch.afterCommit(onRecipeCompleted);
return true;
});
plan.commit(selected -> commitInventory(selected.outputs()));
```

`take` 按槽位顺序预约原料并返回保留实际组件的消耗快照;不足时返回空列表且不改变预约。多种有重叠匹配条件的原料应由调用方先确定分配顺序。`catalyst` 在当前批次独占物品,批次结束后归还给下一批使用。产物在全部原料规划结束后才参与输出布局,禁止成为本次触发的后续原料。

输出容量回调返回该槽允许存放给定物品的最大数量,零表示拒绝,负数会报错。回调必须是无副作用的槽位规则,不能根据尚未提交的真实槽位内容拒绝即将被消耗的原料所占据的槽位。规划器会合并组件相同的物品,并在空槽有不同物品限制时尝试有效布局;产物无法装入时选择更短的合法前缀,不生成掉落物。

`IPlannedItemOutcome.planWithChance(context)` 只计算一次候选批次的概率、数量、组件函数和目标偏移,并返回 `PlannedItem`。`SpawnItem` 已实现该接口;自定义额外物品奖励也必须通过该接口完整声明。将已确定的 `stack()` 交给 `batch.produce`,提交时复用计划,禁止再次调用结果函数或抽取随机数。对于不同目标位置的产物,调用方须分别规划相应容器,禁止把其他位置的产物算入当前容器。

规划回调只能修改批次对象和私有规划数据。燃料、流体等其他资源也须在调用方的私有快照中验证足够执行所选批次;世界修改、资源消耗及完成事件通过 `afterCommit` 延迟。`afterCommit` 回调必须捕获每批独立的参数快照,禁止在执行时读取后续规划已经覆盖的共享上下文。`commit` 的库存回调负责在服务端同步校验原库存及燃料、流体等外部资源快照、提交输入和输出,成功返回后才执行所选批次的副作用。库存回调失败会向上传递异常且不执行副作用;计划只允许提交一次。共用槽位的 `inputs()` 和 `outputs()` 表示同一最终布局,只写入一次。

此入口由容器配方调用方显式使用,不会自动替换 `InWorldRecipeManager.trigger`。鱼缸接入时应在输入加工阶段选择独立布局,在输出再次加工阶段选择共用布局,并把现有物品条件、额外物品奖励及其他资源约束接入规划回调;不要在候选批次上提前调用有世界副作用的 `assemble`。库发版后由 AnvilCraft 更新依赖并接入。
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package dev.anvilcraft.lib.v2.recipe.outcome;

import dev.anvilcraft.lib.v2.recipe.util.InWorldRecipeContext;

import java.util.List;

/**
* 将物品结果计算与输出分开,使容量规划使用与提交相同的数量、物品、组件和位置。
* 实现只能计算结果及读取规划上下文;禁止修改真实库存、方块、实体或执行其他世界副作用。
* 自定义的额外物品奖励也应实现此接口,并将全部产物包含在返回值中。
*
* @param <O> 配方结果类型
*/
public interface IPlannedItemOutcome<O extends IPlannedItemOutcome<O>> extends IRecipeOutcome<O> {
List<PlannedItem> plan(InWorldRecipeContext context);

/** 每个候选批次调用一次并保存返回值;提交时不得再次调用以免重新抽取随机结果。 */
default List<PlannedItem> planWithChance(InWorldRecipeContext context) {
if (context.getLevel().getRandom().nextDouble() > context.getFloat(this.chance())) {
return List.of();
}
return List.copyOf(this.plan(context));
}

@Override
default void accept(InWorldRecipeContext context) {
this.plan(context).forEach(item -> item.accept(context));
}

@Override
default void acceptWithChance(InWorldRecipeContext context) {
this.planWithChance(context).forEach(item -> item.accept(context));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.anvilcraft.lib.v2.recipe.outcome;

import dev.anvilcraft.lib.v2.recipe.cache.ItemCache;
import dev.anvilcraft.lib.v2.recipe.util.InWorldRecipeContext;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec3;

/** 保存已经完成随机数量和组件函数计算的产物及其目标位置,供容量规划与最终提交复用。 */
public record PlannedItem(ItemStack stack, Vec3 pos) {
public PlannedItem {
stack = stack.copy();
}

@Override
public ItemStack stack() {
return this.stack.copy();
}

/** 使用世界内配方的常规输出规则提交已确定的产物;容器批次应将其交给槽位规划器。 */
public void accept(InWorldRecipeContext context) {
if (this.stack.isEmpty()) {
return;
}
ItemCache cache = context.computeIfAbsent(ItemCache.ITEM_CACHE);
cache.getOutput(this.stack.copy(), this.pos).grow(this.stack.copy(), true);
context.putAcceptor(ItemCache.ITEM_CACHE.location(), ItemCache.DEFAULT_ACCEPTOR);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import dev.anvilcraft.lib.v2.codec.CodecUtil;
import dev.anvilcraft.lib.v2.codec.StreamCodecUtil;
import dev.anvilcraft.lib.v2.recipe.cache.BlockCache;
import dev.anvilcraft.lib.v2.recipe.cache.ItemCache;
import dev.anvilcraft.lib.v2.recipe.cache.item.ICacheOutput;
import dev.anvilcraft.lib.v2.recipe.init.recipe.LibRecipeOutcomeTypes;
import dev.anvilcraft.lib.v2.recipe.outcome.function.ApplyTagToComponent;
import dev.anvilcraft.lib.v2.recipe.outcome.function.IOutcomeFunction;
Expand Down Expand Up @@ -46,7 +44,7 @@
* @param functions 函数列表
*/
public record SpawnItem(ItemStackTemplate item, Vec3 offset, ContextIntProvider count, List<IOutcomeFunction<?>> functions)
implements IRecipeOutcome<SpawnItem> {
implements IPlannedItemOutcome<SpawnItem> {
/**
* 构造一个新的生成物品配方结果
*
Expand All @@ -65,7 +63,13 @@ public record SpawnItem(ItemStackTemplate item, Vec3 offset, ContextIntProvider
* @param offset 偏移量
* @param count 数量
*/
public SpawnItem(Holder<Item> item, DataComponentPatch patch, Vec3 offset, ContextIntProvider count, List<IOutcomeFunction<?>> functions) {
public SpawnItem(
Holder<Item> item,
DataComponentPatch patch,
Vec3 offset,
ContextIntProvider count,
List<IOutcomeFunction<?>> functions
) {
this(new ItemStackTemplate(item, 1, patch), offset, count, functions);
}

Expand Down Expand Up @@ -99,16 +103,17 @@ public Type getType() {
}

/**
* 接受配方上下文并处理生成物品的结果
* 计算最终产物,保留随机数量、结果函数和方块偏移计算后的值
*
* @param context 配方上下文
*/
@Override
@SuppressWarnings("unchecked")
public void accept(InWorldRecipeContext context) {
ItemCache cache = context.computeIfAbsent(ItemCache.ITEM_CACHE);
public List<PlannedItem> plan(InWorldRecipeContext context) {
int count = context.getInt(this.count, 0, 99);
if (count == 0) return;
if (count == 0) {
return List.of();
}
ItemStack stack = this.item.withCount(1).create().copyWithCount(count);
BlockCache blockCache = context.computeIfAbsent(BlockCache.BLOCK_CACHE);
Vec3 offset = context.getPos().add(this.offset);
Expand All @@ -122,9 +127,7 @@ public void accept(InWorldRecipeContext context) {
IOutcomeFunction<ItemStack> function1 = (IOutcomeFunction<ItemStack>) function;
stack = function1.apply(context, stack);
}
ICacheOutput output = cache.getOutput(stack, offset);
output.grow(stack, true);
context.putAcceptor(ItemCache.ITEM_CACHE.location(), ItemCache.DEFAULT_ACCEPTOR);
return stack.isEmpty() ? List.of() : List.of(new PlannedItem(stack, offset));
}

/**
Expand Down
Loading
Loading