Skip to content

Attributes — System Guarantees Matrix

The behavioural contract

This page defines the behavioural contract of the attribute system.

No marketing. No implication. Just guarantees — and explicit non-guarantees.


Quick Navigation

  • Container
  • Ids
  • Bounds and the Sign Convention
  • Reads and Combining
  • Events
  • Persistence
  • Non-Guarantees
  • Final Summary

1. Container

AttributeSet

Aspect Guarantee
Shape One component per owner; consumers find it as IAttributeSource on the parent chain
Schema ❌ None — no attribute id is named by the framework, ever
Creation SetBaseValue with a new id creates the attribute; created ids are unbounded
Authored rows ✔ Seeded on first use; unusable rows are reported and skipped, never repaired
Lifecycle ✔ Works without Awake — state is created at the head of every public member, so EditMode and editor tooling see player behaviour
Value type float; integer consumers convert at their adapter, visibly

2. Ids

Aspect Guarantee
Form Plain strings, compared ordinal, case-sensitive
Trimming ✔ Leading and trailing whitespace ignored on every path — " speed " and "speed" are one attribute
Blank ❌ Refused loudly on write, false on read; nothing is ever stored under a blank id
Duplicate authored rows First wins, later reported and skipped

3. Bounds and the Sign Convention

Aspect Guarantee
Negative values ✔ Permitted everywhere
Negative bounds ✔ Permitted everywhere
Non-finite values ❌ Refused loudly; the stored value is unchanged
Non-finite bounds Dropped per side, reported
Inverted pair (min > max) Reported and ignored as a pair — never swapped, never clamped into something unauthored
Authored base outside its own bounds Seeds clamped, and reported — a rewrite of an authored number is never silent
Where clamping happens ✔ At every base write, and on effective values after the combiner runs
Runtime-created ids Unbounded — bounds are authored configuration
Restore ✔ Saved values clamp into the bounds that exist now

Nothing is silently normalised

The cautionary tale is Currency's cap rule, which quietly rewrites a negative bound to zero. This system's posture is the opposite: a number you authored either means what it says or it is reported and inert.

4. Reads and Combining

Aspect Guarantee
TryGetValue totality ✔ Never throws — null, blank and unknown ids return false
No combiner ✔ Effective value is the clamped base; providers are not consulted at all
Providers exist, no combiner ✔ Explained once per component in the console
Provider discovery GetComponentsInParent, inactive GameObjects included
Disabled provider ❌ Does not run — unticking the box is a deliberate switch-off
Destroyed provider / combiner Dropped, via a real liveness check — interface references never reach Unity's operators on their own. A destroyed code-supplied combiner releases back to the inspector slot
Throwing provider ✔ Isolated and logged; the read and later providers continue, and anything it appended before throwing is rolled back with it
Throwing combiner ✔ Logged; the read serves the base value
Non-finite combiner result ✔ Reported once per component; the base value is served
Re-entrant reads ✔ A combiner or provider reading another attribute of the same owner is supported; nested reads allocate fresh buffers
Cyclic derived reads ✔ Capped at AttributeSet.MaxReadDepth; the read bottoms out on clamped base values and reports once, instead of overflowing the stack
Combination order Contributions arrive in provider order; how they fold is entirely the combiner's

5. Events

Aspect Guarantee
Payload AttributeDelta — owner, id, before and after
When ✔ Only on an actual change of the stored value
Creation SetBaseValue creating an attribute raises once, with before equal to after — the id is the news, and no fabricated crossing. Authored rows seeding on first use are the starting state, not a change, and raise nothing
Clamped writes ✔ The delta reports the value as stored, not as requested
Emit order ✔ Designer UnityEvent first, then the C# event — pinned by test
Subscriber isolation ✔ A throwing C# subscriber is logged and later subscribers still run
Effective values No event, by design — computed reads have no owner of "changed"
Restore ❌ Silent — state is written directly; presentation reconciles from RevSaveManager.LoadCompleted

6. Persistence

Aspect Guarantee
What is saved ✔ Base values only — never effective values, never contributions, never bounds
Double writes ✔ No value in a save file is written by this participant and any other
Restore order Default — the participant deliberately does not implement IRevSaveOrdered, because its restore touches no other system
Restore semantics ✔ A rewind per owner: authored rows re-seeded, saved values on top, runtime-created ids the save does not mention removed
Unmentioned authored rows ✔ Return to their authored base — adding a row to your project does not break old saves
Deleted authored rows ✔ A saved id restores as a runtime-created attribute — ids are data, nothing dangles
Empty owners ✔ An identified owner with no attributes still writes an entry, so "you had none" survives a load
Ownerless containers ❌ Not captured — an AttributeSet without a StableId could never be matched again
Missing owners at restore Skipped silently per owner; a section matching no owner is reported rather than read as success
Unreadable owners Refused whole — a blank id, a non-finite value, an id named twice all refuse the owner; partial-restore reported when other owners had already landed
A snapshot stripped from the file Indistinguishable from a legitimately empty owner by construction, so it rewinds to authored defaults — a stated cost of the empty-entry decision, argued on the participant
Payload shape ✔ Sorted by id — identical state serialises identically

7. Non-Guarantees

Not guaranteed Why
❌ A stacking model The combine step is the project's; a shipped default would contradict seams already sold
❌ Effective-value change events Pull seams have no "changed" moment the container can see
❌ Thresholds, ranks or named tiers Threshold semantics (hysteresis above all) belong to the consumer of a value, not its container
❌ Canonical attribute ids A shipped stat sheet decides what kinds of characters every game has
❌ Per-context keys Keyed by owner and id only; an (owner, context) axis would absorb whole domains by accident
❌ Modifier-read performance Provider collection walks the parent chain per read when a combiner is wired; unmeasured, and stated rather than promised
❌ Attribute removal No API today; a restore rewinding is the one way an id disappears

Final Summary

What Attributes guarantees

Values keyed by owner and id, trimmed and validated loudly; bounds that clamp without ever rewriting what you authored; change events carrying both values, exactly once per change; reads that stay total under throwing project code; and a save that stores every fact exactly once, rewinding on restore.

What it will never do

Name your stats, stack your bonuses, own your thresholds, or key a value by anything except its owner.