Перейти до основного вмісту
Сервіси навколо гри

Цю сторінку ще не перекладено — показано англійську версію.

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

Як оголошувати через атрибути в 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.
Unity C# is the same C# API here — працює в Unity без змін, зі згенерованими типами
// 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

Як оголошувати через атрибути в 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.
Unity C# is the same C# API here — те саме оголошення й виклик компілюються в Unity на базовій версії 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");

Оголошене повідомлення приходить у тій самій підписці (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

Як оголошувати через атрибути в 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

Як оголошувати через атрибути в 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. Тут ці ліміти не повторюємо.