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:
IInventorySearchIInventorySorter
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:
IInventorySearchIInventorySorterIInventoryAuthorityIItemDatabaseIUseEffect
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.