English
Recipe Definitions
Crafting recipes live under plugins/EmakiStation/recipes/, one recipe per file. The file name does not identify the recipe: the recipe id comes from the id field inside the file
Matching is unordered set matching: only material kinds and totals matter, never where you place them, and there is no shaped concept
Full Example
yaml
id: "iron_ingot_block"
display_name: "<white>Iron Block Pressing</white>"
station_ids:
- "blacksmith"
tags:
- "blacksmith"
permission: ""
visible: true
duration_seconds: 30
cost:
currency:
type: "vault"
amount: 0
materials:
- material_id: "iron_ingot"
requirement_id: "iron_ingot_input"
count_key: "iron_ingot"
item_sources:
- "minecraft-iron_ingot"
amount: 9
consume: true
result:
outputs:
- item_source: "minecraft-iron_block"
amount: 1
actions: []
condition:
invalid_as_failure: true
display_condition:
invalid_as_failure: falseTop-Level Fields
| Field | Type | Required | Default | Accepted range | Description |
|---|---|---|---|---|---|
id | string | Yes | — | — | Recipe id, unique across the whole recipe directory. A missing id refuses the file (station.recipe_missing_id) |
display_name | string | No | the id | MiniMessage text | Display name |
station_ids | list<string> | No | [] | station ids | Which stations this recipe belongs to; empty means open to every station. Values are lower-cased before matching |
tags | list<string> | No | [] | strings | Still usable for other purposes such as API queries, but no longer drives station ownership |
permission | string | No | "" | a permission node | Permission required to use this recipe; empty means unrestricted |
visible | boolean | No | true | true, false | Whether the recipe appears in the catalog. false hides it completely, usually paired with API submission |
duration_seconds | long | No | 0 | ≥0; negatives clamp to 0 | Duration of one craft, in seconds. 0 or omitted completes immediately without entering the queue |
cost | section | No | free | see below | Extra currency charged per craft, multiplied by the batch |
materials | list | Yes | — | non-empty list | Material requirement list. An empty list refuses the recipe (station.recipe_no_materials) |
result | section | Yes | — | see below | Output section. Missing or output-less refuses the recipe (station.recipe_no_outputs) |
actions | section | No | all empty | see below | Per-stage action lines; parsed but not executed today |
condition | section | No | empty | see below | Condition gate checked before submission |
display_condition | section | No | empty | see below | Unlock condition deciding whether the catalog entry is greyed out |
The station-side recipes section (include_tags / include_ids / exclude_ids) is deprecated; declare ownership on the recipe side through station_ids instead
Without the permission node, the recipe does not appear in the catalog at all -- it does not even show greyed out
yaml
# Example: a recipe open to every station, permission-free and instant
id: "quick_charcoal"
display_name: "<gray>Quick Charcoal</gray>"
duration_seconds: 0
materials:
- item_sources:
- "minecraft-oak_log"
amount: 1
result:
outputs:
- item_source: "minecraft-charcoal"
amount: 1cost — Currency Cost
| Field | Type | Required | Default | Accepted range | Description |
|---|---|---|---|---|---|
cost | section | No | free | — | A missing currency sub-section is silently treated as free, with no log line |
cost.currency.type | string | No | "" | vault, excellent (excellenteconomy is equivalent) | Economy provider. Any other non-blank value logs station.recipe_bad_currency and is treated as free. When both it and amount are empty the recipe is free |
cost.currency.amount | long | No | 0 | >0 to charge | Amount. Zero or below logs station.recipe_bad_cost_amount and is treated as free |
WARNING
type and amount must live inside the cost.currency sub-section, not directly under cost. When the currency sub-section is missing the plugin reports nothing, logs nothing and simply treats the recipe as free, so watch out -- such a configuration fails silently
The plugin checks your balance before spending materials and charges after they are spent; if the charge fails, the materials are refunded in full. Cancelling a queue entry refunds at the station's cancel_refund_rate
materials — Material Requirements
materials is a list, and each element is one requirement
| Field | Type | Required | Default | Accepted range | Description |
|---|---|---|---|---|---|
material_id | string | No | "<recipe_id>.material.<index>" | — | Material selection identity. When omitted it is derived from the index and a station.recipe_derived_material_identity warning is logged; new configuration should declare it explicitly. A duplicate with another material_id/requirement_id blocks loading (station.recipe_duplicate_material_identity) |
requirement_id | string | No | material_id | — | Requirement-record and queue-credential identity |
count_key | string | No | material_id | — | Quantity aggregation/consumption-record identity |
item_sources | list<string> | No | [] | item source tokens (hyphen shorthand) | An "any of these" set whose amount is counted across the whole set. Omitting it leaves the item source unrestricted |
item_source | string | No | — | a single item source token | Legacy form. Written together with item_sources it is refused (station.recipe_source_field_conflict); written alone it is read but logs station.recipe_legacy_item_source |
matcher | section/object | No | — | general item matcher | Only non-item-source conditions (component, PDC, lore). ANDed with the sibling item_sources. See Item Matcher |
amount | long | No | 1 | >0 | Total amount required. Zero or below logs station.recipe_bad_amount and skips that requirement |
consume | boolean | No | true | true, false | false validates possession without spending, for requirements like "hold a tool" |
Leave both item_sources and matcher out and the requirement is skipped. For a requirement of 64, 30 coal plus 34 charcoal also satisfies it
The number of material kinds per recipe is not limited by GUI slots; passing limits.warn_material_types only logs a warning. The warehouse channel deducts long amounts directly, while the inventory channel is bounded by the vanilla 64-per-slot limit
Material allocation uses a deterministic maximum-flow solver: when you submit, the plugin puts every inventory stack and every warehouse item source into one graph and searches for a complete assignment across all requirements, instead of relying on declaration order. Narrower matcher requirements join the graph first, but the outcome is still judged globally; a failure means the current candidates genuinely cannot satisfy all requirements at once
General item matcher
Two sibling fields decide a materials[] entry, item_sources and matcher, and both must pass: item_sources carries the allowed item sources, matcher carries only non-item-source conditions such as component, PDC, or lore. Leave both out and the material never matches. Full syntax is in Item Matcher
yaml
materials:
# Iron ingots that have not been renamed
- item_sources:
- minecraft-iron_ingot
matcher:
type: component
component: custom_name
operator: absent
amount: 9
consume: trueWARNING
Never write item source conditions inside matcher. type: item_source / item_sources / source / sources are rejected with a warning, and the matcher never matches
A material with a matcher is supplied from the inventory only
The one material that declares a matcher can be supplied from the inventory only; storage lookup skips it. Storage counts stock by item source and cannot inspect a real item's components, so it cannot prove a component condition
At load time a single storage_unreachable_material WARN is logged for each such material (only when the recipe belongs to at least one station with channels.storage enabled and storage.enabled: true)
If a material must draw from storage, declare only item_sources for it, with no matcher. Other materials in the same recipe that only declare item_sources can still use the warehouse channel normally
result — Outputs
| Field | Type | Required | Default | Accepted range | Description |
|---|---|---|---|---|---|
result.outputs | list | Yes | — | non-empty list | Output list |
result.outputs[].item_source | string | Yes | — | a single item source token | Output source, singular; a list or an unparsable token logs station.recipe_bad_source and refuses the whole recipe |
result.outputs[].item_sources | list | No | — | exactly 1 element | Legacy form; still read but logs station.recipe_legacy_item_source, and refused when combined with item_source |
result.outputs[].amount | long | No | 1 | >0 | Output amount per craft; zero or below logs station.recipe_bad_amount and skips that entry |
result.actions | list<string> | No | [] | action pipeline lines | Post-settlement action lines; parsed but not executed today |
An output node does not allow a matcher; writing one logs station.recipe_bad_source and refuses the whole recipe
actions — Per-Stage Action Lines
| Field | Type | Required | Default | Accepted range | Description |
|---|---|---|---|---|---|
actions.pre | list<string> | No | [] | action pipeline lines | Pre-submit action lines; parsed but not executed today |
actions.success | list<string> | No | [] | action pipeline lines | Success action lines; parsed but not executed today |
actions.failure | list<string> | No | [] | action pipeline lines | Failure action lines; parsed but not executed today |
These four lists (including result.actions) live on the recipe object and are readable through the API, but the current runtime has no execution entry point, so writing them has no effect
The Two Condition Gates
| Section | Type | Required | Default | Accepted range | Description |
|---|---|---|---|---|---|
condition | section | No | empty (always passes) | see Conditions | Decides whether it can be crafted. Evaluated during submission validation; on failure it returns station.recipe_condition_failed |
display_condition | section | No | empty (always unlocked) | same as above | Decides whether it is unlocked. Evaluated on every catalog repaint; on failure the entry shows as a greyed-out placeholder and clicking does nothing |
| Section | Decides | Behaviour when unsatisfied |
|---|---|---|
condition | Whether it can be crafted | The recipe still shows; clicking is refused |
display_condition | Whether it is unlocked | The recipe shows as a greyed-out placeholder and clicking does nothing |
invalid_as_failure behaves as in other modules, and the source default for both condition and display_condition is true (the shipped example writes display_condition explicitly as false, so a broken condition leans towards "unlocked" rather than locking the recipe away). Note that both sections only evaluate the condition group itself: on_pass.actions / on_fail.actions / on_fail.message are not executed today
A greyed-out placeholder instead of hiding tells you there is more content ahead. To hide something completely, use visible: false or permission
display_condition is evaluated on every catalog repaint, so heavy conditions slow the window down