Chat
Channels and whispers over Core, with profanity masking and rate caps.
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.
ChatSend Client -> server: post a message to a channel.
ChatMessage Server -> client: a delivered chat message.
ChatJoinChannel Client -> server: subscribe to a (party/guild/custom) channel's broadcasts.
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> MessageReceivedRaised (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.