chat

Chat

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

Category Social Seams 3 Services 2 Options 1 Wire ids 4 Unity types 2
Server
services.AddCrossplayChat();
Unity package
com.crossplay.chat
Depends on
core

Seams you implement 1

Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayChat(); and it wins.

IChatAudienceResolver game SPI

Resolves WHO hears a channel whose audience is not a subscriber list (GAP-15). The gap this closes. Every channel Chat had was a membership list: a room you joined, a guild id, a name to whisper. A "say" channel is not that — its audience is whoever happens to be standing near you, changing every time anyone moves, so there is nothing to join and no list to keep. The piece could not express it, and it could not learn to: Chat references nothing spatial by design, and it must keep working in a card game that never installed World. So the spatial half lives OUTSIDE the piece. Chat asks this seam; the Hosting bridge answers it from World interest, and when World is absent nothing implements it and the sender is TOLD the channel is unavailable — never left shouting into a void that looks like success.

  • bool TryResolveAudience(ISession sender, ChatChannelType channel, string channelId, List<ISession> into)

    Fills into with the sessions that should receive this message.

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.

  • float ProximityRadius { get; set; }

    Hearing distance for the Proximity ("say") channel, in world units. 0 (the default) = everyone who can SEE the sender, i.e. the world's own interest radius. Setting it smaller than interest is the usual choice and the reason it exists: you can see much further than you should be able to hear, so a say channel that reached the whole interest set would carry across a whole town square. It is a plain number, so the Chat piece still references nothing spatial — resolving it is the audience resolver's job.

  • 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.