Skip to content

RecipeBuilder

QinShenYu edited this page Jun 7, 2026 · 2 revisions

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.


Quick Start

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

RecipeBuilder is a chainable builder. Every configuration method returns this; commit with .Register().

Creation

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 & Difficulty

.Category(RecipeCategory category)
.RequireINT(int level)              // default: 0

RecipeCategory 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.

Ingredients — Specific ID

.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.

Ingredients — By Quality

.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).

Ingredients — Liquid

.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 — Item

.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 = true

By default, the game drains any liquid stack on a freshly-spawned solid result; this flag preserves it.

Result — Liquid

.ResultLiquid(string liquidId, float amount)

Produces a liquid result. At craft time, ScavLib follows RecipeResult.SpawnResult exactly:

  1. Scan the player's inventory for a WaterContainerItem whose stack contains exactly one liquid matching liquidId and has at least amount ml of space remaining. If found, the result is poured in.
  2. Otherwise, instantiate a craftingbottle Prefab 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.

Registration

.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 Behavior

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 RequireINT close 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 RequireINT at 0 so even a fresh player gets full quality.
  • The INT-failure scaler only attenuates resultCondition. Liquid results in the craftingbottle fallback path will still spawn the bottle; only the contained volume is reduced.

Worked Example — Quality-Matched Salve

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();
}

Worked Example — Distilled Liquid

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.


Inspecting Recipes

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.

Clone this wiki locally