Aller au contenu principal
Briques de base

Cette page n’est pas encore traduite : la version anglaise est affichée.

Extensibility

Chaque scénario de la plateforme est une chaîne de fonctions enregistrées : remplacez un maillon ou enveloppez-le. Vous modifiez le comportement de la plateforme en remplaçant ses étapes, pas en forkant son code source.

Quand l’utiliser

  • Une étape de la plateforme doit exécuter votre logique : déclarez le remplaçant d’un maillon nommé avec [Override(…)].
  • Il vous faut des vérifications ou des effets de bord autour d’une étape : du middleware Before/After ordonné, qui peut opposer un veto ou notifier.
  • Du code doit s’exécuter selon un planning, sur un événement ou sur un webhook : les déclencheurs (trigger) vous fournissent un contexte typé, déjà analysé.
  • Vous devez savoir ce qui s’exécutera réellement avant de déployer : faites une exécution à blanc d’une chaîne et lisez l’ordre résolu.
  • Inutile quand la règle porte sur les écritures d’une seule entité. Un hook de Data & Subscriptions est la forme la plus légère.

Qui fait quoi

ActeurCe qu’il fait sur cette page
backend-servicesurcharge des maillons, enveloppe des étapes avec du middleware, écrit des handlers de déclencheurs
operatorinspecte les chaînes, fixe l’ordre, lit les secrets, résout les chaînes à blanc

En bref

Trois formes d’extension : filtrer SignIn, envelopper grant avec du middleware, exécuter du code sur un 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(): ...
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

S’exécute hors du moteur

Surcharges, middleware et déclencheurs s’exécutent tous comme des fonctions cloud sur la plateforme, pas dans le moteur. Écrivez-les en C#, TypeScript ou Python — Unreal s’abonne aux événements qui en résultent. Des fonctions écrites côté moteur sont prévues : des graphes Blueprint, et du C++ (traduit vers une cible de script validée par la plateforme). Les deux sont analysés et publiés comme n’importe quelle déclaration ; l’exécution reste sur la plateforme.

S’exécute hors du moteur

Surcharges, middleware et déclencheurs s’exécutent tous comme des fonctions cloud sur la plateforme, pas dans le moteur. Écrivez-les en C#, TypeScript ou Python — Unity s’abonne aux événements qui en résultent.

Le modèle

Ce à quoi la plateforme vous permet de vous brancher.

TermeCe que c’est
fonction enregistréeune étape de la plateforme qu’on peut surcharger — « créer le profil », « calculer le prix »
scenariola chaîne ordonnée qu’exécute un flux de la plateforme : authentification, entrée en salle, achat
overridabilityindique si un maillon peut être remplacé, seulement enveloppé, ou s’il est figé
middlewareun handler ordonné qui s’exécute avant ou après un maillon
triggerce qui lance votre code : un événement, un planning, un webhook
secretune valeur que votre handler peut lire
invocationune exécution, avec sa trace
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
Votre code dans un scénario de la plateforme : un maillon remplacé, un autre enveloppé ; les maillons voisins ignorent quelle implémentation a répondu

Ce que déclare un hook.

DéclareCe que c’est
positionl’étape nommée à laquelle il se rattache
kindgatekeeper — un contrôle d’admission ou une validation, qui bloque en cas d’échec : l’étape ne s’exécute pas quand le hook lui-même plante ; ou observer — un journal, une notification, un compteur, qui laisse passer en cas d’échec : l’étape s’exécute, et l’échec est tout de même signalé au lieu d’être passé sous silence. Il n’y a pas de valeur par défaut
momentbefore — avant la validation, il reçoit le payload typé et peut le modifier ou le rejeter ; ou after — une fois l’étape validée, il reçoit la requête et le résultat, pour des effets de bord uniquement, et il ne peut jamais faire échouer l’opération ni modifier la réponse
effectce que le hook fait, pas seulement l’endroit où il se trouve. L’ordre s’exprime alors en termes de travail plutôt que de numéros, et il tient bon quand quelqu’un d’autre ajoute un hook à côté du vôtre
version et conditionun handler qui s’applique à un seul environnement ou à une seule audience est une version déclarée de ce hook, et non une branche dans son corps ; c’est ce que bascule l’interrupteur du panneau

Trois façons de brancher du code.

FormeÀ utiliser pour
attribut [Before(Step)] / [After(Step)]une règle sur une étape nommée — la plupart des hooks
attribut [Override(Link)]remplacer purement et simplement l’implémentation d’un maillon
Extend.Scenario("…").Before("link", fn, order: n)envelopper un maillon à l’intérieur d’une chaîne, quand l’ordre par rapport aux autres middlewares compte
ToujoursCe que c’est
les troisse déploient avec playserv push
les deux formes par attributsont celles qu’affiche le panneau, car la déclaration transporte le nom de l’étape ou du maillon dans le modèle publié
la forme middlewaretransporte un ordre à la place, et c’est ce dont une chaîne a besoin
l'affectation au démarrage(Scenario.OnX = fn) reste disponible pour un handler qui n’a pas besoin d’apparaître dans l’arborescence d’administration
remplacer un maillonlaisse les maillons voisins intacts, et aucun d’eux ne sait quelle implémentation a répondu — l’étape de la plateforme ou la vôtre

Où va un appel, et ce qui vaut pour toute route.

ToujoursCe que c’est
quatre directionsune fonction cloud · le backend externe du consommateur · le serveur de jeu · une autre fonction déclarée
le routeurfonctionne par messages et signaux ; request-response n’est qu’un adaptateur posé dessus, pas sa nature
correspondancese fait sur le nom déclaré de l’opération ou du signal, et sur rien d’autre : ni la forme du payload, ni l’appelant, ni la charge
la directionne fait pas partie du contrat de l’opération : déplacer un handler d’une direction à une autre n’est pas un changement cassant
« le serveur de jeu »se définit par ce qu’il est, pas par qui l’héberge — notre flotte et l’hébergement propre d’un studio sont une seule direction, et la déclaration ne porte aucune marque indiquant à qui appartient l’infrastructure
RPC du serveur de jeus’enregistrent dans le même routeur : en déclarer un, c’est l’enregistrer, et il n’existe pas d’autre moyen
ordrele middleware s’exécute de haut en bas, et quand une étape a plusieurs implémentations, le routeur choisit de gauche à droite selon la condition ; la version marquée par défaut répond quand rien ne correspond

Une chaîne avec surcharges et middleware peut être résolue et lue avant toute exécution. L’ordre résolu s’inspecte dans le panneau et depuis le code.

Ce qu’est une contrainte entre hooks, et quand elle est vérifiée.

Ce que c’est
une contrainte nomméeun point d’extension peut nommer les effets qu’il contraint — un contrôle anti-triche doit précéder un placement, un reçu exige un débit à cet endroit — et ne contraindre rien d’autre
un effet non nommén’est pas contraint, mais n’est pas refusé non plus
une violationest un défaut de configuration, et le refus nomme les deux hooks et la contrainte qu’ils ont enfreinte — pas un avertissement, ni un réordonnancement silencieux
moment de la vérificationà chaque acte qui peut changer ce qui s’exécute à un point donné : enregistrement, déploiement, modification de l’agencement. Un agencement qui atteint l’exécution a donc déjà été admis
jamais revérifiée à l'exécutionun agencement admis n’est pas revérifié quand il s’exécute
ToujoursCe que c’est
handlerstypés en entrée et en sortie : pas de dynamic, pas de sacs de contexte. Le handle de la plateforme est ambiant, et le contexte d’invocation — appelant, déclencheur, trace — arrive déjà analysé
ce que voit un build du moteurles événements que le scénario émet ensuite, car une surcharge ou un middleware s’exécute sur la plateforme, et un runtime de moteur n’est pas fait pour en héberger. C’est ce que montrent les onglets moteur des exemples de cette page
Deux implémentations d’une même fonction, choisies selon une condition avec une valeur par défaut ; une version de hook filtrée de la même façon
[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)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

S’exécute hors du moteur

Surcharges, middleware et déclencheurs s’exécutent tous comme des fonctions cloud sur la plateforme, pas dans le moteur. Écrivez-les en C#, TypeScript ou Python — Unreal s’abonne aux événements qui en résultent. Des fonctions écrites côté moteur sont prévues : des graphes Blueprint, et du C++ (traduit vers une cible de script validée par la plateforme). Les deux sont analysés et publiés comme n’importe quelle déclaration ; l’exécution reste sur la plateforme.

S’exécute hors du moteur

Surcharges, middleware et déclencheurs s’exécutent tous comme des fonctions cloud sur la plateforme, pas dans le moteur. Écrivez-les en C#, TypeScript ou Python — Unity s’abonne aux événements qui en résultent.

La logique propre de la plateforme peut être récupérée, modifiée et redéployée. Ce sont des fonctions, comme les vôtres :

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

Ce que vous personnalisez ici, c’est de la logique : les surcharges, le middleware et les versions dont traite cette page.

Vous ne pouvez pas ajouter de champs aux entités de la plateforme

Un joueur, une salle, un enregistrement de classement et une commande sont de l’état système avec leurs propres machines à états, pas le point de départ de votre modèle de données. Vos données forment votre propre entité, déclarée dans Le schéma dans le code et rattachée à l’état de la plateforme par un prédicat : le propriétaire est ce joueur, la portée est cette salle. Elles restent ainsi les vôtres quand le modèle propre de la plateforme change.

Erreurs

  • Un nom qui ne correspond à aucun handler enregistré répond not found — un appel n’est jamais abandonné en douce parce que personne n’écoutait.
  • Un nom enregistré deux fois, ou un ensemble où deux implémentations revendiquent la même condition, est refusé dès la déclaration de l’ensemble : au déploiement, pas au moment de l’appel.
  • Un refus de hook porte le code et le motif propres au hook : « une règle du jeu a dit non » n’arrive donc jamais sous l’apparence d’une panne de transport.
  • Un hook qui échoue se comporte selon le type déclaré — fail-open ou fail-closed — et ce type a été déclaré, pas déduit de ce qui s’est passé.
  • Un hook ne peut pas modifier ce qui est déjà établi : ni le propriétaire, ni la cible, ni le classement ou la conversation auxquels l’appel était adressé. Il corrige des entrées et rend un verdict.

Limites

Les valeurs chiffrées arriveront avec le chapitre sur les limites de la plateforme ; ce qui se passe à chaque plafond est déjà fixé.

  • Délai d’exécution d’un hook : au-delà, le comportement en cas d’échec que déclare son type.
  • Hooks à une même position et implémentations d’une même méthode : l’enregistrement d’un élément supplémentaire est refusé.
  • Profondeur d’imbrication de « un hook invoque une opération qui a des hooks » : un refus déclaré, jamais un épuisement des ressources.
  • Taille du contexte passé à un hook : la troncature est interdite ; l’enregistrement est donc refusé plutôt que de laisser un handler recevoir un contexte à moitié.

Leçons et recettes : un classement dans Tanks utilise les hooks de ce module ; un tournoi quotidien passe par ce module.