配方搜索与自定义扩展
Since after 21.1.221.1.2 之后的版本中,空闲的机器不再每 5 tick 把所有配方试一遍:它只在用到的东西变化时搜索,并由按配方类型建立的索引跳过明显无法运行的配方。本页写给自定义配方 capability、Trait 和 handler 的作者:需要改什么,以及可以加什么。
我必须改什么吗?
不用。 为 21.1.2 编写的 capability、Trait 或 handler 原样就能正常工作。MBD2 只是无法知道它什么时候变化、里面有什么,所以用到它的机器会像 21.1.2 及以前所有机器那样被搜索:每次 5 tick 的轮询都搜索,不会因为它跳过任何配方;asyncRecipeSearching 开启且索引无法缩小范围时,在服务端线程之外匹配。唯一与正确性有关的变化,涉及保存单个配方状态的 handler 以及有多个配方逻辑的机器。
你能增加的是速度。下面每个 hook 都是可选的;但每个也是一个承诺,违背承诺的代价比不实现更大。
| Hook | 位置 | 带来什么 | 违背承诺的后果 |
|---|---|---|---|
getLookupKeys(content) | RecipeCapability | 没有任何输入持有你的内容时,需要它的配方会被跳过 | 漏掉的键会对所有机器隐藏配方 |
getAvailableLookupKeys() | handler | 你的 handler 参与这种跳过 | 漏掉的键会对这台机器隐藏配方 |
notifiesAllChanges() | Trait | 用到它的机器只在变化后搜索,而不是每次轮询 | 不发通知的修改最多晚 100 tick 才被发现 |
supportsMultipleRecipeLogics() | handler | — | 如果它为所服务的那一条配方保存状态,就返回 false |
标量 capability(如能量,或自定义 Trait 示例里的热量)没有可作为键的东西:两个键 hook 都不用管,只实现 notifiesAllChanges()。
搜索怎样进行
没有理由搜索的轮询只会重新检查已匹配但未能开始的配方,所以条件变为满足时配方照样会开始。完整流程见空闲逻辑何时搜索。
查找键
索引把每条配方按其可作为键的输入内容归档;只要其中某个内容的键没有任何输入 handler 持有,就去掉这条配方。它只是必要条件,从不代替匹配:留下来的配方仍然要模拟。
假设你的 capability 的内容是你自己的 GasStack(Gas gas, int amount),以气体为键。capability 说明一个内容可以被哪些键满足:
@Override
public Collection<?> getLookupKeys(Object content) {
if (!(content instanceof GasStack gas) || gas.amount() <= 0) return null;
return List.of(gas.gas());
}handler 说明它此刻持有同一 capability 的哪些键:
@Override
public Collection<?> getAvailableLookupKeys() {
var gases = new HashSet<Object>();
for (var tank : tanks) {
if (!tank.isEmpty()) gases.add(tank.getGas());
}
return gases;
}规则:
- 内容的键: 所有可能满足该内容的键。持有其中任意一个可能就够了;一个都不持有则绝不能满足。说不准时返回
null——比如自定义匹配的内容、数量为 0 的内容——这样的内容永远不会把配方排除。 - handler 的键: 它能为该 capability 提供的全部东西,说不准时返回
null。只要有一个 handler 返回null,使用它的配方逻辑就不再按这个 capability 跳过配方。 - 两边用同样的对象: 键用
equals比较,所以两边要用同一种类型,就像物品用Item、流体用Fluid。 - 标签: 读取标签当前的内容。配方或标签重载后、服务器停止时,索引都会重建;跨重载缓存的东西(如材料缓存的物品列表)会过时。
getAvailableLookupKeys()只会问输入 handler,在服务端线程上,每次搜索一次。
变化通知
只有当 Trait 里内容的每一次变化都会调用 notifyListeners() 时,它的 notifiesAllChanges() 才能返回 true:
- 配方 IO、机器 UI、自动 IO、其他模组经由你的方块 capability 的存取、脚本;
- 从掉落的机器物品恢复的数据,
Trait.onDataRestored()已经会通知; - 模拟操作不算:只在确实有变化时通知。
runtime value 不需要你处理:修改机器或其 Trait 的任何 runtime value,都会让所有空闲的逻辑重新搜索。只要机器上有一个 handler 不发通知,这台机器就每次轮询都搜索,所以一个没有做出承诺的 handler 会让整台机器保持旧的行为。
线程
simulate == true 的 handleRecipeInner 可能在后台线程运行。这只发生在机器有一个既不发通知、也不列出键的 handler,且 asyncRecipeSearching 开启时——21.1.2 及以前每次搜索都是这样。让模拟保持只读,并能容忍内容在它运行期间变化。
继承内置 Trait
物品槽、流体储罐、Forge Energy 和 Mekanism 化学品 Trait 会通知每一次变化,并从自己的 storage 列出键。子类会继承这两个承诺。如果子类不经过 storage 的通知就修改内容,或从别处取输入,就把它们改回去:
@Override
public boolean notifiesAllChanges() {
return false;
}并让它的 handler 的 getAvailableLookupKeys() 返回 null。
多个配方逻辑
- 单个配方的状态: 跟踪自己所服务的那条配方的 handler——比如按配方速度运转的发电机,Create 的转速就是这样——让
supportsMultipleRecipeLogics()返回false。它只会交给第一个允许使用它的配方逻辑。 preWorking/postWorking: 每个preWorking恰好对应一个postWorking,在取走输入之后发出;配方等待、暂停、中断或机器卸载时也一样。传入的 holder 仍然是机器。见自定义 Trait。- 作用域: Trait 的 Recipe Logics 列表通过
getRecipeLogicScope()传给机器,RecipeCapabilityTrait和RecipeHandlerTrait已经实现了它。只有从零编写的IRecipeHandlerTrait需要自己返回。 - 匹配:
handleRecipeInner和配方匹配收到的IRecipeCapabilityHolder是RecipeLogic,而不是机器。用它的getMachine(),不要强转 holder。 - 条件: 查找 Trait 的条件应调用
machine.getRecipeLogicTraits(recipeLogic),它会排除限定给其他逻辑的 Trait,而不是getRecipeLogicTraits()。
自己的搜索
重写了 RecipeLogic.searchRecipe() 或 MBDRecipeType.searchRecipe(...) 的附属,会让那个逻辑或配方类型保留 21.1.2 的搜索:没有索引、每次轮询、传入机器。它返回的配方在开始前仍会用该逻辑自己的 handler 匹配。只是为了记录日志或过滤而加的重写同样会关掉索引,所以过滤请改用条件或配方修饰器。