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

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

Extensibility

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

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

  • Шаг платформы должен выполнять вашу логику. Объявите замену именованного звена через [Override(…)].
  • Вокруг шага нужны проверки или побочные эффекты. Для этого есть упорядоченные промежуточные обработчики (middleware) Before/After: они могут запретить шаг или уведомить о нём.
  • Код должен запускаться по расписанию, по событию или по вебхуку. Триггеры передают вам уже разобранный типизированный контекст.
  • До деплоя нужно знать, что на самом деле выполнится. Сделайте пробный прогон цепочки и посмотрите итоговый порядок.
  • Не нужно, если правило касается записей одной сущности. Для этого есть более лёгкий вариант — хук Data & Subscriptions.

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

АкторЧто делает на этой странице
backend-serviceпереопределяет звенья, оборачивает шаги промежуточными обработчиками, пишет обработчики триггеров
operatorпросматривает цепочки, задаёт порядок, читает секреты, делает пробный прогон разрешения цепочки

Коротко

Три способа расширения: проверка перед SignIn, промежуточный обработчик вокруг grant и код по cron
// gate one named step of the auth scenario — a before hook may refuse, fail-closed
[Before(Auth.SignIn)]
public static Task<Verdict> GateRegion(SignInAttempt a) =>
    a.Region == "sanctioned"
        ? Hook.Reject(Problem.Forbidden, "region not served")
        : Hook.Continue(a);

// wrap a step with ordered middleware
PlayServ.Extend.Scenario("commerce.purchase")
    .Before("grant", LogPurchaseIntent)
    .After("grant", NotifySquad, order: 10);

// customer code on a trigger
[OnSchedule("0 4 * * *")]
public static async Task NightlyCleanup() { ... }
// gate one named step of the auth scenario — a before hook may refuse, fail-closed
export const gateRegion = before(Auth.signIn, (a: SignInAttempt) =>
  a.region === 'sanctioned'
    ? Hook.reject(Problem.forbidden, 'region not served')
    : Hook.continue(a));

// wrap a step with ordered middleware
playserv.extend.scenario('commerce.purchase')
  .before('grant', logPurchaseIntent)
  .after('grant', notifySquad, { order: 10 });

// customer code on a trigger
export const nightlyCleanup = onSchedule('0 4 * * *', async () => { /* ... */ });
# gate one named step of the auth scenario — a before hook may refuse, fail-closed
@before(auth.sign_in)
async def gate_region(a):
    if a.region == "sanctioned":
        return hook.reject(problem.FORBIDDEN, "region not served")
    return hook.cont(a)

# wrap a step with ordered middleware
playserv.extend.scenario("commerce.purchase") \
    .before("grant", log_purchase_intent) \
    .after("grant", notify_squad, order=10)

# customer code on a trigger
@on_schedule("0 4 * * *")
async def nightly_cleanup(): ...
Скоро — Go

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

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

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

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

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

Модель

К чему можно подключиться.

ТерминЧто это
зарегистрированная функцияодин переопределяемый шаг платформы — «создать профиль», «определить цену»
scenarioупорядоченная цепочка, которую выполняет процесс платформы: авторизация, вход, покупка
overridabilityможно ли звено заменить, только обернуть или его нельзя трогать
middlewareупорядоченный обработчик до или после звена
triggerто, что запускает ваш код: событие, расписание, вебхук
secretзначение, которое может читать ваш обработчик
invocationодин запуск вместе с его трассировкой
a platform scenario — a purchasevalidateresolve price — your [Override], chosen by condition; ours is the defaultyour Before hook — a gatekeeper: may reject; if it breaks, grant does not rungrantyour After hook — an observer: side effects only; if it breaks, grant still standsnotify
Ваш код внутри сценария платформы: одно звено заменено, другое обёрнуто, а соседние звенья не знают, какая реализация ответила

Что объявляет хук.

Что объявляетЧто это
positionименованный шаг, к которому подключается хук
kindgatekeeper — проверка допуска или валидация. При сбое он блокирует: если сломался сам хук, шаг не выполняется. observer — журнал, уведомление, счётчик. При сбое он пропускает: шаг выполняется, но об ошибке всё равно сообщается, а не замалчивается. Значения по умолчанию нет
momentbefore — до валидации: хук получает типизированные данные и может их изменить или отклонить. after — когда шаг уже зафиксирован: хук получает запрос и результат и может только добавить побочные эффекты. Сорвать операцию или изменить ответ он не может
effectчто хук делает, а не только где он стоит. Тогда порядок описывается через саму работу, а не через числа, и не ломается, когда кто-то добавит хук рядом с вашим
версия и условиеобработчик, который действует только для одного окружения (environment) или одной аудитории, — это объявленная версия хука, а не ветка внутри его тела. Именно её включает и выключает переключатель в панели

Три способа подключить код.

СпособДля чего
атрибут [Before(Step)] / [After(Step)]одно правило на одном именованном шаге — так пишется большинство хуков
атрибут [Override(Link)]полная замена реализации звена
Extend.Scenario("…").Before("link", fn, order: n)обёртка звена внутри цепочки, когда важен порядок относительно других промежуточных обработчиков
ВсегдаЧто это
все три способадеплоятся через playserv push
два способа с атрибутамиих показывает панель, потому что объявление переносит имя шага или звена в загруженную модель
способ с промежуточным обработчикомвместо имени несёт порядок — а цепочке нужен именно он
присваивание при запуске(Scenario.OnX = fn) по-прежнему доступно для обработчика, которому не нужно появляться в дереве админ-панели
замена одного звенасоседние звенья остаются нетронутыми, и ни одно из них не знает, какая реализация ответила — собственный шаг платформы или ваш

Куда уходит вызов и что верно для любого маршрута.

ВсегдаЧто это
четыре направленияоблачная функция · внешний бэкенд потребителя · игровой сервер · другое объявленное направление
маршрутизаторработает на сообщениях и сигналах; request-response — лишь один из адаптеров к нему, а не его природа
сопоставлениеидёт по объявленному имени операции или сигнала и больше ни по чему: не по форме данных, не по вызывающему, не по нагрузке
направлениене входит в контракт операции: перенос обработчика в другое направление не ломает совместимость
«игровой сервер»определяется тем, что он делает, а не тем, кто его хостит. Наш парк серверов и собственный хостинг студии — одно направление, и в объявлении нет пометки о том, чья это инфраструктура
RPC игрового серверарегистрируются в том же маршрутизаторе: объявить такой RPC — это и есть его зарегистрировать, второго способа нет
порядокпромежуточные обработчики выполняются сверху вниз. Если у шага несколько реализаций, маршрутизатор выбирает слева направо по условию, а если ничего не подошло, отвечает версия, помеченная как версия по умолчанию

Цепочку с переопределениями и промежуточными обработчиками можно разрешить и прочитать до того, как что-либо запустится. Итоговый порядок виден и в панели, и из кода.

Что такое ограничение между хуками и когда оно проверяется.

Что это
названное ограничениеточка расширения может назвать эффекты, которые она ограничивает: проверка античита должна идти перед размещением, чек требует списания в этой точке. Больше она ничего не ограничивает
неназванный эффектничем не ограничен, но и не запрещён
нарушениедефект конфигурации. В отказе названы оба хука и ограничение, которое они нарушили. Это не предупреждение и не молчаливая перестановка
когда проверяетсяпри каждом действии, которое может изменить то, что выполняется в точке: регистрации, деплое, изменении расстановки. Поэтому расстановка, дошедшая до выполнения, уже прошла проверку
без повторной проверки при выполнениипринятую расстановку при выполнении повторно не проверяют
ВсегдаЧто это
обработчикитипизированы на входе и выходе: никаких dynamic и мешков с контекстом. Доступ к платформе есть неявно, а контекст запуска — вызывающий, триггер, трассировка — приходит уже разобранным
что видит сборка движкасобытия, которые сценарий отправляет после шага. Переопределения и промежуточные обработчики выполняются на платформе, и среде выполнения движка они не место. Именно это и показывают вкладки движков в примерах на этой странице
Две реализации одной функции, выбор по условию с вариантом по умолчанию, и версия хука, которая включается так же
[Rpc("resolve_price", Default = true)]
public static Price ResolvePrice(Sku sku) => Pricing.Base(sku);

[Rpc("resolve_price", When = "env == 'staging'")]
public static Price ResolvePriceStaging(Sku sku) => Pricing.WithDiscount(sku, 0.5f);

// a hook can carry a version too, gated by its own condition
[After("grant", When = "audience == 'beta'")]
public static void NotifySquadBeta(GrantResult r) => Messaging.PingBeta(r.Squad);
export const resolvePrice = rpc('resolve_price', { default: true },
  (sku: Sku) => Pricing.base(sku));

export const resolvePriceStaging = rpc('resolve_price', { when: "env == 'staging'" },
  (sku: Sku) => Pricing.withDiscount(sku, 0.5));

// a hook can carry a version too, gated by its own condition
export const notifySquadBeta = after('grant', { when: "audience == 'beta'" },
  (r: GrantResult) => Messaging.pingBeta(r.squad));
@rpc("resolve_price", default=True)
def resolve_price(sku: Sku) -> Price:
    return pricing.base(sku)

@rpc("resolve_price", when="env == 'staging'")
def resolve_price_staging(sku: Sku) -> Price:
    return pricing.with_discount(sku, 0.5)

# a hook can carry a version too, gated by its own condition
@after("grant", when="audience == 'beta'")
def notify_squad_beta(r: GrantResult):
    messaging.ping_beta(r.squad)
Скоро — Go

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

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

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

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

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

Собственную логику платформы можно выгрузить, изменить и задеплоить заново. Это такие же функции, как ваши:

playserv functions pull matchmaking.match     # the platform's implementation, as source
# edit: widen the skill window for the weekend event
playserv push                                 # your version registers; the default stays as fallback

Настраиваете вы здесь именно логику: переопределения, промежуточные обработчики и версии, о которых эта страница.

В сущности платформы нельзя добавлять поля

Игрок, комната, запись в таблице лидеров и заказ — это системное состояние со своими машинами состояний, а не заготовка для вашей модели данных. Ваши данные — это ваша собственная сущность (entity). Её объявляют так, как описано на странице Схема в коде, и привязывают к состоянию платформы предикатом: владелец — этот игрок, область — эта комната. Так данные остаются вашими, даже когда меняется модель самой платформы.

Ошибки

  • Если имени не соответствует ни один зарегистрированный обработчик, ответ — not found. Вызов никогда не пропадает молча из-за того, что его никто не слушал.
  • Имя, зарегистрированное дважды, и набор, где две реализации претендуют на одно и то же условие, отклоняются в момент объявления набора — при деплое, а не при вызове.
  • Отказ хука несёт собственный код и причину хука, поэтому «правило игры сказало нет» никогда не будет выглядеть как сбой транспорта.
  • Хук, который упал, ведёт себя по объявленному виду — fail-open или fail-closed. Какой именно, объявлено заранее, а не выводится из того, что произошло.
  • Хук не может менять то, что уже заявлено: ни владельца, ни цель, ни таблицу или беседу, которым адресован вызов. Он исправляет входные данные и возвращает вердикт.

Ограничения

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

  • Срок выполнения хука: если он превышен, хук ведёт себя так, как объявлено для сбоя в его виде.
  • Число хуков на одном шаге и число реализаций одного метода: регистрация сверх предела отклоняется.
  • Глубина вложенности вида «хук вызывает операцию, у которой есть хуки»: объявленный отказ, а не исчерпание ресурсов.
  • Размер контекста, передаваемого хуку: обрезать его запрещено, поэтому отклоняется регистрация, а не обработчик получает половину контекста.

Уроки и рецепты: таблица лидеров в Tanks использует хуки этого модуля, а ежедневный турнир целиком построен на нём.