Lobbies
Overview
The package Altruist.Gaming.Lobbies adds private lobbies to a room host. LobbyModule<TSim, TInput, TPlayer> is an IRoomHostModule that provides:
- lobbies joined by invite code, or created with an empty code;
- host migration: the first member is the host, and the next member takes over when the host leaves;
- starting a room with the members: teams alternate and bots fill each team to the mode's size;
- returning to the lobby after the room, where the host can end the room for everyone;
- rejoining a lobby room after a reconnect.
Like the host, the module runs on the engine thread.
Setup
using Altruist.Gaming.Lobbies;
using Altruist.Gaming.Rooms;
[Service]
[Service(typeof(IWorldStepper))]
public sealed class FieldHost : RoomHost<FieldSim, Move, Pilot>
{
public FieldHost(FieldGame game)
: base(game, game, new RoomHostOptions(), fixedHz: 30)
{
Lobbies = new LobbyModule<FieldSim, Move, Pilot>(game, new LobbyOptions { MaxMembers = 6 });
Use(Lobbies);
}
public LobbyModule<FieldSim, Move, Pilot> Lobbies { get; }
}
The game implements ILobbyGame<TSim, TInput, TPlayer> next to its IRoomGame:
| Member | Meaning |
|---|---|
TeamSizeOf(mode) | Seats per team of the mode (bots fill up to it; a lobby with more members than both teams' seats cannot start it), 0 for a mode without fixed teams (every member plays, no bots), or null when the mode is not allowed in lobbies. |
LobbyState(lobby, to) | The lobby's state for one member (members, host, whether a room runs). |
Rejected(reason, code) | The packet for a refused request. |
public int? TeamSizeOf(string mode) => mode switch { "duel" => 1, "duo" => 2, "trio" => 3, _ => null };
public IPacketBase LobbyState(Lobby<FieldSim, Move, Pilot> lobby, LobbyMember<Pilot> to) => new FieldLobby
{
Code = lobby.Code,
Members = lobby.Members.Select(m => m.Player.Name).ToArray(),
YouAreHost = lobby.IsHost(to.PrincipalId),
Playing = lobby.Room is not null,
};
public IPacketBase Rejected(LobbyRejectReason reason, string code) => new FieldLobbyRejected { Reason = reason.ToString(), Code = code };
Joining and Creating
Join(session, code) trims and upper-cases the code, then:
- empty code: creates a lobby with a new code of
CodeLengthcharacters fromCodeAlphabet(cryptographic random, unique on this server) and makes the player its host; - known code: adds the player, unless the lobby already has
MaxMembersmembers (Full); - unknown code:
NotFound(in a fleet the code is looked up on the other servers first).
Every member gets LobbyState when someone joins or leaves. Join checks the connection's state: a connection playing in a room is ignored, a queued one leaves the queue (it gets an Idle status; host.TakeOver), and a member of another lobby leaves that one first. Joining the lobby the connection is already in just sends the state again.
engine.WaitForNextTick(() =>
{
if (host.SessionOf(clientId) is { } s) host.Lobbies.Join(s, packet.Code);
});
Queueing works the other way round: Enqueue takes the connection out of its lobby.
Leave(session, sendUpdate) takes the player out of the lobby. A connection that closes leaves its lobby on its own, except during the lobby's room (see Rejoining a Lobby Room). When the host leaves, the next connected member (in join order) becomes host. When the last member leaves, the lobby closes, and a room it is playing is disposed.
LobbyOf(session) returns the connection's lobby, Lobbies all lobbies on this server. A Lobby has its Code, its Members (LobbyMember: ClientId, PrincipalId, MemberId, Player, Connected), HostPrincipalId, IsHost(principalId), the Room it is playing, and MakeHost(principalId), which hands the host role to a member. A profile change through the host's UpdatePlayer is copied into the member and sent to the lobby.
Starting a Room
Start(session, mode) starts a room for the whole lobby. It does nothing unless the caller is the host, TeamSizeOf(mode) is not null and the lobby is not already playing. When the members do not fit the mode (more than 2 × TeamSizeOf(mode)), the host gets Rejected(TooManyPlayers). When the server cannot open the room (CanOpenRoom or TryStart is false: full, not ready or draining), the host gets Rejected(Unavailable).
- Members alternate between team 0 and team 1 (team 0 when both are equal). Each member's seat id is its
MemberId, which is stable for the life of the lobby. - Bots fill each team up to the mode's team size, with seat ids from
BotSeatIdBaseupward (50, 51, ...). A mode with team size0seats every member and adds no bots. - The room carries the playlist
Options.Playlist("private"), the lobby's code asLobbyCode, andOptions.Rules. - Every member gets a welcome, the roster, the lobby state (now playing) and a snapshot.
After the Room
A lobby room never sends ReturnedToMenu; its players go back to the lobby:
- The room ends:
ReturnAfterEndSecondsafter the end the room is disposed and every member getsLobbyState(not playing). - The host returns (
host.Return(clientId)): the room is ended for everyone at once and everybody is back in the lobby. - A guest returns: only that player steps out (a voluntary leave under the room's rules); the lobby is updated and the room plays on.
The lobby can start the next room once it is not playing.
Rejoining a Lobby Room
A member who drops during a lobby room keeps its place in the lobby (Connected is false; the host keeps the host role) and its seat as the room's rules say (by default the seat waits for the reconnect grace). On a new connection the host seats the player again and the module puts them back in the lobby. The rejoining player gets the lobby state along with the welcome and snapshot.
A member who does not come back leaves the lobby when it can no longer rejoin: when its grace runs out, or when the room ends. The host role then moves to the next connected member. Even when every member of a lobby room drops at once, the lobby and its room wait for the grace; the lobby closes only when its last member is gone.
RoomHost.ActiveRoomOf does not report lobby rooms: they are rejoined through their lobby, simply by connecting again.
Reject Reasons
LobbyRejectReason | When |
|---|---|
NotFound | No lobby has this code (on this server, or anywhere in the fleet). |
Full | The lobby already has MaxMembers members. |
TooManyPlayers | Start: the lobby has more members than the mode has seats. |
Unavailable | The server cannot take the request: it is draining (join or start), or full when starting a room. |
Full and Draining Servers
With the host bound to the server node (UseServer, see Match Rooms):
- Full: joining and creating lobbies still work;
Startis refused withUnavailableuntil a room's load fits again. - Draining:
JoinandStartare refused withUnavailable(in a fleet, joins are sent to another server instead). Lobbies that are playing a room keep playing until the room ends. Members without a room get the host'sServerDraining()packet.
In a Fleet
With the host in a fleet (UseFleet), a lobby lives on the server it was created on and its code is claimed there under the unit kind lobby. Joining the code on any other server leads to the lobby's server (IRoomGame.Redirect, reason lobby). A draining server moves its lobbies, with code, members and host, to another server once they are not playing a room, and sends new lobby requests elsewhere (reason drain). The receiving server re-opens the lobby under the same code and gives the old host the host role back with MakeHost. See Fleet & Scaling.
Configuration Reference
LobbyOptions is passed in code (the package does not read it from config.yml):
| Property | Default | Meaning |
|---|---|---|
MaxMembers | 4 | Members per lobby. |
CodeLength | 5 | Characters in a new code. |
CodeAlphabet | ABCDEFGHJKLMNPQRSTUVWXYZ23456789 | Code characters (no I, O, 0 or 1). |
BotSeatIdBase | 50 | First seat id of the bots added at start. Members use their member ids. |
Playlist | "private" | The playlist name lobby rooms carry. |
Rules | new RoomRules() | The room rules of lobby rooms. By default a seat waits for its player, and leaving removes it. |