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/Afterordenado 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é
| Actor | Qué hace en esta página |
|---|---|
backend-service | sobrescribe eslabones, envuelve pasos con middleware, escribe handlers de triggers |
operator | inspecciona cadenas, fija el orden, lee secretos, hace ejecuciones de prueba de la resolución |
En resumen
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(): ...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.
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.
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érmino | Qué es |
|---|---|
función registrada | un paso de la plataforma que se puede sobrescribir: “crear perfil”, “calcular precio” |
scenario | la cadena ordenada que ejecuta un flujo de la plataforma: autenticación, entrada, compra |
overridability | si un eslabón se puede reemplazar, solo envolver o es fijo |
middleware | un handler ordenado que se ejecuta antes o después de un eslabón |
trigger | lo que pone en marcha tu código: un evento, un horario, un webhook |
secret | un valor que tu handler puede leer |
invocation | una ejecución, con su traza |
Qué declara un hook.
| Declara | Qué es |
|---|---|
position | el paso con nombre al que se engancha |
kind | gatekeeper: 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 |
moment | before: 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 |
effect | lo 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ón | un 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.
| Forma | Para 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 |
| Siempre | Qué es |
|---|---|
las tres | se despliegan con playserv push |
las dos formas con atributos | son 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 middleware | lleva 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ón | deja 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.
| Siempre | Qué es |
|---|---|
cuatro direcciones | una función en la nube · el backend externo de quien consume · el servidor de juego · otra función declarada |
el enrutador | funciona con mensajes y señales; request-response es un adaptador sobre él, no su naturaleza |
coincidencia | se 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ón | no 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 juego | se registran en el mismo enrutador: declarar uno ya es registrarlo, y no hay otra forma |
orden | ejecuta 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 nombre | un 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 nombre | no tiene restricciones, pero tampoco se rechaza |
una infracción | es 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 verifica | en 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 ejecutar | una disposición admitida no se verifica otra vez cuando se ejecuta |
| Siempre | Qué es |
|---|---|
handlers | tienen 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 motor | los 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 |
[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)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.
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.
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-openofail-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.