forums

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.

Category Social Seams 3 Services 1 Options 1 Wire ids 20 Unity types 1
Server
services.AddCrossplayForums();
Unity package
com.crossplay.forums
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.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> Posted

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

7501 ForumThreadsRequest

Client -> server: a page of a board's threads - pinned first, then most recently active.

7502 ForumThreads

Server -> client: a page of threads.

7503 ForumPostsRequest

Client -> server: a page of a thread's posts, oldest first, after AfterPostId.

7504 ForumPosts

Server -> client: a page of posts.

7505 ForumCreateThreadRequest

Client -> server: start a thread on a board.

7506 ForumReplyRequest

Client -> server: reply in a thread.

7507 ForumEditRequest

Client -> server: edit my post (within the edit window).

7508 ForumDeleteRequest

Client -> server: delete a post - my own, or any as a moderator of its board.

7509 ForumModerateRequest

Client -> server: a moderator pins, unpins, locks, unlocks or deletes a thread.

7510 ForumResult

The answer to every forum verb.

7511 ForumBoardsRequest

Client -> server: the boards under a key prefix I may read.

7512 ForumBoards

Server -> client: the boards under a prefix, by key.

7513 ForumCreateBoardRequest

Client -> server: create or retitle a named board (the board's managers).

7514 ForumDeleteBoardRequest

Client -> server: delete a board and every thread on it (the board's managers).

7515 ForumThreadPageRequest

Client -> server: a page of a board's threads with names and my unread marks.

7516 ForumThreadPage

Server -> client: a page of threads (pinned first, then most recently active).

7517 ForumPostPageRequest

Client -> server: a page of a thread's posts with names; fetching marks them read for me.

7518 ForumPostPage

Server -> client: a page of posts with names, and the thread as it is now for me.

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

7520 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).