Cette page n’est pas encore traduite : la version anglaise est affichée.
Messaging
Salles, groupes, joueurs : un seul modèle d’adressage pour le chat et les notifications. Les messages arrivent dans une conversation ; une conversation est un canal auquel s’ajoutent l’historique, la modération et la livraison hors bande.
Quand l’utiliser
- Les joueurs discutent (chat de salle, canaux de guilde, messages privés) en s’appuyant sur l’adressage que vous avez déjà : salle (room), groupe, joueur.
- Les joueurs hors ligne doivent quand même être prévenus : des notifications construites à partir de modèles et planifiables sont livrées hors bande, en notifications push.
- La modération doit intervenir avant la livraison : un hook avant envoi filtre ou rejette, et la plateforme fait respecter partout la mise en sourdine et le blocage.
- Les joueurs qui reviennent doivent rattraper la conversation :
History(take: 50)la parcourt page par page au lancement suivant. - Inutile quand la charge utile (payload) est de l’état de jeu et non une conversation. Les champs synchronisés de Data & Subscriptions et les canaux Core le diffusent déjà.
Qui fait quoi
| Acteur | Ce qu’il fait sur cette page |
|---|---|
player | envoie et reçoit des messages ; lit l’historique ; met en sourdine ou bloque |
moderator | filtre et masque des messages, bannit des termes |
backend-service | envoie ou planifie des notifications construites à partir de modèles |
En bref
Send par cible d’adressage — salle, guilde, message direct — plus l’abonnement et l’historique// 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)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.
// 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 message structuré est un événement déclaré, et la conversation le transporte par son nom, sans classe de charge utile à construire au moment de l’appel :
RallyCall déclaré une seule fois ; la conversation de guilde l’envoie par son nom[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")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.
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");Le message déclaré arrive typé dans le même abonnement : un client qui connaît RallyCall reçoit des champs, pas un blob.
Les notifications sont hors bande, construites à partir de modèles et planifiables ; elles s’envoient avec l’autorité fn ou adm, jamais depuis une session joueur :
raid-starts construite à partir d’un modèle, envoyée depuis une fonction cloud et livrée hors bande// 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})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.
L’appel existe dans Unreal. Envoyer une notification exige l’autorité fn/adm : la plateforme le refuse donc sur une session joueur, quel que soit le binding qui passe l’appel ; Unreal reçoit la notification livrée. Voir Access & Roles.
L’appel existe dans Unity. Envoyer une notification exige l’autorité fn/adm : la plateforme le refuse donc sur une session joueur, quel que soit le binding qui passe l’appel ; Unity reçoit la notification livrée. Voir Access & Roles.
La modération passe par des hooks, avec le même contrat que partout ailleurs :
[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)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.
Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — Unreal s’abonne aux événements qu’il produit. Des fonctions écrites côté moteur sont prévues : graphes Blueprint et C++ (traduit vers un langage de script validé par la plateforme). Les unes comme les autres seront analysées et publiées comme n’importe quelle déclaration ; l’exécution reste sur la plateforme.
Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — Unity s’abonne aux événements qu’il produit.
Le modèle
Ce que déclare un type de conversation.
| Déclaration | Ce que c’est |
|---|---|
le groupe | sa composition — un participant est un acteur, exactement comme dans Groups |
rattachement à une durée de vie | facultativement celle d’une autre entité, pour que le chat d’une salle disparaisse avec sa salle |
Où passe la frontière entre l’enveloppe et la charge utile.
| Partie | À qui elle appartient |
|---|---|
enveloppe | à la plateforme : l’auteur, la conversation, l’instant selon l’horloge déclarée |
charge utile | au studio : déclarée comme un type de message aux champs typés, elle apparaît comme sa propre interface d’envoi plutôt que comme un sac non typé |
Ce module possède trois choses qu’un simple événement n’a pas : l’ordre au sein d’une conversation, la durée de rétention et l’objet de la modération. Aucune ne figure dans les événements, et c’est pourquoi le chat n’est pas « un événement avec un historique ».
Les trois machines à états.
| Machine | États |
|---|---|
| une conversation | created → active → closed |
| un message | sent → published | rejected by the filter, puis modifié ou supprimé, de façon observable |
| une notification | created → queued → delivered | expired |
Ce qui vaut pour tout message.
| Toujours | Ce que c’est |
|---|---|
ordre au sein d'une conversation | est stable et déclaré. L’ordre entre conversations n’est pas garanti |
modification et suppression | sont observables : un message ne disparaît jamais en silence, si bien que l’historique d’un client ne peut pas diverger de celui du serveur sans qu’on s’en aperçoive |
historique | ce sont les messages eux-mêmes, avec une durée de rétention déclarée, lus par pages, avec un curseur, à partir d’une position |
la rétention dure plus longtemps que le délai de signalement | la durée n’est jamais inférieure au temps prévu pour traiter un signalement : le signalement arrive après le message, et un message qui n’existe plus ne laisse rien à traiter |
état de lecture | c’est une position, pas un indicateur : une position par acteur et par conversation, et le marquage comme lu est monotone — la position ne recule jamais, si bien qu’un appel répété ne peut pas annuler la progression. Le nombre de non-lus est dérivé de cette position, et non un compteur à part |
envoi | est idempotent par clé : deux appels, ce sont deux répliques du dialogue, et c’est donc la clé qui rend une nouvelle tentative sûre |
blocage | est un prédicat de livraison, pas un refus d’envoi : l’expéditeur n’est pas prévenu, car un refus révélerait le blocage. L’état lui-même vit dans Social |
l'expéditeur compose la charge utile | la plateforme ne lit pas les données du destinataire pour compléter votre texte. La langue du destinataire peut être une claim de contexte déclarée qui parvient jusqu’au point d’extension : la substitution et la traduction sont donc le travail du hook — le seul endroit qui connaît à la fois le destinataire et sa langue |
la voie de livraison | ne fait pas partie du contrat : push, notification dans l’application ou autre chose, c’est une décision de routage, pas une promesse |
livraison | est observable dans des limites déclarées : « en file d’attente » toujours ; au-delà, dans la mesure où la voie de livraison peut le signaler |
Chaque point d’extension nomme le type qu’il transmet au hook : le message sortant avant publication, le message publié après. Un filtre peut corriger le contenu qu’on lui a transmis — masquer un mot est une correction — mais jamais l’expéditeur ni la conversation.
Erreurs
- Une conversation inexistante ou masquée, et un acteur qui n’y participe pas, reçoivent tous deux not found : un refus ne révèle ainsi jamais une conversation dont vous ne faites pas partie.
- Une conversation fermée est un conflit.
- L’absence de permission d’écrire dans ce type répond forbidden, et il est inutile de réessayer.
- Un rejet par le filtre est un verdict, pas un refus. L’appel a été exécuté, le contenu a été examiné, la décision est négative et le motif est une valeur déclarée — c’est pourquoi on le distingue d’un refus pour cause de permissions, et pourquoi la suite à donner dépend du motif.
- Un filtre indisponible répond unavailable, et il vaut la peine de réessayer avec un délai croissant (backoff) — mais rien n’a été publié entre-temps.
- Un type de message non déclaré pour cette conversation, et un message trop volumineux, sont des refus de validation ; le contenu n’est jamais tronqué en silence.
- Un dépassement de la fréquence d’envoi reçoit une réponse de la catégorie limitation de débit (rate limit), avec un délai.
- Modifier le message de quelqu’un d’autre répond forbidden.
- Une notification expirée est un conflit : envoyez-en une nouvelle.
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é.
- Taille des messages, et pièces jointes avec leur taille : l’envoi est refusé comme échec de validation, jamais tronqué. Les fichiers eux-mêmes relèvent de Files & UGC.
- Fréquence d’envoi par acteur : une limitation de débit avec un délai.
- Profondeur de l’historique : passé la durée prévue, un message est évincé de la rétention avec un événement, au lieu de disparaître discrètement.
- Conversations par acteur : en rejoindre une de plus est refusé comme conflit.
- Notifications en file d’attente par acteur : une nouvelle notification est refusée, et l’éviction est interdite : une notification abandonnée en silence ressemblerait à une notification jamais envoyée.
- Expiration d’une notification :
expired, avec un événement. - Participants à une conversation : c’est la limite de Groups, et blocages par acteur, celle de Social ; aucune n’est répétée ici.