Social

Chat

Channels and whispers over Core, with profanity masking and rate caps.

Server
services.AddCrossplayChat();
Unity package
com.crossplay.chat
Depends on
core

Providers you can swap 2

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

IChatClock provider

Wall-clock seam (unix UTC ms) so mute-expiry math is testable — implement it to feed a fake clock in tests. Piece-local on purpose (Chat does not depend on any shared clock abstraction). The default SystemChatClock reads the system clock; override before AddCrossplayChat.

  • long UtcNowMs { get; }

    The current time as Unix UTC milliseconds.

IChatMuteStore provider

Mute-persistence SPI — the seam that decides where mutes are stored. The framework ships an in-memory default (InMemoryChatMuteStore); the persistence package's AddCrossplayPersistentStores swaps in a document-backed store so mutes survive restarts.

  • IReadOnlyList<ChatMuteRecord> All()

    Every stored mute — read once at service construction to warm the cache. Expired entries may still be present (the service prunes them on load); no ordering is required.

  • void Put(ChatMuteRecord record)

    Inserts or replaces the mute for AccountId.

  • bool Remove(long accountId)

    Deletes the account's mute. Returns false if the account had none.

Services you call 2

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

IChatMuteService

Server-enforced chat mutes — the enforcement check the chat send path runs on every message, plus the mute / unmute / list operations a moderation surface drives (Hosting exposes those as /admin/mute etc.). The piece deliberately ships no wire message for muting, so a client can never mute another player: muting is a server/admin authority only.

  • IReadOnlyList<ChatMuteRecord> Active(int count)

    The active (unexpired) mutes, soonest-to-expire first, capped at count entries (for an admin listing).

  • bool IsMuted(long accountId)

    True while the account has an unexpired mute; expired entries are pruned on access.

  • ChatMuteRecord Mute(long accountId, string name, int seconds)

    Mutes the account for seconds from now; re-muting replaces the previous record. Returns the stored record.

  • bool Unmute(long accountId)

    Lifts the account's mute. Returns false when the account was not muted.

IChatService

Server-side entry point for System-channel messages — the only way to put text on the System channel (clients cannot post to it). A game or another piece injects this to surface announcements ("maintenance in 5 min", "X joined the server").

  • void BroadcastSystem(string text)

    Sends a System-channel message to every online session, across all cluster nodes.

  • void SendSystem(ISession session, string text)

    Sends a System-channel message to one session.

Configuration 1

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

ChatOptions

Configurable chat rules — every tunable for the Chat piece. Defaults live here (never inline in a handler), so a game overrides only what it needs via the configure callback of AddCrossplayChat. All values have working defaults; the object is safe to use unconfigured.

  • int DefaultMuteSeconds { get; set; }

    Mute length (seconds) the /admin/mute admin call applies when no seconds argument is given. Default 300 (5 minutes).

  • int MaxChannelIdLength { get; set; }

    Maximum channel-id length; longer ids are treated as invalid. Default 64.

  • int MaxMessageLength { get; set; }

    Maximum message length after trimming; longer sends are rejected. Default 256.

  • bool MuteAppliesToWhispers { get; set; }

    Server-enforced mutes also silence whispers (default). Whispers are the direct harassment channel, so a mute that only covers public rooms has no teeth exactly where it matters most; a game that wants "public-channels-only" mutes turns this off.

  • int MutesAdminPageSize { get; set; }

    Maximum active mutes the /admin/mutes listing returns. Default 100.

  • string ProfanityMask { get; set; }

    Replacement string for a masked profanity word. Default "***".

  • List<string> ProfanityWords { get; }

    Words masked in outgoing messages (whole-word, case-insensitive). Empty = no filter (the default). The game supplies its own list; keeping it here (not inline) means no hardcoded content in the handler. The regex is compiled once at handler construction, so add words before AddCrossplayChat runs.

  • string SystemChannelId { get; set; }

    Channel id carried on System-channel messages. Default "system".

  • string SystemSenderName { get; set; }

    Display name stamped as the sender of System-channel messages. Default "System".

Wire messages 4

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

1001 ChatSend

Client -> server: post a message to a channel.

1002 ChatMessage

Server -> client: a delivered chat message.

1003 ChatJoinChannel

Client -> server: subscribe to a (party/guild/custom) channel's broadcasts.

1004 ChatLeaveChannel

Client -> server: unsubscribe from a channel.

Unity services you inject 1

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

IChatClient

Data-layer chat service (no UI). Registers the inbound ChatMessage handler and exposes send/join/leave plus a MessageReceived event for presenters. Self-registers under the Crossplay client context via the framework's attribute discovery.

  • event Action<ChatMessage> MessageReceived

    Raised (main thread) for each inbound chat message.

  • void Send(ChatChannelType channel, string channelId, string text)

    Sends a message on a channel (whisper → channelId is the target name).

  • void JoinChannel(ChatChannelType channel, string channelId)

    Joins a party/guild/custom channel to receive its broadcasts.

  • void LeaveChannel(ChatChannelType channel, string channelId)

    Leaves a channel.

UI views you can replace 1

Each ships a working uGUI panel you can drag into a scene. Substitute your own view to keep the logic and change the look — the presenter never knows.

ICrossplayChatView

Narrow contract the presenter drives. UI-only: render calls in, user intents out.