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é
| Actor | Qué hace en esta página |
|---|---|
player | envía y recibe mensajes; lee el historial; silencia o bloquea |
moderator | filtra y oculta mensajes, prohíbe términos |
backend-service | envía o programa notificaciones con plantilla |
En resumen
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)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.// 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")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.[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:
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})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.
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.
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:
[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)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.
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.
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ón | Qué es |
|---|---|
el grupo | su lista de participantes: un participante es un actor, exactamente igual que en los grupos |
vinculación a un ciclo de vida | opcional, 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.
| Parte | De quién es |
|---|---|
sobre | de la plataforma: el autor, la conversación, el momento según el reloj declarado |
carga útil | del 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áquina | Estados |
|---|---|
| una conversación | created → active → closed |
| un mensaje | sent → published | rejected by the filter, y después editado o eliminado, siempre de forma visible |
| una notificación | created → queued → delivered | expired |
Lo que se cumple en todo mensaje.
| Siempre | Qué es |
|---|---|
orden dentro de una conversación | es estable y está declarado. El orden entre conversaciones no está garantizado |
edición y eliminación | son 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 |
historial | son 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 denuncias | el 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 lectura | es 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ío | es idempotente por clave: dos llamadas son dos líneas de diálogo, así que la clave es lo que hace seguro un reintento |
bloqueo | es 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 útil | la 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 entrega | no forma parte del contrato: push, dentro de la app u otra cosa es una decisión de enrutamiento, no una promesa |
entrega | es 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í.