🧪 Crafting — Validators¶
Validators let you tighten or override preflight rules before a craft is accepted.
They implement ICraftingValidator and are collected from the owner GameObject and its parents
each frame (with per-frame caching for performance).
Validators participate in the preflight decision pipeline of the Crafting system, subject to the current service’s short-circuit rules.
If something is documented here, it reflects actual runtime behaviour as implemented by
CraftingServiceCore.
🧩 Contract¶
public interface ICraftingValidator
{
CraftCheck Validate(ref CraftContext ctx, in CraftCheck proposed);
}
Runtime guarantees:
proposedcontains the service’s computedmaxCraftsand the current blocking reason.- Validators may lower
maxCrafts, and may override thereason. - Negative
maxCraftsvalues are clamped to0by the service. - Validators are not executed when:
- the owner or recipe is invalid
- the inventory adapter is missing
Validators never mutate service state directly and must not perform side effects.
🔄 Validator execution order (exact runtime behaviour)¶
For each preflight call (CanCraftDetailed, Probe), the service evaluates:
- Ingredients check
- Currency check
- Space check (routing + adjusted outputs)
- Validators, only if the service has not already returned:
- Component validators (
ICraftingValidatoron owner + parents) - Internal delegate (
OnValidateCraft) - Service-registered validator hooks
After all validators run, Crafting clamps the result:
maxCrafts = max(0, min(result.maxCrafts, bySpaceUpperBound))
Important consequences¶
- Validators cannot currently bypass ingredient, currency, or space preflight failures.
- Some failures (notably
NoSpacePreflight) short-circuit before validators run. - Validators may still:
- block otherwise-valid crafts
- replace the failure reason for reporting
- The final
CraftFailReasonrepresents the last override applied by validators, unless clamped or short-circuited by the service.
These rules reflect the current implementation and may change if the preflight pipeline is revised.
📦 Built-in validators¶
CooldownValidator¶
Enforces cooldown rules between craft acceptances.
Key characteristics:
- Supports multiple scopes:
PerRecipeGlobalPerOwnerGlobal- Optional
includeStationTagInKeyso different stations do not share cooldowns. - Subscribes to
CraftingServiceCore.OnJobAcceptedto record timestamps. - Stores cooldown state in-memory only, keyed by a composed runtime hash.
- Cooldown state is cleared on
OnDisable().
Behaviour:
If:
elapsed < cooldownSeconds
The validator returns:
maxCrafts = 0
reason = CraftFailReason.OnCooldown
Notes:
- Cooldown keys are not stable across sessions.
- This validator does not provide a persistence API.
- Helper methods exist for tooling/tests but are not gameplay APIs.
LevelGateValidator¶
Blocks crafting when the owner’s level is below the required threshold.
Runtime behaviour:
- Resolves
ICraftingLevelProvideron the owner or its parent chain. - Opt-in design:
- If no provider is found, the validator does not block.
- Supports:
- global minimum level
- per-recipe overrides
- optional station-tag filtering (trimmed, case-insensitive)
- Blocks with
CraftFailReason.LevelTooLow.
The validator respects earlier failures and does not replace them unless explicitly intended.
AllowWithoutSpaceValidator¶
Intended to override a space preflight failure and allow a single craft.
Behaviour (intended):
If:
proposed.maxCrafts == 0
&& proposed.reason == CraftFailReason.NoSpacePreflight
Then the validator would return:
maxCrafts = 1
reason = CraftFailReason.None
⚠️ Current limitation¶
In the current CraftingServiceCore implementation, preflight returns early on
CraftFailReason.NoSpacePreflight before validators run.
As a result:
AllowWithoutSpaceValidatordoes not take effect today- It exists as:
- an explicit illustration of a potential override rule, and
- a forward-compatible helper should the preflight pipeline be relaxed in future
Delivery still enforces space normally and may fail with NoSpaceAtDelivery.
✏️ Writing your own validator¶
Example — station tag whitelist:
[DisallowMultipleComponent]
public sealed class StationWhitelistValidator : MonoBehaviour, ICraftingValidator
{
[SerializeField] string[] allowedTags;
public CraftCheck Validate(ref CraftContext ctx, in CraftCheck proposed)
{
// Respect earlier failures unless intentionally overriding
if (proposed.maxCrafts <= 0 && proposed.reason != CraftFailReason.None)
return proposed;
if (allowedTags == null || allowedTags.Length == 0)
return proposed;
var tag = string.IsNullOrWhiteSpace(ctx.stationTag) ? null : ctx.stationTag.Trim();
foreach (var t in allowedTags)
{
if (string.Equals(t?.Trim(), tag, StringComparison.OrdinalIgnoreCase))
return proposed;
}
return new CraftCheck
{
maxCrafts = 0,
reason = CraftFailReason.InvalidInput
};
}
}
💡 Validator best practices¶
- Do not override earlier failures unless you are explicitly rewriting behaviour.
- Validators run on every preflight — keep logic allocation-free.
- Treat
ctx.stationTagas raw input; trim and normalise before comparison. - Prefer validators for policy decisions, not execution logic.
- Store persistent state outside validators unless you manage it explicitly.
❗ Failure reasons (reference)¶
Common CraftFailReason values validators may encounter:
InvalidInput,NoAdapter,NoInventory,NoInputsNoCurrency,NoSpacePreflight,NoSpaceImmediate,NoSpaceAtDeliveryConsumeInputsFailed,ConsumeCurrencyFailedOnCooldown,LevelTooLow
Validators are a policy layer in the preflight pipeline. They allow you to block or refine crafting rules without modifying Core logic, subject to the service’s current short-circuit behaviour.