Not translated yet — showing English.
Core
Core is the one object you create, and everything else hangs off it. One key in, and you hold context, identity, typed failures, tracing and batching. Every module call goes through it; no module has its own copy.
When to use it
- You need to know who and where you are: identity, roles, unlocked modules, project, environment and region.
- A cloud function has to write as a player. The write is attributed to that player, and the record names both the function and the player.
- Retries must not apply twice: batch operations carry an idempotency key.
- Failures must be branchable and searchable: every throw is a typed
Problemwith a stable code. - Skip it for messaging, calls and state. Those are the primitives: Events, RPC and Data, addressed through Entity; their audiences are Groups. Uploads and downloads are in Files & UGC, mounting and namespacing in Under the Hood.
Who does what
| Actor | On this page |
|---|---|
any actor | reads identity, context, roles via Whoami |
backend-service | acts as a player; batches idempotent operations |
operator | reads traces for failed or retried calls |
At a glance
Whoami, the ambient context, and a batch that retries safelyvar me = PlayServ.Whoami(); // identity, roles, unlocked modules
var env = PlayServ.Context; // project · env · region
// retries never double-apply: the batch carries an idempotency key
await PlayServ.Batch(key: orderId, b =>
{
b.Inventory.Grant(playerId, "starter.pack");
b.Inventory.Grant(playerId, "starter.emote");
});const me = playserv.whoami(); // identity, roles, unlocked modules
const env = playserv.context; // project · env · region
// retries never double-apply: the batch carries an idempotency key
await playserv.batch(orderId, (b) => {
b.inventory.grant(playerId, 'starter.pack');
b.inventory.grant(playerId, 'starter.emote');
});me = playserv.whoami() # identity, roles, unlocked modules
env = playserv.context # project · env · region
# retries never double-apply: the batch carries an idempotency key
async with playserv.batch(key=order_id) as b:
b.inventory.grant(player_id, "starter.pack")
b.inventory.grant(player_id, "starter.emote")Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
const FPSActor Me = Client->Whoami(); // identity, roles, unlocked modules
const FPSPlatformContext Env = Client->Context(); // project · env · region
// retries never double-apply: each keyed operation is safe to repeat
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(PackGrantId), PlayerId, PSKeys::Item::StarterPack);
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(EmoteGrantId), PlayerId, PSKeys::Item::StarterEmote);
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.var me = PlayServ.Whoami(); // identity, roles, unlocked modules
var env = PlayServ.Context; // project · env · region
// retries never double-apply: the batch carries an idempotency key
await PlayServ.Batch(key: orderId, b =>
{
b.Inventory.Grant(playerId, "starter.pack");
b.Inventory.Grant(playerId, "starter.emote");
});Identity lives in Core, so no call takes a session or ctx parameter.
The model
What every error carries.
| Field | What it is |
|---|---|
code | the machine-readable name of the refusal. Stable: it reuses the platform's existing codes instead of inventing new ones |
category | the class of the refusal; it tells you whether a retry makes sense |
trace identifier | the id of this occurrence. Always present, local errors included, so support never needs a reproduction |
explanation | text for a person to read. Not stable: it changes and is localised at any time |
per-field errors | for validation refusals: field, code and message for each rejected field |
Branch on the code, not the text
Put code and category in your logic. explanation is for people and changes without notice. A binding that exposes only the message has a bug.
Three origins.
| Origin | What happened |
|---|---|
platform | the platform refused, with a code from its catalogue |
local | the SDK refused before sending, with a code from its own published list |
unknown | the call was sent and no answer came back, so the outcome is unknown |
True of every refusal.
- A refused operation applied nothing, so an error branch needs no compensating read. The two exceptions say so where they occur: a timeout, whose outcome is unknown, and a batch with per-element semantics.
- The error is the same however it reaches you: the same code, category and origin whether the binding throws, returns a result or calls back on a subscription.
- A timeout is the
unknownorigin, not an answer. What to do about it is declared per operation.
Errors
rate_limited also says when a retry is allowedtry { await PlayServ.Inventory.Grant(playerId, "starter.pack"); }
catch (Problem p) when (p.Code == "rate_limited")
{
Hud.RetryAt(p.RetryAfter);
}try { await playserv.inventory.grant(playerId, 'starter.pack'); }
catch (p) {
if (Problem.code(p) === 'rate_limited') hud.retryAt(p.retryAfter);
else throw p;
}try:
await playserv.inventory.grant(player_id, "starter.pack")
except Problem as p:
if p.code == "rate_limited":
hud.retry_at(p.retry_after)
else:
raiseAttribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
// UE builds run without exceptions — the completion carries the result, read explicitly
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(GrantId), PlayerId, PSKeys::Item::StarterPack,
TPSOnResult<void>::CreateWeakLambda(this, [this](const TPSResult<void>& Result)
{
if (Result.IsRefused() && Result.Refusal().Code == FPSFailureCode::RateLimited)
{
Hud->RetryAt(Result.Refusal().RetryNotBefore); // TOptional<FDateTime> — an instant, not a delay
}
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.try { await PlayServ.Inventory.Grant(playerId, "starter.pack"); }
catch (Problem p) when (p.Code == "rate_limited")
{
Hud.RetryAt(p.RetryAfter);
}A caller without the role is refused, not served less. For a player or client session the fn and adm rows fail: calling act as player, emitting a trace or reading one returns a Problem with code forbidden. The credential is valid; its roles don't carry the right. Retrying with the same idempotency key changes nothing.
Limits
Don't retry an accepted call
Limits apply at admission. An accepted call has passed the limit and will run, however long it waits. A refusal of some later call doesn't affect it, so retrying the accepted call does the work twice.
You learn a limit by being refused. The SDK doesn't expose the value in force, the headroom left or a warning that you are close, and players never see limits. The refusal carries what you need:
- the category, which says whether a retry makes sense
- whose limit it was
- when a retry is allowed, and over what window