Item Vault ships a Crafting module under addons/item_vault/crafting/. It
is an optional layer built on real Item Vault item and inventory types: recipe
definitions, one atomic craft transaction, typed results, and crafting-domain
persistence.
Crafting depends on Item Vault. Item Vault core does not depend on Crafting. Deleting
addons/item_vault/crafting/removes Crafting completely; the rest of the plugin stays loadable, testable, and distributable. The repo enforces this with a removability gate.
This guide shows how to author recipes, register them against a real
ItemDatabase, run an atomic craft through CraftingService, read the typed
CraftingResult, and persist crafting-domain state. It assumes you already
have an inventory and item definitions (see
Getting Started and
Item Data and Stack Metadata).
Module layout
| Path | Purpose |
|---|---|
recipes/crafting_ingredient.gd |
One recipe line: real item id + quantity |
recipes/crafting_recipe.gd |
Immutable recipe data + fail-closed validation |
recipes/crafting_recipe_registry.gd |
Id-keyed registry, duplicate detection |
results/crafting_result.gd |
Typed craft outcome (stable reasons + facts) |
results/crafting_fact.gd |
Exact consumed/produced fact |
results/crafting_inventory_outcome.gd |
Nested Item Vault typed outcome |
services/crafting_service.gd |
Orchestrator: validate → preflight → atomic craft |
services/crafting_request.gd |
Plain request data |
services/crafting_metrics.gd |
Outcome counters (HUD/telemetry) |
services/crafting_log.gd |
Structured event payload |
transactions/crafting_transaction.gd |
The one atomic Item Vault mutation path |
persistence/crafting_save.gd |
Crafting-domain save/load |
Recipe authoring
Recipes are CraftingRecipe resources referencing real ItemDefinition.id
values. A recipe is pure data — it describes requirements and outputs by
identity, never holding live inventory nodes.
var wood:= CraftingIngredient.new()
wood.item_id= &"wood"
wood.quantity= 2
var plank:= CraftingIngredient.new()
plank.item_id= &"plank"
plank.quantity= 1
var recipe:= CraftingRecipe.new()
recipe.id= &"plank_from_wood"
recipe.display_name= "Plank from Wood"
recipe.inputs= [wood]
recipe.outputs= [plank]CraftingIngredient fields: item_id (the ItemDefinition.id), quantity
(positive int), and an optional item_definition reference for editor authoring
(fails closed if its id disagrees).
CraftingRecipe additionally carries optional station_tags
(Array[StringName]) with workbench / unlock tags for consuming games; the
module does not interpret them.
Recipes serialize through to_dict() / CraftingRecipe.from_dict() (fail-closed
on malformed or unsupported-schema data), so you can author them as .tres
resources or build them at runtime.
Register recipes
Register against a real ItemDatabase so unknown ids and duplicate ids are
rejected up front.
var registry:= CraftingRecipeRegistry.new()
var registered:= registry.register(recipe, database)
if not registered.success:
push_error("recipe rejected:%s" % registered.reason)register() validates fail-closed:
| Condition | Reason |
|---|---|
| structurally invalid recipe (empty id/inputs/outputs, non-positive quantity, null ingredient, id mismatch) | REASON_INVALID_RECIPE |
| duplicate recipe id | REASON_DUPLICATE_ID |
| any item id unknown to the database | REASON_UNKNOWN_ITEM |
| null database | REASON_INVALID_DATABASE |
Pass p_persist = true for runtime-authored recipes you want included in
CraftingSave (shipped .tres recipes pass false so save data never
duplicates them).
Run an atomic craft
CraftingService is stateless by convention. One call to craft() validates,
dry-runs output capacity on a transient clone, then executes one atomic
InventoryTransaction — consume inputs, insert net outputs, commit. On any
failure nothing is partially consumed, nothing is duplicated, and Item Vault
state is unchanged.
var service:= CraftingService.new()
var result:= service.craft(recipe, inventory, database,1)
if result.accepted():
for factin result.consumed:
print("consumed%d x%s" % [fact.quantity, fact.item_id])
for factin result.produced:
print("produced%d x%s" % [fact.quantity, fact.item_id])
else:
print("rejected:%s" % result.reason_code())craft() signature:
func craft(
p_recipe:CraftingRecipe,
p_inventory:Inventory,
p_database:ItemDatabase,
p_quantity:int = 1,
p_request_id:StringName = &""
)-> CraftingResultPass a non-empty p_request_id to reject duplicate requests
(REASON_DUPLICATE_REQUEST). Tracked ids are bounded (1024) and cleared via
clear_request_tracking().
Check craftability without crafting
can_craft() is a pure query — safe for UI previews, tooltips, and AI scoring.
It never mutates metrics, logs, or inventory state.
func can_craft(p_recipe:CraftingRecipe, p_inventory:Inventory, p_database:ItemDatabase)-> bool:
return service.can_craft(p_recipe, p_inventory, p_database).accepted()Handle typed results
CraftingResult carries a schema version, request id, operation id, recipe id,
requested quantity, success, a stable reason code, exact consumed /
produced CraftingFacts, a nested CraftingInventoryOutcome (Item Vault's
typed InventoryResultCode + detail), and an inventory state fingerprint.
Stable reason codes (use the REASON_* constants, never raw literals):
invalid_recipe · invalid_quantity · missing_inventory ·
invalid_inventory · item_definition_unavailable · missing_ingredient ·
insufficient_quantity · output_capacity_unavailable ·
transaction_rejected · transaction_rollback · stale_state ·
duplicate_request · unsupported_recipe_data
Branch on the stable reason so the UI shows why a craft failed:
match result.reason_code():
&"":# success
show_success(result.consumed, result.produced)
&"missing_ingredient",&"insufficient_quantity":
show_missing_inputs(result.detail)
&"output_capacity_unavailable":
show_inventory_full()
&"duplicate_request":
show_already_queued(result.request_id)
_:
show_error(result.reason_code(), result.detail)A failed craft that hit the mutation path exposes the nested Item Vault outcome
on result.inventory_outcome — Item Vault's InventoryResultCode is preserved,
never flattened into a string.
Metrics and logs
CraftingService.metrics (CraftingMetrics) tracks outcome counters for HUD /
telemetry. Route structured event payloads with set_log_sink() — one
CraftingLog per attempt and outcome is delivered; logs also flow through Item
Vault's ItemVaultLogger. Reset counters with reset_metrics().
Persist crafting-domain state
Crafting saves only crafting-domain state: the runtime-authored recipe registry identity and the craft operation counter. Item Vault continues to own item instances, stacks, inventories, containers, and transaction state — the crafting save never holds a second copy of inventory contents.
# Save alongside your Item Vault inventory save.
var crafting_save:= CraftingSave.to_dict(registry, service.operation_counter)
# Restore.
var restored:CraftingSaveData = CraftingSave.from_dict(loaded, database)
service.set_operation_counter(restored.operation_counter)
# restored.registry now holds the runtime-authored recipes; re-register
# shipped .tres recipes as before.Loaders fail closed on unsupported schema versions and malformed recipe data; restoring never duplicates recipes, operations, inputs, or outputs.
Where your game code lives
Keep recipe tables, crafting UI, station simulation, timed queues, skill progression, economy pricing, and AI/GOAP task policy in your game. The Crafting module owns only recipe validation, the atomic inventory mutation, its typed result, and crafting-domain persistence — it never interprets recipe meaning, pawn intent, or game-specific scope boundaries. This keeps the inventory authority single-owner and the crafting logic free to evolve against a stable API.
Try the demo
demo/addon_demos/08_crafting_bench.tscn registers real item definitions,
builds a real inventory, stocks ingredients, shows live craftability, crafts
(real consumption/production), demonstrates a capacity rejection and a
missing-ingredient rejection, saves, reloads, and crafts again.
Related API
CraftingService—craft,can_craft,set_log_sink,reset_metrics,clear_request_tracking,operation_counter,metricsCraftingRecipe—id,display_name,inputs,outputs,station_tags,is_valid,validate_item_ids,to_dict,from_dictCraftingIngredient—item_id,quantity,item_definitionCraftingRecipeRegistry—register,get_recipe,has,all,persisted_recipes,load_dictCraftingResult—success,accepted(),reason_code(),consumed,produced,inventory_outcome,detail,REASON_*constantsCraftingTransaction—build_ops,preflight_fits,execute,net_outputsCraftingSave—to_dict,from_dictInventoryTransaction— the atomic primitive the module builds on (create_transaction,add_item,remove_item,execute, automatic rollback)