Friends
Friend lists, requests, and online presence.
Providers you can swap 1
Infrastructure seams. Crossplay ships a working implementation of each — replacing one changes where data lives or how it moves, never a game rule.
IFriendStore provider
Friendship persistence SPI. Friendships are unordered account-id pairs; pending requests and blocks are directed (requester/blocker -> target/blocked). The GAME supplies a durable store; the base ships an in-memory default (InMemoryFriendStore) for dev and tests only.
-
void AddFriendship(long a, long b)Records a mutual friendship between a and b.
-
void AddRequest(long requester, long target)Records a pending directed request from requester to target.
-
bool AreFriends(long a, long b)True if a and b are friends.
-
void Block(long blocker, long blocked)Records a directed block by blocker against blocked. Implementations MUST also drop any existing friendship and pending requests between the two (blocking implies unfriend). Idempotent — blocking an already-blocked player is a no-op.
-
IReadOnlyList<long> BlockedBy(long accountId)All account ids that accountId has blocked.
-
int FriendCount(long accountId)Number of friends accountId currently has.
-
IReadOnlyList<long> FriendsOf(long accountId)All account ids that are friends of accountId.
-
bool HasRequest(long requester, long target)True if a pending request from requester to target exists.
-
bool IsBlocked(long blocker, long blocked)True if blocker has blocked blocked.
-
void RemoveFriendship(long a, long b)Removes any friendship between a and b.
-
void RemoveRequest(long requester, long target)Removes a pending request from requester to target.
-
void Unblock(long blocker, long blocked)Lifts a directed block by blocker against blocked.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
IFriendsService
Server-side friends presence entry point — the API the game calls to publish who is online and what they are doing. The implementation is also wired as a Core ISessionLifecycleListener, so raw connect/disconnect drive presence automatically; a game only calls these directly to fan out an out-of-band change (e.g. a status update, or re-announcing after a custom login step). Meaning of the status blob is entirely the game's — the framework stores and relays opaque bytes.
-
void NotifyPresence(ISession session, bool online)Best-effort: notifies each online friend of session's account that the player is now online, by pushing a FriendPresenceChanged. With a cluster IAccountDirectory registered, the fan-out reaches friends on any node.
-
void SetStatus(ISession session, byte[] status)Sets the caller's rich-presence blob (GAME vocabulary; empty clears; a blob over MaxStatusBytes is dropped) and pushes the change to online friends — the push IS the confirmation. Ephemeral: the status clears when the session disconnects.
-
byte[] StatusOf(long accountId)The account's current rich-presence blob, or an empty array when unset/offline.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
FriendsOptions
Configurable friends rules, passed to AddCrossplayFriends. Defaults live here, never inline in handlers (the no-hardcoded-values rule).
-
int MaxFriends { get; set; }Maximum number of friends a single account may have. A request past this cap is rejected with LimitReached.
-
int MaxStatusBytes { get; set; }Largest rich-presence blob a client may set; an oversize blob is dropped silently (a well-behaved client never sends one).
Wire messages 15
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
FriendRequestSend Client -> server: request friendship with a player by display name.
FriendRequestResponse Server -> client: result of a FriendRequestSend.
FriendRequestNotification Server -> client (to the target): another player requested friendship.
FriendAcceptRequest Client -> server: accept a pending friend request.
FriendAcceptResponse Server -> client: result of a FriendAcceptRequest.
FriendRemoveRequest Client -> server: remove an existing friend.
FriendRemoveResponse Server -> client: result of a FriendRemoveRequest.
FriendListRequest Client -> server: request the caller's friend list.
FriendList Server -> client: the caller's current friend list. Sent both as the reply to a FriendListRequest AND, unsolicited, as a live update pushed to each affected online player after an accept / remove / block / unblock — the same message, no extra wire type, so drop-in panels refresh without polling.
FriendPresenceChanged Server -> client: a friend came online, went offline, or changed their rich status.
FriendStatusSet Client → server: set my rich-presence blob. GAME vocabulary — one game sends a UTF-8 activity line, another a packed struct of zone/party/score; the framework only stores and relays it. Empty clears. Fire-and-forget (the confirmation IS the presence push friends receive).
FriendBlockRequest Client -> server: block a player by account id. Blocking removes any existing friendship and pending requests between the two, and prevents future friend requests.
FriendBlockResponse Server -> client: result of a FriendBlockRequest.
FriendUnblockRequest Client -> server: lift a block on a player by account id.
FriendUnblockResponse Server -> client: result of a FriendUnblockRequest.
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
IFriendsClient
Data-layer friends service (no UI): async request/accept/remove/list RPCs plus events for inbound friend requests and presence changes. Self-registers under the Crossplay client context.
-
event Action<FriendRequestNotification> RequestReceivedRaised when another player sends this client a friend request.
-
event Action<FriendPresenceChanged> PresenceChangedRaised when a friend comes online or goes offline.
-
event Action<FriendList> ListChangedRaised whenever a FriendList arrives — both as the reply to ListAsync and as the server's unsolicited push after a friends mutation (accept/remove/block/unblock) touched this player's list. UI can re-render from it directly.
-
UniTask<FriendRequestResponse> RequestAsync(string targetName, CancellationToken ct = default)Requests friendship with a player by display name.
-
UniTask<FriendAcceptResponse> AcceptAsync(long requesterAccountId, CancellationToken ct = default)Accepts a pending friend request from the given requester.
-
UniTask<FriendRemoveResponse> RemoveAsync(long friendAccountId, CancellationToken ct = default)Removes an existing friend.
-
UniTask<FriendBlockResponse> BlockAsync(long targetAccountId, CancellationToken ct = default)Blocks a player by account id. Blocking also removes any existing friendship and pending requests between the two, and prevents future friend requests either way.
-
UniTask<FriendUnblockResponse> UnblockAsync(long targetAccountId, CancellationToken ct = default)Lifts a block on a player by account id.
-
UniTask<FriendList> ListAsync(CancellationToken ct = default)Requests the caller's current friend list.
-
void SetStatus(byte[] status)Sets my rich-presence blob (GAME vocabulary; empty clears). Fire-and-forget — friends receive it as a PresenceChanged push.
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.
ICrossplayFriendsView
Narrow contract the presenter drives. UI-only: render calls in, user intents out — no data or services.