Crafting — System Guarantees Matrix¶
The behavioural contract
This page defines the behavioural contract of the Crafting system.
No marketing. No implication. Just guarantees — and explicit non-guarantees.
Quick Navigation¶
- Core Service
- Preflight
- Accept / Enqueue
- Jobs / Scheduling
- Delivery
- Refunds / Compensation
- Escrow Path
- Authority
- Time / Offline Progress
- Persistence / Restore
- Events
- Modifiers / Validators
- Routing
- Determinism / RNG
- Public API
- Non‑Guarantees
- Final Summary
1. Core Service¶
CraftingService¶
| Aspect | Guarantee |
|---|---|
| Orchestration root | ✔ Single service owns crafting truth |
| Job creation | ✔ Service-only |
| Job mutation | ✔ Service-only |
| Runtime usage | Main thread |
| Adapter-driven | ✔ Yes |
| Inspector required | ❌ No |
| UI required | ❌ No |
2. Preflight¶
| Aspect | Guarantee |
|---|---|
| Mutates state | ❌ Never |
| Safe for UI / tools / teachables | ✔ Yes |
| Check order | Inputs → Currency → Space |
| Validators run | ✔ Only after core checks succeed |
| No-space override via validator | ❌ Not guaranteed |
| Routing included in space check | ✔ Yes |
| Chance-output space policy applied | ✔ Yes |
| Same item through several outputs | ✔ Budgeted cumulatively, matching what delivery attempts |
| Item guid casing | Ignored — "Plank" and "plank" are one item, as ItemDatabase resolves them |
3. Accept / Enqueue¶
| Aspect | Guarantee |
|---|---|
| Authority checked before mutation | ✔ Yes |
| Inputs consumed immediately (default path) | ✔ Yes |
| Currency debited immediately (default path) | ✔ Yes |
| Job created only after accept succeeds | ✔ Yes |
| Partial failure compensation | ✔ Best-effort |
| Batch support | ✔ Yes |
| Timed craft support | ✔ Yes |
| Cross-system atomicity | ❌ Not guaranteed |
4. Jobs / Scheduling¶
| Aspect | Guarantee |
|---|---|
| Queued jobs supported | ✔ Yes |
| Running jobs supported | ✔ Yes |
| Paused jobs supported | ✔ Yes |
| Per-station caps | ✔ Yes |
| Global parallel cap | ✔ Yes |
| Queue reorder support | ✔ MoveQueuedFirst |
| Scheduler fairness | Basic fairness |
| Deterministic scheduling across external state changes | ❌ Not guaranteed |
5. Delivery¶
| Aspect | Guarantee |
|---|---|
| Modifiers re-evaluated at delivery | ✔ Yes |
| Output routing resolved at delivery | ✔ Yes |
| Outputs added immediately on success | ✔ Yes |
| Delivery failure possible after accept | ✔ Yes |
| Timed jobs deliver later | ✔ Yes |
| Delivery atomicity (default path) | ❌ Not guaranteed |
6. Refunds / Compensation¶
| Aspect | Guarantee |
|---|---|
| Currency refund on delivery failure | ✔ Attempted |
| Input refund on delivery failure | Optional |
| Input refund success | Best-effort |
| Currency refund success | Depends on adapter |
| Refunds outside service | ❌ Not supported |
| Net-zero guarantee | ❌ Not guaranteed |
7. Escrow Path¶
TryCraftImmediateEscrow¶
| Aspect | Guarantee |
|---|---|
| Scope | Immediate (zero-duration) crafts |
| Job created | ❌ No |
| Inventory reservations required | ✔ Yes |
| Currency holds required | ✔ Yes |
| Routing used | ❌ No |
| Timed crafts supported | ❌ No |
| Refuses missing escrow seams | ✔ Yes — NoAdapter, before anything is consumed |
| Shipped adapter support | ✔ Yes. InventoryCraftingAdapter reserves inputs and outputs |
| Currency holds shipped | ✔ Yes — CurrencyHoldCraftingAdapter, when the stack is escrow-composed |
| Atomicity | Depends on adapter |
How the shipped reservations hold
They reserve by doing: inputs are consumed and outputs added when the reservation is taken, Release puts them back, and TryCommit only finalises. That is the same model the currency escrow uses, where EscrowCurrencyService debits at hold time and treats commit as a no-op.
It is not a stylistic choice. The escrow path unwinds nothing if an output commit fails — by then the inputs are committed — so a reservation whose commit could fail would destroy them. Doing the work up front makes commit unable to fail, which is the invariant the service is written against.
The consequence worth knowing: between reserving and committing, the inputs are already gone and the outputs already present. Escrow crafts are immediate and synchronous, so nothing observes that window except code re-entered from the same call — but a currency-hold event handler is exactly such code.
A currency cost needs an escrow-composed currency service
CurrencyHoldCraftingAdapter maps the crafting hold seam onto ICurrencyEscrow, so holds work — but only where the composed currency stack actually has an escrow layer. Without one TryHold refuses and the craft declines, having consumed nothing. Recipes with no currency cost do not need it at all.
8. Authority¶
| Aspect | Guarantee |
|---|---|
| Mutation gating supported | ✔ Yes |
| Enqueue gated | ✔ Yes |
| Cancel / Pause / Resume gated | ✔ Yes |
| Scheduler control gating | Optional (Gate Scheduler Controls). Covers the four station-cap setters and SetMaxParallelJobs, which changes the global concurrency cap and promotes queued jobs exactly as they do |
Preflight (CanCraft, CanCraftDetailed, Probe) consults authority | ❌ No — authority is evaluated at Enqueue. A preflight that passes can still be refused Unauthorized; use CanMutate(owner, out reason) if you need the answer up front |
| Restore / offline reconciliation gated | ❌ No — a load is a rewind, not a mutation. RestoreJobs clears the live job list and re-creates jobs with no authority call, so drive it only from a trusted load path |
| Destroyed assigned authority | ⚠ Still the gate. The reference is interface-typed, so Unity's destroyed-object null never applies: HasAuthorityGate stays true and the component keeps deciding until SetAuthority or Configure(authority:) replaces or clears it. Crafting assigns rather than discovers, so the component's lifetime is the project's |
| Asks per operation | Not promised. Enqueue(count: n) asks once for n jobs and EnqueueMany asks per job; a serverOnly workbench with no bench authority asks the service authority twice. ICraftingAuthority states that it does not enforce when callers invoke it |
| Read queries gated | ❌ No |
| Multiplayer replication | ❌ Not provided |
9. Time / Offline Progress¶
| Aspect | Guarantee |
|---|---|
| Gameplay time abstraction | ✔ ITimeProvider |
| Wall-clock abstraction | ✔ IWallClockProvider |
| Offline progress optional | ✔ Yes |
| Paused jobs tick offline | ❌ Never |
| Queued jobs tick offline | ❌ Never — only a job that was already running accrues offline time |
| Offline elapsed cap | Optional |
| acceptedAtUtc captured | ✔ Yes |
Persistence / Restore¶
| Aspect | Guarantee |
|---|---|
| Snapshot type | Active jobs |
| Queued jobs restorable | ✔ Yes |
| Running jobs restorable | ✔ Yes |
| Paused jobs restorable | ✔ Yes |
| Inputs/currency re-consumed on restore | ❌ No |
| Offline completion applied during restore | ✔ When enabled |
| Idempotent completion across repeated restore | ✔ Repeated loads converge — the applied-completion record clears on restore (a load rewinds the world it described) and still dedupes within a single restore, so an offline completion applies once per load into state the other participants just rewound |
| Applied-completion record is bounded | ❌ No — it grows for the life of the service. ClearAppliedCompletions() is the only prune; editor and development builds warn once when it gets large. A size cap is deliberately not offered: evicting ids would silently re-open double delivery for exactly the completions a restore carries |
| Save system provided | ❌ Not provided |
| Persistence storage (files/cloud) | ❌ Not provided |
| Save/restore timing responsibility | Developer-defined |
Persistence responsibility
Crafting defines what job data means and how it resumes —
your game defines when, where, and how it is persisted.
11. Events¶
| Aspect | Guarantee |
|---|---|
| Lifecycle events exposed | ✔ Yes |
| Progress events throttled | ✔ Yes |
| Preflight rejection event | ✔ Yes |
| Designer UnityEvent mirrors | ✔ Yes |
| C# event vs its designer mirror | ✔ C# first, then the mirror, phase by phase |
| That order across enqueue modes | ✔ Identical for a single batch and for separate crafts |
12. Modifiers / Validators¶
| Aspect | Guarantee |
|---|---|
| Modifiers supported | ✔ Yes |
| Validators supported | ✔ Yes |
| Owner/parent discovery | ✔ Yes |
| Modifiers run at accept | ✔ Yes |
| Modifiers run at delivery | ✔ Yes |
| Validators mutate state | ❌ Never |
| Modifier/validator ordering contract | ❌ Not guaranteed |
13. Routing¶
| Aspect | Guarantee |
|---|---|
| Output router supported | ✔ Yes |
| Routing used in preflight | ✔ Yes |
| Routing used at delivery | ✔ Yes |
| Router auto-discovered | ❌ No |
| Router used by escrow | ❌ No |
14. Determinism / RNG¶
| Aspect | Guarantee |
|---|---|
| Deterministic RNG supported | ✔ Yes, opt-in via UseDeterministicRng(seed) |
| Deterministic by default | ❌ No — the default is UnityEngine.Random, the engine-wide stream |
| Seeded results repeat with same call sequence | ✔ Yes |
| Binomial approximation deterministic | ❌ No |
| Exact chance rolls available | ✔ Yes |
| Identical results if call order changes | ❌ Not guaranteed |
Which determinism, exactly. "Deterministic" answers three different questions and the answers differ. Stated once here so nothing above has to be read hopefully:
| Question | Answer |
|---|---|
| Same process, same run, replayed from one seed | ✔ Yes, for a flow you drive explicitly — roll, deliver, tick. ❌ No for a session running through Update, where frame timing enters the outcome |
| Two processes, same build, same machine | ❌ Not guaranteed. Nothing reconciles wall-clock or frame-count state between them, and a crafting save carries a fresh transaction GUID |
| Two platforms, or two CPU architectures | ❌ Not guaranteed wherever a transcendental is involved. The pure roll paths are integer and float arithmetic only, and are bit-identical across Mono/IL2CPP and x86/ARM |
| Deterministic at all without opting in | ❌ No. The default RNG is UnityEngine.Random, the engine-wide shared stream |
The first row is what a drop-rate check and a bug repro need, and it is the one covered by tests. The second and third are what a lockstep or rollback netcode needs, and this framework does not provide them — see Authority for what it does provide.
15. Public API¶
| Aspect | Guarantee |
|---|---|
| Stable supported service surface | ✔ Yes |
| Public recipe construction seam | ✔ RecipeCore.Create |
| Public adapter seams | ✔ Yes |
| Internal hooks public | ❌ No |
| Integration adapters public | ❌ No |
Non‑Guarantees¶
Crafting does not guarantee
- True multi-operation atomicity on the default job path
- Multiplayer replication
- Prediction / reconciliation
- Server authority implementation
- Validator execution after failed space preflight
- Escrow support for timed crafts
- Routing support in escrow path
- Deterministic outcomes if RNG call order changes
-
Safe mutation outside CraftingService
-
Built-in save system or storage layer
- Automatic cross-session identity resolution (owner / recipe)
- Automatic save/load timing or lifecycle integration
Final Summary¶
| Layer | Strong Guarantee | Best Effort | Not Guaranteed |
|---|---|---|---|
| Preflight purity | ✔ | ||
| Single-service job ownership | ✔ | ||
| Default-path compensation | ✔ | ||
| Escrow immediate craft | ✔* | ||
| Timed craft delivery atomicity | ❌ | ||
| Multiplayer replication | ❌ | ||
| Cross-system atomicity | ❌ | ||
| Persistence integration | ❌ |
*Depends on reservation/hold adapter correctness.
System Philosophy¶
System philosophy
Crafting is:
- Explicit
- Job-driven
- Adapter-based
- Authority-aware
- Honest about guarantees