Skip to content
ct

Item Vault v1.0

Crafting Integration

Build a recipe-driven crafting system on Item Vault's atomic InventoryTransaction and typed result API.

Status
Current
Version
v1.0
Updated
2026-07-31

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 = &""
)-> CraftingResult

Pass 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

  • CraftingServicecraft, can_craft, set_log_sink, reset_metrics, clear_request_tracking, operation_counter, metrics
  • CraftingRecipeid, display_name, inputs, outputs, station_tags, is_valid, validate_item_ids, to_dict, from_dict
  • CraftingIngredientitem_id, quantity, item_definition
  • CraftingRecipeRegistryregister, get_recipe, has, all, persisted_recipes, load_dict
  • CraftingResultsuccess, accepted(), reason_code(), consumed, produced, inventory_outcome, detail, REASON_* constants
  • CraftingTransactionbuild_ops, preflight_fits, execute, net_outputs
  • CraftingSaveto_dict, from_dict
  • InventoryTransaction — the atomic primitive the module builds on (create_transaction, add_item, remove_item, execute, automatic rollback)