Lobbies
Create/join/list lobbies with slots and metadata.
Seams you implement 1
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayLobbies(); and it wins.
ILobbyStartSink game SPI
The start seam this piece OWNS (the IMatchFormedSink idiom): called inside TryStartMatch after the lobby's state flips to InProgress, BEFORE the MatchStarting broadcast, with the lobby id, its game-defined mode string, and every member — cross-node aware. The piece defaults it to NullLobbyStartSink (a TryAdd seam), so with nothing registered ahead of it both lobby twins behave byte-identically to before the seam existed. The composition layer (Hosting/game) registers a bridge FIRST to route lobby starts somewhere (e.g. the Director allocates an instance) — Lobbies itself never references what listens. MatchStarting is still broadcast either way.
sealed class DirectorLobbyBridge : ILobbyStartSink
{
public void LobbyStarted(long lobbyId, string mode, IReadOnlyList<LobbyStartMember> members)
{
var instance = _director.Allocate(mode, members.Count);
foreach (var m in members)
_push.InstanceAssignment(m, instance); // local via session, remote via account+node
}
}
services.AddSingleton<ILobbyStartSink, DirectorLobbyBridge>(); // before AddCrossplayLobbies -
void LobbyStarted(long lobbyId, string mode, IReadOnlyList<LobbyStartMember> members)The host just started lobbyId with these members.
Services you call 1
Crossplay implements these. Resolve them from DI and call them from your own systems.
ILobbyService
Genre-neutral lobby directory: browsable, joinable game sessions with metadata + a host who can start the match. A session may be in at most one lobby at a time. Roster/state changes are pushed to members through the Core IRoomRegistry. Depends only on Core primitives (sessions + identity + rooms). Crossplay never inspects what a "match" means to the game.
-
IReadOnlyList<LobbyInfo> Browse()Snapshot of the browsable lobbies (open, public lobbies only), ordered by id.
-
bool TryCastVote(ISession session, byte optionIndex, out ushort reason)Casts (or changes) the caller's pick for the lobby's active vote.
-
bool TryCreate(ISession session, string name, string mode, int maxPlayers, bool isPrivate, string password, out long lobbyId, out ushort reason)Creates a lobby owned by session and adds it as the host. On success the new id is returned and the host is sent the initial roster. Returns false with reason set to a notification code.
-
bool TryJoin(ISession session, long lobbyId, string password, out ushort reason)Adds session to lobbyId (checking password/capacity/state). On success every member is sent the updated roster. Returns false with reason.
-
bool TryLeave(ISession session, out ushort reason)Removes session from whatever lobby it is in, reassigning the host or closing the lobby when it empties, and broadcasting the updated roster. Returns false with reason when the session is in no lobby.
-
bool TryStartMatch(ISession session, out ushort reason)Host-only: moves the caller's lobby to InProgress and broadcasts MatchStarting + the updated roster. Returns false with reason (not in a lobby / not host / already in progress).
-
bool TryStartVote(ISession session, string[] options, out ushort reason)Host opens a vote among opaque options — map names, mode keys, anything (a new vote replaces a stale one). Default: unsupported — the single-node service implements it; cross-node lobby votes are a future Redis-twin addition.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
LobbyOptions
Configurable lobby rules (defaults live here, never inline in the service/handlers).
-
int DefaultMaxPlayers { get; set; }Max-players used when a create request does not specify one (0 or less).
-
int MaxModeLength { get; set; }Maximum length of the game-defined mode tag.
-
int MaxNameLength { get; set; }Maximum lobby name length.
-
int MaxPasswordLength { get; set; }Maximum password length.
-
int MaxPlayersCap { get; set; }Largest max-players any lobby may be created with (requests are clamped down to this).
-
int MaxVoteOptionLength { get; set; }Longest allowed vote-option text.
-
int MaxVoteOptions { get; set; }Most options a single vote may offer.
-
int MinMaxPlayers { get; set; }Smallest max-players a lobby may be created with.
-
int MinNameLength { get; set; }Minimum lobby name length.
Wire messages 17
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
LobbyCreateRequest Client -> server: create a new lobby owned by the sender.
LobbyCreateResponse Server -> client: result of a LobbyCreateRequest.
LobbyListRequest Client -> server: request the browsable list of open, public lobbies.
LobbyListResponse Server -> client: the browsable lobby list (open, public lobbies only).
LobbyJoinRequest Client -> server: join the given lobby (with an optional password).
LobbyJoinResponse Server -> client: result of a LobbyJoinRequest.
LobbyLeaveRequest Client -> server: leave the lobby the sender is currently in.
LobbyLeaveResponse Server -> client: result of a LobbyLeaveRequest.
LobbyStartMatchRequest Client -> server: the host asks to start the match for their lobby.
LobbyStartMatchResponse Server -> client: result of a LobbyStartMatchRequest (host only).
LobbyRoster Server -> client: the current roster + metadata of a lobby, pushed to every member whenever membership, host, or state changes.
MatchStarting Server -> client: the host started the match; broadcast to every lobby member.
LobbyVoteStartRequest Client → server: the host opens a vote among OPAQUE options — map names, mode keys, anything the game encodes as strings. The framework only tallies; a new vote replaces a stale one.
LobbyVoteStartResponse Server → client: vote-start result.
LobbyVote Server → client: a vote began (broadcast to the lobby with the options).
LobbyVoteCast Client → server: my pick.
LobbyVoteState Server → client: tallies after every cast; when every member voted, the round is complete and WinnerIndex holds the result (ties break to the lowest index).
Unity services you inject 1
Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.
ILobbyClient
Data-layer lobby service (no UI): async create/list/join/leave/start RPCs plus events for the server-pushed roster/state and match-starting broadcasts. Self-registers under the Crossplay client context. Depends only on Crossplay Core.
-
event Action<LobbyRoster> RosterChangedRaised whenever the server pushes the roster/state of the lobby this client is in.
-
event Action<MatchStarting> MatchStartingRaised when the host starts the match for the lobby this client is in.
-
UniTask<LobbyCreateResponse> CreateAsync(string name, string mode, int maxPlayers, bool isPrivate, string password, CancellationToken ct = default)Creates a lobby owned by this client (public unless isPrivate).
-
UniTask<LobbyListResponse> ListAsync(CancellationToken ct = default)Requests the browsable list of open, public lobbies.
-
UniTask<LobbyJoinResponse> JoinAsync(long lobbyId, string password, CancellationToken ct = default)Joins a lobby by id (with an optional password).
-
UniTask<LobbyLeaveResponse> LeaveAsync(CancellationToken ct = default)Leaves the current lobby.
-
UniTask<LobbyStartMatchResponse> StartMatchAsync(CancellationToken ct = default)Host-only: starts the match for the current lobby.
-
UniTask<LobbyVoteStartResponse> StartVoteAsync(string[] options, CancellationToken ct = default)Host opens a vote among opaque options (map names, mode keys, anything).
-
void CastVote(byte optionIndex)Casts (or changes) my pick. Fire-and-forget — the broadcast state is the feedback.
-
event Action<LobbyVote> VoteStartedRaised when a vote begins.
-
event Action<LobbyVoteState> VoteStateChangedRaised after every cast (Complete carries the winner).
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.
ICrossplayLobbiesView
Narrow contract the presenter drives. UI-only: render calls in, user intents out.