Skip to content

Inventory — System Guarantees Matrix

What this page is

This page defines the behavioural contract of the Inventory system.

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


Quick Navigation

  • Core Service
  • Containers
  • Mutations
  • Results
  • Events
  • Authority
  • Searching / Sorting
  • Equipment
  • Item Usage
  • Snapshots / Persistence
  • Determinism
  • Public API
  • Non‑Guarantees
  • Final Summary

1. Core Service

SceneInventoryService

Aspect Guarantee
Orchestration root ✔ Single service owns container truth
Container creation ✔ Service-controlled (lazy)
Container mutation ✔ Service-controlled
Delta computation ✔ Internal (not user-managed)
Supported mutation entry point ✔ IInventoryService
Runtime usage Main thread
Inspector required ❌ No
UI required ❌ No

2. Containers

Aspect Guarantee
Container ownership ✔ Owned by GameObject
Container identifier ContainerId
Container creation ✔ Lazy (on demand by service)
Container storage Slot-based
Slot count mutable ✔ Yes (via ResizeContainer)
External container construction ❌ Not supported

3. Mutations

Aspect Guarantee
Mutations routed through service ⚠ Yes for the service API; CharacterInventory is the documented exception
Deterministic results ✔ Yes
Partial operations allowed ✔ Yes
Container auto-creation on mutation ✔ Yes
Atomicity within single operation ✔ Best-effort (rollback applied where supported)
Atomicity across containers ❌ Not guaranteed
Cross-system atomicity ❌ Not guaranteed
Stack data survives a mutation ✔ Durability and metadata move with the item

Durability and metadata belong to the stack, and every mutation moves the stack rather than rebuilding it from the item definition. That includes TransferResult, which moves between owners by GUID: it takes the units from the source's own slots, so two slots of the same item with different durability transfer as themselves rather than as two copies of whichever was found first.

Supported mutation classes:

  • add
  • remove
  • split
  • merge
  • swap
  • move
  • transfer
  • resize

4. Operation Results

Aspect Guarantee
Result returned for every mutation ✔ Yes
Deterministic failure codes ✔ Yes
Partial operations signaled ⚠ Usually InvOpCode.Partial — see below
Silent failure ❌ Never, with one documented exception — see below

All mutations return:

InvOpResult


5. Events

Aspect Guarantee
Container change events ✔ Yes
Slot-level delta information ✔ Yes
Event emission timing ⚠ Not uniformly after the mutation completes — see below
Event emitted after resize ✔ Yes
Event emitted for no-op operations ❌ No

Primary event surface:

SceneInventoryService.OnContainerChanged


6. Authority

Aspect Guarantee
Mutation gating supported ✔ Yes
Authority interface IInventoryAuthority
Unauthorized mutation result InvOpCode.NoAuthority — for a refusal. A null owner is InvOpCode.InvalidArgs on every mutation, Sort and ResizeContainer included
Authority evaluated before mutation ⚠ Yes on the service path; not on CharacterInventory, CharacterEquipment, or snapshot/save restore
Read operations gated ❌ No
Scope Process-wide. The resolver keeps one static authority with no scene key and no scene hooks, so the first usable one found anywhere answers for every SceneInventoryService in every loaded scene — including a service that wakes up in an additively-loaded scene carrying its own binder
Disabled or destroyed authority ⚠ Counts as absent, and absent is permissive. The service re-resolves on its next check; if nothing replaces it, mutations are allowed again, with no log. Switching the only binder off does not pause the gate, it removes it
Authority published after the service Call SceneInventoryService.RefreshAuthority() on the services it should gate. It discards the resolver's cache and re-runs the search, so a provider on the service's own GameObject or a parent takes over. Among scene-wide providers the search keeps the first it finds, so a newcomer on its own root does not outrank a live one on an earlier root
Multiplayer replication ❌ Not provided

Inventory enforces authority only during mutation calls.

ClearForOwner and PruneDestroyedOwners are lifecycle cleanup rather than mutations and are not authority-gated: the first is what InventoryOwnerHook calls when an owner is destroyed, and a destroy hook that could be refused would leak.


7. Searching / Sorting

Aspect Guarantee
Search supported ✔ Yes
Sort supported ✔ Yes
Strategy interfaces ✔ Yes
Default implementations ✔ Yes
Deterministic sort order ✔ Stable sort
Operates on existing containers only ✔ Yes
Implicit container creation ❌ No
Cross-system deterministic ordering ❌ Not guaranteed

Extension seams:

  • IInventorySearch
  • IInventorySorter

8. Equipment

Aspect Guarantee
One item per equipment slot ✔ Yes
Equipment filters enforced ✔ Yes
Equip operations transactional ✔ Yes
When OnEquipped / OnUnequipped are raised ✔ After the transaction commits and both deferred containers have flushed. A handler reads finished state, and a throwing handler cannot unwind a move that already succeeded. Before 1.3.0 this was true of OnEquipped only — OnUnequipped was raised inside both DeferEvents scopes
Inventory integration ✔ Yes
Equipment managed by SceneInventoryService ❌ No

Equipment operates through:

CharacterEquipment


9. Item Usage

Aspect Guarantee
Use effects supported ✔ Yes
Effects executed sequentially ✔ Yes
Item consumed after successful use ✔ Yes
A refusing effect vetoes the consume ✔ Yes — one refusal keeps the item, whatever its siblings did
Effects rolled back when the consume fails ❌ No — deliberately non-transactional
Use system required ❌ No
External effect integration ✔ Yes
Deterministic resolver ordering ❌ Not guaranteed

External effects integrate through:

UseEffectResolver.ExternalResolvers


Snapshots / Persistence

Aspect Guarantee
Inventory snapshot supported ✔ Yes
Equipment snapshot supported ✔ Yes
JSON helpers provided ✔ Yes
Missing item handling policy ✔ Yes
Deferred event emission on apply ✔ Yes
Save slot management ❌ Not provided
Automatic persistence ❌ Not provided
Versioning / migration system ❌ Not provided

Snapshots capture:

  • container contents
  • equipment contents
  • item stacks

11. Determinism

Aspect Guarantee
Mutation determinism ✔ Yes
Event emission ordering ✔ Deterministic per mutation
Cross-system determinism ❌ Not guaranteed
Snapshot restore determinism ✔ Best-effort (dependent on item database consistency)

12. Public API

Aspect Guarantee
Stable service surface ✔ Yes
Public extension interfaces ✔ Yes
Internal containers exposed ❌ No
Internal helpers public ❌ No
DTO types exposed ❌ No

Public extension seams include:

  • IInventorySearch
  • IInventorySorter
  • IInventoryAuthority
  • IItemDatabase
  • IUseEffect

Non‑Guarantees

Inventory does not guarantee

  • multiplayer replication
  • prediction / rollback
  • cross-system atomic transactions
  • automatic persistence
  • deterministic external resolver ordering
  • deterministic behaviour across external systems

Final Summary

Layer Strong Guarantee Best Effort Not Guaranteed
Service-controlled mutations
Deterministic results
Partial operations
Snapshot restore
Cross-system atomicity
Multiplayer replication

System Philosophy

Inventory is

  • Service-driven
  • Slot-based
  • Deterministic
  • Authority-aware
  • Explicit about guarantees

Where these guarantees are qualified

Three rows above carry a ⚠ rather than a ✔. Each was an unqualified absolute, and in each case the code is deliberate, documented at the method, and pinned by a test — so the matrix was the thing that was wrong, not the behaviour.

Move can partially succeed and report Ok

Move is documented at the method, in about thirty lines of XML, as being able to move some of a stack and report success. InvOpCode.Partial is not returned in that case. Read Moved/Remaining on the result rather than treating Ok as "all of it".

This is the one place "silent failure — never" does not hold literally, and it is not a defect: the alternative is failing a move that legitimately half-fits.

Three surfaces write without going through the service

CharacterInventory mutates the container directly, which means the service's authority gate is not evaluated for it. This is stated on its own PublicAPI page and pinned by a truth test. If you need every write authority-checked, go through the service.

CharacterEquipment.TryEquipFromInventoryResult / TryUnequipToInventoryResult write the same containers on the same terms, stated in the Equipment folder README.

InventorySnapshots.ApplyJson, and therefore InventorySaveParticipant.Restore, write containers directly with no authority check. A save load is a world rewind rather than a gameplay mutation, and Health and Crafting restore on the same basis; drive it only from a trusted load path.

Event timing is not uniformly post-mutation

At least one emitter fires part-way through the operation rather than after it completes, and the framework's own source says so. Do not assume a handler observes the finished state of a multi-step operation; read the result the call returns.