Saltar al contenido principal
Servicios alrededor del juego

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

Messaging

Salas, grupos, jugadores: un solo modelo de direccionamiento para el chat y las notificaciones. Los mensajes llegan a una conversación, y una conversación es un canal al que se suman el historial, la moderación y la entrega por fuera de la conexión del juego.

Cuándo usarlo

  • Los jugadores (player) conversan — en el chat de la sala, en los canales del gremio, por mensaje directo — con el direccionamiento que ya tienes: sala (room), grupo, jugador.
  • Los jugadores desconectados también tienen que enterarse: las notificaciones con plantilla, que se pueden programar, llegan por fuera de la conexión del juego como notificaciones push.
  • La moderación tiene que ejecutarse antes de la entrega: un hook previo al envío filtra o rechaza, y la plataforma aplica en todas partes el silenciamiento y el bloqueo.
  • Los jugadores que vuelven necesitan ponerse al día: History(take: 50) recorre la conversación por páginas la próxima vez que abren el juego.
  • No lo uses cuando lo que se envía es estado del juego y no una conversación. Eso ya lo distribuyen los campos sincronizados de Data & Subscriptions y los canales de Core.

Quién hace qué

ActorQué hace en esta página
playerenvía y recibe mensajes; lee el historial; silencia o bloquea
moderatorfiltra y oculta mensajes, prohíbe términos
backend-serviceenvía o programa notificaciones con plantilla

En resumen

Un Send por cada destino — sala, gremio, mensaje directo —, más la suscripción y el historial
// conversations map to the addressing you already have
await playserv.Messaging.Send(Conversation.Room(roomId), "gg!");
await playserv.Messaging.Send(Conversation.Group(guildId), rally);
await playserv.Messaging.Send(Conversation.Direct(friendId), "re?");

playserv.Messaging.Subscribe(Conversation.Group(guildId), msg => Chat.Add(msg));
var history = await playserv.Messaging.History(Conversation.Room(roomId), take: 50);
// conversations map to the addressing you already have
await playserv.messaging.send(Conversation.room(roomId), 'gg!');
await playserv.messaging.send(Conversation.group(guildId), rally);
await playserv.messaging.send(Conversation.direct(friendId), 're?');

playserv.messaging.subscribe(Conversation.group(guildId), (msg) => chat.add(msg));
const history = await playserv.messaging.history(Conversation.room(roomId), { take: 50 });
# conversations map to the addressing you already have
await playserv.messaging.send(Conversation.room(room_id), "gg!")
await playserv.messaging.send(Conversation.group(guild_id), rally)
await playserv.messaging.send(Conversation.direct(friend_id), "re?")

playserv.messaging.subscribe(Conversation.group(guild_id), lambda msg: chat.add(msg))
history = await playserv.messaging.history(Conversation.room(room_id), take=50)
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.

// conversations map to the addressing you already have
Client->Messaging->Conversations->Get(FPSConversation::Room(RoomId),
    TPSOnResult<FPSConversation*>::CreateWeakLambda(this, [this](const TPSResult<FPSConversation*>& Result)
    {
        if (!Result.HasValue()) { return; }
        FPSConversation* RoomChat = Result.Value();
        RoomChat->Send->Text({ TEXT("gg!") });

        // history pages under the same node that carries the messages
        RoomChat->Messages->Select().Page(50).Then(
            TPSOnResult<TPSPage<FPSMessage>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<FPSMessage>>& History)
            {
                if (!History.HasValue()) { return; }
                Chat->Show(History.Value().Rows);
            }));
    }));

// group and direct targets resolve the same way
Client->Messaging->Conversations->Get(FPSConversation::Group(GuildId), OnConversation);
Client->Messaging->Conversations->Get(FPSConversation::Direct(FriendId), OnConversation);

// live messages: one handler, every target
TPSSubscription GuildFeed = Guild->Subscribe([this](const FPSMessage& Message) { Chat->Add(Message); });
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — funciona tal cual en Unity con los tipos generados
// conversations map to the addressing you already have
await playserv.Messaging.Send(Conversation.Room(roomId), "gg!");
await playserv.Messaging.Send(Conversation.Group(guildId), rally);
await playserv.Messaging.Send(Conversation.Direct(friendId), "re?");

playserv.Messaging.Subscribe(Conversation.Group(guildId), msg => Chat.Add(msg));
var history = await playserv.Messaging.History(Conversation.Room(roomId), take: 50);

Un mensaje estructurado es un evento declarado, y la conversación lo transporta por su nombre, sin que tengas que construir una clase de carga útil (payload) en el lugar de la llamada:

RallyCall se declara una vez; la conversación del gremio lo envía por su nombre
[Message("rallyCall")]
public class RallyCall
{
    public Vector3 At;
    public string Note = "";
}

var guild = PlayServ.Group(guildId).Conversation;
await guild.Send.RallyCall(at: northGate, note: "push now");
@Message('rallyCall')
export class RallyCall {
  at!: Vector3;
  note = '';
}

const guild = playserv.group(guildId).conversation;
await guild.send.rallyCall({ at: northGate, note: 'push now' });
@message("rallyCall")
class RallyCall:
    at: Vector3
    note: str = ""

guild = playserv.group(guild_id).conversation
await guild.send.rally_call(at=north_gate, note="push now")
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.

USTRUCT(PSMessage = (Name = "rallyCall"))
struct FRallyCall
{
    GENERATED_BODY()

    UPROPERTY() FVector At;
    UPROPERTY() FString Note;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI

// the group's conversation is an address you resolve, then send into
Client->Messaging->Conversations->Get(FPSConversation::Group(GuildId),
    TPSOnResult<FPSConversation*>::CreateWeakLambda(this, [this](const TPSResult<FPSConversation*>& Result)
    {
        if (!Result.HasValue()) { return; }
        Result.Value()->Send->RallyCall({ NorthGate, TEXT("push now") });
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — la misma declaración y la misma llamada compilan en Unity con la versión base 2021.3
[Message("rallyCall")]
public class RallyCall
{
    public Vector3 At;
    public string Note = "";
}

var guild = PlayServ.Group(guildId).Conversation;
await guild.Send.RallyCall(at: northGate, note: "push now");

El mensaje declarado llega tipado en la misma suscripción, así que un cliente que conoce RallyCall recibe campos y no un blob.

Las notificaciones van por fuera de la conexión del juego, usan plantillas y se pueden programar. Se envían con autoridad fn o adm, nunca desde la sesión de un jugador:

Una notificación raid-starts con plantilla, enviada desde una función en la nube y entregada por fuera de la conexión del juego
// cloud function — Notify needs fn/adm authority
await PlayServ.Messaging.Notify(playerId, Template.Named("raid-starts"),
                                args: new { at = start });
// cloud function — notify needs fn/adm authority
await playserv.messaging.notify(playerId, Template.named('raid-starts'),
  { args: { at: start } });
# cloud function — notify needs fn/adm authority
await playserv.messaging.notify(player_id, Template.named("raid-starts"),
                                args={"at": start})
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.

Esto lo llama otro actor

La llamada existe en Unreal. Enviar una notificación requiere autoridad fn/adm, así que la plataforma la rechaza en una sesión de jugador, sin importar qué binding haga la llamada; Unreal recibe la notificación entregada. Consulta Access & Roles.

Esto lo llama otro actor

La llamada existe en Unity. Enviar una notificación requiere autoridad fn/adm, así que la plataforma la rechaza en una sesión de jugador, sin importar qué binding haga la llamada; Unity recibe la notificación entregada. Consulta Access & Roles.

La moderación se hace con hooks, con el mismo contrato que en todas partes:

Un hook previo al envío: las groserías se rechazan antes de llegar a ningún lado
[Before(Messaging.Send)]
public static Verdict Filter(OutgoingMessage m) =>
    Profanity.Hits(m.Text) ? Hook.Reject("filtered") : Hook.Continue(m);
export const filter = before(Messaging.send, (m: OutgoingMessage) =>
  Profanity.hits(m.text) ? Hook.reject('filtered') : Hook.continue(m));
@before(messaging.send)
def filter_message(m: OutgoingMessage) -> Verdict:
    return Hook.reject("filtered") if profanity.hits(m.text) else Hook.continue_(m)
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

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unreal se suscribe a los eventos que genera. Están previstas funciones escritas en el propio motor: grafos de Blueprint y C++ (traducido a un script que valida la plataforma). Ambas se analizan y se suben como cualquier declaración, y se siguen ejecutando en la plataforma.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; Unity se suscribe a los eventos que genera.

El modelo

Qué declara un tipo de conversación.

DeclaraciónQué es
el gruposu lista de participantes: un participante es un actor, exactamente igual que en los grupos
vinculación a un ciclo de vidaopcional, al de otra entidad: así el chat de una sala desaparece junto con la sala

Dónde está el límite entre el sobre y la carga útil.

ParteDe quién es
sobrede la plataforma: el autor, la conversación, el momento según el reloj declarado
carga útildel estudio: se declara como un tipo de mensaje con campos tipados y aparece como su propia interfaz de envío, no como una bolsa sin tipos

Tres cosas que tiene este módulo y que un evento simple no tiene: el orden dentro de una conversación, el período de retención y el objeto de la moderación. Ninguna de ellas está en Events, y por eso el chat no es “un evento con historial”.

Las tres máquinas de estados.

MáquinaEstados
una conversacióncreated → active → closed
un mensajesent → published | rejected by the filter, y después editado o eliminado, siempre de forma visible
una notificacióncreated → queued → delivered | expired

Lo que se cumple en todo mensaje.

SiempreQué es
orden dentro de una conversaciónes estable y está declarado. El orden entre conversaciones no está garantizado
edición y eliminaciónson visibles: un mensaje nunca desaparece en silencio, así que el historial de un cliente no puede separarse del historial del servidor sin que se note
historialson los propios mensajes: con un período de retención declarado, y se leen por páginas con un cursor a partir de una posición
la retención dura más que la ventana de denunciasel período no es más corto que el plazo para atender una denuncia: la denuncia llega después del mensaje, y si el mensaje ya no existe, no queda nada que revisar
estado de lecturaes una posición, no un flag: una posición por actor en cada conversación, y marcar como leído es monótono: la posición nunca retrocede, así que una llamada repetida no puede deshacer el avance. El número de no leídos se deriva de esa posición; no es un contador aparte
envíoes idempotente por clave: dos llamadas son dos líneas de diálogo, así que la clave es lo que hace seguro un reintento
bloqueoes un predicado de entrega, no un rechazo al enviar: al remitente no se le avisa, porque un rechazo revelaría el bloqueo. El estado en sí vive en Social
el remitente arma la carga útilla plataforma no lee los datos del destinatario para completar tu texto. El idioma del destinatario puede ser una claim de contexto declarada que viaja hasta el punto de extensión, así que sustituir y traducir es trabajo del hook: el único lugar que conoce tanto al destinatario como su idioma
la ruta de entregano forma parte del contrato: push, dentro de la app u otra cosa es una decisión de enrutamiento, no una promesa
entregaes visible dentro de los límites declarados: “en cola” siempre; lo que venga después, hasta donde la ruta pueda informarlo

Cada punto de extensión indica qué tipo le entrega al hook: antes de publicar, el mensaje saliente; después, el mensaje publicado. Un filtro puede corregir el contenido que recibió — enmascarar una palabra es una corrección —, pero nunca el remitente ni la conversación.

Errores

  • Una conversación que no existe o está oculta, y un actor que no es participante, reciben not found: así, un rechazo nunca revela una conversación en la que no estás.
  • Una conversación cerrada es un conflicto.
  • Sin permiso para escribir en este tipo, la respuesta es forbidden, y no tiene sentido repetir.
  • Que el filtro rechace un mensaje es un veredicto, no una negativa. La llamada se ejecutó, el contenido se evaluó, la decisión es negativa y el motivo es un valor declarado. Por eso se distingue de una negativa por permisos, y por eso lo que hagas después depende del motivo.
  • Si el filtro no está disponible, la respuesta es unavailable y vale la pena reintentar con backoff; mientras tanto no se publicó nada.
  • Un tipo de mensaje no declarado para esta conversación y un mensaje demasiado grande son rechazos de validación; el contenido nunca se trunca en silencio.
  • Si se supera la frecuencia de envío, la respuesta cae en la categoría de límite de frecuencia e indica un plazo.
  • Editar el mensaje de otra persona recibe forbidden.
  • Una notificación vencida es un conflicto: envía una nueva.

Límites

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

  • Tamaño del mensaje, y adjuntos con su tamaño: el envío se rechaza como error de validación; nunca se trunca. Los archivos en sí están en Files & UGC.
  • Frecuencia de envío por actor: un límite de frecuencia con plazo.
  • Profundidad del historial: pasado el período, el mensaje sale de la retención con un evento, en lugar de desaparecer sin avisar.
  • Conversaciones por actor: unirse a otra se rechaza como conflicto.
  • Notificaciones en cola por actor: se rechaza la nueva, y está prohibido expulsar: una notificación descartada en silencio se vería igual que una que nunca se envió.
  • Vencimiento de una notificación: expired, con un evento.
  • Participantes en una conversación es un límite de Groups, y bloqueos por actor, uno de Social; ninguno se repite aquí.