Skip to content

Crafting System

The Crafting system is a scene-scoped, data-driven job service that performs preflight checks, accept-time consumption, queued job execution, delivery, persistence, modifiers, validators, routing, and offline progress — all through a clean, result-based API.

✔ Deterministic when seeded (subject to call order and batching rules) ✔ Adapter-driven (Inventory + Currency) ✔ Validators, modifiers, cooldowns, and authority gates ✔ Full offline progress + save/restore ✔ Suitable for survival, RPG, idle, and base-builder loops

If something is documented here, it reflects actual runtime behaviour as implemented by CraftingService.


Folder Overview

Concept Description
CraftingService Orchestrates preflight → accept → queue → run → deliver → lifecycle → offline restore.
RecipeCore / RecipeDefinition Pure runtime recipe vs Unity-authored designer asset (optional).
Adapters Connect Crafting to Inventory, Currency, and routing backends.
Validators Gate or block crafts (cooldowns, level gates, global rules).
Modifiers Adjust duration, outputs, cost, and bonus chances.
Jobs Accepted crafting tasks with lifecycle events and persistence.

Lifecycle signals are exposed via: OnJobAccepted, OnJobStarted, OnJobProgress, OnJobCompleted, OnJobFailed, OnJobCancelled.

Persistence is handled with: SaveActiveJobs() and RestoreJobs().


Usage Guidance

Add the Service

Add CraftingService to a scene. The service is scene-scoped and manages all crafting jobs within that scene.

Bind Adapters

  • Inventory adapter → required
  • Currency adapter → optional (crafts are free if missing)

Built-in adapters may auto-resolve if present; custom projects can supply their own implementations.

Define a Recipe

Use either:

  • RecipeCore — pure runtime, dependency-free asset
  • RecipeDefinition — Unity-authored convenience asset (optional, requires REV_INVENTORY_PRESENT)

Always resolve recipes via RecipeResolve.

Preflight & Enqueue

var svc   = FindFirstObjectByType<CraftingService>();
var check = svc.CanCraftDetailed(player, recipe, requested: 1);

if (check.maxCrafts > 0)
{
    var req = new CraftRequest(player, recipe, 1, "Backpack", "Forge");
    var job = svc.Enqueue(req);
}
else
{
    Debug.LogWarning($"Cannot craft: {check.reason}");
}

Subscribe to Lifecycle Events

svc.OnJobAccepted  += j     => Debug.Log($"Accepted {j.recipe.name}");
svc.OnJobCompleted += j     => Debug.Log($"Completed {j.recipe.name}");
svc.OnJobFailed    += (j,r) => Debug.LogWarning($"Failed: {r}");

What Lives Here

CraftingService

The orchestration root. It:

  • Runs deterministic preflight
  • Consumes inputs and currency at accept-time
  • Schedules jobs with global and per-station concurrency
  • Re-applies modifiers at delivery
  • Handles refunds, failures, and lifecycle events
  • Restores and completes jobs offline using wall-clock time

Recipes

  • RecipeCore — authoritative runtime data
  • RecipeDefinition — Unity-only authoring helper (optional)
  • Conversion and resolution are performed via RecipeResolve

Adapters

Adapters form the integration boundary:

  • Inventory → counts, space checks, consume/add
  • Currency → balance checks, debit, refund
  • Output routing → destination container resolution

Crafting Core does not depend on any concrete backend.


Validators

Validators run after core preflight (subject to service short-circuit rules) and may:

  • Block crafting entirely
  • Replace the reported failure reason
  • Apply global or contextual policy checks

Validators do not bypass ingredient, currency, or space preflight failures.


Modifiers

Modifiers adjust runtime behaviour:

  • Duration
  • Output multipliers
  • Currency multipliers
  • Extra and chance-based outputs

They are evaluated multiple times per craft:

  • during preflight/accept
  • again at delivery

Jobs

Jobs represent accepted crafts and expose:

  • Owner and recipe
  • Duration and progress
  • State transitions
  • Persistence snapshots
  • Lifecycle callbacks

Delivery & cost semantics

  • Output delivery is all-or-nothing. A craft's outputs (base, extra, and rolled chance outputs) are committed together. If any one cannot be placed — e.g. the inventory filled up during a timed craft, so the accept-time space preflight no longer holds — the adds already applied are rolled back and the job fails (NoSpace*).

The rollback is best-effort, and that limit is real. It is performed by giving the items back through the adapter, so it depends on the adapter and container behaving — a container that has since filled, or an adapter that refuses the give-back, leaves partial state. The service says so at CraftingService's refund documentation, and the give-back failures are reported rather than swallowed. This paragraph used to claim a failed craft never leaves a partial set of outputs behind, which is stronger than the code can promise.

(The opt-in escrow path remains the strong route for inputs+currency+outputs atomicity on immediate crafts.) * Currency cost rounds per craft. The priced unit is ceil(amountPerCraft × multiplier); a batch of N costs exactly N of those. Batching and crafting one-at-a-time cost the same, and the debit matches what preflight reports as affordable. * Space preflight budgets each item cumulatively. A recipe that produces the same item through several outputs (e.g. a base output plus a chance output of that item) is checked against the summed quantity, so preflight does not over-count free space. Distinct items competing for slots in one container are approximated — the adapter exposes no whole-set space query — but atomic delivery makes an optimistic approximation safe (a clean fail, never a partial delivery).


Diagnostics

Preflight

var check = svc.CanCraftDetailed(player, recipe, requested: 10);
Debug.Log($"Max crafts: {check.maxCrafts} — Reason: {check.reason}");

Probe

var probe = svc.Probe(player, recipe, requested: 10);
Debug.Log($"Items:{probe.byItems} Currency:{probe.byCurrency} Space:{probe.bySpace}");

Preflight Order

  1. Inputs
  2. Currency
  3. Space (routing + adjustments)
  4. Validators (if reached)
  5. Final clamp

Purpose

The Crafting system provides a structured way to:

  • validate crafting requests before execution
  • execute crafting over time using jobs
  • integrate with inventory and currency through adapters
  • support offline progress and persistence

It is designed for systems that require queued, time-based, or policy-driven crafting flows.


Important Notes

  • Crafting is adapter-driven and backend-agnostic
  • Offline progress depends on IWallClockProvider
  • Modifiers apply in both live and offline flows; validators are acceptance-time only
  • Determinism depends on RNG seeding and call order
  • Batch approximation may affect deterministic behaviour if enabled

Not for Production Use

Teachables and demo panels are not part of the runtime pipeline

  • Teachables and demo panels in this module are for learning and validation only
  • They are not part of the runtime crafting pipeline

  • Inventory (for item storage and consumption)
  • Currency (for cost handling and refunds)
  • Integrations (for adapter implementations)

Jobs, Persistence & Offline Progress

  • Active jobs serialize via SaveActiveJobs()
  • Restoration uses RestoreJobs()
  • Offline progress is computed using acceptedAtUtc and IWallClockProvider
  • Delivery logic, routing and modifiers apply during offline completion — validators do not

Validators run when a craft is accepted, not when it completes. By the time an offline job is restored its inputs have already been consumed, so a validator refusing at that point would leave a job that cannot proceed and cannot be undone — there is no defined recovery for it, which is why the restore path has never called them. If you need a rule enforced at delivery time, put it in routing or in the delivery adapter, both of which do run there.

Paused jobs do not progress offline, and neither do queued ones. Offline elapsed time is credited only to a job that was already running when it was saved: a queued job has not started crafting, so its wait in the queue is not treated as craft time and a backed-up queue does not complete on reload.

Completion idempotency

Each job carries a stable completionTxnId (persisted in its snapshot). When a job reaches a terminal delivery attempt — completed live, completed during offline restore, or failed at delivery — the service records that id. Restoring the same snapshot set again (e.g. a crash before re-save, or a cloud-save conflict) is then a no-op for those jobs rather than a duplicate delivery or refund.

This guard is in-memory and scoped to a single CraftingService instance. It fully covers re-entrant / repeated RestoreJobs calls within a session. Across process restarts it cannot help by itself: the host must re-save the post-restore state after RestoreJobs and must not replay a stale snapshot set.

Call ClearAppliedCompletions() when you load a save

A load is a rewind, and this record does not rewind with it. It cannot tell a completion applied by an earlier restore from one a craft applied by finishing live a moment ago — both name the same id. So a save taken mid-craft, loaded after that craft finished, had its job skipped as already-applied while the inventory and currency around it rewound to before it finished. The player lost the inputs, the outputs and the job, and the only trace was a log line.

Clear the history immediately before restoring, and the record still dedups everything it was built for — an offline completion reconciled during that load is not reconciled twice:

craftingService.ClearAppliedCompletions();
craftingService.RestoreJobs(snapshots, ownerResolver, recipeResolver);

CraftingSaveParticipant already does this, so a project loading through the save coordinator needs nothing. It applies to hosts driving RestoreJobs themselves.

The same call is also how you release the history after persisting post-restore state, which is what it was originally for: offline-completed jobs are no longer in any live snapshot, so they can no longer be replayed.


Advanced: Immediate Escrow Crafting

CraftingService exposes an optional escrow path for atomic, immediate crafts via TryCraftImmediateEscrow.

Characteristics:

  • Immediate only (zero-duration crafts)
  • No CraftJob is created
  • No job lifecycle events are emitted
  • Inputs, currency, and outputs are reserved and committed atomically
  • Uses a single inventory container (no output routing)
  • Requires reservation-capable adapters

This path is intended for advanced use cases (e.g. transactional or server-authoritative flows).

For time-based or offline crafting, use the standard job pipeline.


Final Notes

  • Nothing in Core depends on Unity UI, Editor tooling, or teaching panels
  • Integration behaviour depends on installed adapters
  • Optional systems (Inventory, Currency) extend behaviour but are not required