-
Notifications
You must be signed in to change notification settings - Fork 0
RecipeBuilder
Namespace: ScavLib.crafting
Registers custom crafting recipes into the game's Recipes.recipes list.
Recipes added through RecipeBuilder plug into the existing crafting UI,
INT-skill check, and result-spawn pipeline — no extra wiring required.
Registration is safe at Awake() time. ScavLib applies a Harmony Postfix to
Recipes.SetUpRecipes() and flushes queued recipes at the correct moment.
After flush, ScavLib also runs the same finalization pass vanilla does:
each recipe gets a stable index, and any RecipeItem whose specificId
matches the result is back-filled with ignoredId so a recipe cannot
consume its own output.
using ScavLib.crafting;
private void Awake()
{
RecipeBuilder.Create("mymod_salve_recipe")
.Category(RecipeCategory.Medicine)
.RequireINT(3)
.Ingredient("bandage", minCondition: 0.5f)
.IngredientLiquid("alcohol", amount: 50f)
.Result("mymod_salve", amount: 1, resultCondition: 1f)
.Register();
}The recipe shows up under the Medicine tab once the world loads and respects the standard player-INT failure curve described below.
RecipeBuilder is a chainable builder. Every configuration method returns
this; commit with .Register().
RecipeBuilder.Create(string recipeId)| Parameter | Type | Description |
|---|---|---|
recipeId |
string |
Unique recipe ID. Used for the Owner Ledger and for scavlib check output. Does not need to match the result item ID. Prefix with your mod name to avoid collisions. |
.Category(RecipeCategory category)
.RequireINT(int level) // default: 0RecipeCategory mirrors the vanilla enum and exposes exactly the five
values the game ships with:
| Value | Crafting Tab |
|---|---|
RecipeCategory.Materials |
Materials |
RecipeCategory.Medicine |
Medicine |
RecipeCategory.Utilities |
Utilities |
RecipeCategory.Tools |
Tools |
RecipeCategory.Food |
Food |
RequireINT(level) is the recipe's recommended INT skill. The number is
fed directly into RecipeResult.SpawnResult(recipeInt), which computes
(player.INT - recipeInt) and applies the failure curve described in
Failure Behavior.
.Ingredient(string specificId,
float minCondition = 0.9f,
bool destroyItem = true)Matches a concrete item ID. Internally sets RecipeItem.specific = true
and RecipeItem.specificId = specificId — callers never touch the
specific flag directly.
| Parameter | Default | Description |
|---|---|---|
specificId |
— | Exact item ID required. |
minCondition |
0.9f |
Minimum item condition (0 ~ 1) to be accepted. The matching item must satisfy item.condition >= minCondition. |
destroyItem |
true |
If true, the matched item is destroyed on craft. If false, its condition is reduced by minCondition and it is kept in inventory. |
.IngredientByQuality(CraftingQuality quality,
bool destroyItem = true)
.IngredientByQuality(string qualityId, float amount,
bool destroyItem = true)Matches any item whose ItemInfo.qualities contains a CraftingQuality
that meets the criteria — the same mechanism vanilla uses for "any blade",
"any disinfectant", etc. The matched item's condition is scaled down by
(required.amount / matched.amount); if the matched quality fully covers
the recipe requirement, condition loss is proportional rather than total.
CraftingQuality is the vanilla (string id, float amount) data class.
The string overload is shorthand for new CraftingQuality(qualityId, amount).
.IngredientLiquid(string liquidId, float amount)
.IngredientLiquidByQuality(CraftingQuality quality)
.IngredientLiquidByQuality(string qualityId, float amount)Liquid ingredients require the player to carry a WaterContainerItem
holding the requested liquid. For specific-ID matches, the container must
hold at least amount ml of liquidId. For quality matches, any liquid
whose scaled qualities meet the criteria is accepted — the consumed volume
is computed proportionally, mirroring RecipeItem.UseItem.
Liquid ingredients always set isLiquid = true and destroyItem = false
on the underlying RecipeItem; the container itself is never destroyed,
only drained.
.Result(string id,
int amount = 1,
float resultCondition = 1f)| Parameter | Default | Description |
|---|---|---|
id |
— | Item ID to spawn. Can be a vanilla ID or a CustomItemBuilder-registered ID. |
amount |
1 |
Number of items produced (each goes through SpawnResult and auto-pickup individually). |
resultCondition |
1f |
Base condition (0 ~ 1). The actual delivered condition is resultCondition * num2, where num2 is the INT-failure scaler (see below). |
If the result item is itself a WaterContainerItem and you do not want
it spawned pre-drained, chain .DontDrainResultLiquid():
.DontDrainResultLiquid() // sets RecipeResult.dontDrainResultLiquid = trueBy default, the game drains any liquid stack on a freshly-spawned solid result; this flag preserves it.
.ResultLiquid(string liquidId, float amount)Produces a liquid result. At craft time, ScavLib follows
RecipeResult.SpawnResult exactly:
- Scan the player's inventory for a
WaterContainerItemwhose stack contains exactly one liquid matchingliquidIdand has at leastamountml of space remaining. If found, the result is poured in. - Otherwise, instantiate a
craftingbottlePrefab at the player's position, auto-pickup, fill it, and schedule it for destruction after 300 seconds. The player has five minutes to decant it into permanent storage.
This is the same code path the vanilla recipes use; ScavLib does not modify the 300 s timer or the single-liquid filtering rule.
.Register()Commits the recipe. Calling Register() twice with the same recipeId
logs a warning and is ignored; the first call's owner is preserved in the
Owner Ledger. There is no overwrite overload — recipes are additive by
design, and overriding a vanilla recipe is intentionally out of scope.
Failure handling is unchanged from vanilla and lives in
RecipeResult.SpawnResult. The relevant logic is:
int diff = player.INT - recipeINT;
// diff >= 0 : full success, result at 100% of resultCondition.
// diff == -1, 50% RNG : result quality scaled by Random.Range(0.2f, 0.9f).
// diff == -3, 50% RNG : no item produced; both upper legs (limbs 5 & 8)
// take 40 pain, -15 skin health, and 2~5 bleed each.
// other negative diffs: silently produce nothing.Practical guidance:
- Set
RequireINTclose to your target player level. A recipe gated three levels above the typical player will routinely maim them. - If you need a "training" recipe, keep
RequireINTat0so even a fresh player gets full quality. - The INT-failure scaler only attenuates
resultCondition. Liquid results in thecraftingbottlefallback path will still spawn the bottle; only the contained volume is reduced.
A medical salve that accepts any disinfectant liquid and any
bandage-quality cloth, and produces a custom mymod_salve:
using ScavLib.crafting;
private void Awake()
{
RecipeBuilder.Create("mymod_salve_recipe")
.Category(RecipeCategory.Medicine)
.RequireINT(3)
.IngredientByQuality("dressing", 1f) // any cloth/bandage
.IngredientLiquidByQuality("disinfectant", 30f) // any disinfectant, 30ml
.Result("mymod_salve", amount: 1, resultCondition: 1f)
.Register();
}Boil 200 ml of dirty water down to 150 ml of clean water using fuel:
RecipeBuilder.Create("mymod_distill_water")
.Category(RecipeCategory.Utilities)
.RequireINT(1)
.IngredientLiquid("dirtywater", 200f)
.IngredientByQuality("fuel", 1f) // any fuel quality
.ResultLiquid("water", 150f)
.Register();The first time it is crafted the player either tops off an existing
single-liquid water bottle they carry, or receives a craftingbottle they
have 300 s to drain into permanent storage.
RecipeBuilder.GetOwner("mymod_salve_recipe");
RecipeBuilder.GetAllRegistered(); // IReadOnlyDictionary<string, string>scavlib check lists every ScavLib-registered recipe alongside its owner
mod and flags duplicate recipeIds.