Skip to content

Inventory System — Public API

This page defines the supported, stable public API for the RevFramework Inventory System.

No lock-in to framework implementations; all systems are interface-driven and replaceable.

This is the contract

If something is not listed here, it is not supported as a public integration point, even if it appears accessible in code.

Audience: Developers integrating Inventory into gameplay, UI, or persistence
Scope: Runtime public API only (Editor / Teaching helpers excluded)
Stability: Breaking changes to items listed here are avoided or versioned


Core Concepts

Inventory in RevFramework is:

  • service-first (with optional convenience components)
  • container-based
  • result-driven

Key characteristics:

  • Containers hold slot-based item stacks
  • All mutations return InvOpResult
  • Items are defined by ItemDefinition assets
  • Container identifiers use ContainerId
  • Runtime logic lives behind IInventoryService

Internal container structures are never exposed publicly.


Service Entry Points

IInventoryService

Primary runtime interface for inventory mutations.

Supported operations include:

  • GiveExact(owner, stack, container)
  • AddMax(owner, stack, out remainder, container)
  • RemoveByGuid(owner, itemGuid, amount, container)
  • RemoveFromSlot(owner, container, index, amount)
  • SetAt(owner, container, index, stack, out previous)
  • Swap(owner, container, a, b)
  • MergeThenSwap(owner, container, from, to)
  • SplitResult(owner, container, srcIndex, amount, targetIndex)
  • Move(owner, fromContainer, fromIdx, toContainer, toIdx)
  • TransferResult(from, to, itemGuid, amount, fromContainer, toContainer)
  • Sort(owner, container, spec) — sorts existing containers only (fails if container does not exist)
  • ResizeContainer(owner, container, newSize, allowTruncate, out truncated)

All mutations return InvOpResult.


Read-Only Access

Read-only operations are exposed via the inventory service and container views.

Supported operations:

  • Get(owner, container) — resolves a container view and will create the container if it does not already exist
  • Search(owner, container, query) — returns matches for existing containers only; does not create containers

Events:

  • OnContainerChanged

SceneInventoryService also carries a serialized mirror of it — On Container Changed, under Events (Inspector – designer hooks) — so a scene can react without a script. See Designer hooks below before wiring one.


IReadOnlyInventoryContainer

Read-only view of a container.

Properties:

  • Owner
  • Id
  • Capacity
  • Slots

Methods:

  • Peek(index)

Service Resolution

Inventory services are resolved using:

SceneInventoryService svc = InventoryResolve.ServiceFrom(context);

Resolution order:

  1. SceneInventoryService.Instance
  2. Scene-local service search
  3. Global fallback search

SceneInventoryService is the default runtime implementation and reference Unity host, but consumers should resolve services via InventoryResolve.


Runtime Components

SceneInventoryService

Scene-level inventory service.

Responsibilities:

  • container creation and tracking
  • mutation orchestration
  • delta publication (via internal tracking)
  • authority enforcement

InventorySizeSync

Applies the service's configured container size policy to one owner's container at startup.

public SceneInventoryService invSvc;   // leave empty to resolve from this hierarchy
public GameObject owner;               // required
public string container = "Backpack";

Add it from Add Component ▸ RevFramework ▸ Inventory ▸ Inventory Size Sync. It runs once, in Start, at execution order −350 — after the service has built its containers and before ordinary gameplay scripts read them. Changing the policy afterwards does not re-apply it, and resizing never truncates: a container already holding more than the policy allows keeps its contents.

Public since 1.2.0. It was internal while its own README told buyers to attach it, so the documentation described the intent and the accessibility contradicted it.


CharacterInventory

Convenience wrapper around a character’s inventory container.

Provides:

  • container access
  • local mutation helpers
  • inspector debugging
  • owner id field (ownerGuid) — not unique across prefab copies, and read by nothing in the framework; the shipped save integration keys inventories by Core.Identity's StableId

For multiplayer logic, prefer using IInventoryService directly.


CharacterEquipment

Equipment system managing fixed equipment slots.

Supports:

  • strict equipping
  • unequipping
  • equipment filtering
  • transactional inventory interaction

Events:

  • OnEquipped
  • OnUnequipped

Both have serialized mirrors — On Equipped and On Unequipped — under Events (Inspector – designer hooks). See Designer hooks below.


Designer hooks

Four components carry serialized UnityEvent mirrors of their C# events, so a scene can respond without a script:

Component Inspector hook Mirrors Arguments
SceneInventoryService On Container Changed OnContainerChanged InventoryDelta
CharacterEquipment On Equipped OnEquipped string slotId, ItemStack
CharacterEquipment On Unequipped OnUnequipped string slotId, ItemStack
CharacterInventory On Changed OnChanged none

Each mirror is raised from the same place as the event it names, with the same arguments, immediately before it. Code should still subscribe to the C# event; the mirror is for the inspector.

A throwing listener is contained. An exception from an inspector-wired method is logged and does not reach the operation that raised it, or the C# event raised after it. That matters more here than elsewhere: the container hooks fire part-way through anything touching more than one slot, so an escaping exception would abandon a move or a transfer half-applied. The equipment hooks fire after a committed transaction, where escaping would report a failure for an equip that already happened.

These are hooks, not a mutation surface. Wiring a method that moves items is allowed and is exactly the case the warning under CharacterInventory covers — a UnityEvent calling into CharacterInventory bypasses IInventoryAuthority. If your authority rules must hold, wire the hook to something that calls the service.

CharacterInventory.On Changed and OnChanged are not identical, deliberately

OnChanged is a forwarding event: += attaches your handler to whichever container is bound at that moment, and nothing re-attaches it if the component is disabled and re-enabled — so a handler added once at startup silently stops firing after that. The inspector hook is wired by the component's own bind path instead, so it survives a rebind.

The mirror is the better-behaved of the pair. The asymmetry is documented rather than resolved, because changing what OnChanged does to existing subscribers would break projects that rely on it. For code, prefer SceneInventoryService.OnContainerChanged, which the service raises rather than forwards.


ItemUseSystem

Consumes inventory items and executes their configured use effects.

Works with:

  • ItemDefinition.usable
  • ItemDefinition.useEffects
  • IUseEffect
  • IUseEffectReportsDelivery

Effects may be resolved through UseEffectResolver.

A failed use is not an undone use

UseResult applies the item's effects first, then consumes one from the slot through the service. The two steps are not a transaction, and the order has two consequences that a "failed use = no use" reading of the return value gets wrong:

  • A failed consume does not roll back effects that already applied. If the removal is rejected — inventory authority says no, the slot changed underneath the call — UseResult returns a failure carrying the removal's own InvOpCode, but the potion has already healed the target. Nothing is undone, because an effect may be external, project-specific, or simply not reversible.
  • Authority denial does not consume the item. The stack is left exactly as it was, so the same use can be attempted again — and each attempt applies the effects again.

Together those mean a denied use can heal repeatedly while never spending the item.

Call this only from the authoritative instance

The non-transactional order is deliberate, not a gap to be patched around: the guarantee is that a caller permitted to consume is the caller that applies. In multiplayer, invoking UseResult on a non-authoritative instance is what turns that into a client applying effects it was never permitted to pay for.

Both behaviours are pinned by tests (UseResult_WhenConsumeFails_DoesNotRollbackAppliedEffects, UseResult_DoesNotConsume_WhenAuthorityDeniesRemoval).

The reverse case is handled: an item whose every effect refused is not consumed and the call fails with InvOpCode.UnknownError before the removal is ever attempted. See IUseEffectReportsDelivery below for how an effect reports a refusal.

IUseEffectReportsDelivery

Optional capability for a use effect whose payload can be refused — a grant the target container will not take, a heal on a target already at full health.

bool TryApply(IDamageable dmg, GameObject target);

IUseEffect.Apply returns nothing, so without this an effect that ran and an effect that delivered look identical to ItemUseSystem — and what ItemUseSystem does with that answer is decide whether to consume the item. An item whose every effect refused is now not consumed and the call fails with InvOpCode.UnknownError ("No effects applied"), which is what the matrix row "item consumed after successful use" has always claimed.

Implement the body in TryApply and have Apply delegate to it, so both paths behave identically.

An effect that does not implement this is assumed to have applied

That is the honest default and it is what the void path assumed silently, so every use effect written against IUseEffect alone keeps behaving exactly as before.


Operation Results

InvOpResult

Returned by all inventory mutations.

Fields:

  • Success
  • Code
  • Message

InvOpCode

Stable outcome codes including:

  • Ok
  • InvalidArgs
  • OutOfRange
  • Empty
  • ServiceMissing
  • NoSpace
  • NotEnoughQuantity
  • NotFound
  • AlreadyFull
  • FilterMismatch
  • SlotInvalid
  • SlotMissing
  • SwapBlocked
  • Partial
  • NoAuthority

Change Events

InventoryDelta

Emitted after successful mutations or container resizes.

Contains:

  • owner
  • container
  • slot-level change list

Data Types

Stable value types used by the system:

  • ItemStack
  • InventorySlot
  • ItemMetaEntry
  • ContainerId
  • ItemGuid
  • InventorySortSpec
  • InventorySortKey

These types are safe for:

  • UI
  • persistence
  • gameplay logic

Item Definitions

ItemDefinition

ScriptableObject representing an item.

Contains:

  • GUID
  • display metadata
  • stack size
  • rarity
  • classification
  • tags
  • use effects
  • equip visuals

ItemDatabase

ScriptableObject storing all known items.

Supports:

  • GetByGuid
  • All
  • MissingPlaceholder — the item substituted for unresolvable GUIDs under MissingItemPolicy.SubstitutePlaceholder. It is optional and unset by default, and the policy quietly degrades to Skip when it is null. See Snapshots below.

Extension Points

Inventory supports extension via the following interfaces:

  • IInventorySearch
  • IInventorySorter
  • IItemDatabase
  • IInventoryAuthority
  • IUseEffect

Optional external modules may integrate via:

  • UseEffectResolver.ExternalResolvers

Supported Extensions

Public helper extensions include:

  • InventoryReadExtensions
  • InventoryServiceSortExtensions
  • CharacterEquipmentExtensions
  • InvOpResultExtensions

These provide convenience helpers without modifying core interfaces.


Snapshots

Inventory snapshots allow save/restore of inventory and equipment state.

Public helpers include:

  • InventorySnapshots.CaptureJson
  • InventorySnapshots.ApplyJson
  • InventorySnapshots.SaveJsonToFile
  • InventorySnapshots.LoadJsonFromFile

Snapshot behavior is controlled via:

  • InventorySnapshotOptions
  • MissingItemPolicy

SubstitutePlaceholder needs a placeholder, or it is Skip

MissingItemPolicy.SubstitutePlaceholder substitutes only when the ItemDatabase has a MissingPlaceholder item assigned. With none, it behaves exactly like Skip — the item is dropped, the slot is left empty, and nothing reports that the policy did not apply.

MissingPlaceholder is unset by default on a database you create. The sample ItemDatabase carries oneMissing Item (placeholder) — so the policy is demonstrable in the sample scenes; your own database does nothing until you assign a placeholder item on the asset.

The fallback is not cosmetic. A snapshot in which no item resolves is accepted when the items became placeholders (Substituted > 0) and refused when they were skipped instead — so the same save can load or fail depending on whether a placeholder is assigned.

Loading Untrusted Or Damaged Saves

ApplyJson returns false rather than throwing when the payload is null, empty, unparseable, declares a schema version newer than the installed build supports, or names items of which none exist in the database.

All of those are detected before any state is mutated, so a rejected snapshot leaves the target inventory and equipment exactly as they were. A failed load never half-clears a container.

The last one used to empty the inventory and report success

Restore resolved each saved item after clearing the container. A database that does not match the save — a patch that re-GUID'd items, a reference to the wrong asset, an index that was never built — therefore emptied the player's bag while ApplyJson returned true, the save participant reported the section applied, and the coordinator recorded it Ok. The next autosave made it permanent. Resolution now happens first, and a snapshot in which nothing resolves is refused with the inventory untouched.

Skipping some items is still the documented behaviour of MissingItemPolicy.Skip — a patch removed an item and the player loses it. What is refused is every item being unknown, which is not a player losing an item.

Knowing what actually came back

InventorySnapshots.ApplyJson(inv, eq, json, db, opts, out var report);
if (report.Incomplete) WarnPlayer(report.Skipped, report.Named);

InventoryRestoreReport carries Named, Resolved, Substituted and Skipped, covering the container and equipment together. The bool says whether the snapshot was readable; the report says whether anything in it survived the trip. InventorySaveParticipant logs a warning when a character restores short.

Parse and version failures are logged as errors with the underlying reason, so a corrupt save is diagnosable from a player's log.

Forward compatibility

A snapshot written by a newer build is refused, not applied blind — applying unknown fields or semantics would silently mangle the save. Older and matching versions load normally.

Check the return value before assuming a load succeeded:

var json = InventorySnapshots.LoadJsonFromFile("slot1");

if (!InventorySnapshots.ApplyJson(inventory, equipment, json, database))
{
    // Corrupt, missing, or from a newer build — keep the current state and tell the player.
}

Explicitly Not Supported

The following are not public API

  • InventoryContainer
  • EquipmentContainer
  • snapshot DTO classes
  • All types under RevGaming.RevFramework.Inventory.Internal
  • resolver implementation classes
  • default search/sort implementations
  • direct container mutation outside the service

Runtime Guarantees

Runtime guarantees

  • Inventory mutations are deterministic.
  • All operations return InvOpResult.
  • Container change events emit after successful mutations.
  • Container change events also emit after container resize operations.
  • Authority policies are respected for mutations made through the service (SceneInventoryService / IInventoryService).
  • Snapshot application uses deferred change events and emits consolidated deltas.

CharacterInventory mutates without consulting authority

That guarantee is scoped to the service on purpose. CharacterInventory's own TryAdd, TryRemoveResult, SetStackAtResult and friends forward straight to the bound container — the same container instance the service tracks — and do not consult IInventoryAuthority. This is deliberate and pinned by tests, not an oversight: the component is a direct handle on a container.

It matters because the component is public. A UnityEvent wired in the inspector, or any script holding a CharacterInventory reference, can move items that an authority policy would have refused. If your authority rules must hold, route mutations through the service and treat CharacterInventory as a read/bind surface.


Not Guaranteed

Not guaranteed

  • network replication
  • cross-system transactional atomicity
  • deterministic ordering of external effect resolvers
  • persistence format stability beyond JSON snapshot helpers

TL;DR

TL;DR

If it is not listed here, it is not part of the supported Inventory API.