Forums
Boards of threads of posts: a parish board, a guild board, a general board - board keys are the game's. Threads list pinned first, then by latest activity, a page at a time; posts page oldest first from a cursor. Authors edit within a window and delete their own; moderators pin, lock and delete. Who may read, post and moderate each board is the game's IForumAccess. Bounded (boards, threads per board, posts per thread, text length, a posting cooldown) and durable across restarts. Nothing is pushed; Posted lets the game tell players.
Seams you implement 1
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayForums(); and it wins.
IForumAccess game SPI
Who may read, post on and moderate a board - the game's rules (a parish board for its villagers, a house board for its members, a staff-only board). Asked on every request. The default lets every authenticated account read and post anywhere and nobody moderate.
-
int? MaxNamedBoards(string parent)The most named boards under one parent (the key up to its last ForumOptions.BoardParentSeparator); null = ForumOptions.MaxNamedBoardsPerParent. Defaulted (rule 25).
-
bool MayManageBoards(long accountId, string board)May the account create, retitle or delete this named board? Default: whoever may moderate it. Defaulted so adding it breaks no existing access rule (rule 25).
-
bool MayModerate(long accountId, string board)May the account moderate the board (pin, lock, delete anybody's posts and threads)?
-
bool MayPost(long accountId, string board)May the account start threads and reply on the board?
-
bool MayRead(long accountId, string board)May the account read the board?
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.
IForumClock provider
The clock posts are stamped with (a seam so tests control time).
-
long UtcNowMs { get; }Now, unix ms UTC.
IForumStore provider
Where threads and posts are kept. The default is in memory; persistence composes a document-backed one.
-
void DeleteBoard(string key)Removes a named board's record (its threads are deleted by the caller).
-
void DeleteThread(long threadId)Deletes a thread and all its posts.
-
IReadOnlyDictionary<long, long> LastReads(long accountId)One account's read markers: thread id to the newest post id it has read.
-
IReadOnlyList<ForumBoardRecord> LoadBoards()Every named board.
-
IReadOnlyList<ForumPostRecord> LoadPosts(long threadId)A thread's posts, oldest first.
-
IReadOnlyList<ForumThreadRecord> LoadThreads()Every thread header.
-
long NextId(bool thread)The next thread or post id (one sequence each).
-
void SaveBoard(ForumBoardRecord board)Saves (adds or replaces) a named board.
-
void SavePost(ForumPostRecord post)Saves (adds or replaces) a post.
-
void SaveThread(ForumThreadRecord thread)Saves (adds or replaces) a thread header.
-
void SetLastRead(long accountId, long threadId, long postId)Sets an account's read marker for a thread.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
IForumService
Forums: boards (game-defined keys) of threads of posts. Who may read, post and moderate is the game's IForumAccess; everything is bounded by ForumOptions and durable through IForumStore. Nothing is pushed: clients pull pages, and Posted lets the game tell its players (a reply flag, a mail, a badge).
-
long Announce(string board, long authorAccountId, string title, string body)Server-side: starts a thread as authorAccountId (an announcement), skipping access, prefixes and the cooldown but not the size limits. Returns the thread id (0 = refused).
-
bool CreateBoard(string key, string title)Creates or retitles a named board as the server (no access check, no cap). Defaulted (rule 25).
-
bool DeleteBoard(string key)Deletes a board and every thread on it, as the server. Defaulted (rule 25).
-
bool DeleteThread(long threadId)Server-side: deletes a thread and its posts.
-
event Action<string, long, long, long> PostedRaised after a thread is started or a reply posted: (board, thread, post, author).
-
ForumThreadRecord Thread(long threadId)A thread header, or null.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
ForumOptions
Forums tuning (defaults here; never inlined in logic). Bound from Crossplay:Forums.
-
char BoardParentSeparator { get; set; }A board key's parent is everything up to and including its last occurrence of this character ("guild:12:trade" -> "guild:12:"). Named-board caps count boards under the same parent.
-
List<string> BoardPrefixes { get; set; }Board keys a client may use must start with one of these (ordinal). Empty = any key the game's access rule allows. A board exists from its first thread.
-
double EditWindowSeconds { get; set; }How long after posting its author may edit it, in seconds. 0 = posts cannot be edited.
-
int MaxBoardKeyLength { get; set; }Longest board key, in characters.
-
int MaxBoardTitleLength { get; set; }Longest named-board title, in characters.
-
int MaxBoards { get; set; }Most boards that may exist at once (a new board's first thread is refused past this).
-
int MaxBodyLength { get; set; }Longest post, in characters.
-
int MaxNamedBoardsPerParent { get; set; }Most named boards under one parent key (see BoardParentSeparator), unless the access rule says otherwise.
-
int MaxPostsPerThread { get; set; }Most posts one thread may hold (a full thread takes no more replies).
-
int MaxThreadsPerBoard { get; set; }Most threads one board may hold.
-
int MaxTitleLength { get; set; }Longest thread title, in characters.
-
int MaxUnreadThreads { get; set; }Most threads one unread-count request may ask about; the rest of the request is ignored.
-
double PostCooldownSeconds { get; set; }Least time between two posts (threads or replies) by one account, in seconds. 0 = no limit.
-
int PostPageSize { get; set; }Posts per ForumPostsRequest page.
-
int ThreadPageSize { get; set; }Threads per ForumThreadsRequest page.
Wire messages 20
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
ForumThreadsRequest Client -> server: a page of a board's threads - pinned first, then most recently active.
ForumThreads Server -> client: a page of threads.
ForumPostsRequest Client -> server: a page of a thread's posts, oldest first, after AfterPostId.
ForumPosts Server -> client: a page of posts.
ForumCreateThreadRequest Client -> server: start a thread on a board.
ForumReplyRequest Client -> server: reply in a thread.
ForumEditRequest Client -> server: edit my post (within the edit window).
ForumDeleteRequest Client -> server: delete a post - my own, or any as a moderator of its board.
ForumModerateRequest Client -> server: a moderator pins, unpins, locks, unlocks or deletes a thread.
ForumResult The answer to every forum verb.
ForumBoardsRequest Client -> server: the boards under a key prefix I may read.
ForumBoards Server -> client: the boards under a prefix, by key.
ForumCreateBoardRequest Client -> server: create or retitle a named board (the board's managers).
ForumDeleteBoardRequest Client -> server: delete a board and every thread on it (the board's managers).
ForumThreadPageRequest Client -> server: a page of a board's threads with names and my unread marks.
ForumThreadPage Server -> client: a page of threads (pinned first, then most recently active).
ForumPostPageRequest Client -> server: a page of a thread's posts with names; fetching marks them read for me.
ForumPostPage Server -> client: a page of posts with names, and the thread as it is now for me.
ForumUnreadRequest Client -> server: my unread post count in each of a batch of threads - a "Room (3)" badge per tab, one request for all of them. Capped server-side (Forums:MaxUnreadThreads); duplicates collapse. A post page clears a count by moving my read marker to the newest post it returned. The reply is a ForumUnread.
ForumUnread Server -> client: the reply to a ForumUnreadRequest. A thread that does not exist, or that I may not read, is left out (the two look the same).
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IForumClient
Client SDK for Forums: every verb is one awaitable call (Core's request/reply helper - a call that gets no answer completes as a timeout, it never throws). Boards are game-defined keys; who may read, post and moderate is the server's rule. Nothing is pushed - pull a page when the player opens a board. Pure SDK, no UI.
-
UniTask<ForumThreads> ThreadsAsync(string board, int skip = 0, CancellationToken ct = default)A page of a board's threads (pinned first, then most recently active).
-
UniTask<ForumPosts> PostsAsync(long threadId, long afterPostId = 0, CancellationToken ct = default)A page of a thread's posts after afterPostId, oldest first.
-
UniTask<ForumResult> CreateThreadAsync(string board, string title, string body, CancellationToken ct = default)Starts a thread.
-
UniTask<ForumResult> ReplyAsync(long threadId, string body, CancellationToken ct = default)Replies in a thread.
-
UniTask<ForumResult> EditAsync(long threadId, long postId, string body, CancellationToken ct = default)Edits my post (within the server's edit window).
-
UniTask<ForumResult> DeleteAsync(long threadId, long postId, CancellationToken ct = default)Deletes a post (mine, or any as a moderator).
-
UniTask<ForumResult> ModerateAsync(long threadId, byte action, CancellationToken ct = default)(Moderator) pins, unpins, locks, unlocks or deletes a thread - see ForumModeration.
-
UniTask<ForumBoards> BoardsAsync(string prefix, CancellationToken ct = default)The boards under a key prefix I may read (named ones with titles, and ones with threads). Defaulted so an existing implementation of this interface still compiles (rule 25).
-
UniTask<ForumResult> CreateBoardAsync(string key, string title, CancellationToken ct = default)Creates or retitles a named board (its managers). Defaulted (rule 25).
-
UniTask<ForumResult> DeleteBoardAsync(string key, CancellationToken ct = default)Deletes a board and its threads (its managers). Defaulted (rule 25).
-
UniTask<ForumThreadPage> ThreadPageAsync(string board, int skip = 0, CancellationToken ct = default)A page of a board's threads with author names and my unread marks. Defaulted (rule 25).
-
UniTask<ForumPostPage> PostPageAsync(long threadId, long afterPostId = 0, CancellationToken ct = default)A page of a thread's posts with author names; fetching marks them read. Defaulted (rule 25).
-
UniTask<ForumUnread> UnreadAsync(long[] threadIds, CancellationToken ct = default)My unread post count in each of these threads, in one request (a badge per tab). Opening a thread with PostPageAsync clears its count. Defaulted (rule 25).