Цю сторінку ще не перекладено — показано англійську версію.
Messaging
Кімнати, групи, гравці: одна модель адресації для чату й сповіщень. Повідомлення надходять у розмову, а розмова — це канал, до якого додано історію, модерацію й доставку поза ігровим з’єднанням.
Коли це потрібно
- Гравці спілкуються — у чаті кімнати, каналах гільдії, особистих повідомленнях — через адресацію, яку ви вже маєте: кімната, група, гравець.
- Гравці поза мережею мають усе одно отримувати звістки: сповіщення за шаблонами можна запланувати, і вони надходять як push поза ігровим з’єднанням.
- Модерація має спрацьовувати до доставки: хук перед надсиланням фільтрує або відхиляє повідомлення, а приглушення й блокування платформа забезпечує всюди.
- Гравцям, які повертаються в гру, треба наздогнати розмову (conversation):
History(take: 50)посторінково завантажує її під час наступного запуску. - Не використовуйте його, якщо передаєте ігровий стан, а не листування. Його й так розсилають синхронізовані поля з Data & Subscriptions і канали Core.
Хто що робить
| Актор | Що робить на цій сторінці |
|---|---|
player | надсилає й отримує повідомлення; читає історію; приглушує або блокує |
moderator | фільтрує й приховує повідомлення, забороняє слова |
backend-service | надсилає сповіщення за шаблонами або планує їх надсилання |
Коротко
Send на кожного адресата — кімнату, гільдію, особисте повідомлення, — а також підписка й історія// 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)Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
// 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);Стр уктуроване повідомлення — це оголошена подія (event). Розмова передає її за назвою, тож у місці виклику не треба створювати клас корисного навантаження:
RallyCall оголошено один раз, а розмова гільдії надсилає його за назвою[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")Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
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");Оголошене повідомлення приходить у тій самій підписці (subscription) уже типізованим, тож клієнт, який знає RallyCall, отримує поля, а не сирі дані.
Сповіщення доставляють поза ігровим з’єднанням, за шаблонами й за розкладом. Надсилають їх із повноваженнями fn або adm і ніколи — із сесії гравця:
raid-starts за шаблоном: його надсилає хмарна функція, а доставка йде поза ігровим з’єднанням// 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})Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
Виклик в Unreal є. Але для надсилання сповіщення потрібні повноваження fn/adm, тож платформа відхиляє його із сесії гравця, хоч би з якого біндингу його зробили. Unreal отримує вже доставлене сповіщення. Див. Access & Roles.
Виклик в Unity є. Але для надсилання сповіщення потрібні повноваження fn/adm, тож платформа відхиляє його із сесії гравця, хоч би з якого біндингу його зробили. Unity отримує вже доставлене сповіщення. Див. Access & Roles.
Модерація — це хуки з тим самим контрактом, що й усюди:
[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)Як оголошувати через атрибути в Go, поки не вирішено (O-1). Приклади з’являться, коли оберуть спосіб запису.
Хук — це хмарна функція: він виконується на платформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а Unreal підписується на події, які він породжує. Функції, написані в самому рушії, заплановано: графи Blueprint і C++ (з трансляцією в скриптовий формат, який перевіряє платформа). Їх аналізують і надсилають на платформу так само, як будь-яке оголошення, а виконуються вони все одно на платформі.
Хук — це хмарна функція: він виконується на п латформі, а не в рушії. Пишіть його на C#, TypeScript або Python, а Unity підписується на події, які він породжує.
Модель
Що оголошує тип розмови.
| Оголошення | Що це |
|---|---|
група | її склад (roster): учасник — це актор, так само як у групах |
прив’язка до часу життя | необов’язкова, до часу життя іншої сутності (entity): так чат кімнати зникає разом із кімнатою |
Де проходить межа між конвертом і корисним навантаженням.
| Частина | Чия вона |
|---|---|
конверт | платформи: автор, розмова й момент за оголошеним годинником |
корисне навантаження | студії: його оголошують як тип повідомлення з типізованими полями, і воно отримує власний інтерфейс надсилання, а не стає нетипізованим мішком даних |
Три речі, які має цей модуль і не має звичайна подія: порядок усередині розмови, строк зберігання й предмет модерації. Нічого з цього немає в Events, тому чат — це не «подія з історією».
Три машини станів.
| Машина | Стани |
|---|---|
| розмова | created → active → closed |
| повідомлення | sent → published | rejected by the filter, а потім редагування чи видалення, які завжди видно |
| сповіщення | created → queued → delivered | expired |
Що справджується для кожного повідомлення.
| Завжди | Що це |
|---|---|
порядок усередині розмови | стабільний і оголошений. Порядку між розмовами платформа не обіцяє |
редагування й видалення | завжди видно: повідомлення ніколи не зникає мовчки, тож історія на клієнті не може непомітно розійтися із серверною |
історія | — це самі повідомлення. Вона має оголошений строк зберігання (retention), а читають її посторінково за курсором, починаючи з певної позиції |
зберігання довше за вікно скарг | строк зберігання не коротший за час, відведений на розгляд скарги. Скарга надходить після повідомлення, а якщо повідомлення вже немає, то й розглядати нічого |
стан прочитання | — це позиція, а не прапорець: одна позиція на актора в кожній розмові. Позначка про прочитання монотонна: позиція ніколи не зменшується, тож повторний виклик не скасує прогресу. Кількість непрочитаних — похідна від цієї позиції, а не окремий лічильник |
надсилання | ідемпотентне за ключем: два виклики — це дві репліки в розмові, тож безпечним повтор робить саме ключ |
блокування | — предикат доставки, а не заборона надсилати. Відправникові нічого не повідомляють, бо відмова видала б блокування. Сам стан живе в Social |
навантаження формує відправник | платформа не читає даних одержувача, щоб підставити їх у ваш текст. Локаль одержувача може бути оголошеним твердженням контексту, яке доходить до точки розширення, тож підстановка й переклад — робота хука: лише він знає і одержувача, і його локаль |
маршрут доставки | не входить у контракт: push, сповіщення в застосунку чи щось інше — це рішення маршрутизації, а не обіцянка |
доставка | видно в оголошених межах: «queued» — завжди, а далі — наскільки може повідомити маршрут |
Кожна точка розширення називає тип, який передає хуку: перед публікацією — вихідне повідомлення, після неї — опубліковане. Фільтр може виправити вміст, який отримав (замаскувати слово — це виправлення), але ніколи — відправника чи розмову.
Помилки
- Розмова, якої не існує або яку приховано, і актор, який не є учасником, отримують not found, тож відмова ніколи не видасть розмову, у якій вас немає.
- Закрита розмова — конфлікт.
- Немає дозволу писати в розмови цього типу — відповідь forbidden, і повторювати марно.
- Відхилення фільтром — це вердикт, а не відмова. Виклик виконано, вміст розглянуто, рішення негативне, а причина — оголошене значення. Тому його можна відрізнити від відмови через дозволи, і тому подальші дії залежать від причини.
- Якщо фільтр недоступний, відповідь — unavailable, і спробу варто повторювати зі зростаючою паузою (backoff). Але тим часом нічого не опубліковано.
- Тип повідомлення, не оголошений для цієї розмови, і завелике повідомлення — відмови валідації; вміст ніколи не обрізають мовчки.
- Перевищено частоту надсилання — відповідь із категорії rate limit і час, після якого можна спробувати знову.
- Редагування чужого повідомлення дає forbidden.
- Сповіщення з вичерпаним строком дії — конфлікт: надішліть нове.
Обмеження
Конкретні числа з’являться разом із розділом про ліміти платформи, а що відбувається на кожній стелі, визначено вже зараз.
- Розмір повідомлення, а також вкладення та їхній розмір: надсилання відхиляють як помилку валідації, але ніколи не обрізають. Самі файли описано в Files & UGC.
- Частота надсилання на актора: обмеження частоти з часом, після якого можна повторити.
- Глибина історії: коли строк минає, повідомлення вилучають зі зберігання з подією, а не дають йому тихо зникнути.
- Розмови на актора: приєднання до ще однієї відхиляють як конфлікт.
- Сповіщення в черзі на актора: нове відхиляють, і витіснення заборонене: мовчки відкинуте сповіщення виглядало б так, ніби його ніколи не надсилали.
- Строк дії сповіщення:
expired, з подією. - Кількість учасників розмови обмежує Groups, а кількість блокувань на актора — Social. Тут ці ліміти не повторюємо.