Перейти до основного вмісту
Будівельні блоки

Цю сторінку ще не перекладено — показано англійську версію.

Extensibility

Кожен сценарій платформи — це ланцюжок зареєстрованих функцій: замініть ланку або обгорніть її. Поведінку платформи ви змінюєте, підміняючи її кроки, а не форкаючи її код.

Коли це потрібно

  • Крок платформи має виконувати вашу логіку: оголосіть заміну іменованої ланки через [Override(…)].
  • Навколо кроку потрібні перевірки чи побічні ефекти: упорядковані проміжні обробники (middleware) Before/After, які можуть заборонити крок або повідомити про нього.
  • Код має запускатися за розкладом, за подією чи через вебхук: тригери передають вам уже розібраний типізований контекст.
  • Треба знати, що насправді виконається, ще до деплою: проженіть ланцюжок у тестовому режимі (dry run) і прочитайте остаточний порядок.
  • Не потрібно, якщо правило стосується записів однієї сутності (entity). Для цього є легший варіант — хук (hook) у 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що хук робить, а не лише де він стоїть. Тоді порядок описують через саму роботу, а не через числа, і він не зламається, коли хтось додасть свій хук поруч із вашим
версія і умоваобробник, що діє лише для одного середовища чи аудиторії, — це оголошена версія хука, а не гілка в його тілі. Саме її вмикає й вимикає перемикач у панелі

Три способи під’єднати код.

СпосібДля чого
атрибут [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

Тут ви налаштовуєте логіку: перевизначення, проміжні обробники й версії, про які йдеться на цій сторінці.

До сутностей платформи не можна додавати поля

Гравець, кімната, запис у таблиці лідерів і замовлення — це системний стан із власними машинами станів, а не основа вашої моделі даних. Ваші дані — це ваша власна сутність, оголошена так, як описано на сторінці Схема в коді і прив’язана до стану платформи предикатом: власник — цей гравець, область — ця кімната. Так дані лишаються вашими, навіть коли змінюється модель самої платформи.

Помилки

  • Назва, якій не відповідає жоден зареєстрований обробник, отримує not found: виклик ніколи не зникає тихо лише тому, що його ніхто не слухав.
  • Назву, зареєстровану двічі, і набір, де дві реалізації претендують на ту саму умову, відхиляють ще під час оголошення набору: на деплої, а не під час виклику.
  • Відмова хука несе власний код і причину хука, тож «правило гри сказало „ні“» ніколи не виглядає як збій транспорту.
  • Хук, що зазнав збою, поводиться згідно з оголошеним видом — fail-open або fail-closed, — і цей вид оголошено заздалегідь, а не виведено з того, що сталося.
  • Хук не може змінити те, що вже заявлено: ні власника, ні ціль, ні таблицю чи розмову, якій адресовано виклик. Він виправляє вхідні дані й повертає вердикт.

Обмеження

Конкретні числа з’являться разом із розділом про обмеження платформи, а що стається на кожній межі, визначено вже зараз.

  • Граничний час виконання хука: коли його перевищено, діє поведінка у разі збою, яку оголошує вид хука.
  • Кількість хуків на одній позиції і реалізацій одного методу: реєстрацію ще одного відхиляють.
  • Глибина вкладеності «хук викликає операцію, у якої теж є хуки»: оголошена відмова, а не вичерпання ресурсів.
  • Розмір контексту, який передають хукові: обрізати його заборонено, тож відхиляють реєстрацію, а не передають обробникові половину контексту.

Уроки й рецепти: таблиця лідерів у Tanks використовує хуки цього модуля, а щоденний турнір цілком побудовано на ньому.