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 assetRecipeDefinition— Unity-authored convenience asset (optional, requiresREV_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¶
- Inputs
- Currency
- Space (routing + adjustments)
- Validators (if reached)
- 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
Related Documentation¶
- 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
acceptedAtUtcandIWallClockProvider - 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
CraftJobis 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