Skip to main content
Services around the game

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

ActorOn this page
playerreads their own holdings and spends from them
backend-servicegrants, increments and revokes on a player's behalf, naming the player it acts for

At a glance

From 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)
Coming soon — Go

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.

DeclaresWhat it is
an owned typethe holding belongs to an owner, and selection by owner is entity's own operation
a ref to a catalog itemthe 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 incrementa stack changes by a delta rather than by overwriting what was read (see below)
a per-owner cap with its boundary behaviourone 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.

a grant arrives; the owner is at the capthe cap's declared behaviour (no default)the loot stays where it wasinto a declared owner bucketgone, with an event refuseredirectdiscard with event
A grant into a full bag: the cap's declared behaviour decides, and there is no default

What is true of every holding.

AlwaysWhat it is
the cap has no defaultthe 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 immutablenothing 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 playersis a different promise: it needs escrow and anti-fraud, and it is outside this version
a rowdisplays 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.