Not translated yet — showing English.
Inventory
One owned set of rows that shots, drops, abilities and movement all use. Stacks change by increment, and a per-owner cap has the behaviour at the edge you choose.
When to use it
- Players hold things, and a holding is a row with an owner: read by owner, capped per owner, with the overflow behaviour declared rather than defaulted.
- A quantity accumulates: a stack changes by an increment with an idempotency key, so a retried debit does not debit twice.
- Other modules spend from one set: shots debit ammo, drops grant loot, purchases show up as rows against their entitlement.
- Skip it when the number is not ownable. HP, XP and cooldowns belong in stats.
Who does what
| Actor | On this page |
|---|---|
player | reads their own holdings and spends from them |
backend-service | grants, increments and revokes on a player's behalf, naming the player it acts for |
At a glance
fn authority: grant ammo, move an item to the primary equipment slot, check affordability before spending// fn authority — a cloud function, or a dedicated server holding a host key
var bag = await player.Inventory.Container("bag");
var equipment = await player.Inventory.Container("equipment");
await player.Inventory.Grant("ammo.shell", count: 20);
await bag.Move(itemId, to: equipment, slot: "primary");
if (await player.Inventory.CanAfford("ammo.shell", 1))
await player.Inventory.Consume("ammo.shell", 1);// fn authority — a cloud function, or a dedicated server holding a host key
const bag = await player.inventory.container('bag');
const equipment = await player.inventory.container('equipment');
await player.inventory.grant('ammo.shell', { count: 20 });
await bag.move(itemId, { to: equipment, slot: 'primary' });
if (await player.inventory.canAfford('ammo.shell', 1))
await player.inventory.consume('ammo.shell', 1);# fn authority — a cloud function, or a dedicated server holding a host key
bag = await player.inventory.container("bag")
equipment = await player.inventory.container("equipment")
await player.inventory.grant("ammo.shell", count=20)
await bag.move(item_id, to=equipment, slot="primary")
if await player.inventory.can_afford("ammo.shell", 1):
await player.inventory.consume("ammo.shell", 1)Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
// fn authority — a cloud function, or a dedicated server holding a host key
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(GrantId), PlayerId, PSKeys::Item::AmmoShell);
// spending is an instance act on the entitlement you hold
Entitlement->Spend(FPSIdempotencyKey(SpendId), /*Amount*/ 1,
TPSOnResult<void>::CreateLambda([](const TPSResult<void>& Result)
{
// short on the item is a declared refusal, not a silent no-op
if (Result.IsRefused()) { DeclineReload(Result.Refusal()); }
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.// fn authority — a cloud function, or a dedicated server holding a host key
var bag = await player.Inventory.Container("bag");
var equipment = await player.Inventory.Container("equipment");
await player.Inventory.Grant("ammo.shell", count: 20);
await bag.Move(itemId, to: equipment, slot: "primary");
if (await player.Inventory.CanAfford("ammo.shell", 1))
await player.Inventory.Consume("ammo.shell", 1);A player session runs the reads, the moves and the affordability check with the same calls. Grant, consume and destroy are not its to make: the platform refuses them as forbidden and names the right the caller is missing, whatever binding made the call.
The model
An inventory introduces no notions of its own. It is a preset — a shape assembled from what entity already gives, so everything below is an entity declaration rather than a mechanism of this page.
| Declares | What it is |
|---|---|
| an owned type | the holding belongs to an owner, and selection by owner is entity's own operation |
a ref to a catalog item | the reference stores the item's id and never its key, so renaming a key is safe. The definition itself lives in commerce |
| a stack aspect with an increment | a stack changes by a delta rather than by overwriting what was read (see below) |
| a per-owner cap with its boundary behaviour | one of three, and there is no default: refuse · redirect into a declared owner bucket · discard with event |
Pass an idempotency key with every increment
An increment is not idempotent: two increments are two increments. It must carry an idempotency key, or a retried debit debits twice; to learn the outcome, make an addressed read.
What is true of every holding.
| Always | What it is |
|---|---|
the cap has no default | the three answers to a full bag are three different games — a refusal loses the loot in front of the player, a redirect is mail or an overflowing warehouse, a discard is a quiet loss that is lawful only because it was declared and is observable. No default is right for all three, so the declaration chooses |
the owner is immutable | nothing changes hands by editing a field: a holding moves as a revocation plus a new grant with a declared origin, and both facts stay on the record |
a transfer between two players | is a different promise: it needs escrow and anti-fraud, and it is outside this version |
a row | displays an entitlement rather than being a second source of one — what was bought lives in commerce, and the row here represents it |
Errors
- The instance does not exist, or a predicate hides it: the answer is not found either way, so a refusal never reveals that something exists but is not yours.
- A field not declared in the aspect (nested ones included), and an obligatory field with no value, are validation refusals naming the field.
- The version did not match is a precondition failure, worth repeating after re-reading.
- A write on a player's behalf that does not name the player is a validation refusal, not a silent write as somebody else.
- No permission for a read or a write answers forbidden, with the read and the write distinguished.
Limits
The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.
- Instances per owner: by the declared rule above, and there is no default.
- The stored instance's size: the write is refused as a conflict, and the refusal names the offending field and the measured size. The ceiling is reached by accumulation, so the approach to it is observable before the write that fails.
- The rate of changes to one instance: a rate-limit refusal with a deadline.
- The selection page size: the page is trimmed to the cap and the "there is more" flag stays true; returning fewer without the flag is forbidden.