Эта страница ещё не переведена — показана английская версия.
Core
Core — единственный объект, который вы создаёте сами. Всё остальное держится на нём. Передаёте ему ключ — и у вас есть конт екст, учётная запись, типизированные ошибки, трассировка и пакетные операции. Через Core проходит каждый вызов модуля, и своей копии нет ни у одного модуля.
Когда это нужно
- Нужно знать, кто вы и где работаете: учётная запись (identity), роли, открытые модули, проект, окружение и регион.
- Облачная функция должна записывать данные от имени игрока (player). Запись засчитывается этому игроку, а в журнале указаны и функция, и игрок.
- Повторный вызов не должен сработать дважды. Для этого у пакетных операций есть ключ идемпотентности.
- По ошибке нужно ветвить логику и искать её в логах. Любое исключение — это типизированный
Problemсо стабильным кодом. - Не используйте Core для сообщений, вызовов и состояния — для этого есть примитивы: Events, RPC и Data & Subscriptions. К ним обращаются через Entity, а кому они адресованы, задают Groups. Загрузка и скачивание файлов описаны на странице Files & UGC, подключение модулей и пространства имён — на странице Под капотом.
Кто что делает
| Актор | Что делает на этой странице |
|---|---|
любой актор | через Whoami узнаёт свою учётную запись, контекст и роли |
backend-service | действует от имени игрока; объединяет идемпотентные операции в пакеты |
operator | читает трассировки неудавшихся и повторённых вызовов |
Коротко
Whoami, неявный контекст и пакет, который можно безопасно повторить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");
});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")Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
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");
});Учётная запись хранится в Core, поэтому ни один вызов не принимает параметр session или ctx.
Модель
Что есть в каждой ошибке.
| Поле | Что это |
|---|---|
code | машиночитаемое имя отказа. Стабильно: берутся уже существующие коды платформы, новые не придумываются |
category | класс отказа; по нему понятно, есть ли смысл повторять вызов |
идентификатор трассировки | указывает на этот конкретный случай. Есть всегда, даже у локальных ошибок, поэтому поддержке не нужно воспроизводить проблему |
explanation | текст для человека. Не стабилен: его могут в любой момент изменить или перевести |
ошибки по полям | при отказе валидации: поле, код и сообщение для каждого отклонённого поля |
Ветвите логику по коду, а не по тексту
В логике используйте code и category. explanation написан для людей и меняется без предупреждения. Если биндинг отдаёт только сообщение, это ошибка в биндинге.
Три источника.
| Источник | Что произошло |
|---|---|
platform | отказала платформа, код взят из её каталога |
local | SDK отказал ещё до отправки, код взят из его собственного опубликованного списка |
unknown | вызов был отправлен, но ответ не пришёл, поэтому результат неизвестен |
Что верно для любого отказа.
- Отклонённая операция ничего не применила, поэтому в ветке ошибки не нужно перечитывать состояние, чтобы его исправить. Исключений два, и там, где они возникают, об этом сказано явно: таймаут, результат которого неизвестен, и пакет, где каждый элемент обрабатывается отдельно.
- Ошибка одна и та же, каким бы путём она до вас ни дошла. Выбросит ли биндинг исключение, вернёт результат или вызовет колбэк подписки (subscription) — код, категория и источник будут одинаковыми.
- Таймаут — это источник
unknown, а не ответ. Что с ним делать, объявляется для каждой операции отдельно.
Ошибки
rate_limited ещё и сообщает, когда можно повторитьtry { 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:
raiseКак объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.
// 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);
}Если у вызывающего нет роли, он получает отказ, а не урезанный результат. Для игрока или клиентской сессии вызовы из строк fn и adm не проходят: действовать от имени игрока, запись трассировки и её чтение возвращают Problem с кодом forbidden. Учётные данные при этом действительны, просто в их ролях нет такого права. Повтор с тем же ключом идемпотентности ничего не изменит.
Ограничения
Не повторяйте принятый вызов
Ограничения проверяются при приёме вызова. Принятый вызов их уже прошёл и будет выполнен, сколько бы он ни ждал в очереди. Отказ какому-то более позднему вызову на него не влияет, поэтому если повторить принятый вызов, работа выполнится дважды.
Об ограничении вы узнаёте, только получив отказ. SDK не показывает ни действующее значение, ни оставшийся запас и не предупреждает, что вы близки к границе, а игроки ограничений не видят вовсе. Всё нужное есть в самом отказе:
- категория — есть ли смысл повторять;
- чьё это было ограничение;
- когда можно повторить и за какое окно оно считается.