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

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

Auth & Players

Вход — это шаг, который можно переопределить, а не чёрный ящик. Провайдеры, сессии, привязка учётных записей, баны. Каждая точка сценария — до и после входа, до и после привязки, до и после слияния, при смене статуса — объявлена как точка расширения определённого вида. Это либо проверка, которая может отклонить шаг, либо наблюдатель, который этого не может.

Когда это нужно

  • Игроки (player) должны входить в игру — через устройство, email, Apple, Google, Steam или собственный провайдер. Создание игрока при первом входе (sign-in) включается флагом, а не отдельным сценарием.
  • Гостевой аккаунт позже нужно превратить в постоянный: Link привязывает Steam без потери прогресса, а слияние (merge) сводит два аккаунта в одного игрока.
  • Политика должна выполняться там, где её не обойти: регион проверяется до входа, а стартовый набор выдаётся после того входа, при котором игрок был создан.
  • Модерация должна срабатывать сразу: отозвать сессии, приостановить аккаунт, забанить устройство — и событие banned услышат все работающие системы.
  • Объявленный контекст (регион, платформа, сборка) должен доходить до всех последующих хуков, чтобы ни одному из них не пришлось заново читать игрока ради этих данных.
  • Облегчённой замены у этого модуля нет. Все остальные модули узнают, кто их вызывает, через него. Пока хоть одному из них нужен актор-игрок, auth выключить нельзя: конфигуратор модулей откажет и перечислит зависимые модули.

Кто что делает

АкторЧто делает на этой странице
playerвходит, привязывает и отвязывает учётные записи (identity), обновляет учётные данные, выходит
moderatorотзывает сессии, банит, приостанавливает и восстанавливает игроков
backend-serviceзакрывает вход из отдельных регионов, создаёт первые строки нового игрока, читает и отзывает сессии

Коротко

Один вызов SignIn на провайдера, создание игрока при первом входе включается флагом, а Link добавляет Steam
// client — one call per provider; create-on-first-sign-in is a flag
var session = await PlayServ.Auth.SignIn(Provider.Device, create: true);
await PlayServ.Auth.Link(Provider.Steam);          // one player, many identities
// client — one call per provider; create-on-first-sign-in is a flag
const session = await PlayServ.auth.signIn(Provider.Device, { create: true });
await PlayServ.auth.link(Provider.Steam);    // one player, many identities
# client — one call per provider; create-on-first-sign-in is a flag
session = await playserv.auth.sign_in(Provider.DEVICE, create=True)
await playserv.auth.link(Provider.STEAM)     # one player, many identities
Скоро — Go

Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.

// client — one call per provider; create-on-first-sign-in is a flag
Client->Auth->SignInWithProvider(FPSProviderId::Device, Credential,
    TPSOnResult<FPSSession>::CreateWeakLambda(this, [this](const TPSResult<FPSSession>& Result)
    {
        if (!Result.HasValue()) { return; }
        // one player, many identities — add Steam to the same account
        Client->Auth->Providers->Link(FPSProviderId::Steam, SteamCredential);
    }));
// 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 без изменений, со сгенерированными типами
// client — one call per provider; create-on-first-sign-in is a flag
var session = await PlayServ.Auth.SignIn(Provider.Device, create: true);
await PlayServ.Auth.Link(Provider.Steam);          // one player, many identities

Каждая точка настраивается там же, где объявлена. Все формы, которые может принимать обработчик, собраны на странице Extensibility:

Проверка до входа не пускает игрока из региона, а наблюдатель после входа, создавшего игрока, выдаёт стартовый набор
[Before(Auth.SignIn)]                          // a gate: it may refuse, and it is fail-closed
public static Verdict GateRegion(SignInAttempt a) =>
    a.Region == "sanctioned"
        ? Hook.Reject(Problem.Forbidden, "region not served")
        : Hook.Continue(a);

[After(Auth.SignIn, created: true)]            // an observer: it watches, it cannot refuse
public static async Task GrantStarterPack(Player player)
{
    await player.Inventory.Grant("chest.gold", count: 1);
}
// a gate: it may refuse, and it is fail-closed
export const gateRegion = before(Auth.signIn, (a: SignInAttempt) =>
  a.region === 'sanctioned'
    ? Hook.reject(Problem.forbidden, 'region not served')
    : Hook.continue(a));

// an observer: it watches, it cannot refuse
export const grantStarterPack = after(Auth.signIn, { created: true },
  async (player: Player) => {
    await player.inventory.grant('chest.gold', { count: 1 });
  });
@before(auth.sign_in)                          # a gate: it may refuse, and it is fail-closed
def gate_region(a: SignInAttempt) -> Verdict:
    if a.region == "sanctioned":
        return Hook.reject(Problem.FORBIDDEN, "region not served")
    return Hook.continue_(a)

@after(auth.sign_in, created=True)             # an observer: it watches, it cannot refuse
async def grant_starter_pack(player: Player):
    await player.inventory.grant("chest.gold", count=1)
Скоро — Go

Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.

Выполняется вне движка

И переопределение, и хук — облачные функции: они выполняются на платформе, а не в движке. Напишите их на C#, TypeScript или Python, а Unreal подпишется на события, которые они порождают. В планах — функции, написанные прямо в движке: графы Blueprint и C++, который будет транслироваться в скрипт, проверяемый платформой. Их будут анализировать и загружать так же, как любые объявления, а выполняться они по-прежнему будут на платформе.

Выполняется вне движка

И переопределение, и хук — облачные функции: они выполняются на платформе, а не в движке. Напишите их на C#, TypeScript или Python, а Unity подпишется на события, которые они порождают.

Админ-панель показывает именно объявленную форму: у каждой точки видны её обработчики, их вид и итоговый порядок вызова. От вида зависит, что происходит, когда обработчик падает:

ВидЕсли упал сам обработчикПри отказе
проверка (gate)шаг отклоняется: если проверку региона не удалось выполнить, она не считается пройденнойкод из справочника платформы и понятная человеку причина. Вызывающий код ветвится по коду, а текст причины может меняться и переводиться
наблюдатель (observer)шаг остаётся выполненным: если стартовый набор не выдался, игрок недосчитается сундука, но вход не сорвётсяотказать не может

Хук никогда не решает, кто вошёл

Проверка лишь отвечает «да» или «нет» про игрока, которого платформа уже опознала. Она не называет игрока, не выдаёт учётную запись и не заменяет подтверждение от провайдера.

Модель

device · subject (the guest)Steam · subject (via Link)another player, same persona player — one stable id, never reused linklink: anonymous → registered, one waymerge: terminal; its id keeps resolving
Привязка — это тройка провайдер · субъект · игрок. Она необратимо переводит гостя в зарегистрированные, а после слияния id объединённого игрока по-прежнему разрешается

Игрок — это носитель учётных записей, а не строка в вашей схеме (schema). Его идентификатор стабилен и никогда не используется повторно — даже при слиянии: id объединённого игрока по-прежнему разрешается и не становится висячей ссылкой. Привязка — это тройка провайдер · внешний субъект · игрок.

ВсегдаЧто это
провайдер + субъектуникальная пара, и её уникальность порождает конфликт, а не запрет: если аккаунт уже занят, выход — выбрать слияние
не больше одной привязки на провайдера у игрокавторая учётная запись того же провайдера — конфликт
внешний субъектникогда не становится идентификатором игрока. Он принадлежит провайдеру: если использовать его как свой, наши id окажутся привязаны к чужим
тип учётной записи и статус доступа— разные оси. anonymous или registered — одна, active / suspended / banned — другая, поэтому можно выразить и «забаненного анонима», и «приостановленного зарегистрированного»
сессия и учётные данные— разные вещи. Сессия — это запись, а учётные данные (credential) — то, что вы предъявляете. Отзыв сессии аннулирует все её учётные данные и закрывает её открытые подписки (subscription)
несколько одновременных сессийкаждую можно отозвать отдельно
утверждения в учётных данных— объявленный контекст (регион, локаль) и только контекст. Утверждение (claim) никогда не даёт полномочий
отпечаток устройства— не учётная запись. Это никогда не повод впустить, только повод отказать. Хранят и сравнивают его в необратимом виде

Три машины состояний.

МашинаСостояния
тип учётной записиanonymous → registered, и переход односторонний
статус доступаactive ⇄ suspended, а при разбане active → banned → active
игрокalive → merged, причём merged — конечное состояние: объединённый игрок больше не входит

Что объявляете вы.

ОбъявлениеЧто это
политика входаразрешён ли анонимный вход и остальные правила входа. Объявляется атрибутом на точке монтирования модуля — не конфигом рядом с кодом и не во время выполнения
роль по умолчаниюнабор ролей (role), с которым новый игрок приходит при первом входе. У значения по умолчанию нет значения по умолчанию: если ничего не объявить, новые игроки придут без ролей — и это законное объявление, а не упущение
политика сессийчто происходит, когда достигнут потолок одновременных сессий: вытеснить самую старую с событием (event) или отказать новой. Значения по умолчанию нет
политика удалениякак удаление игрока распространяется на данные, которые на него ссылаются

Где настраивается провайдер. На операторском уровне, а не в коде: учётным данным магазина не место в репозитории. Объявленное попадает в админ-консоль только для чтения.

Что даёт выдача роли. Роли — забота не только оператора. У игрока есть операции выдачи и отзыва, поэтому игра может своим кодом назначить офицера гильдии или передать организатору турнира его полномочия.

ВсегдаЧто это
это не самоназначениечтобы выдать роль, нужно объявленное для этого атомарное разрешение (permission atom). Актор без него получит обычный forbidden, а не молчаливое бездействие
выдача идемпотентнавыдать роль, которая у игрока уже есть, — успех, а не конфликт. Состояние — это набор ролей, а не история вызовов, поэтому, в отличие от входа, этой операции не нужен ключ идемпотентности
изменение ролей не мгновеннооно вступает в силу без перевыпуска учётных данных и становится заметно не позже объявленного предела устаревания кэша прав. Если код выдаёт роль и сразу проверяет её на подключённом клиенте, это окно нужно учитывать

Из чего состоят роли и что они открывают, описано на странице Access & Roles.

Ошибки

  • Нет учётных данных или они истекли — ответ not authenticated, и помогает обновление. Отозванные учётные данные дают тот же ответ, но обновление уже не поможет — только новый вход.
  • Повторно предъявленные учётные данные, которые уже прошли ротацию, — конфликт. Именно поэтому ротацию можно обнаружить, а не молча терпеть.
  • Забаненный или приостановленный игрок, как и забаненный отпечаток устройства, получают forbidden, и повторять запрос бессмысленно.
  • Пара провайдер+субъект уже занята — это конфликт, и повторить можно после того, как выбрано слияние. Вторая учётная запись того же провайдера — конфликт, который повтор не исправит.
  • Отвязать последний способ входа нельзя — это отказ валидации: получился бы аккаунт, в который никто не может войти.
  • Слияние уже объединённого игрока — конфликт: merged — конечное состояние.
  • Если провайдер недоступен, ответ — unavailable, и стоит повторять с нарастающей задержкой. Если провайдер отверг учётные данные, ответ — not authenticated: стоит повторить один раз, но не в цикле.
  • При превышении частоты попыток входа ответ относится к категории ограничения частоты и содержит срок, после которого можно повторить.

Ограничения

Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждой границе уже определено.

  • Одновременные сессии игрока: по объявленной политике — либо вытеснение самой старой с событием, либо отказ новой. Значения по умолчанию нет.
  • Попытки входа за период и попытки привязать занятую пару: ограничение частоты со сроком повтора, а счётчик попыток остаётся в истории.
  • Привязки на игрока: привязка ещё одного провайдера отклоняется как конфликт.
  • Время жизни учётных данных: not authenticated, после обновления можно повторить. Время жизни учётных данных для обновления: только новый вход.
  • Хранение анонимного игрока, который не входит в игру: удаление по объявленной политике, с событием. Политику объявляют явно, значения по умолчанию нет.
  • Записи в бан-листе отпечатков: новая запись отклоняется, а старые никогда не вытесняются молча.