conversations

Conversations (Threaded Mailbox)

A conversation-style player mailbox. Threads have a subject and several participants, each with their own copy and read state: start one by player names (unknown names are reported back), reply, forward a thread to new recipients, list newest activity first with paging, open with only-newer-posts refresh, mark posts or whole threads read or unread. Players create, rename and delete their own folders (the inbox is built in), move and delete threads in batches, keep favourites (recent recipients are kept for them), block players (optionally hiding threads with any or all participants blocked) and search names by prefix. Game code posts system threads to any set of accounts under a sender label; posts can be reported for moderators. Push on arrival, unread counts per folder, every limit an option, durable across restarts. Names are the IConversationDirectory seam.

Category Social Seams 3 Services 1 Options 1 Wire ids 23 Unity types 1
Server
services.AddCrossplayConversations();
Unity package
com.crossplay.conversations
Depends on
core

Providers you can swap 3

Infrastructure seams. Crossplay ships a working implementation of each — replacing one changes where data lives or how it moves, never a game rule.

IConversationClock provider

The clock posts are stamped by (a seam so tests control time).

  • long UtcNowMs { get; }

    Now, unix ms UTC.

IConversationDirectory provider

Player names to account ids and back - how recipients typed by name are found and how senders are named. The default uses account usernames; a game that names players otherwise (characters, display names) registers its own BEFORE AddCrossplayConversations. Search runs over the names the piece has seen (every player who used conversations or was written to), so a name directory needs no search of its own.

  • string DisplayName(long accountId)

    The name to show for an account (a stable fallback such as "#42" when it has none).

  • IReadOnlyList<string> Search(string prefix, int max)

    Player names starting with prefix (case-insensitive), at most max - back it with the game's own account index so a "find player" box finds anyone. Return null (the default) to use the names the piece has seen and been told about (NoteName). Defaulted so adding it breaks no existing directory (rule 25).

  • bool TryResolve(string name, out long accountId)

    The account a player name belongs to. False for an unknown name - never throw.

IConversationStore provider

Where conversations are kept. The default is in memory; persistence composes a document-backed one.

  • void AddPost(ConversationPostRecord post)

    Adds a post.

  • void AddReport(ConversationReportRecord report)

    Adds a report.

  • void DeleteCopy(long accountId, long threadId)

    Removes a player's copy.

  • void DeleteThread(long threadId)

    Removes a thread and its posts.

  • ConversationBookRecord LoadBook(long accountId)

    One player's book, or null.

  • IReadOnlyList<ConversationCopyRecord> LoadCopies(long accountId)

    Every copy one player holds.

  • ConversationCopyRecord LoadCopy(long accountId, long threadId)

    One player's copy of a thread, or null.

  • IReadOnlyList<ConversationNameRecord> LoadNames()

    Every name seen.

  • IReadOnlyList<ConversationPostRecord> LoadPosts(long threadId)

    A thread's posts (any order).

  • ConversationThreadRecord LoadThread(long threadId)

    One thread, or null.

  • long NextId()

    The next id (threads, posts and reports share one sequence).

  • IReadOnlyList<ConversationReportRecord> RecentReports(int count)

    The newest reports, newest first.

  • void SaveBook(ConversationBookRecord book)

    Saves a player's book.

  • void SaveCopy(ConversationCopyRecord copy)

    Saves a player's copy of a thread.

  • void SaveName(ConversationNameRecord name)

    Saves a name seen for an account.

  • void SaveThread(ConversationThreadRecord thread)

    Saves a thread.

Services you call 1

Crossplay implements these. Resolve them from DI and call them from your own systems.

IConversationService

Conversations: a threaded player mailbox. A thread has a subject and several participants; each participant holds their own copy (folder, unread posts) of the shared posts. Players start threads by name, reply, forward a thread to new recipients, file threads in their own folders, keep favourites (recent recipients are kept for them), block players, search names and report posts. Game code posts system threads to any set of accounts under a sender label. Bounded by ConversationOptions and durable through IConversationStore.

  • void NoteName(long accountId, string name)

    Tells the piece an account's name - at account creation, login or rename - so name search finds the player before anyone has written to them. Durable (stored) and live at once; a later call replaces the name. Empty or over-long names and account 0 are ignored. Defaulted so adding it breaks no existing implementation.

  • long Post(IReadOnlyList<long> recipientAccountIds, string systemLabel, string subject, string body)

    Posts a system thread (no reply possible) to every recipient (duplicates and 0 ignored) under systemLabel - an area announcement, a server notice. Returns the thread id (0 = no recipients). Throws ArgumentException when a text or the recipient count breaks a limit - that is a game bug.

  • IReadOnlyList<ConversationReportRecord> RecentReports(int count)

    The newest reports, newest first - for a moderation tool.

  • event Action<ConversationReportRecord> Reported

    Raised when a player reports a post.

  • event Action<long, long[], long, long> Sent

    Raised after a PLAYER send or forward was delivered - never for Post system threads and never for a refused send: (senderAccountId, recipientAccountIds, threadId, postId). The recipients are every other participant of the thread the post went into (a reply reaches all of them; a block only keeps it out of the blocker's unread list), and postId is the sender's post (for a forward without a note, the last copied post). The hook for a "sent a message to another player" quest or achievement counter. Defaulted to an event that never fires (rule 25), so an existing implementation keeps compiling; the shipped ConversationService raises it.

Configuration 1

Every tunable lives in an options object — there are no magic numbers to hunt for.

ConversationOptions

Conversations limits, bound from Crossplay:Conversations.

  • uint ArrivalReplyFlag { get; set; }

    A Core reply flag raised for a recipient when a post arrives (0 = none) - for a client that polls instead of listening for the push.

  • int MaxBlocked { get; set; }

    Most players one player may block.

  • int MaxBodyLength { get; set; }

    Longest post body, in characters.

  • int MaxFavourites { get; set; }

    Most favourites a player may keep.

  • int MaxFolderNameLength { get; set; }

    Longest folder name, in characters.

  • int MaxFolders { get; set; }

    Most folders a player may create (the inbox not counted).

  • int MaxIdsPerRequest { get; set; }

    Most thread ids in one mark, move or delete.

  • int MaxNameLength { get; set; }

    Longest player name accepted from the wire, in characters.

  • int MaxPostsPerThread { get; set; }

    Most posts one thread may hold; a reply past it is refused (Full).

  • int MaxRecent { get; set; }

    How many recent recipients are kept.

  • int MaxRecipients { get; set; }

    Most recipients a player may name in one send or forward.

  • int MaxReportSummaryLength { get; set; }

    Longest report summary, in characters.

  • int MaxSearchResults { get; set; }

    Most names one search returns.

  • int MaxSubjectLength { get; set; }

    Longest subject, in characters.

  • int MaxSystemLabelLength { get; set; }

    Longest system sender label, in characters.

  • int MaxSystemRecipients { get; set; }

    Most recipients one system post may go to (game code; a larger set is a game bug and throws).

  • int MaxThreadsPerAccount { get; set; }

    Most threads one player holds; past it the thread with the oldest activity is dropped from that player's copy.

  • int MinSearchLength { get; set; }

    Shortest search prefix, in characters.

  • int PageSize { get; set; }

    Threads per list page.

  • bool PushOnArrival { get; set; }

    Push ConversationArrived to online recipients.

  • double ReportCooldownSeconds { get; set; }

    Least time between two reports by one player, in seconds.

  • double SendCooldownSeconds { get; set; }

    Least time between two sends or forwards by one player, in seconds.

Wire messages 23

The protocol this piece speaks. Ids are allocated per piece so they can never collide.

7701 ConversationSendRequest

Client -> server: start a thread (ThreadId = 0) or reply into one (recipients ignored).

7702 ConversationSendResult

Server -> client: the answer to a send or a forward.

7703 ConversationForwardRequest

Client -> server: forward a thread to new recipients - a new thread with copies of its posts, and Body as my note when it is not empty.

7704 ConversationListRequest

Client -> server: a page of my threads, newest activity first.

7705 ConversationList

Server -> client: a page of thread headers.

7706 ConversationOpenRequest

Client -> server: open a thread.

7707 ConversationContent

Server -> client: a thread's header and posts, oldest first.

7708 ConversationMarkRequest

Client -> server: mark whole threads read, or unread (unread marks the newest post).

7709 ConversationMarkPostRequest

Client -> server: mark one post read or unread.

7710 ConversationMoveRequest

Client -> server: move threads to a folder.

7711 ConversationDeleteRequest

Client -> server: delete my copy of threads (a later reply brings a thread back to the inbox).

7712 ConversationResult

The answer to every change that returns no data.

7713 ConversationFoldersRequest

Client -> server: my folders with their counts.

7714 ConversationFolderEditRequest

Client -> server: create, rename or delete a folder. Answered with the folders as they are after it.

7715 ConversationFolders

Server -> client: my folders, the inbox first.

7716 ConversationContactsRequest

Client -> server: my favourites, recent recipients and block list.

7717 ConversationContacts

Server -> client: my contacts, by name.

7718 ConversationContactEditRequest

Client -> server: add a player to, or remove one from, a contact list.

7719 ConversationBlockModeRequest

Client -> server: how threads with blocked participants are shown to me.

7720 ConversationSearchRequest

Client -> server: player names starting with a prefix (debounce on the client - it is rate limited).

7721 ConversationNames

Server -> client: player names.

7722 ConversationReportRequest

Client -> server: report a post for moderators.

7723 ConversationArrived

Server -> client (push): a post arrived in one of your threads.

Unity services you inject 1

Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.

IConversationClient

Client SDK for Conversations - a threaded player mailbox. Every verb is one awaitable call (Core's request/reply helper - a call that gets no answer completes as a timeout, it never throws), and Arrived fires when the server pushes a new post. Naming the inbox, laying out threads and debouncing the name search are the game's. Pure SDK, no UI.

  • event Action<ConversationHeader, long> Arrived

    A post arrived in one of my threads (the thread's header as it is now, and the new post's id).

  • UniTask<ConversationSendResult> StartAsync(string subject, string body, string[] recipientNames, CancellationToken ct = default)

    Starts a thread to players by name. Unknown names come back in the result; the rest are sent to.

  • UniTask<ConversationSendResult> ReplyAsync(long threadId, string body, CancellationToken ct = default)

    Replies into a thread.

  • UniTask<ConversationSendResult> ForwardAsync(long threadId, string[] recipientNames, string note = "", CancellationToken ct = default)

    Forwards a thread to new recipients (a new thread), with an optional note.

  • UniTask<ConversationList> ListAsync(long folderId = ConversationFolderIds.Inbox, bool unreadOnly = false, long beforeUtcMs = 0, long beforeThreadId = 0, CancellationToken ct = default)

    A page of my threads in a folder, newest activity first. Pass the last header's LastPostUtcMs and ThreadId for the next page (0, 0 = from the newest).

  • UniTask<ConversationContent> OpenAsync(long threadId, long afterPostId = 0, bool markRead = false, CancellationToken ct = default)

    Opens a thread: posts newer than afterPostId (0 = all), optionally marking them read.

  • UniTask<ConversationResult> MarkAsync(long[] threadIds, bool read, CancellationToken ct = default)

    Marks whole threads read or unread.

  • UniTask<ConversationResult> MarkPostAsync(long threadId, long postId, bool read, CancellationToken ct = default)

    Marks one post read or unread.

  • UniTask<ConversationResult> MoveAsync(long[] threadIds, long folderId, CancellationToken ct = default)

    Moves threads to a folder.

  • UniTask<ConversationResult> DeleteAsync(long[] threadIds, CancellationToken ct = default)

    Deletes my copy of threads.

  • UniTask<ConversationFolders> FoldersAsync(CancellationToken ct = default)

    My folders with thread and unread counts, the inbox first.

  • UniTask<ConversationFolders> EditFolderAsync(ConversationFolderOp op, long folderId, string name, CancellationToken ct = default)

    Creates, renames or deletes a folder; answered with the folders after it.

  • UniTask<ConversationContacts> ContactsAsync(CancellationToken ct = default)

    My favourites, recent recipients, block list and block mode.

  • UniTask<ConversationResult> EditContactAsync(ConversationContactList list, string name, bool add, CancellationToken ct = default)

    Adds a player to, or removes one from, my favourites or block list.

  • UniTask<ConversationResult> SetBlockModeAsync(ConversationBlockMode mode, CancellationToken ct = default)

    Sets how threads with blocked participants are shown to me.

  • UniTask<ConversationNames> SearchAsync(string prefix, CancellationToken ct = default)

    Player names starting with a prefix (debounce before calling - the server rate limits it).

  • UniTask<ConversationResult> ReportAsync(long threadId, long postId, byte reason, string summary, CancellationToken ct = default)

    Reports a post for moderators (the reason is the game's enum value).