Saltar al contenido principal
Bloques de construcción

Esta página aún no está traducida: se muestra la versión en inglés.

Extensibility

Cada escenario de la plataforma es una cadena de funciones registradas: reemplaza un eslabón o envuélvelo. Cambias el comportamiento de la plataforma reemplazando sus pasos, no haciendo un fork de su código fuente.

Cuándo usarlo

  • Un paso de la plataforma tiene que ejecutar tu lógica: declara el reemplazo de un eslabón con nombre con [Override(…)].
  • Necesitas verificaciones o efectos secundarios alrededor de un paso: middleware Before/After ordenado que puede vetar o notificar.
  • Hay código que debe ejecutarse según un horario, un evento o un webhook: los triggers te entregan un contexto ya interpretado y tipado.
  • Necesitas saber qué se va a ejecutar realmente antes del deploy: haz una ejecución de prueba de la cadena y lee el orden resuelto.
  • No lo uses cuando la regla se refiere a las escrituras de una sola entidad. Un hook de Data & Subscriptions es la forma más liviana.

Quién hace qué

ActorQué hace en esta página
backend-servicesobrescribe eslabones, envuelve pasos con middleware, escribe handlers de triggers
operatorinspecciona cadenas, fija el orden, lee secretos, hace ejecuciones de prueba de la resolución

En resumen

Tres formas de extender: filtrar SignIn, envolver grant con middleware y ejecutar código con 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(): ...
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

Se ejecuta fuera del motor

Las sobrescrituras, el middleware y los triggers se ejecutan como funciones en la nube en la plataforma, no en el motor. Escríbelos en C#, TypeScript o Python; Unreal se suscribe a los eventos que producen. Están previstas funciones escritas desde el motor: grafos de Blueprint y C++ (traducido a un lenguaje de scripts que valida la plataforma). Ambos se analizan y se suben como cualquier declaración; la ejecución sigue en la plataforma.

Se ejecuta fuera del motor

Las sobrescrituras, el middleware y los triggers se ejecutan como funciones en la nube en la plataforma, no en el motor. Escríbelos en C#, TypeScript o Python; Unity se suscribe a los eventos que producen.

El modelo

A qué te puedes enganchar en la plataforma.

TérminoQué es
función registradaun paso de la plataforma que se puede sobrescribir: “crear perfil”, “calcular precio”
scenariola cadena ordenada que ejecuta un flujo de la plataforma: autenticación, entrada, compra
overridabilitysi un eslabón se puede reemplazar, solo envolver o es fijo
middlewareun handler ordenado que se ejecuta antes o después de un eslabón
triggerlo que pone en marcha tu código: un evento, un horario, un webhook
secretun valor que tu handler puede leer
invocationuna ejecución, con su traza
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
Tu código dentro de un escenario de la plataforma: un eslabón reemplazado y otro envuelto; los eslabones de cada lado no saben qué implementación respondió

Qué declara un hook.

DeclaraQué es
positionel paso con nombre al que se engancha
kindgatekeeper: una verificación de admisión o una validación que ante un fallo bloquea (fail-closed): si el propio hook se rompe, el paso no se ejecuta; o observer: un log, una notificación, un contador, que ante un fallo deja pasar (fail-open): el paso se ejecuta y el fallo igual se reporta en lugar de perderse. No hay valor por defecto
momentbefore: antes de la validación, recibe el payload tipado y puede modificarlo o rechazarlo; o after: una vez confirmado el paso, recibe la petición y el resultado, solo para efectos secundarios, y nunca puede hacer fallar la operación ni alterar la respuesta
effectlo que el hook hace, no solo dónde está. Así el orden se expresa en términos del trabajo y no con números, y sigue valiendo cuando alguien más agrega un hook junto al tuyo
versión y condiciónun handler que aplica a un solo entorno o audiencia es una versión declarada de ese hook, no una rama dentro de su cuerpo, y es lo que activa o desactiva el interruptor del panel

Tres formas de enganchar código.

FormaPara qué usarla
atributo [Before(Step)] / [After(Step)]una regla sobre un paso con nombre: la mayoría de los hooks
atributo [Override(Link)]reemplazar por completo la implementación de un eslabón
Extend.Scenario("…").Before("link", fn, order: n)envolver un eslabón dentro de una cadena, cuando importa el orden respecto de otro middleware
SiempreQué es
las tresse despliegan con playserv push
las dos formas con atributosson las que muestra el panel, porque la declaración lleva el nombre del paso o del eslabón al modelo que se sube
la forma de middlewarelleva un orden en su lugar, que es lo que necesita una cadena
asignar al arrancar(Scenario.OnX = fn) sigue disponible para un handler que no necesita aparecer en el árbol del panel de administración
reemplazar un eslabóndeja intactos los eslabones de cada lado, y ninguno de ellos sabe qué implementación respondió: el paso propio de la plataforma o el tuyo

A dónde va una llamada, y lo que vale para cualquier ruta.

SiempreQué es
cuatro direccionesuna función en la nube · el backend externo de quien consume · el servidor de juego · otra función declarada
el enrutadorfunciona con mensajes y señales; request-response es un adaptador sobre él, no su naturaleza
coincidenciase hace por el nombre declarado de la operación o señal y por nada más: ni la forma del payload, ni quien llama, ni la carga
la direcciónno forma parte del contrato de la operación: mover un handler de una dirección a otra no es un cambio incompatible
“el servidor de juego”se define por lo que es, no por quién lo aloja: nuestra flota y el hosting propio de un estudio son una sola dirección, y la declaración no lleva ninguna marca de quién es dueño de la infraestructura
RPC del servidor de juegose registran en el mismo enrutador: declarar uno ya es registrarlo, y no hay otra forma
ordenejecuta el middleware de arriba hacia abajo, y donde un paso tiene más de una implementación, el enrutador elige de izquierda a derecha según la condición, y responde la versión marcada como predeterminada cuando nada coincidió

Una cadena con sobrescrituras y middleware se puede resolver y leer antes de que se ejecute nada. El orden resuelto se puede inspeccionar en el panel y desde el código.

Qué es una restricción entre hooks, y cuándo se verifica.

Qué es
una restricción con nombreun punto de extensión puede nombrar los efectos que restringe (una verificación antitrampas debe ir antes de una colocación, un recibo requiere un cobro en este punto) y no restringir nada más
un efecto sin nombreno tiene restricciones, pero tampoco se rechaza
una infracciónes un defecto de la configuración, y el rechazo nombra ambos hooks y la restricción que rompieron: no es una advertencia ni un reordenamiento silencioso
cuándo se verificaen cada acción que puede cambiar lo que se ejecuta en un punto: registrar, desplegar, cambiar la disposición. Así, una disposición que llega a ejecutarse ya fue admitida
nunca se vuelve a verificar al ejecutaruna disposición admitida no se verifica otra vez cuando se ejecuta
SiempreQué es
handlerstienen tipos de entrada y de salida: nada de dynamic ni bolsas de contexto. El handle de la plataforma es implícito, y el contexto de la invocación (quién llama, el trigger, la traza) llega ya interpretado
lo que ve un build del motorlos eventos que el escenario emite después, porque una sobrescritura o un middleware se ejecuta en la plataforma, y el runtime de un motor no es lugar para alojarlos. Eso es lo que muestran las pestañas de motor de los ejemplos de esta página
Dos implementaciones de una misma función, elegidas por condición con una predeterminada; una versión de hook filtrada de la misma forma
[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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

Se ejecuta fuera del motor

Las sobrescrituras, el middleware y los triggers se ejecutan como funciones en la nube en la plataforma, no en el motor. Escríbelos en C#, TypeScript o Python; Unreal se suscribe a los eventos que producen. Están previstas funciones escritas desde el motor: grafos de Blueprint y C++ (traducido a un lenguaje de scripts que valida la plataforma). Ambos se analizan y se suben como cualquier declaración; la ejecución sigue en la plataforma.

Se ejecuta fuera del motor

Las sobrescrituras, el middleware y los triggers se ejecutan como funciones en la nube en la plataforma, no en el motor. Escríbelos en C#, TypeScript o Python; Unity se suscribe a los eventos que producen.

La lógica propia de la plataforma se puede descargar, editar y volver a desplegar. Son funciones, como las tuyas:

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

Lo que personalizas aquí es lógica: las sobrescrituras, el middleware y las versiones de los que trata esta página.

No puedes agregar campos a las entidades de la plataforma

Un jugador, una sala, un registro de una tabla de clasificación y un pedido son estado del sistema con sus propias máquinas de estados, no el punto de partida de tu modelo de datos. Tus datos son tu propia entidad, declarada en Esquema como código y vinculada al estado de la plataforma mediante un predicado: el dueño es este jugador, el ámbito es esta sala. Así también siguen siendo tuyos cuando cambia el modelo propio de la plataforma.

Errores

  • Un nombre que no coincide con ningún handler registrado responde not found: una llamada nunca se descarta en silencio porque nadie estaba escuchando.
  • Un nombre registrado dos veces, y un conjunto con dos implementaciones que reclaman la misma condición, se rechazan al declarar el conjunto: en el deploy, no en el momento de la llamada.
  • El rechazo de un hook lleva el código y el motivo del propio hook, así que “una regla del juego dijo que no” nunca llega con aspecto de fallo del transporte.
  • Un hook que falla se comporta según el tipo declarado, fail-open o fail-closed, y cuál de los dos es se declaró de antemano, no se dedujo de lo que pasó.
  • Un hook no puede cambiar lo que ya está establecido: ni el dueño, ni el destino, ni la tabla o la conversación a la que iba dirigida una llamada. Corrige entradas y devuelve un veredicto.

Límites

Los números llegarán con el capítulo sobre los límites de la plataforma; lo que pasa en cada tope ya está definido.

  • El plazo de ejecución de un hook: al superarlo, se aplica el comportamiento ante fallos que declara su tipo.
  • Hooks en una misma posición e implementaciones de un mismo método: se rechaza registrar uno más.
  • La profundidad de anidamiento de “un hook invoca una operación que tiene hooks”: un rechazo declarado, nunca un agotamiento de recursos.
  • El tamaño del contexto que se pasa a un hook: truncarlo está prohibido, así que se rechaza el registro en lugar de que un handler reciba medio contexto.

Lecciones y recetas: una tabla de clasificación en Tanks usa los hooks de este módulo; un torneo diario pasa por este módulo.