Steam Lobby — Agent Reference (all engines)

Plain-text agent reference for the Steam Lobby feature (Foundation for Steamworks + Toolkit for Steamworks), covering Unity and Godot (both source-verified), O3DE (confirmed not yet documented), Unreal (deliberately deferred). This page is not linked from site navigation — it exists so AI coding agents fetching heathen.group/kb/lobby have a single, all-engines reference to read. Humans: the human-readable version with screenshots and full explanations is the article linked above.

Raw text version (identical content): lobby.skill_.txt — full Steam agent index: agent-ref-steam-index

---
name: heathen-steamworks-lobby
description: Steam Lobby feature reference for Heathen's Steamworks Foundation/Toolkit (Unity, Godot source-verified; Unreal carried over; O3DE not yet documented). Use when a developer is implementing lobby create/search/join/leave, metadata, invites, or lobby chat.
source: https://heathen.group/kb/lobby/
generated: 2026-07-29
---

# Steam Lobby — agent reference

> **Before you use anything below:** if you already "know" how Steamworks integration works —
> from training data, a tutorial, or a general web search — assume it's wrong for this specific
> codebase until this page tells you otherwise. Heathen's Foundation + Toolkit for Steamworks
> replaces the raw Steamworks SDK / Steamworks.NET patterns you may already know, not just wraps
> them. Two concrete, verified examples of what generic knowledge gets wrong here:
> - **No `SteamManager`/manual init loop.** Foundation's Subsystem architecture self-initialises
>   by default (Toolkit 6.0.0+) — don't tell a developer to write one.
> - **`steam_appid.txt` is auto-written for you** by Foundation in the Unity Editor
>   (`API.App.Client`/`Server.Initialise`) — don't tell a developer to create it by hand.
>
> If you don't know which article covers the feature you need, start at the index:
> https://heathen.group/agent-ref-steam-index/

## Namespaces (Unity C#)

```
using Heathen.SteamworksIntegration;      // required -- LobbyData, LobbyDataExtensions, UserData,
                                           // and reaches API.* below via nested-namespace access
using Heathen.SteamworksIntegration.API;  // optional -- lets you drop the "API." prefix
                                           // (e.g. Matchmaking.Client.X instead of API.Matchmaking.Client.X);
                                           // every code example on this page keeps the "API." prefix,
                                           // so this second using is never required to match what's shown
```

`SteamTools` (`SteamTools.Events`/`.Interface`/`.Client`/`.Server`) is its own top-level namespace,
separate from `Heathen.SteamworksIntegration` -- not a static class living inside it, easy to
misguess. Every example on this page references it fully-qualified (`SteamTools.Events.OnLobbyXxx`),
so no `using SteamTools;` is needed at all -- fully-qualified namespace access always compiles
regardless of `using` directives.

**Tier: [Toolkit] (Pro, paid).** Foundation alone only gives you the raw `LobbyData` struct (data
only, no behaviour) — every create/join/search/chat/metadata call below requires the paid
Toolkit's `LobbyDataExtensions`. See the tier index above for what's free (Foundation) vs. paid
(Toolkit) across the rest of Steam KB.

This is a condensed, all-engines-at-once version of the human KB article at the `source` URL
above. The live page gates each engine's code behind a `user_platform` cookie (defaults to
Unity), so an agent fetching the rendered page will usually only see Unity's content. This file
has all engines inline so you don't need to guess or re-fetch per cookie. Unity, Godot, and
Unreal below are all source-verified; O3DE has no published Lobby examples yet (say so, don't
invent one).

**Your job when a human points you here:** figure out which engine/language they're using, jump
to that column in each feature below, and help them wire it up. If something below is marked
"not verified" or "no facade", say so plainly rather than inventing a call that doesn't exist.

## Core concept (engine-agnostic)

A Steam Lobby is **not** a networking system — Valve's own backend calls it a "chat room," not a
lobby. It's independent of your netcode: you can join/use a lobby with zero network connections
active, and vice versa. It has two kinds of metadata:
- **Lobby metadata** — key/value strings, set by the owner, public to anyone who can see/search
  the lobby (map, mode, rules).
- **Member metadata** — key/value strings, set by each member for themselves, visible only to
  other members of that same lobby (loadout, ready-state, team choice).

A user can be in 1 "Normal" lobby + up to 2 "Invisible" lobbies at once. Three conventional *uses*
of a plain lobby (not distinct Steam types — just a `z_heathenMode` metadata convention Heathen's
Toolkit applies on top of Steam's real `ELobbyType`):
- **Session Lobby** — where matchmaking/game-prep happens (the "Play" button lobby).
- **Party Lobby** — friends/team grouping before picking a session (a "Fire Team").
- **General Lobby** — anything else; just a chat room with metadata.

Lobby chat messages are raw `byte[]` — you decide the payload format (text/JSON/binary). No
ordering/delivery guarantees; treat as lightweight coordination, not a reliable channel.

## Toolkit 6.0.0+ API shape change (read this before trusting any Unity static call)

As of **Toolkit 6.0.0 / Foundation 1.63.13** (Unity), `LobbyData` moved to **Foundation for
Steamworks** as a pure data struct — it no longer carries any of its own methods. **All
functionality — static and instance — now lives on Toolkit's `LobbyDataExtensions` class**
(C# extension methods). Practical effect:
- Instance calls are unaffected: `lobby.Leave()`, `lobby.SendChatMessage(...)`, etc. still just work.
- **Static calls changed namespace**: `LobbyData.Create(...)` is now `LobbyDataExtensions.Create(...)`,
  same for `CreateParty`/`CreatePublicSession`/`CreatePrivateSession`/`CreateFriendOnlySession`/
  `Request`/`MemberOfLobbies`/`SessionLobby`/`PartyLobby`.
- If you're reading an old snippet (blog post, old Asset Store review, pre-6.0.0 forum answer)
  that calls `LobbyData.Create(...)` directly, mentally rewrite it to `LobbyDataExtensions.Create(...)`.
- Events also moved to a real C# event pattern: `SteamTools.Events.OnLobbyXxx += Handler;`
  (not `.AddListener(...)` — that's the old, unshipped API some pre-6.0.0-era docs describe).
  `Matchmaking.Client.OnLobbyXxx` re-fires the same notifications as a convenience wrapper — either
  works, but `SteamTools.Events` is the primary/canonical source.

## Engine status at a glance

| Engine | Status |
|---|---|
| **Unity** | Full C# API + visual/inspector "Code Free" components. Reference below. |
| **Godot** | Full GDScript + C# facade for every feature below, including lobby/chat/invite signals via `Heathen.SteamworksIntegration.SteamToolsEvents` (confirmed against source this pass — see the Toolkit 6.0.0+-style facade note below). No visual/no-code authoring exists yet (Toolkit's editor plugin is a placeholder) — Godot is always code. `GetMemberOfLobbies` remains GDScript/native-singleton-only, no C# facade. |
| **Unreal** | Full Blueprint + C++ API. |
| **O3DE** | **Not yet documented.** No verified Lobby examples exist for O3DE at time of writing (confirmed again this pass — no O3DE-gated content exists in the source KB article at all) — don't fabricate one; tell the human to check with Heathen directly or watch the KB article for updates. |

---

## Member of (get your current General/Session/Party lobby)

**Unity C#**
```csharp
foreach (var lobby in LobbyDataExtensions.MemberOfLobbies) { }
if (LobbyDataExtensions.SessionLobby(out var lobby)) { }
if (LobbyDataExtensions.PartyLobby(out var lobby)) { }
```
Also available Code Free via the Lobby component's "Load" field (Any/General/Session/Party).

**Godot GDScript**
```gdscript
for lobby_id in SteamApi.GetMemberOfLobbies():
    pass
```
**Godot C#** — no facade; call the native singleton directly:
```csharp
Engine.GetSingleton("SteamApi").Call("GetMemberOfLobbies");
```

**Unreal C++**
```cpp
USteamToolsSubsystem* SteamTools = USteamToolsSubsystem::GetSteamToolsSubsystem();
const TArray<int64>& Lobbies = SteamTools->MemberOfLobbies;
```
Blueprint: same data exposed as a subsystem property, no C++ required.

---

## Create a lobby

**Unity C#**
```csharp
ELobbyType type;         // Private / FriendsOnly / Public / Invisible
SteamLobbyModeType mode; // General / Session / Party
int slots;
LobbyDataExtensions.Create(type, mode, slots, HandleLobbyCreate);

LobbyDataExtensions.CreateParty(slots, HandleLobbyCreate);              // Invisible + Party
LobbyDataExtensions.CreatePublicSession(slots, HandleLobbyCreate);       // Public + Session
LobbyDataExtensions.CreatePrivateSession(slots, HandleLobbyCreate);      // Private + Session
LobbyDataExtensions.CreateFriendOnlySession(slots, HandleLobbyCreate);   // FriendsOnly + Session

void HandleLobbyCreate(EResult result, LobbyData lobby, bool ioError) { }
```
Code Free: Lobby component's Create settings expose Party Wise / Usage Hint / Slots / Type
directly in the inspector — "Party Wise" auto-invites/joins your party when a session is created.

**Godot** — one generic call, no per-type shortcuts:
```gdscript
SteamApi.CreateLobby(0, 0, 4, func(lobby, result, io_error): pass) # type=Private, hint=General
```
```csharp
API.Matchmaking.Client.CreateLobby(LobbyType.Private, LobbyUseHint.General, 4, (lobby, result, ioError) => { });
```

**Unreal C++** (raw Steamworks callback pattern, no Toolkit wrapper shown):
```cpp
CCallResult<YourClassName, LobbyCreated_t> m_LobbyCreate_t;
SteamAPICall_t handle = SteamMatchmaking()->CreateLobby(static_cast<ELobbyType>(type), maxMembers);
m_LobbyCreate_t.Set(handle, this, &YourClassName::SteamCallback);
```
Blueprint: a single Create Lobby node, same params.

---

## Search for lobbies

**Unity C#**
```csharp
SearchArguments args = new();
args.distance = ELobbyDistanceFilter.k_ELobbyDistanceFilterDefault;
args.stringFilters.Add(new() { key = "SomeKey", value = "SomeValue", comparison = ELobbyComparison.k_ELobbyComparisonEqual });
LobbyDataExtensions.Request(args, 1, (Lobbies, IOError) => { });
```
Filters available: distance, slots-available, string, near-value, numeric, max-results (Steam
hard-caps results at 50 — it's matchmaking, not a full server browser).

**Godot**
```gdscript
var q := LobbyQuery.new()
q.SetStringFilter("SomeKey", "SomeValue", 0) # 0 = Equal
q.ExecuteQuery(func(lobbies, io_error): pass)
```
```csharp
LobbyQuery q = API.Matchmaking.Client.NewQuery();
API.Matchmaking.Client.Search(q, (lobbies, ioError) => { });
```

**Unreal C++** (raw filter calls, then `RequestLobbyList` + callback):
```cpp
SteamMatchmaking()->AddRequestLobbyListDistanceFilter(filter);
SteamMatchmaking()->AddRequestLobbyListFilterSlotsAvailable(slotsAvailable);
SteamMatchmaking()->AddRequestLobbyListStringFilter(Key, Value, Comparison);
SteamMatchmaking()->AddRequestLobbyListResultCountFilter(maxResults);
SteamAPICall_t handle = SteamMatchmaking()->RequestLobbyList();
```

---

## Join a lobby

**Unity C#**
```csharp
lobby.Join((Result, IOError) => {
    // Result.Lobby, Result.Response, Result.Locked
});
```
Code Free: Lobby component's Join field, by typed hex ID or an Input Field.

**Godot**
```gdscript
SteamApi.JoinLobby(some_lobby, func(lobby, response, locked): pass)
SteamApi.JoinLobbyByHex(hex_id, func(lobby, response, locked): pass)
```
```csharp
API.Matchmaking.Client.JoinLobby(someLobby, (lobby, response, locked) => { });
```

**Unreal C++**
```cpp
CCallResult<YourClassName, LobbyEnter_t> m_LobbyEnter_t;
SteamAPICall_t handle = SteamMatchmaking()->JoinLobby(lobbyId);
m_LobbyEnter_t.Set(handle, this, &YourClassName::SteamCallback);
```

---

## Leave a lobby

**Unity C#**
```csharp
lobby.Leave();
```

**Godot**
```gdscript
SteamApi.LeaveLobby(my_lobby)
```
```csharp
API.Matchmaking.Client.LeaveLobby(myLobby);
```

**Unreal C++**
```cpp
USteamToolsSubsystem* SteamTools = USteamToolsSubsystem::GetSteamToolsSubsystem();
SteamTools->MemberOfLobbies.Remove(LobbyIdValue); // keep local state in sync
SteamMatchmaking()->LeaveLobby(LobbyId);
```

---

## Invite a user to a lobby

Accepting isn't automatic — see "Accept lobby invite" below for the full flow.

**Unity C#**
```csharp
User.InviteToLobby(Lobby);          // from the user
Lobby.InviteUserToLobby(User);      // or from the lobby
Overlay.Client.ActivateInviteDialog(Lobby); // or let Steam's own overlay UI handle picking a friend
```

**Godot**
```gdscript
my_friend.InviteToLobby(my_lobby)
SteamApi.InviteUserToLobby(my_lobby, my_friend)
SteamApi.ActivateLobbyInviteDialog(my_lobby)
```
```csharp
myFriend.InviteToLobby(myLobby);
myLobby.InviteUser(myFriend);
myLobby.ActivateInviteDialog();
```

**Unreal C++**
```cpp
SteamMatchmaking()->InviteUserToLobby(lobbyId, userId);
```

---

## Accept a lobby invite

Two paths: the invited user is **already in-game** (you get an event, still must call Join
yourself), or **not running the game** (Steam launches it with the lobby ID on the command
line — see next section). Clicking "Accept" in Steam Friend Chat never auto-joins for you.

**Unity C#**
```csharp
SteamTools.Events.OnLobbyInvite += HandleLobbyInvite;
private void HandleLobbyInvite(UserData fromUser, LobbyData forLobby, GameData inGame) { }

SteamTools.Events.OnLobbyJoinRequested += HandleLobbyJoinRequest;
private void HandleLobbyJoinRequest(LobbyData Lobby, UserData User) { }
```
Code Free: add "General Events" and use the Lobby Invite Received / Lobby Join Requested events.

**Godot** — signal names are prefixed `On`, not `Event` (confirmed against the native
`ADD_SIGNAL` bindings), and each carries typed args, not one generic callback param:
```gdscript
SteamApi.OnLobbyInvite.connect(func(user_id, lobby_id): pass)
SteamApi.OnGameLobbyJoinRequested.connect(func(lobby_id, friend_id): pass)
```
**Godot C#** — real facade, `Heathen.SteamworksIntegration.SteamToolsEvents` (auto-connected by
`SteamToolsInterface.Initialise()`):
```csharp
SteamToolsEvents.OnLobbyInvite += HandleLobbyInvite; // (ulong userId, ulong lobbyId)
SteamToolsEvents.OnGameLobbyJoinRequested += HandleJoinRequested; // (ulong lobbyId, ulong friendId)
```

**Unreal** — Blueprint events "Lobby Invite Received" / "Lobby Join Requested" on the Steam Game
Instance.
```cpp
SteamToolsSubsystem->SteamLobbyJoinRequested.AddDynamic(this, &YourClassName::FunctionName);
UFUNCTION() void FunctionName(int64 LobbyId, int64 UserId);
```

---

## Detect a lobby on game launch (cold-start invite accept)

If the user accepted an invite while the game wasn't running, Steam launches it with the lobby ID
on the command line.

**Unity C#**
```csharp
LobbyData targetLobby = Matchmaking.Client.GetCommandLineConnectLobby();
if (targetLobby.IsValid) { /* join it once your game is in an appropriate state */ }
```
Code Free: Lobby component's "Command Line" option raises Lobby Join Requested automatically.

**Godot** — partial: only the raw command line is exposed, parsing is DIY, both languages:
```gdscript
var args := SteamApi.GetLaunchCommandLine()
```
```csharp
string args = SteamTools.LaunchCommandLine;
```

**Unreal C++** — parse `+connect_lobby` out of `FCommandLine::Get()` yourself (no built-in
helper); Blueprint side just checks the returned value is non-zero.

---

## Detect join/leave (chat member state changes)

**Unity C#**
```csharp
SteamTools.Events.OnLobbyChatUpdate += HandleChatUpdate;
private void HandleChatUpdate(LobbyData lobby, UserData user, EChatMemberStateChange state)
{
    if (state == EChatMemberStateChange.k_EChatMemberStateChangeLeft) { }
    else if (state == EChatMemberStateChange.k_EChatMemberStateChangeEntered) { }
    else if (state == EChatMemberStateChange.k_EChatMemberStateChangeDisconnected) { }
}
```
Code Free: General Events' On User Left / On User Joined.

**Godot** — signal is `OnLobbyChatUpdate`, not `EventLobbyChatUpdate`, with four typed args:
```gdscript
SteamApi.OnLobbyChatUpdate.connect(func(lobby_id, user_id, making_change_id, change_flags): pass)
```
**Godot C#** — real facade via `SteamToolsEvents`, same as invite events above:
```csharp
SteamToolsEvents.OnLobbyChatUpdate += HandleChatUpdate; // (ulong lobbyId, ulong userId, ulong makingChangeId, int changeFlags)
```

**Unreal** — Blueprint: bind "Lobby Chat Update" on the Steam Game State (gives lobby, who
changed, what changed — the "who made the change" field is Valve-internal, used for
kick/ban plumbing you won't normally see).

---

## Metadata (get/set)

**Unity C#**
```csharp
Lobby.SetData("a simple field", "a simple value");
LobbyMemberData Me = Lobby.GetMe();
Me.SetData("a simple field", "a simple value");
LobbyMemberData Owner = Lobby.GetOwner();
var ownerValue = Owner.GetData("a simple field");
```
Code Free: Metadata settings block, with an "On Changed" per-key callback.

**Godot** — method calls, not indexer syntax:
```gdscript
SteamApi.SetLobbyData(my_lobby, "a simple field", "a simple value")
var value := SteamApi.GetLobbyData(my_lobby, "a simple field")
SteamApi.SetLobbyMemberData(my_lobby, "a simple field", "a simple value")
```
```csharp
myLobby.SetData("a simple field", "a simple value");
string value = myLobby.GetData("a simple field");
myLobby.SetMemberData("a simple field", "a simple value");
```

**Unreal** — Blueprint only shown in the source article (any member can read lobby data; only
the owner can set it; you can also read/set your own member data, but never another member's).

---

## Members (enumerate, ready-state)

**Unity C#**
```csharp
foreach (LobbyMemberData Member in Lobby.GetMembers())
{
    string name = Member.user.Name;
    Member.user.LoadAvatar(texture => { });
    string val = Member.GetData("SomeKey");
    if (Member.GetIsReady()) { } else { }
}
```
Code Free: Members field with Show Self / Template / Content settings — Template should
implement the Steam Lobby Member Data component.

**Godot** — ready-state helpers are Toolkit C#-only sugar over metadata, no native GDScript
equivalent confirmed:
```gdscript
for member in SteamApi.GetLobbyMemberList(my_lobby):
    print(member.GetName())
    member.GetAvatar(func(texture): pass)
```
```csharp
List<UserData> members = myLobby.GetMemberList();
foreach (var member in members) Console.WriteLine(member.GetName());
// ready-state: IsMemberReady / SetReady / IsAllReady
```

**Unreal C++**
```cpp
int32 Count = SteamMatchmaking()->GetNumLobbyMembers(LobbyID);
for (int32 i = 0; i < Count; i++) Result[i] = SteamMatchmaking()->GetLobbyMemberByIndex(LobbyID, i);
```

---

## Notify ready to connect (Game Server info)

Steam's "Game Server" mechanism notifies lobby members a session is ready to connect to — used
for both dedicated servers **and** listen-server/P2P setups.

**Unity C#**
```csharp
Lobby.SetGameServer();                          // owner (listen server) is the server
Lobby.SetGameServer(fakeServerID);               // by CSteamID
Lobby.SetGameServer("0.0.0.0", 7777);            // by IP:Port
Lobby.SetGameServer("0.0.0.0", 7777, fakeServerID);

if (Lobby.GetHasServer())
{
    LobbyGameServer server = Lobby.GetGameServer();
    // server.id, server.IpAddress, server.port
}

SteamTools.Events.OnLobbyGameServer += HandleGameServerSet;
private void HandleGameServerSet(LobbyData lobby, CSteamID serverId, string ip, ushort port) { }
```

**Godot** — real native calls, not metadata sugar (confirmed bound directly on `SteamApi`):
```gdscript
SteamApi.SetLobbyListenServer(my_lobby)
SteamApi.SetLobbyDedicatedServer(my_lobby, server_id, "0.0.0.0", 7777)
if SteamApi.LobbyHasGameServer(my_lobby):
    var id := SteamApi.GetLobbyServerId(my_lobby)
    var ip := SteamApi.GetLobbyServerIp(my_lobby)
    var port := SteamApi.GetLobbyServerPort(my_lobby)
SteamApi.OnLobbyGameCreated.connect(func(lobby_id, game_server_id, ip, port): pass)
```
```csharp
myLobby.SetListenServer(...) / SetDedicatedServer(...) / HasGameServer() / GetServerId() / GetServerIp() / GetServerPort()
SteamToolsEvents.OnLobbyGameCreated += HandleGameServerCreated; // (ulong lobbyId, ulong gameServerId, uint ip, ushort port)
```

**Unreal** — Blueprint: owner sets Game Server info, members get an event on the Steam Game
Instance telling them to connect; also check for an already-set server on join.

---

## Chat (send/receive)

**Unity C#**
```csharp
Lobby.SendChatMessage("Hello World");      // string
Lobby.SendChatMessage(SomeObjectIHave);    // or a serialisable object (JsonUtility)

SteamTools.Events.OnLobbyChatMsg += HandleChatMessage;
private void HandleChatMessage(LobbyChatMsg ChatMsg)
{
    // ChatMsg.Message, ChatMsg.FromJson<T>(), ChatMsg.sender, ChatMsg.lobby,
    // ChatMsg.ReceivedTime, ChatMsg.type (normally k_EChatEntryTypeChatMsg)
}
```

**Godot** — both send and receive have a real facade now; the signal is `OnLobbyChatMsg`, not
`EventLobbyChatMsg`, and carries the lobby/user IDs alongside the message:
```gdscript
SteamApi.SendLobbyChatMessage(my_lobby, "Hello World")
SteamApi.OnLobbyChatMsg.connect(func(lobby_id, user_id, message): pass)
```
```csharp
myLobby.SendChatMessage("Hello World");
SteamToolsEvents.OnLobbyChatMsg += HandleChatMessage; // (ulong lobbyId, ulong userId, string message)
```

**Unreal** — messages are serialized `byte[]`; Blueprint auto-parses to string if you sent a
string, otherwise deserialize the bytes yourself.

---

## If you're stuck

- Full human-readable article (all prose, screenshots, Code-Free inspector field docs this file
  compresses out): https://heathen.group/kb/lobby/
- This file is generated from that article's raw content; if something here looks wrong, the
  article is the fallback source of truth, not this file's memory.

---
Back to the full Steam agent index: https://heathen.group/agent-ref-steam-index/