party

Party

Ephemeral parties: leader, ready checks, kicks.

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

Seams you implement 3

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

IPartyMemberLocator game SPI

The member-location seam the Party piece OWNS — how the roster's Zone/Endpoint fields fill without Party (a Core-only feature piece) ever referencing the World piece. A composition layer that knows where players are (Hosting's World-backed bridge, or a game's own source) registers an implementation BEFORE AddCrossplayParty (TryAdd) to win the seam. The shipped default (NullPartyMemberLocator) locates nobody, so roster locations stay empty and the piece behaves exactly as before — removable by construction. Location is only ever consumed into PartyRoster broadcasts, which go only to the party's own room: party members see each other's location, nobody else does.

  • event Action<long> LocationChanged

    Raised (with the account id) when a located account's answer changes — entered a world, travelled zones, or left the world. The party service re-broadcasts the account's party roster, which is the location surface.

  • bool TryLocate(long accountId, out string endpoint, out string zone)

    Where accountId currently is. zone is the FULL zone/instance name (e.g. level2:7, never the bare template); endpoint is the advertised "host:port" of the node hosting that zone — the same string a NodeRedirect carries — or empty when the zone is local/unadvertised. False (with empty outs) when the account is not in a world or cannot be located.

IPartyReadySink game SPI

The post-ready-check seam the Party piece OWNS — the analog of Matchmaking's IMatchFormedSink and Lobbies' ILobbyStartSink: fired once when a party's ready check completes with EVERY member ready, so a composition layer can start a match / instance for exactly that group of that size — without Party ever referencing Matches or the Director. The shipped default (NullPartyReadySink) does nothing; a game or a Hosting bridge registers its own BEFORE AddCrossplayParty (TryAdd) to win the seam. Removable: with no sink a passed ready check just broadcasts PartyReadyState exactly as before.

  • void PartyReady(long partyId, IReadOnlyList<PartyReadyMember> members)

    A party's ready check passed with all members ready. members carries each member's account/name and their LocalSession when local.

IPartyStatePolicy game SPI

Who may write the party's shared opaque state blob (PartySetStateRequest). The shipped default (LeaderOnlyPartyStatePolicy) allows the party leader only — a griefing-safe default, since the blob replays to every member. A game registers its own policy BEFORE AddCrossplayParty (TryAdd) to widen or tighten it (e.g. any member, or nobody — making the state server-authored only via IPartyService.TrySetState callers the game controls).

  • bool MayWrite(ISession session, long partyId, bool isLeader)

    True when session may replace the state of party partyId. The caller is always an authenticated member of that party; isLeader is precomputed so the common leader check needs no store read.

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.

IPartyStore provider

Persistence SPI for live parties and pending invites. One party per account (a member belongs to at most one). Abstracted so a game can swap in a durable implementation; the framework ships an in-memory default (InMemoryPartyStore) for dev and single-node servers.

  • void AddInvite(long accountId, long partyId)

    Records a pending invite of accountId to partyId.

  • bool AddMember(long partyId, long accountId)

    Adds a member to a party. Returns false if the party is missing or already a member.

  • PartyRecord Create(long leaderAccountId)

    Creates a new party led by (and containing) leaderAccountId.

  • PartyRecord GetByAccount(long accountId)

    Finds the party containing accountId, or null.

  • PartyRecord GetById(long partyId)

    Looks up a party by id, or null.

  • bool HasInvite(long accountId, long partyId)

    True if there is a pending invite of accountId to partyId.

  • IReadOnlyList<long> Members(long partyId)

    The current member account ids of a party (empty if missing).

  • bool Remove(long partyId)

    Removes (disbands) a party. Returns false if it did not exist.

  • void RemoveInvite(long accountId, long partyId)

    Clears a pending invite of accountId to partyId.

  • bool RemoveMember(long partyId, long accountId)

    Removes a member from a party. Returns false if the party is missing or not a member.

Services you call 1

Crossplay implements these. Resolve them from DI and call them from your own systems.

IPartyService

The party service the game calls to drive group membership. The same instance also runs the disconnect-cleanup lifecycle (it is registered as a Core ISessionLifecycleListener), so a leaving player is pulled from their party whether they used the leave message or simply dropped their connection. Every Try* operation returns a PartyResult and, on failure, sends the caller a coded notification (a PartyNotificationCodes value); any membership change broadcasts an updated PartyRoster to the party room.

  • void BroadcastRoster(long partyId)

    Broadcasts the current roster of partyId to its room.

  • event Action<ISession, long, bool> MembershipChanged

    Raised when a session enters (true) or leaves (false) a party — the typed hook the composition layer uses for optional cross-piece integration (party chat channels, a party's shared instanced world). Fires on create, accept, kick, leave and disconnect alike.

  • PartyResult TryAccept(ISession session, long partyId)

    Accepts a pending invite to partyId and joins that party.

  • PartyResult TryCreate(ISession session)

    Creates a party led by (and containing) the session, if it is not already in one.

  • bool TryGetPartyOf(long accountId, out IReadOnlyList<long> members)

    The members of accountId's party (donor-gap E2: the membership READ seam reward-sharing composition builds on — XP splits, party-aware loot, assist credit). False when the account is not in a party; members includes the account itself.

  • bool TryGetPartyState(long accountId, out byte[] state)

    The current shared state blob of accountId's party — the server-side READ seam (games and composition bridges; clients receive it over the wire instead). False, with an empty blob, when the account is not in a party or the party has no state. Default interface implementation reports no state, so a custom IPartyService stays honest.

  • PartyResult TryInvite(ISession inviter, string targetName)

    Invites the online player named targetName to the inviter's party, sending them a PartyInviteNotification.

  • PartyResult TryKick(ISession session, long targetAccountId)

    Kicks a member from the caller's party (leader only). The target — online or offline — is removed; an online target is notified with Kicked.

  • PartyResult TryLeave(ISession session)

    Removes the session from its current party (the explicit leave).

  • PartyResult TryPromote(ISession session, long targetAccountId)

    Transfers party leadership to targetAccountId (leader only). The fresh roster broadcast — IsLeader flags flipped — is the party's feedback. Combined with the standing rules this completes the leadership lifecycle: the creator leads, the leader may hand leadership off, a leader who leaves with members remaining passes it automatically, and the last member leaving disbands the party. Default interface implementation honestly refuses with NotAllowed so a custom IPartyService written before this member keeps compiling and never pretends to transfer.

  • PartyResult TryReady(ISession session, bool ready)

    Records the caller's ready answer for the active round and broadcasts the updated PartyReadyState (which is the client's feedback).

  • PartyResult TrySetState(ISession session, byte[] data)

    Replaces the shared opaque state blob of the caller's party (empty clears it) and broadcasts the new PartyStateChanged to the party room; the blob also replays alongside every roster, so joiners always have it. Refusals: NotInParty, StateTooLarge (over PartyOptions.MaxStateBytes, checked before the policy), or NotAllowed (the composed IPartyStatePolicy said no — leader-only by default). Default interface implementation honestly refuses with NotAllowed so a custom IPartyService written before this member keeps compiling and never pretends to store state.

  • PartyResult TryStartReadyCheck(ISession session)

    Starts a ready check across the party (leader only; a new round replaces any active one). Broadcasts a PartyReadyCheck plus the initial PartyReadyState.

Configuration 1

Every tunable lives in an options object — there are no magic numbers to hunt for.

PartyOptions

Configurable party rules, passed to AddCrossplayParty. Defaults live here, never inline in handlers/services (the no-hardcoded-values rule).

  • bool BridgeMemberLocation { get; set; }

    Gates the Hosting composition bridge that fills the roster's member-location fields (full instance zone + hosting-node endpoint) from the World piece through the IPartyMemberLocator seam. Purely additive and members-only (location only ever travels inside the party's own roster), so it defaults to on; set false to compose no locator — roster locations then stay empty and the piece never knows the difference. Ignored when the World piece is not composed, and a game's own locator registration always wins the seam either way.

  • double InviteTtlSeconds { get; set; }

    Seconds a pending party invite stays acceptable before expiring (checked lazily — no timers). An expired accept answers NoInvite, exactly like never having been invited; a re-invite refreshes the window. 0 = invites never expire. The Redis cluster twin TTLs its shared-store invites via its own Crossplay:Redis:PartyInviteTtlSeconds (same default).

  • int MaxMembers { get; set; }

    Maximum members in a party, leader included. Invites past this cap are rejected with PartyFull.

  • int MaxStateBytes { get; set; }

    Byte cap for the party's shared state blob (PartySetStateRequest.Data). An oversized write is refused with StateTooLarge before the write policy is even asked. The blob replays to every member alongside each roster, so keep it a summary ("level, difficulty, seed"), not a payload channel.

  • int StateWriteBurst { get; set; }

    Burst allowance for PartySetStateRequest on top of StateWritesPerSecond.

  • double StateWritesPerSecond { get; set; }

    Sustained per-session rate for PartySetStateRequest, seeded into Core's per-message rate limits by AddCrossplayParty. A composer's configureRateLimit entry or Crossplay:RateLimit:PerMessage config wins over the seed.

Wire messages 22

The protocol this piece speaks. Ids are allocated per piece so they can never collide.

1201 PartyCreateRequest

Client -> server: create a new party led by the caller.

1202 PartyCreateResponse

Server -> client: result of a PartyCreateRequest.

1203 PartyInviteRequest

Client -> server: invite a player (by display name) to the caller's party.

1204 PartyInviteResponse

Server -> client: result of a PartyInviteRequest.

1205 PartyInviteNotification

Server -> client: the invited player is told about a pending invite.

1206 PartyAcceptRequest

Client -> server: accept a pending invite to a party.

1207 PartyAcceptResponse

Server -> client: result of a PartyAcceptRequest.

1208 PartyLeaveRequest

Client -> server: leave the caller's current party.

1209 PartyLeaveResponse

Server -> client: result of a PartyLeaveRequest.

1210 PartyRoster

Server -> party members: the current roster. Sent to the party room on every membership change, as the reply to a successful create/accept, and whenever a member's location (zone/endpoint) changes — the roster IS the party's location surface.

1211 PartyKickRequest

Client → server: kick a member (leader only).

1212 PartyKickResponse

Server → client: kick result.

1213 PartyReadyCheckStartRequest

Client → server: start a ready check (leader only; replaces any active round).

1214 PartyReadyCheckStartResponse

Server → client: ready-check start result.

1215 PartyReadyCheck

Server → client: a ready check began (broadcast to the party).

1216 PartyReadyResponse

Client → server: my ready answer.

1217 PartyReadyState

Server → client: the round state after every answer (broadcast to the party).

1218 PartySetStateRequest

Client -> server: replace the party's shared opaque state blob (empty clears it). Policy-gated server-side — leader-only by default — and size-capped by PartyOptions.MaxStateBytes.

1219 PartySetStateResponse

Server -> client: result of a PartySetStateRequest.

1220 PartyStateChanged

Server -> party members: the party's shared opaque state blob — broadcast to the party room on every change and replayed (when non-empty) right after every PartyRoster, so a member who joins mid-run receives it without asking.

1221 PartyPromoteRequest

Client → server: transfer party leadership to another member (leader only). The fresh roster broadcast — its IsLeader flags flipped — is the party's feedback.

1222 PartyPromoteResponse

Server → client: promote result.

Unity services you inject 1

Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.

IPartyClient

Data-layer party service (no UI): async create/invite/accept/leave RPCs plus push events for inbound invites and roster changes. Self-registers under the Crossplay client context.

  • event Action<PartyInviteNotification> InviteReceived

    Raised when this client receives a pending party invite.

  • event Action<PartyRoster> RosterChanged

    Raised whenever the party roster changes (server push on every membership change).

  • UniTask<PartyCreateResponse> CreateAsync(CancellationToken ct = default)

    Creates a new party led by the caller.

  • UniTask<PartyInviteResponse> InviteAsync(string targetName, CancellationToken ct = default)

    Invites a player (by display name) to the caller's party.

  • UniTask<PartyAcceptResponse> AcceptAsync(long partyId, CancellationToken ct = default)

    Accepts a pending invite to the given party.

  • UniTask<PartyLeaveResponse> LeaveAsync(CancellationToken ct = default)

    Leaves the caller's current party.

  • UniTask<PartyKickResponse> KickAsync(long targetAccountId, CancellationToken ct = default)

    Kicks a member (leader only).

  • UniTask<PartyPromoteResponse> PromoteAsync(long targetAccountId, CancellationToken ct = default)

    Transfers party leadership to another member (leader only). The fresh roster — its IsLeader flags flipped — is the party's feedback.

  • UniTask<PartySetStateResponse> SetStateAsync(byte[] data, CancellationToken ct = default)

    Replaces the party's shared opaque state blob (empty clears it). Leader-only by default server-side; refused with StateTooLarge over the server's size cap.

  • event Action<PartyStateChanged> StateChanged

    Raised when the party's shared state blob changes — on every write and replayed (when non-empty) right after every roster, so a mid-run joiner receives it unprompted.

  • byte[] LatestState

    The most recent party state blob seen (empty when none, or after leaving).

  • UniTask<PartyReadyCheckStartResponse> StartReadyCheckAsync(CancellationToken ct = default)

    Starts a ready check (leader only).

  • void Ready(bool ready)

    Answers the active ready check. Fire-and-forget — the broadcast state is the feedback.

  • event Action<PartyReadyCheck> ReadyCheckStarted

    Raised when a ready check begins.

  • event Action<PartyReadyState> ReadyStateChanged

    Raised as members answer (Complete/AllReady flags close the round).

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.

ICrossplayPartyView

Narrow contract the presenter drives. UI-only: render calls in, user intents out.