English
Recipes
recipes/*.yml describes one item's strengthening rules: success rate per star, required materials, failure penalties, effects, and actions. Recipe files live in recipes/*.yml; a recipe's identity comes from the required id field, not the file name -- the file name only organizes files and locates errors
Top-level fields
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
schema_version | integer | No | 1 | 1~2 (outside this range the whole recipe is skipped) | Material schema version; material entries are canonicalized by version |
id | string | Yes | — | — | Unique recipe id; missing or blank skips the whole recipe |
display_name | string | No | The id | — | Recipe display name (MiniMessage) |
gui_template | string | No | strengthen_gui | — | GUI template reference (a template name under gui/, without .yml) |
economy | object | No | enabled depends on currencies | — | Economy configuration charged per attempt |
limits | object | No | Limits.defaults() | — | Star/temper limits; see Limits |
success_rates | map | No | Empty (falls back to the global config.yml values) | Keys are positive target stars | Base success chance per star, in percent |
matcher | object | No | none | CoreLib general matcher | Non-item-source conditions deciding which items can use this recipe. See Match rules |
source_patterns | list | No | Empty (any source) | Regex strings | Item source matching (case-insensitive partial match), AND-combined with the other three fields. See Match rules |
slot_groups | list | No | Empty (any slot group) | weapon / armor / offhand / generic | Slot group matching (lower-cased), AND-combined with the other three fields. See Match rules |
stats_any | list | No | Empty (any attributes) | EmakiAttribute attribute ids | Passes when any one attribute is present; AND-combined with the other three fields |
match | object | No | none | — | Deprecated and no longer read. See Migrating from match |
stat_lines | map | No | Empty | Keys are attribute ids; subkeys below | Stat-line template definitions |
name_actions | list | No | none | Name operation entries | Global name operations |
lore_actions | list | No | none | Lore operation entries | Global lore operations |
stars | map | No | Empty | Keys are positive target stars (≤0 skipped) | Per-star stage definitions; see Star stage definition |
branch_tree | object | No | none (linear recipe) | — | Branch tree definition. See Branch Trees |
condition | object | No | none | CoreLib ConditionBlock | Activation conditions for this recipe |
Subkeys of stat_lines.<attribute id>:
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
template | string | No | "" | — | Stat-line template; blank falls back to the built-in default line |
section_id | string | No | "" | — | Display section this line belongs to; blank uses strengthen.stats.secondary |
section_order | integer | No | 0 | ≥ 0 | Section ordering weight; 0 infers from whether the section id contains primary |
Economy
yaml
economy:
enabled: true
currencies:
- provider: "vault"
currency_id: ""
base_cost: 320
cost_formula: "%base_cost% * %star%"
display_name: "<gold>Coins</gold>"cost_formula is an expression; you can reference variables such as %base_cost% and %star%
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
enabled | boolean | No | true when currencies is non-empty | true / false | Whether currency costs apply; when off, no currency is charged |
currencies | list<map> | No | [] | — | Currency entries, listed below |
currencies[].provider | string | No | "" | Economy provider id | Economy provider identifier (e.g. vault) |
currencies[].currency_id | string | No | "" | — | Currency id within the provider; blank lets the provider decide |
currencies[].base_cost | long | No | 0 | ≥ 0 (negatives become 0) | Base cost referenced by cost_formula |
currencies[].cost_formula | string | No | %base_cost% | Expression | Effective cost expression; may reference %base_cost%, %star%, and similar |
currencies[].display_name | string | No | "" | — | Cost display label |
Example: a cost that grows linearly with the target star:
yaml
economy:
enabled: true
currencies:
- provider: "vault"
currency_id: ""
base_cost: 100
cost_formula: "%base_cost% * %star%"
display_name: "<gold>Coins</gold>"Limits
yaml
limits:
max_star: 12
max_temper: 4
temper_chance_bonus_per_level: 5.0
success_chance_cap: 90.0| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
max_star | integer | No | 12 | ≥ 1 (below 1 becomes 1) | Maximum reachable star; strengthening stops once reached |
max_temper | integer | No | 4 | ≥ 0 (negatives become 0) | Maximum temper level accumulated from failures |
temper_chance_bonus_per_level | double | No | 5.0 | ≥ 0 (negatives become 0) | Success chance bonus per temper level, in percentage points |
success_chance_cap | double | No | 90.0 | 0~100 (clamped) | Upper success chance after all bonuses |
Each star stage can also override its own cost with economy_override.currencies (same shape as the top-level economy.currencies)
Match rules
Four top-level fields decide whether the target matches, and all four must hold:
yaml
slot_groups:
- "weapon"
# Item source condition at the top level goes in source_patterns
# Anchor the pattern to bind one exact source
source_patterns:
- "^emakiitem-example_item$"| Field | Type | Description |
|---|---|---|
matcher | object | Non-item-source conditions only: component, PDC, lore, variable. Item source conditions written inside it (type: item_source and aliases) are rejected at load time and never match. Syntax is in Item Matcher |
source_patterns | list | Match the item source shorthand by regex. Case-insensitive partial match, so anchor it (^...$) to bind one exact source. Matching any one pattern passes |
slot_groups | list | Coarse slot/equipment type groups (weapon/armor/offhand/generic), not concrete slots such as main_hand or helmet |
stats_any | list | Has any of the given attributes |
Migrating from match
The old match block is no longer read. Each field moves like this:
| Old field | Current form |
|---|---|
match.source_ids | An anchored entry in the top-level source_patterns, such as "^minecraft-diamond_sword$" |
match.lore_contains | A type: component child with component: lore and operator: contains, one per fragment |
match.slot_groups | Promoted to a top-level slot_groups |
match.stats_any | Promoted to a top-level stats_any |
match.source_patterns | Promoted to a top-level source_patterns |
match.source_types | Cannot be converted automatically; rewrite it as a matcher condition by hand |
/emakistrengthen convert-legacy performs the conversion:
| Command | Behaviour |
|---|---|
/emakistrengthen convert-legacy | Dry run; prints the diff it would produce without touching files |
/emakistrengthen convert-legacy confirm | Rewrites recipes/*.yml and leaves a .bak backup per file |
--apply is the same as confirm, and the command requires emakistrengthen.admin
WARNING
Files containing match.source_types are reported as unconvertible and skipped. Files declaring both match and an existing matcher (or the corresponding top-level field) are reported as conflicts and skipped. Both cases need manual handling
match.source_ids is converted automatically into anchored regexes merged into the top-level source_patterns, never into a node inside matcher, so the output needs no manual fix-up. Those regexes are exact matches: ^ / $ anchors are added and special characters in the ID are escaped, giving entries like ^\Qemakiitem-xxx\E$
Legacy material normalization and rejection
Stage-material parsing normalizes a legacy scalar item into item_sources, then resolves material_id in this order: explicit material_id, legacy id, count_key, a single source, and finally the stage index. An omitted count_key falls back to material_id. When no identity is declared but multiple sources are supplied, the parser cannot choose a safe unique identity and rejects the material without rewriting the source map. Note: these rules are established by the current JVM parser and local tests; old configuration and persisted-save behavior has not been verified on a live Paper or Folia server
TIP
To bind an exact item ID, anchor the source_patterns entry so the recipe does not accidentally match every weapon
General item matcher
A recipe has two matcher spots: the top-level matcher narrows the target item, and stars.*.materials[].matcher re-verifies strengthening materials. The syntax is the same, but the item source lives in a different place:
| Location | How the item source is expressed |
|---|---|
Top-level matcher | Through the top-level source_patterns (regex). source_patterns, matcher, slot_groups, and stats_any are ANDed; all must hold |
materials[].matcher | Through the sibling item_sources. Both are evaluated by the same ItemRequirement.test path: the source and non-item-source conditions must both hold |
NOTE
Item source conditions never belong inside matcher here either -- at the top level use source_patterns, in materials use the sibling item_sources
The full syntax is in Item Matcher
yaml
id: enchanted_sword_strengthen
# Item source condition: only diamond swords
source_patterns:
- "^minecraft-diamond_sword$"
# Component condition: with Sharpness 3+
matcher:
type: component
component: enchantments
path: sharpness
operator: '>='
value: 3
stars:
1:
materials:
# Source and matcher conditions are evaluated together; both must hold
- item_sources:
- minecraft-copper_ingot
matcher:
type: component
component: custom_name
operator: absent
amount: 3WARNING
Item sources use a hyphen (minecraft-copper_ingot), not a colon. Written as minecraft:copper_ingot in item_sources the parser treats it as invalid and silently discards it, leaving the item source unrestricted. See Item sources use hyphens, component IDs use colons
target.filter in enhancement recipes
enhancement_recipes/*.yml is a separate recipe set from recipes/*.yml, and its target.filter also accepts a general item matcher: when filter contains a type key it is parsed as a matcher, otherwise it keeps the existing provider-specific key/value behaviour
yaml
target:
provider: equipment
# Has a type key -> parsed as a general item matcher
filter:
type: component
component: enchantments
operator: existsyaml
target:
provider: equipment
# No type key -> still interpreted as free-form provider keys (existing behaviour)
filter:
item_type: SWORDIts materials[] entries follow the standard shape: item_sources for the allowed item sources plus a sibling matcher for non-item-source conditions, ANDed. Item source conditions that used to sit inside matcher go in item_sources now
NOTE
The remaining fields of the enhancement_recipes/ set are documented in Generic enhancement recipes
Star stage materials
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
material_id | string | No | Falls back to id, count_key, a single source, then the stage index | — | Stable material-selection and stage-rule identity. New configurations should declare it explicitly; declaring multiple sources without an identity rejects that material |
count_key | string | No | material_id | — | Quantity aggregation and consumption identity. Reuse one explicit value when several definitions share a counter |
item_sources | list | No | Empty (any source) | Item source shorthand (hyphenated, e.g. minecraft-copper_ingot) | Allowed material item sources. Legacy scalar item is normalized only while reading |
matcher | object | No | none | CoreLib general matcher | Non-item-source conditions. The sibling item_sources and matcher are ANDed through the same ItemRequirement.test path; with both omitted the material never matches. See Item Matcher |
amount | integer | No | 1 | -1 or ≥ 1 | Required amount. Use -1 to require possession without consuming |
optional | boolean | No | false | true / false | Whether the material is optional |
protection | boolean | No | false | true / false | Prevents a star drop on failure |
temper_boost | integer | No | 0 | ≥ 0 (negatives become 0) | Extra temper levels supplied for this attempt |
yaml
materials:
- material_id: "copper_ingot"
count_key: "copper_ingot"
item_sources:
- "minecraft-copper_ingot"
amount: 3
# Protection material: not consumed, only checked
- material_id: "protection_gold_nugget"
count_key: "protection_gold_nugget"
item_sources:
- "minecraft-gold_nugget"
amount: -1
optional: true
protection: trueSuccess rates
yaml
success_rates:
1: 100.0
2: 95.0
3: 90.0
4: 80.0
5: 70.0Keys are the target star, values are percentages. Temper bonuses apply on top of the base rate and are capped by limits.success_chance_cap
Star stage definition (stars)
Each star stage can carry these fields:
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
name | string | No | "" | — | Milestone name for this star (used by the GUI) |
effects | list<map> | No | [] | effect entries, type values under Effect types | Effect list (one unified format) |
materials | list<map> | No | [] | see Star stage materials | Materials required by this star |
economy_override | object | No | currencies: [] | same shape as the top-level economy.currencies | Overrides this star's cost; when non-empty it takes precedence over the top-level economy |
actions | object | No | none | keys success / failure | Actions run on success/failure |
Star example
yaml
stars:
1:
effects:
- type: "variables"
variables:
physical_attack: 4
- type: "lore_action"
lore_actions:
- action: "append"
content:
- "<gray>Physical Damage: <gold>+4</gold></gray>"
materials:
- item_sources:
- "minecraft-copper_ingot"
amount: 1
- item_sources:
- "minecraft-gunpowder"
amount: 2
2:
effects:
- type: "variables"
variables:
physical_attack: 4
- type: "lore_action"
lore_actions:
- action: "replace_line"
anchor: "Physical Damage:"
content:
- "<gray>Physical Damage: <gold>+8</gold></gray>"
materials:
- item_sources:
- "minecraft-copper_ingot"
amount: 1Effect types
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
effects[].type | string | Yes | — | variables / ea_attribute / es_skill / lore_action / name_action | Effect type; entries outside this list are ignored |
effects[].variables | map | No | none | attribute id → number or expression | Read for type: variables; used by lore, name/lore templates, and placeholder context. They do not automatically become Attribute PDC attributes |
effects[].ea_attributes | map | No | none | attribute id → number or expression | Read for type: ea_attribute; written to the EmakiAttribute PDC attribute layer (explicit override) |
effects[].es_skills | list | No | none | EmakiSkills skill ids | Read for type: es_skill; skill attachment |
effects[].lore_actions | list | No | none | lore operation entries | Read for type: lore_action; lore modification |
effects[].name_actions | list | No | none | name operation entries | Read for type: name_action; item name modification |
Besides the effects list, a star stage also accepts the sibling keys variables, ea_attributes, name_action, and lore_action; they accumulate with the matching effects entries and are read with the same semantics
Current effect boundaries:
variablesonly provide text, placeholder, and operation-template context- Real attribute output must be configured explicitly with
type: "ea_attribute"andea_attributes - Skill attachments use
type: "es_skill", while concrete skill ids are stored in thees_skillslist
Notes
- Use high success rates for early stars and raise material cost gradually
- Use protection materials for high-risk levels when item downgrade or destruction is enabled by recipe logic
- Prefer
replace_linefor repeated stat lines so lore never duplicates - After you change recipe ids or match rules, test old strengthened items
Forge quality variables
The variable.<name> keys of target.filter, the target matcher, and recipe conditions can all reference forge-quality variables read from the target item's PDC; they come from the emakiforge namespace PDC that EmakiForge writes onto items
| Variable | Type | When the item has no forge data | Description |
|---|---|---|---|
forge_quality_id | string | "" | Quality tier id |
forge_quality_display | string | "" | Quality display name |
forge_quality_multiplier | double | 1.0 | Quality multiplier |
forge_recipe_id | string | "" | Forge recipe id |
Every variable has three equivalent spellings -- pick any one:
| Canonical name | Dotted spelling (legacy) | Short spelling |
|---|---|---|
forge_quality_id | forge.quality_id | quality_id |
forge_quality_display | forge.quality_display | quality_display |
forge_quality_multiplier | forge.quality_multiplier | quality_multiplier |
forge_recipe_id | forge.forge_recipe_id | none |
Why the dotted spelling is kept
Forge's PDC keys were renamed from forge.quality_id to forge_quality_id (see PDC key naming). The dotted spelling stays as a legacy alias so existing configuration needs no change; use the canonical name in new configuration
yaml
target:
provider: equipment
filter:
# filter is an equality check: matches only tier legendary
variable.forge_quality_id: legendaryfilter is equality-only
Every entry in filter is an equality comparison; >= and < are not supported. Numbers compare numerically, everything else compares as case-insensitive strings. Writing variable.forge_quality_multiplier: '>= 1.5' does not error, but compares the string ">= 1.5" against the real multiplier and never matches
For numeric ranges, use the target matcher (type: variable_expr) or recipe conditions instead
There is also a helper variable forge_quality_multiplier_valid: it is 1 when the item's multiplier parses as a finite positive number and 0 when it is broken (non-numeric, negative, NaN), and then forge_quality_multiplier falls back to 1.0. Use it to tell "never forged" apart from "forge data is corrupt"
Unknown keys in filter make the whole rule non-matching
A filter key that is neither built-in nor in the variable table makes that filter rule non-matching. The four variables above always exist (taking their defaults without forge data), so they are safe to reference
Generic enhancement recipes (enhancement_recipes)
enhancement_recipes/*.yml holds recipes for the generic enhancement framework (target / materials / costs / chance / pity / actions), a set separate from recipes/*.yml. The split: EmakiStrengthen owns material matching and consumption, currency charging, chance, pity, idempotency, and compensation; target recognition and the level/stage write-back belong to the registered Target Provider
Top-level fields
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
schema_version | integer | No | 1 | 1~2 (outside this range the recipe is skipped) | Material schema version |
id | string | Yes | — | — | Unique recipe id; missing or blank skips the recipe |
mode | string | Yes | — | equipment / affix / gem | Semantic group; missing, blank, or outside the range skips the recipe |
target | object | Yes | — | see target | Target configuration; a blank provider skips the recipe |
materials | list<map> | No | [] | see materials[] | Material slot list; one slot failing to parse skips the recipe |
costs | list<map> | No | [] | see costs[] | Currency cost list; one entry failing to parse skips the recipe |
chance | number | object | No | 1.0 | 0~1 probability, or CoreLib Quantity (fixed / formula / lookup_table) | Success chance; an invalid value skips the recipe |
pity | object | No | none | see pity | Single pity track; present but invalid (e.g. missing group) skips the recipe |
pity_tracks | list<map> | No | [] | see pity | Multi-track pity; one invalid track skips the recipe |
condition | object | No | none | CoreLib ConditionBlock | Entry conditions, evaluated inside prepare() so they apply to both preview and the real attempt |
actions | map | No | {} | free-named CoreLib action line lists | Action groups dispatched per result phase (e.g. on_success / on_failure) |
target
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
provider | string | Yes | — | a registered Target Provider id (built-ins: equipment / affix / gem) | Target provider; blank skips the recipe |
filter | map | No | none | parsed as a general item matcher when it contains a type key, otherwise provider-specific key/value pairs | Target filter, see target.filter in enhancement recipes |
materials[]
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
item_sources | list | No | Empty (any source) | item source shorthand | AND-combined with the sibling matcher; with both omitted the slot fails |
matcher | object | No | none | CoreLib matcher | Component, PDC, lore, and other non-item-source conditions |
quantity | number | object | No | 1 | CoreLib Quantity | Required quantity |
consume | string | No | always | always / success / failure / never | Consumption timing; an invalid value falls back to always |
required | boolean | No | true | true / false | false makes a shortage non-blocking (equivalent to optional: true) |
optional | boolean | No | none | true / false | The inverse spelling of required; when both appear, optional wins |
material_id | string | No | count_key, then source identity, then matcher identity | — | Rule and selection identity |
count_key | string | No | material_id | — | Quantity aggregation and consumption identity |
target_compare | string | No | Empty (no comparison) | same_affix / same_affix_set / same_item_type / same_level / level_at_least | Compares this slot's material with the current target; an invalid value means no comparison |
costs[]
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
provider | string | No | "" | economy provider id | Economy provider identifier |
currency_id | string | Yes | — | — | Currency id; the alias currency also works. Blank makes the entry fail to parse and, because the entry count then differs, skips the whole recipe |
amount | number | object | No | Quantity default | CoreLib Quantity | Cost amount |
pity / pity_tracks[]
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
scope | string | No | item | item / player | Counter scope; an invalid value falls back to item |
group | string | Yes | — | — | Counter group; blank discards the whole pity block |
threshold | integer | No | 10 | ≥ 1 (≤0 discards the block) | Trigger threshold; mutually exclusive with formula |
formula | object | No | none | CoreLib Quantity | Trigger expression; mutually exclusive with threshold |
effect | string | No | force_success | force_success / chance_bonus | Pity effect; an invalid value falls back to force_success |
bonus_value | double | No | none | required and > 0 when chance_bonus | Bonus for chance_bonus; a non-finite value discards the block |
decay.type | string | No | reset | reset / fixed_decay / proportional | Counter decay mode; an invalid value falls back to reset, an out-of-range value discards the block |
decay.value | double | No | 0.0 | fixed_decay ≥ 0; proportional 0~1 | Decay amount |
isolate | list | No | Empty | recipe / mode / target / affix / level | Buckets this track's counter by dimension; unknown or duplicate dimensions make the recipe fail to load |
pity and pity_tracks merge into a single track list; tracks with the same scope + group are silently de-duplicated, and the first one wins
Below is the shipped example (excerpt from enhancement_recipes/example_enhancement_recipe.yml):
yaml
schema_version: 2
id: example_equipment_enhance
mode: equipment
target:
provider: equipment
filter:
item_type: SWORD
materials:
- item_sources:
- "minecraft-diamond"
quantity: 2
consume: always
- matcher:
type: pdc_match
key: "emakiitem:item_type"
value: "enhance_stone"
quantity:
type: formula
expression: "1 + floor(target.level / 5)"
consume: success
costs:
- provider: vault
currency_id: money
amount:
type: formula
expression: "100 * (target.level + 1)"
chance:
type: formula
expression: "max(0.05, 0.9 - target.level * 0.05)"
pity:
scope: item
group: example_equipment_enhance
threshold: 10
effect: force_success
decay:
type: reset
value: 0
actions:
on_success:
- "sound: ITEM_TOTEM_USE"
on_failure:
- "sound: BLOCK_ANVIL_USE"