Getting Started
Installation
Download a pre-compiled binary from the releases page. Pick the binary for your
platform and rename it to luasteam.dll (Windows) or luasteam.so (Linux) or
luasteam.so (macOS). Copy it to the same directory as your Lua files so that
require 'luasteam' works.
When deploying via LOVE, you probably want to put the luasteam binary next to the .love file. Note that, by default, Lua will not load binaries from there, so you need to modify your package.cpath variable to tell Lua where to look for libraries. One example of how to do that is below. Also see the Lua Reference.
local ext = package.config:sub(1, 1) == '\\' and 'dll' or 'so'
package.cpath = string.format("%s;%s/?.%s", package.cpath, love.filesystem.getSourceBaseDirectory(), ext)
On Linux you may need LD_LIBRARY_PATH=. lovec ... when running with LÖVE. This tells bash to look in the current directory for shared libraries.
See the examples/basic directory for a minimal working example.
Warning
You also need the Steamworks shared library in the same directory. Download the SDK from the Steamworks website and copy the file for your platform:
Linux 64:
sdk/redistributable_bin/linux64/libsteam_api.soWindows 64:
sdk/redistributable_bin/win64/steam_api64.dllWindows 32:
sdk/redistributable_bin/steam_api.dllmacOS:
sdk/redistributable_bin/osx/libsteam_api.dylib
This version of luasteam is guaranteed to work with Steamworks SDK v1.64 and likely works with later versions.
When developing, make sure Steam is running and place a steam_appid.txt file
(containing your App ID, or 480 for the Spacewar test app) next to your
executable.
How to Use
luasteam aims to be a thin wrapper around the C++ Steamworks API. It tries to follow the C++ API as closely as possible, with only a few adjustments where necessary to fit Lua’s semantics. If you’re familiar with the C++ API, you should be able to pick up luasteam easily.
Always refer to the official Steamworks API documentation for details on what each function does, what parameters it takes, and what callbacks it triggers. The reference provided here aims to explain how to call the API from Lua, and make explicit any differences from the C++ API, but it does not explain what the API does.
The rest of the document covers the most important differences and adjustments in luasteam compared to the C++ API.
IDE Autocompletion
The luals/ directory contains .d.lua type definitions for the entire API.
Point the Lua Language Server at
it for autocompletion, inline docs, and type checking in your editor.
Add to .luarc.json:
{
"workspace.library": ["path/to/luasteam/luals"]
}
Basic Usage
Naming Conventions
Every interface name and every method name is PascalCase, to match the C++ API as closely as possible. So SteamUserStats()->GetAchievement(ach) becomes Steam.UserStats.GetAchievement(ach).
Steam.Friends.ActivateGameOverlay("achievements")
Steam.UserStats.RequestCurrentStats()
Steam.Apps.BIsDlcInstalled(dlcAppId)
// In C++ this would be:
SteamFriends()->ActivateGameOverlay("achievements");
SteamUserStats()->RequestCurrentStats();
SteamApps()->BIsDlcInstalled(dlcAppId);
The interface is accessed as a sub-table of Steam:
Steam.<Interface>.<Method>(args)
Examples:
Steam.User.GetSteamID()
Steam.Utils.GetAppID()
Steam.NetworkingSockets.CreateListenSocketIP(addr, 0)
// In C++ this would be:
SteamUser()->GetSteamID();
SteamUtils()->GetAppID();
SteamNetworkingSockets()->CreateListenSocketIP(&addr, 0);
For overloaded methods, luasteam appends a type suffix:
GetStatInt32, GetStatFloat, SetStatInt32, SetStatFloat.
Initialization and Shutdown
local Steam = require 'luasteam'
local ok = Steam.Init()
if not ok then
print("Steam initialization failed")
end
-- ... game loop ...
Steam.Shutdown()
// In C++ this would be:
#include <steam/steam_api.h>
if (!SteamAPI_Init()) {
printf("Steam initialization failed\n");
}
// ... game loop ...
SteamAPI_Shutdown();
Steam.Init() returns true on success and false if Steam is not running or
the app ID is not found. Steam.Shutdown() must be called when the game closes.
See the Steamworks overview for more details.
Calling Callbacks Every Frame
Both persistent callbacks and one-shot call results are dispatched only when you call:
Steam.RunCallbacks()
// In C++ this would be:
SteamAPI_RunCallbacks();
Call this once per frame (e.g. in your game loop or love.update). Without it,
no callbacks will fire.
Enums and Constants
All C++ enum constants are flat on the Steam table under their exact C++ names
(including the k_ prefix). There are no grouped sub-tables:
Steam.k_EResultOK
Steam.k_EItemStateInstalled
Steam.k_ELeaderboardSortMethodDescending
Steam.k_ESteamNetworkingSend_Reliable
Compare return values and callback fields against these constants directly:
if data.m_eResult ~= Steam.k_EResultOK then
print("Error:", data.m_eResult)
end
// In C++ this would be:
if (data.m_eResult != k_EResultOK) {
printf("Error: %d\n", data.m_eResult);
}
Bit Flags
Some values are bitmasks. Use the LuaJIT bit library for bitwise tests:
local state = Steam.UGC.GetItemState(itemId)
if bit.band(state, Steam.k_EItemStateInstalled) ~= 0 then
print("Installed!")
end
// In C++ this would be:
EItemState state = (EItemState)SteamUGC()->GetItemState(itemId);
if (state & k_EItemStateInstalled) {
printf("Installed!\n");
}
64-bit Integers
Lua numbers cannot safely represent 64-bit integers. Wherever the Steamworks API uses
uint64 (SteamIDs, leaderboard handles, item handles, etc.), luasteam uses a
userdata value.
Convert to string with
tostring(id)orid:tostring()Compare with
id1 == id2Convert to a Lua number with
id:tonumber()Parse from a string or number with
Steam.Extra.ParseUint64(str)
local myId = Steam.User.GetSteamID() -- uint64 userdata
print(tostring(myId)) -- "76561198000000000"
local parsed = Steam.Extra.ParseUint64("76561198000000000")
assert(parsed == myId) -- true
// In C++ this would be:
CSteamID myId = SteamUser()->GetSteamID();
printf("%llu\n", myId.ConvertToUint64());
CSteamID parsed(76561198000000000ULL);
assert(parsed == myId);
Note
Arithmetic and comparison operators are not supported. Convert to a number first if you need to do math – use x:tonumber() (tonumber(x) is not supported). This conversion is lossy for values above 2^53, so be careful.
Structs
Many functions take or return C++ structs, exposed as Lua userdata. They can be built with Steam.newStructName { field = value, ... } or by setting fields after construction. Structs returned by Steam methods can be read directly. Some examples:
-- Constructing a struct:
local addr = Steam.newSteamNetworkingIPAddr()
addr.m_port = 27015
-- Or inline:
local addr = Steam.newSteamNetworkingIPAddr { m_port = 27015 }
-- Reading struct fields returned by a function:
local ok, entry, details = Steam.UserStats.GetDownloadedLeaderboardEntry(h, i, 0)
print(entry.m_nGlobalRank, entry.m_nScore)
// In C++ this would be:
SteamNetworkingIPAddr addr;
addr.Clear();
addr.m_port = 27015;
LeaderboardEntry_t entry;
int details[1];
SteamUserStats()->GetDownloadedLeaderboardEntry(h, i, &entry, details, 0);
printf("%d %d\n", entry.m_nGlobalRank, entry.m_nScore);
Special: SteamNetworkingMessage_t
This struct works a little differently than the rest. It must be explicitly released after use. Furthermore, it can never be explicitly built (there’s no newSteamNetworkingMessage_t method), and must acquired using Steam methods. Different than other structs, the Lua userdata just carries a pointer to it, not its value. Example usage:
local count, msgs = Steam.NetworkingSockets.ReceiveMessagesOnConnection(conn, 32)
for i = 1, count do
local msg = msgs[i]
print(msg.m_pData) -- payload as a Lua string
msg:Release() -- REQUIRED
end
// In C++ this would be:
SteamNetworkingMessage_t *msgs[32];
int count = SteamNetworkingSockets()->ReceiveMessagesOnConnection(conn, msgs, 32);
for (int i = 0; i < count; i++) {
printf("%s\n", (char *)msgs[i]->m_pData);
msgs[i]->Release();
}
Callbacks
Persistent Callbacks
Steam events (overlay opened, stats received, etc.) are handled by assigning a
function to a field on the interface table. The field name is On + the C++
callback struct name with _t stripped:
-- GameOverlayActivated_t → OnGameOverlayActivated
function Steam.Friends.OnGameOverlayActivated(data)
if data.m_bActive ~= 0 then
print("Overlay opened")
end
end
-- UserStatsReceived_t → OnUserStatsReceived
function Steam.UserStats.OnUserStatsReceived(data)
if data.m_eResult == Steam.k_EResultOK then
print("Stats ready!")
end
end
// In C++ this would be:
class CallbackListener {
private:
STEAM_CALLBACK(CallbackListener, OnGameOverlayActivated, GameOverlayActivated_t);
};
void CallbackListener::OnGameOverlayActivated(GameOverlayActivated_t *data) {
if (data->m_bActive) {
printf("Overlay opened\n");
}
}
// OnUserStatsReceived skipped for brevity...
Note
Some m_b* boolean fields (e.g. m_bActive, m_bSecure) are typed as
uint8 in the C++ headers and arrive as integers in Lua.
Some callbacks carry no data — they are pure notifications:
function Steam.User.OnSteamServersConnected()
print("Connected to Steam servers")
end
CallResults
Async operations accept a callback function as their last argument. It fires once:
Steam.UserStats.FindLeaderboard("HighScores", function(data, err)
if err then
print("IO error")
elseif not data.m_bLeaderboardFound then
print("Not found")
else
print("Handle:", tostring(data.m_hSteamLeaderboard))
end
end)
// In C++ this would be:
class Listener {
public:
void FindLeaderboard(const char *name);
private:
void OnLeaderboardFindResult(LeaderboardFindResult_t *data, bool err);
CCallResult<Listener, LeaderboardFindResult_t> leaderboardFindResult;
};
void Listener::OnLeaderboardFindResult(LeaderboardFindResult_t *data, bool err ) {
if (err)
printf("IO error\n");
else if (!data->m_bLeaderboardFound)
printf("Not found\n");
else
printf("Handle: %llu\n", data->m_hSteamLeaderboard.ConvertToUint64());
}
// Make the request
void Listener::FindLeaderboard() {
SteamAPICall_t call = SteamUserStats()->FindLeaderboard("HighScores");
leaderboardFindResult.Set(call, this, &Listener::OnLeaderboardFindResult);
}
The callback receives (data, err) where err is true on IO failure. Always
check err before accessing data.
Output Parameters
C++ output pointer parameters are not passed in Lua — they become additional return values:
local ok, achieved = Steam.UserStats.GetAchievement("MY_ACH")
// In C++ this would be:
bool achieved;
bool ok = SteamUserStats()->GetAchievement("MY_ACH", &achieved);
Arrays
Input arrays
When C++ takes (T *arr, int count), pass a table and the size:
local users = { id1, id2, id3 }
Steam.UserStats.DownloadLeaderboardEntriesForUsers(handle, users, #users, callback)
// In C++ this would be:
CSteamID users[] = { id1, id2, id3 };
SteamAPICall_t call = SteamUserStats()->DownloadLeaderboardEntriesForUsers(handle, users, 3);
// set up call result callback...
Output arrays
When C++ writes into a caller-provided array, pass only the size of the buffer. luasteam allocates the array with that size, then returns a table with the content:
local ok, ids, count = Steam.Inventory.GetItemDefinitionIDs(256)
for i = 1, count do print(ids[i]) end
// In C++ this would be:
SteamItemDef_t ids[256];
uint32 count = 256;
SteamInventory()->GetItemDefinitionIDs(ids, &count);
for (uint32 i = 0; i < count; i++) { printf("%d\n", ids[i]); }
The way to recover the size of the returned array depends on the API. Some return it as an additional return value (like the example above), but there are other ways. Always check the documentation for the specific API you’re using. The same pattern applies to output string buffers — pass the max byte size, receive a string:
local ok, value, len = Steam.Inventory.GetItemDefinitionProperty(def, "name", 256)
// In C++ this would be:
char value[256];
uint32 len = sizeof(value);
SteamInventory()->GetItemDefinitionProperty(def, "name", value, &len);
Some APIs use void * for buffers that can be either input or output. These also use lua strings, which might have a \0 byte in the middle.
Passing nil
Input arrays and string parameters marked ? accept nil,
which maps to a C++ NULL. Some Steam APIs treat NULL s differently, like Inventory::GetItemDefinitionProperty which returns a comma-separated list of all the available property names instead of a value. Example usage:
local _, pchValueBuffer, punValueBufferSizeOut = Steam.Inventory.GetItemDefinitionProperty(iDef, nil, 1024)
print(pchValueBuffer) -- a comma-separated list of all the available property names
// In C++ this would be:
char pchValueBuffer[1024];
uint32 size = sizeof(pchValueBuffer);
SteamInventory()->GetItemDefinitionProperty(iDef, "name", pchValueBuffer, &size);
For output arrays or strings, wherever a size or count is marked ? in the documentation (e.g. int?), you may
pass nil instead of a number, so that luasteam sends a NULL output array or string. Some Steam APIs treat NULL s differently, like GetItemDefinitionIDs which doesn’t populate the array but returns the correct array size in punItemDefIDsArraySize. Example usage:
-- After SteamInventoryDefinitionUpdate_t callback
-- First call: pass nil to find out the size we need
local _, _, punItemDefIDsArraySize = Steam.Inventory.GetItemDefinitionIDs(nil)
-- Second call: allocate the right size and get the actual data
local _, items, _ = Steam.Inventory.GetItemDefinitionIDs(punItemDefIDsArraySize)
print(items)
// In C++ this would be:
uint32 count = 0;
SteamInventory()->GetItemDefinitionIDs(nullptr, &count);
// Second call: allocate and populate
SteamItemDef_t *items = new SteamItemDef_t[count];
SteamInventory()->GetItemDefinitionIDs(items, &count);
GameServer Variants
Some interfaces have a GameServer variant for dedicated servers. See the Steam Documentation.
-- Initialize the game server session
Steam.GameServerInit(unIP, usSteamPort, usGamePort, usQueryPort,
eServerMode, pchVersionString)
-- Tick every frame
Steam.GameServerRunCallbacks()
-- GameServer interface variants:
Steam.GameServerUtils -- instead of Steam.Utils
Steam.GameServerNetworkingSockets
Steam.GameServerUGC
-- etc.
Steam.GameServerShutdown()
// In C++ this would be:
SteamGameServer_Init(unIP, usSteamPort, usGamePort, usQueryPort,
eServerMode, pchVersionString);
SteamGameServer_RunCallbacks();
SteamGameServerUtils()->... // instead of SteamUtils()
SteamGameServerNetworkingSockets()->...
SteamGameServerUGC()->...
SteamGameServer_Shutdown();
Migrating from v4
If you are upgrading from luasteam v4, see the Migration Guide from v4 to v5 for a complete list of breaking changes and a quick-reference table.