Steam

User identity, friends, achievements, stats, lobbies, game-server hosting, the in-game overlay, Cloud saves, Workshop, Remote Play, and rich presence. All wrapped over the Steamworks SDK.

local Steam = import("Steam")

if Steam.SteamPresent then
    print("Hi, " .. Steam.User.Name)
    Steam.Achievements.Unlock("FIRST_KILL")
    Steam.Achievements.Store()
end

Steam.SteamPresent

Boolean. True if the Steam SDK redistributable (steam_api64.dll / libsteam_api.so / libsteam_api.dylib) is present alongside the executable. That's the only thing it checks. Use ruzit fetch-deps to drop the DLL in.

To make built games work standalone (no Steam launch required), the runtime briefly writes steam_appid.txt next to the exe at SDK init, lets Steam read it, then immediately deletes it. The AppID is pulled from your build.toml [steam] table, falling back to 480 (Spacewar) if unset. The file appears for a few milliseconds during startup and never lingers afterward — the AppID is effectively baked into the runtime binary. The user's Steam client must still be running and logged in for actual Steam API calls to succeed; if init fails, individual Steam calls return / error accordingly.

If a steam_appid.txt is already present next to the exe when the runtime starts (e.g., one you placed manually for testing), the runtime leaves it alone — your file wins, and it won't be deleted on shutdown.

Importing Steam never fails on its own anymore, so you can keep the import at the top of any module. Just gate real Steam calls behind Steam.SteamPresent:

local Steam = import("Steam")

local function on_kill()
    if not Steam.SteamPresent then return end
    Steam.Achievements.Unlock("FIRST_KILL")
    Steam.Achievements.Store()
end
Steam app id still matters. Set steam-app-id = N in build.toml (or the RUZIT_STEAM_APPID env var). With SteamPresent = true but no app id configured, init falls back to a default and most APIs will still work in test mode but won't unlock real achievements / friend data.
The Steam DLL is no longer bundled with Ruzit. Earlier versions embedded steam_api64.dll in the runtime binary so it could be written next to the launcher at startup. As of v3.7 you fetch it explicitly with ruzit fetch-deps. This keeps the runtime binary small and avoids redistributing the SDK without a clear license trail.

Steam.OnDisconnected / OnConnected / OnConnectFailure

Top-level signals reporting the local Steam client's connection to the Steam backend. Affects every Steam-backed feature — lobbies, matchmaking, friends, achievements/stat sync, cloud, etc. — so this is the right place to globally pause networking when Steam goes offline.

.OnDisconnectedSignal<string>

Fires with a reason string when the Steam client loses its connection to the Steam backend (network drop, Steam servers down, etc.). Lobby data and friend lists become stale until OnConnected fires.

.OnConnectedSignal<>

Fires (no args) when the Steam client (re)connects. Refresh any cached lobby / friend / matchmaking state on this signal.

.OnConnectFailureSignal<string, boolean>

Fires with (reason, stillRetrying) when Steam fails to reach its backend. If stillRetrying is true, Steam will keep trying; otherwise it has given up and you should surface the error to the user.

.OnLobbyJoinRequestedSignal<string, string>

Fires with (lobbyId, friendId) when a friend accepts a Steam invite to your game while it's already running (typically by clicking Join Game in the Steam overlay). Steam doesn't auto-join — you decide what to do. The usual handler is to call Steam.Lobby.Join(lobbyId) and switch your UI to the lobby/match flow. For cold launches (game wasn't running when the invite was clicked), Steam puts +connect_lobby <id> in the launch command line — read it at startup via Steam.Launch.GetQueryParam("connect_lobby").

If you only care about lobby disconnects, hook SteamLobby.OnDisconnected on the specific lobby — it distinguishes "Disconnected" from "Kicked" / "Banned", and only fires for that lobby.

local Steam = import("Steam")

Steam.OnLobbyJoinRequested:Connect(function(lobbyId, friendId)
    Steam.Lobby.Join(lobbyId):Connect(function(lobby, err)
        if lobby then enter_match_with(lobby) end
    end)
end)

-- Cold launch: game was offline when the invite was clicked.
local pending = Steam.Launch.GetQueryParam("connect_lobby")
if pending then Steam.Lobby.Join(pending):Connect(...) end
local Steam = import("Steam")

Steam.OnDisconnected:Connect(function(reason)
    -- Pause matchmaking UI, show "reconnecting…" toast, etc.
    print("Steam offline:", reason)
end)

Steam.OnConnected:Connect(function()
    print("Steam back online")
end)

Steam.User

.IDSteam ID string.
.NamePersona name.
.LevelSteam community level.
:AccountID()Lower 32 bits of the Steam ID.
:LoggedOn()True while connected to Steam.
:GetFriends(){ SteamFriend } snapshot.
:GetAvatar(size?)ImageAsset of your own avatar.
:GetFriendAvatar(id, size?)Friend / lobby member's avatar.
:GetAvatarAsync(id, size?)Signal-returning lookup that works for any Steam ID.
:GetFriendInfo(id)Persona state, nickname, current game.
:RequestInfo(id, nameOnly?)Force Steam to re-fetch profile data.
:InviteToGame(id, connectString)Send a Steam invite to your game.

Steam.Achievements

name is the API id from the partner site, not the display name. Always call Store() after a batch of unlocks.

.Unlock(name)
.Clear(name)
.IsUnlocked(name)-> boolean
.Store()Push pending unlocks/stats to Steam.

Steam.Stats

.SetInt(name, value)
.SetFloat(name, value)
.GetInt(name)-> number
.GetFloat(name)-> number
.Store()Push to Steam.

Steam.Lobby

Each entry returns a Signal that fires once with the result. Connect once to handle the response.

.Create(maxMembers?, kind?)-> Signal<SteamLobby?, string?>
.Join(id)-> Signal<SteamLobby?, string?>
.List()-> Signal<{ SteamLobbyInfo }>
Steam.Lobby.Create(8, "FriendsOnly"):Connect(function(lobby, err)
    if err then print("create failed: " .. err) return end
    print("lobby " .. lobby.ID .. " up")
end)

Steam.Overlay

.Show(dialog)One of the built-in panels.
.ShowFriends()
.ShowAchievements()
.ShowSettings()
.ShowProfile(id)
.ShowChat(id)
.ShowWebPage(url)
.ShowStore(appId?)
.ShowInviteDialog(lobbyId)

Steam.App

.IsAppInstalled(appId)-> boolean
.IsDlcInstalled(appId)-> boolean
.IsSubscribed()-> boolean
.IsVacBanned()-> boolean
.BuildId()-> number
.InstallDir(appId)-> string
.OwnerID()-> string
.CurrentLanguage()-> string
.AvailableLanguages()-> { string }

Steam.Cloud

Per-user file storage that auto-syncs across devices. Files are scoped to the calling user; quota is set in the partner site.

.WriteFile(name, data)-> boolean

Returns true on success.

.ReadFile(name)-> string?

Nil if the file doesn't exist.

.DeleteFile(name)-> boolean
.FileExists(name)-> boolean
.ListFiles()-> { SteamCloudFile }
.Quota()-> { Total: number, Available: number }

Both fields in bytes.

Steam.Workshop

.SubscribedItems()-> { string }

Item ids the user is subscribed to.

.ItemState(id)-> WorkshopItemState

Live install state. See WorkshopItemState.

.InstallInfo(id)-> WorkshopInstallInfo?

Local install info; nil while not installed.

.DownloadProgress(id)-> { Downloaded: number, Total: number }?

Live progress; nil if not downloading. Both in bytes.

.DownloadItem(id, highPriority?)-> boolean

Force-trigger a download. highPriority jumps the Steam queue.

.GetItem(id)-> WorkshopItem

Combined fetch: state flags + install info + a Files() helper. See WorkshopItem.

Steam.Server

Steam.Server.Start(opts) -> SteamServer

Spin up a Steam game server. Logon mode is set with :LogOnAnonymous().

Steam.RichPresence

.Set(key, value)Set a rich-presence k/v pair.
.Clear()Clear all keys.

Steam.Launch

Metadata about how the current process started. Useful for handling "Join Game" deep links and for branching dev vs. shipped runs.

.GetCommandLine()Steam URL launch params (+connect 1.2.8.4 +map dust).
.GetQueryParam(key)Pull a single +key value pair.
.GetCurrentBeta()Beta branch name or nil.
.GetRunType()Aggregate metadata table; see below.

RunType payload

Source"steam-url" / "steam" / "direct"
WasLaunchedFromSteamboolean
CommandLinestring
AppIdnumber
BuildId / OwnerID / BetaBranch / InstallDir / Languageoptional metadata.
ProcessArgs{ string }
Env{ [string]: string }, selected Steam-related env vars.

Steam.Utils

.Country()ISO country code of the local user.
.AppCount()Steam API level.

Steam.RemotePlay

.Sessions(){ { UserID, ClientName?, Width?, Height? } }

Object reference

Every type returned by Steam methods, in one place. The string-literal types up top are pure aliases, "Public", "Online", etc.

SteamLobby

Returned by Steam.Lobby.Create and Steam.Lobby.Join. Holds member state, lobby-wide and per-member key/value data, and a chat channel.

.IDstringread-only

Lobby's Steam id (decimal string).

.Ownerstringread-only

Owner's Steam id.

.OnMemberJoinedSignal<string>

Fires with the joining member's Steam id.

.OnMemberLeftSignal<string>
.OnDataUpdateSignal<string?>

nil when lobby-wide data changed; otherwise the member id whose per-member data changed.

.OnDisconnectedSignal<SteamDisconnectReason>

Fires when the LOCAL user is involuntarily removed from the lobby. Reason is one of "Disconnected" (network drop / Steam offline), "Kicked", or "Banned". Does NOT fire when you call :Leave() yourself. After this fires the lobby handle is inert.

.OnCloseSignal<>

Fires on every member (including the owner) when the lobby is gracefully closed via owner:Close(). Use this to show "host ended the lobby" UI. After it fires the local user has already auto-left and the handle is inert.

.OnKickSignal<string>

Fires only on the kicked member when the lobby owner calls owner:Kick(myId). Argument is the kicker's Steam id. Use this to show a "you were kicked" popup. After it fires the local user has already auto-left and the handle is inert. Does NOT fire when you call :Leave() yourself.

.OnMessageSignal<string, string>

Fires with (senderSteamId, bytes) when another lobby member sends a P2P packet via :Send or :Broadcast. Bytes are the exact payload the sender passed in. Routed via Steam Networking Sockets (NAT-friendly, encrypted, identified by SteamID — IPs are never exposed). Messages from peers who aren't current members of any of your lobbies are silently dropped.

.IsOwnerbooleanread-only

True if the local user owns this lobby.

.IsClosedbooleanread-only

True once the lobby has been closed by the owner.

lobby:Members()-> { string }method

Snapshot of every member's Steam id.

lobby:SetData(key, value)method

Lobby-wide key/value store. Visible to every member. Keys beginning with _ruzit_ are reserved by the engine and will raise an error.

lobby:GetData(key)-> string?method
lobby:DeleteData(key)-> booleanmethod

Delete a key from the lobby-wide store. Returns true if the key existed.

lobby:GetAllData()-> { [string]: string }method

Full snapshot of every visible lobby-wide key/value pair. Engine-reserved _ruzit_* keys are filtered out.

lobby:MemberLimit()-> number?method

Configured maximum member count, or nil if Steam hasn't pushed the value yet.

lobby:SendChat(message)method
lobby:SetJoinable(joinable)-> booleanmethodowner-only

Owner-only. Toggle whether new players can join via Steam.Lobby.Join.

lobby:Close()methodowner-only

Owner-only. Politely shut the lobby down: marks it unjoinable, broadcasts a closed flag, fires OnClose on every member (including the owner), and auto-leaves everyone. Use this instead of :Leave() when the host wants to end the session for everyone.

lobby:Kick(userId)methodowner-only

Owner-only. Kick a member by Steam id. The targeted client fires OnKick and auto-leaves. Other members observe the leave via OnMemberLeft. You can't kick yourself — use :Close() or :Leave().

lobby:ShowInviteDialog()method

Open Steam's invite-friends overlay for this lobby.

lobby:Send(memberId, data, reliability?)method

P2P send to one specific lobby member. Uses Steam Networking Sockets — relay-fallback, encrypted, NAT-friendly, addressed by SteamID (no IPs ever leaked). The connection is opened lazily on the first send to that peer (one handshake, ~100ms on the relay path, instant on direct LAN).

data is raw bytes — pass any string / buffer holding your serialized payload. reliability is one of "Reliable" (default, ordered, no-drop), "Unreliable" (fast, lossy, no order), "ReliableNoNagle" (reliable but flush immediately), "UnreliableNoDelay" (drop instead of buffer if the link isn't ready). Max ~1 MB per message.

Raises if memberId isn't a current member of this lobby, or is yourself.

lobby:Broadcast(data, reliability?)method

P2P broadcast to every other member of this lobby. Equivalent to iterating :Members() and calling :Send on each (skipping yourself). Same reliability semantics as :Send.

lobby:Leave()method

Leave the lobby. The handle goes inert after this. If you're the owner and want everyone to leave, use :Close().

How :Close() and :Kick() work under the hood. Steam's matchmaking SDK has no native "close lobby" or "kick from lobby" primitive. Ruzit implements both by writing reserved _ruzit_closed and _ruzit_kick_<steamId> keys to the lobby-data store. Every Ruzit client watches those keys and auto-leaves / auto-fires OnClose / OnKick accordingly. Non-Ruzit clients (third-party tools talking directly to Steam matchmaking) won't honor the flags — kicks/closes only work against players running your built game.

P2P networking via the lobby

:Send / :Broadcast / OnMessage are backed by Steam Networking Sockets — Valve's modern P2P transport with NAT punch-through, free relay fallback, end-to-end encryption, and addressing by SteamID (peers never see each other's IPs). Use this for actual gameplay packets; :SendChat is rate- limited and only fits lobby-room chat.

local Steam = import("Steam")

Steam.Lobby.Create(8, "Public"):Connect(function(lobby)
    -- Receive packets from any other member.
    lobby.OnMessage:Connect(function(senderId, bytes)
        print(senderId, "sent", #bytes, "bytes")
    end)

    -- Reliable (default) — ordered, no-loss. For chat, RPCs, state syncs.
    lobby:Broadcast("hello everyone")

    -- Unreliable — fast, may drop. For positions/inputs.
    Heart:Connect(function()
        lobby:Broadcast(serialize_player_state(), "Unreliable")
    end)
end)
Lazy connect, auto-reject. The first :Send to a given peer opens the P2P connection (one round-trip handshake, ~100ms on relay, instant on direct LAN). Connections are torn down automatically when a peer is no longer a member of any lobby you're in. Incoming connections from peers who aren't a member of any of your lobbies are auto-rejected, so random Steam users on the same AppID can't spam your game.

SteamServer

Returned by Steam.Server.Start. The game-server side of the Steamworks SDK.

.IDstringread-only
.OnClientAuthedSignal<string>

Fires with the client Steam id when a client successfully authenticates.

.OnDisconnectedSignal<string>

Fires with a reason string when this hosted server's connection to the Steam backend is lost. The server is no longer reachable through Steam matchmaking until OnConnected fires.

.OnConnectedSignal<>

Fires (no args) when the hosted server (re)establishes its connection to the Steam backend.

.OnConnectFailureSignal<string, boolean>

Fires with (reason, stillRetrying) when the hosted server fails to reach the Steam backend. If stillRetrying is true, Steam will keep attempting; otherwise it has given up.

.OnCloseSignal<>

Fires when the server is being shut down via :Close() or :Stop().

srv:SetServerName(name)method
srv:SetMapName(map)method
srv:SetMaxPlayers(max)method
srv:LogOnAnonymous()method

Anonymous logon, no game-server-account credentials required.

srv:Kick(userId)method

Revoke a connected client's auth ticket (EndAuthSession on the Steam SDK side). The backend will no longer regard them as authenticated. Your game's networking layer should also drop the underlying socket / NetConnection — Steam's gameserver API only handles authentication, not transport.

srv:Close()method

Gracefully shut the server down. Fires OnClose, then tears down the Steam server instance. Identical to :Stop(); pick whichever name reads better. Idempotent.

srv:Stop()method

Stop and clean up. Fires OnClose. Idempotent.

SteamServerOptions

Plain table passed to Steam.Server.Start.

portnumber?, game-server port. Default 27015.
queryPortnumber?, server-query (master-server) port. Default 27016.
modeSteamServerMode?, "NoAuthentication", "Authentication", or "AuthenticationAndSecure".
versionstring?, free-form version Steam uses for compatibility filtering.
name / mapstring?, display name and map tag for server browsers.
maxnumber?, slot count.

SteamFriend

Returned in lists by Steam.User:GetFriends().

IDstring
Namestring, persona name.
StateSteamPersonaState

SteamFriendInfo

Returned by Steam.User:GetFriendInfo(id). Adds nickname + game.

IDstring
Namestring
Nicknamestring?, the local user's nickname for this friend.
StateSteamPersonaState
Game{ AppID: number }?, populated when the friend is in a game.

SteamLobbyInfo

Returned by Steam.Lobby.List, lighter-weight than a live SteamLobby.

IDstring
MemberCountnumber?
Namestring?, whatever the host set as "name" in lobby data.

SteamCloudFile

One entry from Steam.Cloud.ListFiles().

Namestring
Sizenumber, bytes.

WorkshopItemState

Returned by Steam.Workshop.ItemState(id).

Subscribedboolean, user is subscribed to the item.
Installedboolean, install is on disk.
NeedsUpdateboolean, an update is available but not yet downloaded.
Downloadingboolean
DownloadPendingboolean, queued behind other Steam downloads.

WorkshopInstallInfo

Returned by Steam.Workshop.InstallInfo(id) when the item is on disk.

Folderstring, absolute path to the install directory.
SizeOnDisknumber, bytes.
Timestampnumber, Unix epoch seconds of last update.

WorkshopItem

Combined snapshot from Steam.Workshop.GetItem(id), union of WorkshopItemState + WorkshopInstallInfo + a file lister.

IDstring
Subscribed / Installed / NeedsUpdate / Downloading / DownloadPendingboolean, state flags.
Folderstring?, absolute path to the install dir, when installed.
SizeOnDisknumber?, bytes.
Timestampnumber?, last-update Unix seconds.
DownloadProgress{ Downloaded: number, Total: number }?
Files()(() -> { string })?, lists every file in the install folder. Absolute paths, pass straight to Asset.ImportAsset.

RemotePlaySession

One entry from Steam.RemotePlay.Sessions().

UserIDstring, remote user's Steam id.
ClientNamestring?, client device name.
Width / Heightnumber?, stream resolution.

RunType

Returned by Steam.Launch.GetRunType().

Source"steam-url" | "steam" | "direct"
WasLaunchedFromSteamboolean
CommandLinestring, Steam URL launch params.
AppIdnumber
BuildIdnumber?
OwnerIDstring?
BetaBranchstring?
InstallDirstring?
Languagestring?
ProcessArgs{ string }, argv past the executable.
Env{ [string]: string }, selected Steam-related env vars.

String enums

TypeValues
SteamPersonaState "Offline" | "Online" | "Busy" | "Away" | "Snooze" | "LookingToTrade" | "LookingToPlay"
SteamAvatarSize "small" (32×32) | "medium" (64×64) | "large" (184×184)
SteamLobbyKind "Public" | "Private" | "FriendsOnly" | "Invisible"
SteamServerMode "NoAuthentication" | "Authentication" | "AuthenticationAndSecure"
SteamOverlayDialog "Friends" | "Community" | "Players" | "Settings" | "Stats" | "Achievements" | "OfficialGameGroup"
SteamOverlayUserDialog "steamid" | "chat" | "jointrade" | "stats" | "achievements" | "friendadd" | "friendremove" | "friendrequestaccept" | "friendrequestignore"
SteamOverlayStoreMode "None" | "AddToCart" | "AddToCartAndShow"
SteamNotificationPosition "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight"