Skip to content

🧪 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:

  • proposed contains the service’s computed maxCrafts and the current blocking reason.
  • Validators may lower maxCrafts, and may override the reason.
  • Negative maxCrafts values are clamped to 0 by 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:

  1. Ingredients check
  2. Currency check
  3. Space check (routing + adjusted outputs)
  4. Validators, only if the service has not already returned:
  5. Component validators (ICraftingValidator on owner + parents)
  6. Internal delegate (OnValidateCraft)
  7. 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 CraftFailReason represents 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:
  • PerRecipe
  • GlobalPerOwner
  • Global
  • Optional includeStationTagInKey so different stations do not share cooldowns.
  • Subscribes to CraftingServiceCore.OnJobAccepted to 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 ICraftingLevelProvider on 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:

  • AllowWithoutSpaceValidator does 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.stationTag as 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, NoInputs
  • NoCurrency, NoSpacePreflight, NoSpaceImmediate, NoSpaceAtDelivery
  • ConsumeInputsFailed, ConsumeCurrencyFailed
  • OnCooldown, 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.