-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
- Node 18 or newer.
- The Steam client, running and logged in. The library talks to the client, not to the web API.
- Windows x64, Linux x64, Linux ARM64, or macOS (x64 and Apple silicon; the
library is one universal binary). Any other
process.platformmakes the loader throwsteamwand: unsupported platform <name>. Valve ships no Windows ARM64 library. - A Steamworks partner account if you want to work with your own app id. For learning and testing, app id 480 (Spacewar) is public and needs no account.
There is no compiler step. The only runtime dependency is
koffi, and the steam_api redistributables ship inside
the package.
pnpm add steamwand.js
npm install steamwand.js
Steam needs to know which app the process is running as, before
SteamAPI_InitFlat runs. There are two ways to tell it.
The appId option (recommended for Node). init({ appId: 480 }) sets
process.env.SteamAppId and process.env.SteamGameId to '480' and then
initializes. Nothing on disk is needed, and the value is visible to anything
else in the process.
const steam = init({ appId: 480 });
console.log(steam.appId); // 480steam_appid.txt. The Steam library also reads a file named
steam_appid.txt from the process working directory. It holds the app id as
plain text and nothing else. This is the usual arrangement for a shipped game,
where the file sits next to the executable. If you use it, omit appId and
init falls back to Number(process.env.SteamAppId ?? 0) for the steam.appId
property, which is 0 when no environment variable is set. Pass appId
anyway if you want steam.appId and steam.workshop to target the right app.
Testing with 480. Spacewar is Valve's public sample app. Any Steam account can initialize under it, create workshop items on it, and delete them again. Use it while you learn the API, then switch to your own app id.
Save this as first.ts and run it with npx tsx first.ts, with Steam running.
import { init, SteamInitError } from 'steamwand.js';
async function main() {
const steam = init({ appId: 480 });
// `const char *` returns come back as plain strings.
console.log('user:', steam.friends.GetPersonaName());
console.log('language:', steam.apps.GetCurrentGameLanguage());
// 64-bit values are bigint. accountId() is the lower 32 bits, as a number.
console.log('steam id:', steam.steamId());
console.log('account id:', steam.accountId());
// Out parameters are Buffers that you allocate.
const folder = Buffer.alloc(1024);
steam.apps.GetAppInstallDir(480, folder, folder.length);
console.log('install dir:', folder.toString('utf8', 0, folder.indexOf(0)));
// Async Steam calls resolve through the dispatch pump.
const page = await steam.workshop.getUserItems(1, steam.accountId());
console.log('published items:', page.totalResults);
for (const item of page.items) console.log(' ', item.fileId, item.title);
steam.close();
}
main().catch((err) => {
if (err instanceof SteamInitError) console.error('init failed:', err.message);
else console.error(err);
process.exit(1);
});If init throws, the message is Valve's own diagnostic text, not something
this library wrote. See Troubleshooting.
init does four things: it sets the app id environment variables (only when
you pass appId), loads the redistributable, calls SteamAPI_InitFlat, and
starts the manual dispatch pump.
The pump is a setInterval that calls
SteamAPI_ManualDispatch_RunFrame every pumpIntervalMs milliseconds, 50 by
default. Callbacks and call results only arrive while it runs, so nothing
async resolves faster than one pump tick. Lower the interval if you want
tighter progress reporting:
const steam = init({ appId: 480, pumpIntervalMs: 16 });The timer is unref'd, so it never keeps an idle process alive. While at
least one async Steam call is in flight, the timer is ref'd again, so your
process will not exit in the middle of an upload. When the last pending call
settles, it goes back to unref'd.
steam.close() stops the pump and calls SteamAPI_Shutdown. It is
idempotent, so calling it twice is safe. Any call still in flight rejects with
steamwand: dispatch stopped while call was in flight. Because of the
unref, a short script that forgets close() still exits; call it anyway, so
Steam shuts down in order.
Do not mix this with SteamAPI_RunCallbacks. Manual dispatch owns the whole
callback queue for the process, and only one pump may exist. See
How-It-Works.
The package is CommonJS ("type": "commonjs", main: dist/index.js) and ships
its own dist/index.d.ts.
// CommonJS
const { init, flat } = require('steamwand.js');// TypeScript, or ESM through Node's named-export detection
import { init, flat } from 'steamwand.js';Named imports from a plain .mjs file work through Node's CommonJS
named-export detection. If your bundler or runtime does not do that detection,
import the default and destructure it:
import pkg from 'steamwand.js';
const { init } = pkg;Top-level await needs an ESM context. In a CommonJS file, wrap the awaits in
an async function as the first script above does.
There is no build step to reproduce and no prebuilt binary to fetch. npm pack
includes dist/ and runtime/, and the loader resolves the library by
absolute path relative to the installed package:
| Platform | File |
|---|---|
| Windows x64 | runtime/win64/steam_api64.dll |
| Linux x64 | runtime/linux64/libsteam_api.so |
| Linux ARM64 | runtime/linuxarm64/libsteam_api.so |
| macOS (x64 and arm64) | runtime/osx/libsteam_api.dylib |
Three consequences for packaging. First, if you bundle your app into a single
file (esbuild, pkg, an Electron asar), the runtime/ folder must still exist
on disk two levels above the emitted dist/runtime/platform.js, or the load
fails. Second, when that is inconvenient, point the loader anywhere you like:
const steam = init({ appId: 480, libPath: '/opt/mygame/steam_api64.dll' });Third, koffi 3 ships its native binary as a separate package,
@koromix/koffi-<os>-<arch>, installed as a sibling of koffi in
node_modules (npm installs only the one for the build machine). An Electron
asarUnpack or files rule that keeps steamwand.js/** alone misses it and
fails at runtime with Cannot find the native Koffi module; keep koffi/**
and @koromix/** as well.
Shipping the Steamworks redistributables with your game is normal practice and
allowed under the Steamworks SDK Access Agreement. The SDK headers and
steam_api.json are not, and they are not in this package.
Next: Core-API for the full surface, Workshop for the curated workshop layer, Recipes for worked examples.