Skip to content

Item sets, perk choices, and forging

These functions run in your entry script. Declare a dependency on core when using its items, perks, or forge profiles. A handle is the Lua value returned by a lookup or registration function; pass that value directly instead of its name.

Signature: sf2.itemsets.register(definition)

Returns: An item-set handle.

When: During mod loading.

Requires: content.register.

Groups items into a named set. It does not automatically add a set bonus or grant the items to the player.

Field Meaning
id Required local identifier, such as training_set.
title, text, brief Required localization handles from sf2.localization.key.
members Required, nonempty array of member tables.

Each member requires an item handle in item. Optional presentation fields are scale (default 1), rotate, x, y, and icons_y (all default 0). Values must be finite numbers. Each item can appear only once in this set.

local katana = sf2.items.get("core:items/weapon/weapon_katana")
local set = sf2.itemsets.register {
id = "training_set",
title = sf2.localization.key("set.title"),
text = sf2.localization.key("set.description"),
brief = sf2.localization.key("set.brief"),
members = { { item = katana, scale = 1 } },
}

Add the three localization keys to your mod’s language file. Registering a set does not create a shop listing; use the shop API for items you want players to buy.

Signature: sf2.progression.replace_perk_branch(definition)

Returns: Nothing (nil).

When: During mod loading, before the perk tree is used.

Requires: content.patch; looking up or registering perks also requires content.register.

Replaces the available choices at one perk-tree level. It does not immediately teach a perk, add experience, or change upgrade prices. Two mods replacing the same level conflict.

Field Meaning
level Required integer from 1 to 52.
entries Required, nonempty array of perk choices.
entries[].perk Required perk handle.
entries[].action "unlock" (default) or "upgrade".
-- opening_focus is a perk handle registered earlier in this script.
sf2.progression.replace_perk_branch {
level = 2,
entries = { { perk = opening_focus, action = "unlock" } },
}

An upgrade entry must refer to a perk that supports upgrades. Keep alternative choices appropriate for players who have already progressed through the tree.

Change an existing equipment item’s shop icon, fighter model, or both while keeping its identity and purchase state. Use typed asset handles so the asset host checks their kind and ownership before registration.

Signature: sf2.items.set_presentation { item, icon?, model? }

Returns: Nothing (nil).

When: During mod loading, before shop and fighter copies are built. Apply & Restart to load or remove the patch.

Requires: content.patch; item lookup needs content.register. Declare a dependency on the item’s owner and on any asset owner.

Field Type Meaning
item Equipment handle Required weapon, armor, helm, ranged or magic item.
icon Sprite handle Optional icon from sf2.assets.sprite.
model Model handle Optional model from sf2.assets.model.

Provide at least one of icon or model. An omitted field keeps its existing value. Another presentation patch for the same item conflicts. Removing the mod restores the previous icon and model for future copies; copies already built keep their snapshot. Existing saved item identity stays the same.

local sf2 = require("sf2")
local weapon = sf2.items.get("core:items/weapon/WEAPON_KUNAI")
sf2.items.set_presentation {
item = weapon,
icon = sf2.assets.sprite("core:ui/items/weapon_kunai"),
}

The core dependency is required in mod.toml for this example. You can also use a model owned by your mod. The content fingerprint includes both asset IDs. Native checks cover registration, copying and rollback; verify the actual shop thumbnail and fighter appearance in the game. DE128’s isolated Unity check also selects one changed icon in the live shop and equips a changed-model armor.

Replace an existing equipment definition’s starting level, upgrade level, initial stat snapshot and optionally its upgrade template or legacy paid marker. Use this when the item already exists and a mod changes its progression without replacing its identity or shared purchase price.

Signature: sf2.items.set_initial_profile { item, level, upgrade_level, initial_stats, upgrade_template?, legacy_paid_item?, clear_local_upgrades? }

Returns: Nothing (nil).

When: During mod loading, after obtaining the item handle and before shop, inventory and fighter copies are built. Apply & Restart to load or remove it.

Requires: content.patch; sf2.items.get also needs content.register. Declare a dependency on the item’s owner.

Field Type Meaning
item Equipment handle Required. Weapon, armor, helm, ranged or magic.
level Integer 1–52 Required starting item level.
upgrade_level Integer 0–5200 Required starting upgrade level. This is the item’s progression marker, not a price.
initial_stats Table Required complete initial stat snapshot. Values are integers 0–1,000,000. Allowed keys match the category tables in explicit initial stats. Omitted keys remain absent.
upgrade_template String Optional. Leave out to retain the item’s current template. To replace it, use Weapon_Bonus or Paid_Weapon_Bonus for a weapon, or the matching Armor, Helm, Ranged, or Magic pair. The selected template must exist in the native item catalog.
legacy_paid_item "none", "paid" or "super_paid" Optional. Leave out to retain the item’s current legacy PaidItem marker. "none" clears it; the other values set the corresponding native marker. This is metadata used by legacy conditions and statistics, not the shop price or currency.
clear_local_upgrades Boolean Optional, default false. Set true to remove the item’s own XML-authored upgrade rows from future catalog copies. The selected shared upgrade_template still supplies its rows.
local blade = sf2.items.get("core:items/weapon/WEAPON_BP_S1_GUARDIAN")
sf2.items.set_initial_profile {
item = blade,
level = 15,
upgrade_level = 1500,
initial_stats = { weapon_damage = 342 },
upgrade_template = "Weapon_Bonus",
legacy_paid_item = "none",
}

The patched catalog item supplies new purchase and fighter copies. Its source XML, item ID, coin/gem price and shared upgrade table definitions stay unchanged. Changing upgrade_template selects a different existing table, which can change future upgrade prices and power. Changing legacy_paid_item does not change shop visibility, the acquisition route, or the price; use the shop availability and listing APIs for those behaviors. Use clear_local_upgrades = true when replacing an item whose source definition has one-off upgrade rows that should no longer supplement its selected shared template. Removing the patch restores those rows; already saved inventory is not rewritten. Removing the mod restores the original catalog profile. Already saved inventory is not migrated or re-leveled by this declaration. The host rejects wrong-category stat keys or templates, invalid values, unavailable templates, unknown items and competing patches to the same item. The native copy and rollback paths have detached tests. DE128’s isolated Unity check also buys, upgrades, equips and reloads representative patched items. Validate combat balance for each content pack in the game.

Change the combat family of an existing weapon, ranged item or magic definition. Native animation conditions and projectile selection use this subtype.

Signature: sf2.items.set_subtype { item, subtype }

Returns: Nothing (nil).

When: During mod loading, before profile equipment and fighters are built.

Requires: content.patch; obtaining an item handle also requires content.register. Declare a dependency on the item’s owner.

Field Meaning
item Required weapon, ranged or magic handle. Armor, helms and consumables are rejected.
subtype Required case-sensitive string of 1–128 ASCII letters, digits or underscores.
local weapon = sf2.items.get("core:items/weapon/WEAPON_CHNY22_SPEAR")
sf2.items.set_subtype { item = weapon, subtype = "Naginata" }

Choose a family with compatible native moves, rig edges and projectile behavior, or register that behavior yourself. A syntactically valid family does not create missing animations. For example, changing to ChineseSwords in the current base also needs its stance and attack graph ported; supplying an animation file alone is insufficient.

Item IDs, models, prices, power, enchantments and saved ownership are unchanged. The subtype feeds AI grouping only when the item has no explicit tactic group; use set_tactic_subtype separately to change an explicit group. Competing patches to the same item’s subtype fail atomically, while an AI-group or availability patch is a separate field and can coexist.

The native item must exist when content is applied; an owned item normally needs a supported listing or another supported registration path that creates it. Duplicate legacy names are resolved against their original definition nodes. Missing native targets roll back earlier applied subtype patches. Apply & Restart without the mod restores the original subtype. Existing fighter copies retain their snapshot until rebuilt; this is not a mid-fight transformation operation. Subtype patches participate in the content compatibility fingerprint.

Replace the AI table group of a core or owned weapon without changing its animation subtype.

Signature: sf2.items.set_tactic_subtype { item = ItemHandle, group = string }

Returns: Nothing (nil).

When: During entrypoint registration. The override applies when content is loaded, before subsequent fights are constructed.

Requires: content.patch; item lookup also requires content.register. Declare dependencies for referenced namespaces.

Both fields are required. item must resolve to a weapon. group is a native AI table group containing at most 128 ASCII letters, digits or underscores. An empty string explicitly selects the weapon’s actual subtype instead of an existing separate AI group. It does not create AI tables or animation moves; supply a group supported by your tactics.

local sf2 = require("sf2")
sf2.items.set_tactic_subtype {
item = sf2.items.get("core:items/weapon/WEAPON_TWO_HANDED_MACE"),
group = "TwoHanded",
}

Only one override may target a weapon; duplicate registrations or another mod’s override fail instead of silently winning by load order. This can compose with default enchantments and innate perks on the same item. An owned weapon must have a supported shop listing so its native item exists when applying content. Prefer the tactic_subtype registration field for a new weapon unless a separate override is needed.

The group participates in the content compatibility fingerprint. Disabling the mod and applying/restarting restores the original native group, and partial application failures restore earlier targets. Existing fight copies retain their snapshot; this operation does not refresh a running fighter or edit inventory saves. Native projection, rollback and Lua validation have automated coverage; physical combat acceptance remains a separate playtest.

Signature: sf2.items.set_innate_perks(definition)

Returns: Nothing (nil).

When: During mod loading, before fighters are built.

Requires: content.patch; item/perk lookup or registration also requires content.register. Declare dependencies for referenced core or other-mod content.

Replaces the permanent perks attached to an equipment definition. The normal combat equipment collector includes these perks whenever that equipment is used. This applies to already owned equipment in subsequently built fighters too. It does not write an enchantment to inventory or change acquisition defaults.

Field Meaning
item Required weapon, armor, helm, ranged or magic handle.
entries Required dense array of zero to 64 perk entries. Empty removes the item’s innate perks.
entries[].perk Required existing perk handle, once per loadout. Register owned perks first.
entries[].parameters Optional table of up to 64 named finite numbers. Defaults to empty, preserving the perk’s authored defaults.

Parameter names must begin with an ASCII letter or underscore, contain only ASCII letters, digits or underscores, and be at most 128 characters. Values are stored as single-precision numbers; values that overflow that format are rejected. Strings, expressions, booleans, NaN and infinity are not accepted. Use names understood by the chosen perk: this API does not invent behavior for unknown parameters.

local sf2 = require("sf2")
-- capabilities = ["content.register", "content.patch"]
-- Also declare the core dependency in mod.toml.
sf2.items.set_innate_perks {
item = sf2.items.get("core:items/weapon/WEAPON_KNIVES"),
entries = {
{ perk = sf2.perks.get("core:perks/PERK_ITEM_SPECIAL_PRECISION_WEAPON"),
parameters = { Aspect = 100 } },
},
}

This attaches the native precision perk to Knives as an innate equipment effect. It is separate from the forge enchantment saved on a particular inventory item. For custom procedures, register a Lua-backed perk using the perk API and attach its handle with no loadout parameters. Lua-backed perks take their initial parameters at perk registration; nonempty loadout parameters for them are rejected. Keep conditional behavior in their callbacks rather than encoding expressions in this table.

The target must be present in the applied native item catalog. Owned items need their supported shop registration so they are materialized. Perk instances are cloned per item, preserving the registry and other equipment from the combat collector’s mutable weapon/non-weapon marker.

Two innate loadouts targeting the same item conflict. An innate loadout and a default-enchantment loadout can coexist on that item. Their entry order and parameter values participate in the compatibility fingerprint. Failed application restores earlier changes from that adapter; unloading restores the original innate list. Use Apply & Restart when changing active mods. Already constructed fighters are not refreshed by this operation.

Player-side Lua callbacks for active innate perks receive fighter.source = "innate", item_id, item_type and perk_id. They do not require a saved inventory enchantment, including when a rule supplies the equipment. Native perk filtering still controls eligibility. One perk ID dispatches once per event across equipment: learned/profile perks with an available saved instance take precedence, then innate perks, then saved enchantments. A missing learned save node does not suppress an otherwise active innate perk. An innate instance retains state between events within its fight but has no persistent profile backing; use mod-owned saved state for progress that must survive fights. Opponent callbacks retain their existing "warrior" provenance.

This does not expose automatic activated abilities, arbitrary item metadata or permanent profile mutation. Native collection, ownership, rollback, player dispatch and Lua validation are tested; live combat behavior still needs acceptance testing with the chosen perk.

Signature: sf2.items.set_default_enchantments(definition)

Returns: Nothing (nil).

When: During mod loading.

Requires: content.patch; item/perk lookup or registration also requires content.register. Declare dependencies for referenced content from core or other mods.

Replaces an equipment item’s default enchantment loadout. Shop previews and the native acquisition path use the replacement. This does not grant the item, re-enchant existing inventory, or attach permanent innate effects. Previously saved enchantments retain their values.

Field Meaning
item Required equipment handle, from registration or sf2.items.get. Weapon, armor, helm, ranged and magic items are supported.
entries Required dense array of zero to 64 entries. An empty array removes the acquisition defaults.
entries[].perk Required existing perk handle. Each perk may appear only once. Register owned perks before this call.
entries[].aspect Optional signed 32-bit integer, copied as the enchantment’s explicit aspect. Omit it to preserve the perk’s normal default. This is a value, not a formula or expression.
local sf2 = require("sf2")
-- capabilities = ["content.register", "content.patch"]
-- Also declare the core dependency in mod.toml.
sf2.items.set_default_enchantments {
item = sf2.items.get("core:items/weapon/WEAPON_KNIVES"),
entries = {
{ perk = sf2.perks.get("core:perks/PERK_ITEM_SPECIAL_PRECISION_WEAPON"),
aspect = 100 },
},
}

Choose an aspect appropriate for your content; accepting an integer does not mean every value produces useful gameplay. Prices, equipment levels and shared stat scaling remain unchanged. The native acquisition path still determines when default enchantments are applied.

The target must exist in the applied native item catalog. For an owned item, use its supported shop registration so the adapter materializes it before applying the loadout. This call does not create a missing native item or substitute an acquisition route.

Two mods targeting the same item conflict, even if their loadouts match. Different items can coexist. Unknown fields, duplicate perks, sparse arrays and unresolved dependencies fail loading. A later native apply failure restores earlier loadouts from that adapter. Unloading restores the original preview and acquisition lists; use Apply & Restart after enabling or disabling the mod.

Loadout order, perk IDs and optional aspects participate in the content compatibility fingerprint. Changing defaults does not migrate existing equipment. Automated checks cover Lua registration, native projection and rollback; full-game acquisition, saves and preview rendering still need acceptance testing.

Signature: sf2.forge.profile(reference)

Returns: A forge economy-profile handle.

When: During mod loading.

Requires: content.register, and a declared dependency when reading another mod’s content.

Looks up an existing profile that supplies the forge’s costs and timing. A short name such as "Simple" is resolved as "core:forge-profiles/Simple". "Complex" is another existing core profile. Definition IDs normalize casing; an unknown profile fails loading.

local economy = sf2.forge.profile("Simple")

The handle is an input for a recipe. It is not a table of editable prices. To give your own recipes their own prices, register a profile with sf2.forge.register_profile.

Signature: sf2.forge.register_profile(definition)

Returns: A forge economy-profile handle, usable as a recipe’s economic_profile.

When: During mod loading, before the recipes that use it.

Requires: content.register.

Declares the forge-material prices for your own recipe families. A forge price profile says how many of each forge material (ForgeMaterial1, 2 and 3) one enchantment costs, depending on the player’s level at the moment of enchanting (as in the original game; the level of the item does not matter). Core profiles such as Simple and Complex keep their prices: a profile you register is used only by recipes from your mod, never by core recipes or another mod’s.

Field Type Meaning
id String Required local identifier, such as abilities. Becomes <your mod>:forge-profiles/<id>.
prices Array Required. One price table per equipment category, each category at most once.

Each prices entry has:

Field Type Meaning
equipment Equipment constant Required: sf2.forge.WEAPON, ARMOR, HELM, RANGED or MAGIC.
rows Array Required, 1–64 rows in ascending, non-overlapping level order.

Each row covers a range of player levels:

Field Type Default Meaning
level Integer — One exact player level. Use this or min_level/max_level, not both.
min_level Integer 1 First player level the row applies to, 1–1000.
max_level Integer No upper limit Last player level, up to 1000. Omit it on the final row to cover every higher level.
materials Integer array Required 1–3 counts for ForgeMaterial1, 2 and 3, each 0–10,000,000. At least one must be above zero.

A recipe item can only use a category the profile prices, so give every equipment category in your recipe’s items a table here. The first row whose range contains the player’s current level is the price. Cover every player level: when no row matches, the recipe has no price, and the forge then treats it like a free recipe (the material check passes and nothing is spent), exactly as the original game does. Rows have no delivery timer: the enchantment completes immediately.

local abilities_price = sf2.forge.register_profile {
id = "abilities",
prices = {
{ equipment = sf2.forge.WEAPON, rows = {
{ max_level = 2, materials = { 38, 10, 5 } }, -- levels 1-2
{ level = 3, materials = { 46, 13, 6 } },
{ min_level = 4, materials = { 56, 17, 8 } }, -- level 4 and above
} },
},
}
sf2.forge.register_recipe {
id = "abilities",
economic_profile = abilities_price,
items = { { equipment = sf2.forge.WEAPON } },
candidates = { { perk = my_ability_perk, equipment = sf2.forge.WEAPON } },
}

sf2.forge.exclude_candidate and sf2.forge.override_deviation still target core profiles only. Profile prices are part of the content compatibility fingerprint, so changing them is a content change. Disabling the mod removes the profile together with its recipes.

Signature: sf2.forge.register_recipe(definition)

Returns: A forge-recipe handle.

When: During mod loading.

Requires: content.register.

Creates a recipe using an existing economy profile and a pool of eligible enchantments.

Field Meaning
id Required local identifier.
alias Optional compatibility alias; defaults to an empty string.
economic_profile Required handle from sf2.forge.profile, or from sf2.forge.register_profile in this mod.
items Required array of equipment-category settings.
candidates Required array of eligible perks/enchantments.

Equipment categories are sf2.forge.WEAPON, ARMOR, HELM, RANGED, and MAGIC (string values "weapon", "armor", "helm", "ranged", "magic").

Each items entry requires equipment. Optional fields are enchantments (integer, default 1), bar_scale (string, default empty), min_deviation and max_deviation (integers, default 0), and random_aspect (boolean, default false). These describe the recipe’s enchantment presentation and roll behavior; they are not currency prices.

Each candidates entry requires perk (a perk handle from sf2.perks.register) and equipment. Optional integer min_level and max_level restrict eligibility; omitting them leaves the candidate unrestricted by level. Use explicit sensible bounds for your content.

-- training_perk is a handle from sf2.perks.register.
local recipe = sf2.forge.register_recipe {
id = "training_recipe",
economic_profile = sf2.forge.profile("Simple"),
items = { { equipment = sf2.forge.WEAPON, enchantments = 1 } },
candidates = {
{ perk = training_perk, equipment = sf2.forge.WEAPON,
min_level = 1, max_level = 52 },
},
}

A registered enchantment must still be made obtainable through a recipe or another supported equipment/perk route before its combat behavior can run.

Registering a new family does not change the original family’s candidate pool or costs.

These rules come from the original game (version 2.46.0) and apply to core recipes and to recipes your mod registers. They explain what players will see when they use your recipes.

Which recipes are offered. A recipe is listed for an item when the recipe has an items entry for the item’s equipment category and at least one of its candidate groups accepts the item. A mod candidate counts as such a group when its min_level/max_level range contains the item’s level. Prices, materials and whether a candidate is ready do not hide a recipe; a recipe with no ready candidate is shown as unavailable instead.

What a roll picks. When the player enchants, the forge takes the recipe’s whole candidate pool for the item, shuffles it with the game’s shared random number generator, and then walks the shuffled list. It keeps each candidate that is ready and not already on the item until it has the recipe item’s enchantments count. If fewer candidates qualify, the item gets fewer enchantments; if none qualify, nothing happens and nothing is spent. Every candidate in the pool has the same chance; listing a perk twice gives it two chances.

Ready candidates.

  • A set (combo) perk that names an item set is ready only when the player owns every item of that set. This applies to core set perks in your own recipe families too.
  • A combo perk registered with sf2.perks.register has no item set and is always ready.
  • A perk whose alias is ENCHANTMENT_MAGIC_RECHARGE is not ready while the player’s current zone is ZONE_2.
  • Any other perk is ready.

The forge preview lists every candidate the item does not already have and dims the ones that are not ready.

Costs. The price is the profile row for the player’s current level (see sf2.forge.register_profile). Materials are spent only when an enchantment is actually applied, or when a timed order starts.

Timed orders. When a forge timer policy gives an order a duration, starting it spends the materials and stores the order on the item if the recipe can enchant the item. The roll happens when the order completes. The order is closed even if, by then, no candidate qualifies; the materials are not returned.

Choose recipes. Everything above describes the original random forge, which stays the default. A mod can switch a recipe to choose selection with sf2.forge.configure_recipe. The candidate pool, the readiness rules and the price row stay the same, but there is no roll:

  • The player sees every candidate of the pool once (in data order), except candidates already on the item. Candidates that are not ready are shown locked; sf2.forge.configure_candidate can also lock a candidate until the player has beaten a listed opponent.
  • The player picks as many candidates as the recipe item’s enchantments count (or every available one, when fewer are available) and one strength tier.
  • The tier fixes the enchantment power. A tier’s strength is a fraction of the recipe item’s deviation range: 0 gives min_deviation, 1 gives max_deviation, 0.5 the midpoint, rounded to the nearest integer. The power is the level’s base aspect plus that deviation, the same formula as a roll without the randomness. If the recipe item declares no deviation range, a candidate’s own RandomAspect range is used. Candidates with a fixed power, such as set enchantments and boss abilities, keep it: the forge hides the tier choice when none of the picks has a power range.
  • The price is the price row times the picked candidate’s price multiplier times the tier’s price multiplier. With several picks the largest candidate multiplier is used. The tier multiplier applies only when at least one pick has a power range, so a fixed-power pick never costs more for a higher tier. Each material count is rounded up to a whole number.
  • A timed order stores the picks and the tier and applies exactly them when it completes. Orders saved without picks (older saves, random recipes) complete with the random roll. If none of the stored picks exists any more (for example, the mod that added them was removed), the order completes with the random roll instead of being lost.

Loading saved enchantments. When a profile loads, saved core enchantments pass the original game’s checks:

  • Only the saved Aspect (power) is kept; other saved parameters fall back to the perk’s own values.
  • An Aspect that is at least 105 above the largest value a forge roll can give at the player’s level (the level’s base aspect plus 30) is reset to that largest value.
  • An item keeps one combo enchantment (the first) and normally one single enchantment. Extra saved enchantments, and a single enchantment that neither is one of the item’s default enchantments nor can be rolled by one of its recipes, are removed from the save.

Enchantments placed by mods (mod perks, scripted enchantments and candidates added by sf2.forge.register_recipe) are loaded unchanged and skip these checks.

Signature: sf2.forge.exclude_candidate(definition)

Returns: Nothing (nil).

When: During mod loading.

Requires: content.patch, content.register for handle lookups, and a declared core dependency.

Removes a native enchantment from one core recipe’s eligible pool for one equipment category while the mod configuration is applied. Both candidate previews and subsequent rolls use this pool. It does not remove enchantments already on equipment, delete the perk, or change prices, timers or power deviations.

Field Meaning
profile Required core profile handle from sf2.forge.profile. Selects the native recipe with that profile’s name.
perk Required existing core perk handle from sf2.perks.get.
equipment Required category: sf2.forge.WEAPON, ARMOR, HELM, RANGED, or MAGIC.
local sf2 = require("sf2")
-- capabilities = ["content.register", "content.patch"]
-- Also declare the core dependency in mod.toml.
sf2.forge.exclude_candidate {
profile = sf2.forge.profile("Complex"),
perk = sf2.perks.get("core:perks/PERK_MONK_SET_WHIRL"),
equipment = sf2.forge.WEAPON,
}

This example excludes the Monk set enchantment from Complex weapon rolls. Other equipment categories remain unchanged. All native occurrences of that candidate are filtered; independently added mod candidates are unaffected.

Duplicate exclusions for the same recipe, equipment and perk conflict, including duplicates from different mods. Different targets can coexist. Unknown fields, invalid categories and non-core targets are rejected. Registration validates the handles; the native adapter additionally checks that the recipe supports the equipment and contains the candidate when applying content. Failure aborts application and restores exclusions already applied by that adapter.

Unloading the applied content restores the original native candidate pool. Enable or disable the mod through Apply & Restart to change the active configuration. This function does not yet edit native candidate level conditions, replace entire families or modify native recipe item settings.

Signature: sf2.forge.override_deviation(definition)

Returns: Nothing (nil).

When: During mod loading.

Requires: content.patch, content.register for the profile lookup, and a declared core dependency.

Changes the random enchantment-power delta for one equipment category in an existing core recipe. The game still calculates the base aspect from its normal level curve, then adds a random integer from minimum through maximum, inclusive. Prices, timers and the base curve remain unchanged. Already enchanted equipment keeps its existing power.

Field Meaning
profile Required core recipe profile handle from sf2.forge.profile.
equipment Required sf2.forge.WEAPON, ARMOR, HELM, RANGED, or MAGIC.
minimum Required integer, at least -10000 and no greater than maximum.
maximum Required integer, at most 10000 and no less than minimum.

The limits are validation bounds, not recommended balance values. Start close to the original recipe’s range and test the resulting power at several equipment levels. The base aspect is read at the player’s level. Keep maximum at 134 or less: when a profile loads, a saved core enchantment whose power is 135 or more above its level’s base aspect is reset to base + 30 (see How the forge works).

local sf2 = require("sf2")
-- capabilities = ["content.register", "content.patch"]
-- Also declare the core dependency in mod.toml.
sf2.forge.override_deviation {
profile = sf2.forge.profile("Simple"),
equipment = sf2.forge.WEAPON,
minimum = 15,
maximum = 75,
}

This replaces the Simple weapon category’s native -30..30 range with 15..75. Both the effective recipe item settings and copied roll candidates use the override. Other equipment categories retain their original settings. The source recipe and perk definitions remain intact.

Only categories already using random aspect are supported. Simple and Medium have such categories; Complex’s fixed-aspect categories are rejected when content is applied. A candidate’s complete native RandomAspect expression is updated; fixed values, missing aspect values and compound expressions are preserved. Independently added candidates with a complete random-aspect expression use the same category override.

Two overrides targeting the same recipe/category conflict, even with equal bounds. Different categories compose, and candidate exclusions can coexist with a deviation override. Applying an invalid native target rolls back earlier forge changes from that adapter. Unloading restores the original settings; use Apply & Restart when changing the active mods.

The bounds participate in the content compatibility fingerprint. Changing them is a content change even if the mod’s version is unchanged. This function does not replace entire recipe families, change fixed-aspect recipes into random ones, or edit arbitrary candidate conditions.

Signature: sf2.forge.configure_recipe(definition)

Returns: Nothing (nil).

When: During mod loading.

Requires: content.patch for a core recipe or another mod’s recipe, plus a declared dependency on that namespace (core for core recipes). content.register when the recipe is one your mod registered with sf2.forge.register_recipe. Tier names use sf2.localization.key handles.

Switches one recipe from the original random roll to choose selection: the player picks the exact enchantments and a strength tier, and the price follows the picks. See How the forge works for the full rules.

Field Meaning
recipe Required. A core recipe name such as "Simple", "Medium" or "Complex"; a qualified ID such as "core:forge-profiles/Simple" or "my_mod:forge-recipes/storm"; a handle from sf2.forge.profile; or a handle from sf2.forge.register_recipe.
selection Optional. "random" (default, the original forge) or "choose". "random" changes nothing and cannot undo another mod’s configuration.
tiers Required for "choose", not allowed for "random". An array of 1 to 5 tiers, in the order the forge shows them.

Each tier:

Field Meaning
id Required. 1 to 32 characters of a-z, 0-9, _ or -; unique within the recipe. Saved with timed orders.
name Optional localization handle shown to the player. Default: the id.
strength Required number from 0 to 1: the fraction of the recipe item’s deviation range (0 = minimum, 1 = maximum).
price Optional price multiplier from 0.01 to 100. Default 1.
local sf2 = require("sf2")
-- capabilities = ["content.register", "content.patch"]
-- Also declare the core dependency in mod.toml. The forge.tier.* keys come
-- from your mod's localization files.
sf2.forge.configure_recipe {
recipe = "Simple",
selection = "choose",
tiers = {
{ id = "low", name = sf2.localization.key("forge.tier.low"), strength = 0.0, price = 0.5 },
{ id = "mid", name = sf2.localization.key("forge.tier.mid"), strength = 0.5, price = 1.0 },
{ id = "high", name = sf2.localization.key("forge.tier.high"), strength = 1.0, price = 2.0 },
},
}

With Simple’s weapon range of -30..30, Low gives the base aspect -30, Mid the base aspect and High +30, for half, the normal and double the normal price.

Only one mod can configure a recipe; a second configuration conflicts even if it is identical. The tiers participate in the content compatibility fingerprint. Unloading the mod (Apply & Restart) restores the random roll. Enchantments already made keep their power, and a timed order that is already running still applies its stored picks.

Signature: sf2.forge.configure_candidate(definition)

Returns: Nothing (nil).

When: During mod loading, for a recipe that this mod or a mod loaded before it switched to "choose" with sf2.forge.configure_recipe. The order of the two calls inside your script does not matter; the check runs when your content is committed.

Requires: The same capability as configure_recipe for that recipe (content.patch for core or another mod’s recipe, content.register for your own), and declared dependencies for every namespace you reference.

Sets the price multiplier and an optional unlock requirement for one candidate of a choose recipe. Candidates without a configuration cost the normal price and are available whenever they are ready.

Field Meaning
recipe Required. Same forms as in configure_recipe.
perk Required. A perk handle (sf2.perks.get/sf2.perks.register), a scripted enchantment handle (sf2.enchantments.register), or a qualified perks/... or enchantments/... ID such as "core:perks/PERK_HERMITSTORM".
equipment Optional sf2.forge.WEAPON, ARMOR, HELM, RANGED or MAGIC. Omitted: the setting applies to every equipment category of the recipe. A category-specific setting wins over one without equipment for the same perk.
price Optional multiplier from 0.01 to 100. Default 1.
unlock Optional table { defeated = { ... } }. The candidate stays locked until the player has won a fight against any one of the listed opponents (see below). 1 to 32 entries.
locked_text Optional localization handle shown while the candidate is locked, for either reason (incomplete set or missing win). Default: a short English message.

Each unlock.defeated entry is one of:

  • a warrior handle from sf2.warriors.register, or a qualified ID such as "my_mod:warriors/storm_master";
  • a warrior template handle from sf2.warriors.get_template or sf2.warriors.register_template, or a qualified ID such as "core:warrior-templates/hermit_swords".

Core story opponents, including the bosses, have no warrior IDs of their own: use their template. The template ID is the native template name in lower case, for example core:warrior-templates/lynx_claws, hermit_swords, butcher_backswords, wasp_naginata, shogun_katana. What counts as a win is described in sf2.profile.has_defeated.

local sf2 = require("sf2")
-- capabilities = ["content.register", "content.patch"]
-- Also declare the core dependency in mod.toml.
local storm = sf2.perks.get("core:perks/PERK_HERMITSTORM")
sf2.forge.configure_candidate {
recipe = "Complex",
perk = storm,
price = 1.5,
unlock = { defeated = { "core:warrior-templates/hermit_swords" } },
locked_text = sf2.localization.key("forge.locked.hermit"),
}
-- A mythical set enchantment: available once the whole set is owned (the
-- original rule), at double price.
sf2.forge.configure_candidate {
recipe = "Complex",
perk = sf2.perks.get("core:perks/PERK_MONK_SET_WHIRL"),
equipment = sf2.forge.WEAPON,
price = 2,
}

A set (combo) enchantment remains locked until the player owns every item of its set, whatever unlock says; unlock adds a requirement, it never removes one. Two settings for the same recipe, perk and equipment value conflict, including across mods. A perk that is not in the recipe’s pool is accepted but has no effect. Settings participate in the content compatibility fingerprint and are removed when the mod unloads.