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. Obtain the redist from partner.steamgames.com, drop it next to ruzitrun.exe during development or next to your shipped launcher exe, and list it under include = [...] in build.toml so ruzit build copies it into Generated/.

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 touches the SDK. SteamPresent is a file-existence check only. To actually wire up the SDK you call Steam.Initialize() explicitly — this is what triggers SteamAPI_Init under the hood. Wrap it in pcall so a missing-export delayload failure (caused by shipping the wrong SDK version of steam_api64.dll) doesn't take your script down:

local Steam = import("Steam")

local SteamReady = false
if Steam.SteamPresent then
    local ok, err = pcall(Steam.Initialize)
    if ok then
        SteamReady = true
    else
        print("Steam init failed: " .. tostring(err))
    end
end

local function on_kill()
    if not SteamReady then return end
    Steam.Achievements.Unlock("FIRST_KILL")
    Steam.Achievements.Store()
end
Why explicit init? Earlier versions called SteamAPI_Init eagerly during import("Steam"). If the user's steam_api64.dll didn't match the SDK version Ruzit was built against, a Windows-level delayload failure (STATUS_DELAYLOAD_PROC_NOT_FOUND) crashed the whole runtime before any Rust code could intercept it. As of 26.6.2 the SDK is only loaded when you call Steam.Initialize() — so a bad DLL only breaks Steam, not the whole game.

Supported Steamworks SDK version

Ruzit's steam Cargo feature is built against the steamworks Rust crate version 0.11.x, which is generated from Steamworks SDK ~1.59 / 1.60 (interface set SteamClient020, SteamUser023, SteamFriends017). The steam_api64.dll you ship with your game has to match this exact interface set — Windows will refuse to bind missing exports at first call and raise STATUS_DELAYLOAD_PROC_NOT_FOUND (0xC06D007F), terminating the process before any Rust code can catch it. Symptoms: the game launches, prints SteamInternal_SetMinidumpSteamID: ... [API loaded no], then dies with exit code -1066598273.

Where to get the exact DLL

The steamworks-sys Rust crate bundles the redistributable binaries it was built against. Just copy them from your local cargo registry:

# Windows host
~/.cargo/registry/src/index.crates.io-*/steamworks-sys-0.11.0/lib/steam/redistributable_bin/win64/steam_api64.dll

# Linux host
~/.cargo/registry/src/index.crates.io-*/steamworks-sys-0.11.0/lib/steam/redistributable_bin/linux64/libsteam_api.so

# macOS host
~/.cargo/registry/src/index.crates.io-*/steamworks-sys-0.11.0/lib/steam/redistributable_bin/osx/libsteam_api.dylib

Drop the file next to ruzitrun.exe during development. For shipping, list it in build.toml's include = [...] so ruzit build copies it into Generated/ next to your launcher exe.

Verification hashes

SHA-256 of the redistributables that ship with steamworks-sys 0.11.0:

PlatformFilenameBytesSHA-256
Windows x64 steam_api64.dll 301,928 dcfaa13aa419a0641917205957dbe15aa472e7cf09a28cf8d3cf429598e67799
Windows x86 steam_api.dll 268,136 e0ccfcdde7bbba55dba4c19007299018a2cf86f92c7bc1df4af134c65dbf8003
Linux x64 libsteam_api.so 395,696 b50b9d313eb554792216762f5cc0b972cfae14149f720440e0f0d6c4563c951f
Linux x86 libsteam_api.so 379,848 2cc33d34854148f4bef515e534b06707704185e364f729cc424a259fceb29cb1
macOS libsteam_api.dylib 627,648 363ab50ce9bf4680d8ddb8c6142e566854803f831769fab0428867b9994d0520

If your file size matches but the hash doesn't, your DLL is from a different Steamworks SDK release with the same payload size. The hash is the authoritative check.

What about newer Steamworks SDK releases?

Valve adds new interfaces (SteamFriends018, SteamUser024, etc.) every couple of SDK releases. A newer DLL removes the old interface exports the steamworks crate is hard-wired to import — that's why a 317 KB DLL from a 2025 release crashes against the bindings expecting the 301 KB DLL. To use a newer SDK you'd have to bump the steamworks Cargo dep to a matching version (the crate is maintained out-of-tree from Valve's release cadence). Stick with the bundled binary unless you have a specific reason to chase the latest SDK.

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, and v3.7 shipped a ruzit fetch-deps helper that downloaded it from a user-configured URL. Both are removed as of 26.6.2 — the redistributable now lives next to your binaries by hand or via build.toml's include = [...]. 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"