Recipe Search and Your Extensions
Since after 21.1.2Newer than 21.1.2, an idle machine no longer tries every recipe every five ticks: it searches when something it uses changed, and a per-recipe-type index skips the recipes it plainly cannot run. This page is for the authors of custom recipe capabilities, traits and handlers: what they have to change, and what they can add.
Do I have to change anything?
No. A capability, trait or handler written for 21.1.2 keeps working, unchanged. MBD2 simply cannot tell when it changes or what it holds, so a machine using it is searched the way every machine was up to 21.1.2: on every five-tick poll, with nothing skipped on its account, and off the server thread when asyncRecipeSearching is on and the index cannot narrow anything down. The only correctness changes concern handlers that keep per-recipe state and machines with several recipe logics.
What you can add is speed. Each hook below is optional; each also makes a promise, and breaking it costs more than not making it.
| Hook | Where | Gives | Breaking the promise |
|---|---|---|---|
getLookupKeys(content) | RecipeCapability | Recipes needing your content are skipped when no input holds it | A key left out hides recipes from every machine |
getAvailableLookupKeys() | the handler | Your handler takes part in that skipping | A key left out hides recipes from this machine |
notifiesAllChanges() | the trait | Machines using it search only after a change, not every poll | A change made without notifying is noticed up to 100 ticks late |
supportsMultipleRecipeLogics() | the handler | — | Return false if it keeps state for the one recipe it serves |
A scalar capability, such as energy or the heat of the custom trait example, has nothing to key: leave both key hooks alone and implement only notifiesAllChanges().
How a search runs
A poll with no reason to search only re-checks the recipes that matched but could not start, so a condition coming true still starts its recipe. See When an idle logic searches for the whole cycle.
Lookup keys
The index files every recipe under the keys of its keyed input contents and drops a recipe when one of them has no key held by any input handler. It is a necessary condition, never a replacement for matching: everything that survives is still simulated.
For a capability whose content is a GasStack(Gas gas, int amount) of your own, keyed by its gas, the capability says which keys a content can be satisfied by:
@Override
public Collection<?> getLookupKeys(Object content) {
if (!(content instanceof GasStack gas) || gas.amount() <= 0) return null;
return List.of(gas.gas());
}The handler says which keys it holds right now, for the same capability:
@Override
public Collection<?> getAvailableLookupKeys() {
var gases = new HashSet<Object>();
for (var tank : tanks) {
if (!tank.isEmpty()) gases.add(tank.getGas());
}
return gases;
}The rules:
- Content keys: every key that could satisfy the content. Holding any one may be enough; holding none must never be. Return
nullwhen you cannot say exactly — a content with custom matching, an amount of zero — and that content never rules a recipe out. - Handler keys: everything the handler could give for that capability, or
nullwhen unsure. One handler answeringnullturns the skipping off for that capability, for every recipe logic using the handler. - Same objects on both sides: the keys are compared with
equals, so use the same type for both, as items useItemand fluids useFluid. - Tags: read a tag's current contents. The index is rebuilt after recipes or tags reload and when the server stops; anything cached across a reload, such as an ingredient's cached stack list, is stale.
getAvailableLookupKeys()is asked only of input handlers, on the server thread, once per search.
Change notifications
Return true from the trait's notifiesAllChanges() only if every change of what the trait holds calls notifyListeners():
- recipe IO, the machine UI, auto IO, other mods going through your block capability, scripts;
- data loaded from the dropped machine item, which
Trait.onDataRestored()already notifies; - not simulated operations: notify only when something really changed.
Runtime values need nothing from you: changing any runtime value of the machine or its traits makes every idle logic search again. A machine searches every poll as long as one of its handlers does not notify, so a single handler without the promise keeps the old behaviour for the whole machine.
Threads
handleRecipeInner with simulate == true may run on a background thread. That happens only for a machine with a handler that neither notifies nor lists its keys, with asyncRecipeSearching on — as it did for every search up to 21.1.2. Keep simulation read-only and tolerant of the contents changing under it.
Subclassing a built-in trait
The item slot, fluid tank, Forge Energy and Mekanism chemical traits notify every change and list their keys from their own storage. A subclass inherits both promises. If it changes contents without going through storage's notifications, or takes inputs from somewhere else, override them back:
@Override
public boolean notifiesAllChanges() {
return false;
}and return null from its handler's getAvailableLookupKeys().
Several recipe logics
- Per-recipe state: a handler that tracks the one recipe it serves — a generator running at that recipe's speed, as Create's rotation does — returns
falsefromsupportsMultipleRecipeLogics(). It is then given only to the first recipe logic allowed to use it. preWorking/postWorking: eachpreWorkinggets exactly onepostWorking, after the inputs are taken, also when the recipe waits, is suspended or interrupted, or the machine unloads. The holder passed in is still the machine. See Custom traits.- Scopes: a trait's Recipe Logics list reaches the machine through
getRecipeLogicScope(), whichRecipeCapabilityTraitandRecipeHandlerTraitalready implement. Only anIRecipeHandlerTraitwritten from scratch needs to return it. - Matching:
handleRecipeInnerand recipe matching are handed theRecipeLogicas theirIRecipeCapabilityHolder, not the machine. Use itsgetMachine()instead of casting the holder. - Conditions: a condition that looks for a trait should ask
machine.getRecipeLogicTraits(recipeLogic), which leaves out the traits scoped to other logics, rather thangetRecipeLogicTraits().
Your own search
An addon overriding RecipeLogic.searchRecipe() or MBDRecipeType.searchRecipe(...) keeps the 21.1.2 search for that logic or recipe type: no index, every poll, handed the machine. Whatever it returns is still matched against the logic's own handlers before it starts. An override added only to log or filter switches the index off as well, so prefer a condition or a recipe modifier for filtering.