-
Notifications
You must be signed in to change notification settings - Fork 0
Lobbies
Since v0.3.0. steam.lobbies is the curated layer over ISteamMatchmaking:
create, join and search lobbies, read and write lobby data, send lobby chat.
Four methods are async: create, join and list
await their call result through the dispatch pump, and
requestData awaits a callback. Everything else reads or writes
the client's local lobby cache, which only holds lobbies this user is in or
has just found through list. A cached read for any other lobby answers with an
empty string, an empty array or 0, not with an error.
Failures come out in three shapes: a refused async call throws
SteamResultError with the EResult attached, a flat call that returns false
throws a plain Error, and a rejected join throws an Error naming the
EChatRoomEnterResponse, which is a different enum entirely. See
Errors.
The Lobbies instance is created lazily and cached on the Steam object. All
lobby ids are CSteamID values, so bigint.
create(type: number, maxMembers: number): Promise<bigint>Creates a lobby and joins it. The caller becomes the owner.
| Parameter | Type | Meaning |
|---|---|---|
type |
number |
ELobbyType: 0 private, 1 friends-only, 2 public, 3 invisible, 4 private unique. |
maxMembers |
number |
Member limit including the owner. Steam allows at most 250. |
Steam destroys a lobby that has no members left, so a lobby only outlives the process when somebody else joined it.
import { init, flat } from 'steamwand.js';
const steam = init({ appId: 480 });
const lobbyId = await steam.lobbies.create(flat.ELobbyType.k_ELobbyTypePublic, 4);
steam.lobbies.setData(lobbyId, 'map', 'de_dust2');
steam.close();Throws SteamResultError with operation: 'CreateLobby', for example
k_EResultLimitExceeded.
join(lobbyId: bigint): Promise<void>Joins an existing lobby. Once it resolves, the lobby data and the member list are in the local cache, so getData, listData and getMembers answer for it right away.
A failed join does not carry an EResult. Steam answers LobbyEnter_t with
an EChatRoomEnterResponse, a different enum, so there is no SteamResultError
to catch here: the thrown Error names the response instead, for example
steamwand: JoinLobby failed: k_EChatRoomEnterResponseFull. The other values
you will meet are ...DoesntExist, ...NotAllowed, ...Banned and
...RatelimitExceeded.
const [first] = await steam.lobbies.list({ slotsAvailable: 1 });
if (first) {
await steam.lobbies.join(first);
console.log(steam.lobbies.getMembers(first));
}leave(lobbyId: bigint): voidLeaves a lobby. Steam has no result for this, so it cannot fail from JavaScript. Leaving as the last member destroys the lobby; leaving as the owner hands ownership to another member.
list(opts?: LobbySearchOptions): Promise<bigint[]>Searches for lobbies of this app and returns the matching ids, at most 50, best match first.
The filters apply to this one request and to nothing after it. Steam keeps
them on the client until RequestLobbyList goes out and then clears them, so
every call needs its own opts. There is no way to set a filter once and reuse
it, and a second list() with no opts is unfiltered.
Every match is in the local cache when the promise resolves, so getData and
getMemberLimit answer for a lobby you have not joined.
const ids = await steam.lobbies.list({
stringFilters: { map: 'de_dust2' },
slotsAvailable: 1,
maxResults: 10,
});
console.log(ids.map((id) => steam.lobbies.getData(id, 'map')));getMembers(lobbyId: bigint): bigint[]The Steam ids of everyone currently in the lobby, owner included. Reads the local cache, so it only answers for a lobby this user is in and gives an empty array otherwise.
getOwner(lobbyId: bigint): bigintThe owner's Steam id, or 0n when the user is not in that lobby.
setOwner(lobbyId: bigint, steamId: bigint): voidHands ownership to another member. Only the current owner may do this, and the
new owner must already be in the lobby. Throws
Error: steamwand: SetLobbyOwner returned false (...) otherwise.
getMemberLimit(lobbyId: bigint): numberThe member limit, or 0 when the lobby is not in the local cache.
setMemberLimit(lobbyId: bigint, maxMembers: number): voidChanges the limit, owner only, at most 250. Search results reflect the new limit after the next list.
Since v0.6.0.
setType(lobbyId: bigint, type: number): voidChanges who may find and join the lobby, owner only. type is the same
ELobbyType create takes: 0 private, 1 friends-only, 2 public, 3
invisible, 4 private unique. create fixes the type once, and this is how a
lobby goes public after the party filled up, or back to private.
steam.lobbies.setType(lobbyId, flat.ELobbyType.k_ELobbyTypePublic);Throws Error: steamwand: SetLobbyType returned false (...) when the user is
not the owner.
Since v0.6.0.
setJoinable(lobbyId: bigint, joinable: boolean): voidOpens or closes the lobby to new members, owner only. A lobby that is not joinable still shows up in list, so this is the "match started" switch, not a way to hide the lobby. Use setType for that.
Throws Error: steamwand: SetLobbyJoinable returned false (...) when the user
is not the owner.
inviteUser(lobbyId: bigint, steamId: bigint): voidInvites a user. When they accept, they get a GameLobbyJoinRequested_t
callback carrying the lobby id to pass to join. Subscribe to that with
steam.on, see Core API.
Since v0.6.0.
setGameServer(lobbyId: bigint, server: { steamId?: bigint; ip?: string; port?: number }): voidRecords the game server the members should connect to. This is the handoff at
the end of the lobby screen: the owner starts or picks a server, writes it here,
and every member gets a LobbyGameCreated_t callback carrying the same values.
Give a steamId for a Steam game server, an ip and port for a plain
address, or all three. ip is a dotted quad, which this converts to the
host-order uint32 Steam wants. Anything left out is sent as 0, which Steam
reads as "not set". Steam returns no result, so this cannot fail from
JavaScript.
steam.lobbies.setGameServer(lobbyId, { ip: '192.168.0.10', port: 27015 });Since v0.6.0.
getGameServer(lobbyId: bigint): { steamId: bigint; ip: string; port: number } | nullThe recorded server, or null when the lobby has none yet. A field the owner
did not set reads back as 0n, 0.0.0.0 or 0, so check the one your game
actually uses rather than the object as a whole.
getData(lobbyId: bigint, key: string): stringOne lobby data value. An unset key, or a lobby that is not cached, gives an empty string.
setData(lobbyId: bigint, key: string, value: string): voidWrites one lobby data value, owner only. Keys are capped at 255 UTF-8 bytes and
values at 8192. Every member gets a LobbyDataUpdate_t callback, and lobby data
is exactly what list filters on, so this is how a lobby advertises its
map, mode or version.
Throws Error: steamwand: SetLobbyData returned false (...) when the user is
not the owner or the key is too long.
deleteData(lobbyId: bigint, key: string): voidRemoves one key, owner only. Throws when the user is not the owner or the key was not set.
listData(lobbyId: bigint): Record<string, string>Every key and value at once. Empty when the lobby is not in the local cache.
const lobbyId = await steam.lobbies.create(flat.ELobbyType.k_ELobbyTypePublic, 4);
steam.lobbies.setData(lobbyId, 'map', 'de_dust2');
console.log(steam.lobbies.listData(lobbyId)); // { map: 'de_dust2' }Since v0.6.0.
requestData(lobbyId: bigint): Promise<void>Pulls one lobby's data into the local cache without joining it. Needed for a
lobby this user is not in and did not just find through list, for
example one that arrived in a GameLobbyJoinRequested_t invite. Once it
resolves, getData and listData answer for that lobby,
so an invite can show the map and the mode before the player commits.
steam.on('GameLobbyJoinRequested_t', async (e) => {
await steam.lobbies.requestData(e.m_steamIDLobby);
console.log(steam.lobbies.getData(e.m_steamIDLobby, 'map'));
});Steam answers with a LobbyDataUpdate_t whose member id equals the lobby id.
Throws Error: steamwand: RequestLobbyData failed: lobby <id> no longer exists
when that answer clears its success flag, and
Error: steamwand: RequestLobbyData returned false (...) when Steam refuses the
request outright.
getMemberData(lobbyId: bigint, steamId: bigint, key: string): stringOne value a member set on themselves, for example their chosen character or their ready flag. Empty string when the key is unset.
setMemberData(lobbyId: bigint, key: string, value: string): voidWrites one value on this user's own membership. A member can only write
their own data, which is why there is no Steam id parameter. Every member gets a
LobbyDataUpdate_t callback. Steam has no result for this, so it cannot fail
from JavaScript.
sendChat(lobbyId: bigint, message: string): voidSends a message to every member of the lobby, this user included. The text goes out as UTF-8 with a terminating NUL, which is what onChat and Valve's own samples expect. Max 4096 UTF-8 bytes including the terminator.
Throws Error: steamwand: SendLobbyChatMsg returned false (...) when the user
is not in that lobby or the message is too long.
onChat(lobbyId: bigint, listener: (message: LobbyChatMessage) => void): () => voidSubscribes to the chat of one lobby and returns an unsubscribe function. Messages for other lobbies are filtered out, so one listener per lobby is enough, and this user's own messages arrive too.
The message text is not in the callback. Steam only says "entry number N is
ready", so the payload is fetched with GetLobbyChatEntry inside the same pump
frame, before Steam drops it. The trailing NUL is stripped again, so text round
trips through sendChat unchanged.
const off = steam.lobbies.onChat(lobbyId, (m) =>
console.log(m.senderSteamId, m.message),
);
steam.lobbies.sendChat(lobbyId, 'hello');
// later: off();Calling the returned function more than once is harmless.
Since v0.6.0.
onDataChange(lobbyId: bigint, listener: (change: { memberSteamId: bigint | null }) => void): () => voidSubscribes to the data changes of one lobby and returns an unsubscribe function. Updates for other lobbies are filtered out.
The callback says only that something changed, not what, so read the new values
with getData, listData or
getMemberData inside the listener. memberSteamId is null
when the lobby's own data changed and the member's Steam id when that member
changed their own data. Steam signals the first case by repeating the lobby id
in the member field; this layer turns that into null so there is no bogus
Steam id to filter out.
const off = steam.lobbies.onDataChange(lobbyId, ({ memberSteamId }) => {
if (memberSteamId === null) console.log(steam.lobbies.listData(lobbyId));
else console.log(steam.lobbies.getMemberData(lobbyId, memberSteamId, 'ready'));
});Since v0.6.0.
onMemberChange(lobbyId: bigint, listener: (change: LobbyMemberChange) => void): () => voidSubscribes to the membership changes of one lobby and returns an unsubscribe function. This is how a lobby screen learns that somebody joined, left, dropped or was thrown out.
Steam packs several EChatMemberStateChange bits into one callback, so a
member who timed out arrives as both left and disconnected. The listener
runs once per set bit, in the order entered, left, disconnected, kicked, banned.
const off = steam.lobbies.onMemberChange(lobbyId, (c) => {
console.log(c.steamId, c.change, steam.lobbies.getMembers(lobbyId).length);
});Filters for one list call. Every field is optional, and every field applies to that request only.
| Field | Type | Meaning |
|---|---|---|
stringFilters |
Record<string, string> |
Lobby data keys that must equal these values. Compared as strings, with k_ELobbyComparisonEqual. |
numberFilters |
Record<string, number> |
Lobby data keys that must equal these numbers. Compared as 32-bit integers. |
slotsAvailable |
number |
Only lobbies with at least this many open slots. |
distance |
number |
ELobbyDistanceFilter: 0 close, 1 default, 2 far, 3 worldwide. |
maxResults |
number |
Stop searching after this many matches. Steam returns at most 50 either way. |
Only equality comparisons are exposed. The other ELobbyComparison members and
the near-value and compatible-members filters are on the raw interface, see
below.
Handed to an onChat listener.
| Field | Type | Meaning |
|---|---|---|
senderSteamId |
bigint |
Steam id of the member who sent it. |
message |
string |
The text, decoded as UTF-8, terminator removed. |
Since v0.6.0. Handed to an onMemberChange listener.
| Field | Type | Meaning |
|---|---|---|
steamId |
bigint |
Steam id of the member this happened to. |
bySteamId |
bigint |
Steam id of whoever caused it. The same as steamId unless somebody was kicked or banned. |
change |
'entered' | 'left' | 'disconnected' | 'kicked' | 'banned' |
The EChatMemberStateChange bit that was set. |
| Shape | When |
|---|---|
SteamResultError |
CreateLobby completed with a non-OK EResult. It is the only method here that reports through an EResult. |
Error: steamwand: JoinLobby failed: <EChatRoomEnterResponse name> |
The join was refused. Not an EResult, so not a SteamResultError. |
Error: steamwand: <call> returned false (invalid handle or argument?) |
SetLobbyData, DeleteLobbyData, SetLobbyOwner, SetLobbyMemberLimit, SetLobbyType, SetLobbyJoinable, InviteUserToLobby, RequestLobbyData or SendLobbyChatMsg returned false. Usually: not the owner, not in the lobby, or a value over its cap. |
Error: steamwand: RequestLobbyData failed: lobby <id> no longer exists |
requestData got its answer with the success flag clear. |
SteamApiCallError |
create, join or list never produced a usable result, or completed carrying a different callback struct. See How It Works. |
leave, setMemberData and setGameServer have no failure path at all,
because Steam returns nothing from them.
The rest of ISteamMatchmaking is on the raw generated steam.matchmaking:
-
SetLinkedLobby, which chains a second lobby to this one. - The
LobbyGameCreated_tcallback that setGameServer raises on every member. - The filters
listdoes not expose:AddRequestLobbyListNearValueFilter,AddRequestLobbyListCompatibleMembersFilter, and the non-equalityELobbyComparisonmembers. - The favorite-games list:
GetFavoriteGameCount,GetFavoriteGame,AddFavoriteGame,RemoveFavoriteGame. - Server browsing, which is a separate interface (
ISteamMatchmakingServers) and is not wrapped at all.
GameLobbyJoinRequested_t and LobbyGameCreated_t are not wrapped either.
Subscribe to them with steam.on, which decodes them from the generated
layouts. Flat API explains the calling convention, and Core
API covers steam.on.