Перейти к основному содержимому
Строительные блоки

Эта страница ещё не переведена — показана английская версия.

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

Как объявлять через атрибуты в 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.
Unity C# is the same C# API here — работает в Unity без изменений, со сгенерированными типами
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отказала платформа, код взят из её каталога
localSDK отказал ещё до отправки, код взят из его собственного опубликованного списка
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

Как объявлять через атрибуты в 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.
Unity C# is the same C# API here — работает в Unity без изменений, со сгенерированными типами
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 не показывает ни действующее значение, ни оставшийся запас и не предупреждает, что вы близки к границе, а игроки ограничений не видят вовсе. Всё нужное есть в самом отказе:

  • категория — есть ли смысл повторять;
  • чьё это было ограничение;
  • когда можно повторить и за какое окно оно считается.