-
Notifications
You must be signed in to change notification settings - Fork 0
Flat API
The flat layer is the whole Steamworks C API, generated. scripts/generate.ts
reads Valve's steam_api.json (SDK 1.65) and emits one class per interface into
src/generated/interfaces/: 25 classes, 801 functions. Everything is reachable
through the flat namespace export.
import { init, flat } from 'steamwand.js';flat also carries the generated enums, consts, struct TypeScript interfaces,
structLayouts, layoutOf(), callbacksById, and callbackId.
For the hand-written convenience layer over workshop items, see
Workshop. For Steam itself, see Core API.
Six interfaces have cached getters on Steam:
const steam = init({ appId: 480 });
steam.user; // ISteamUser
steam.friends; // ISteamFriends
steam.utils; // ISteamUtils
steam.apps; // ISteamApps
steam.userStats; // ISteamUserStats
steam.ugc; // ISteamUGCEvery other class is constructed directly from steam.native:
const http = new flat.ISteamHTTP(steam.native);
const remote = new flat.ISteamRemoteStorage(steam.native);The constructor calls the versioned accessor for that interface (for example
SteamAPI_SteamUGC_v021) and stores the returned pointer on this.ptr. If the
accessor returns null it throws
steamwand: SteamAPI_SteamUGC_v021 returned null (is Steam initialized?). So
construct only after init() has returned.
Construction is cheap but not free: each new calls the accessor again. Hold on
to one instance per interface instead of building one per call. The six getters
already do this through an internal cache.
SteamNative.func() caches every koffi function object by symbol name, so two
instances of the same class share the registered functions.
Every generated method carries the information you need to call it, so hover it
in your editor before reaching for Valve's site. Each one gives you the original
C signature, the flat symbol name, a @param line for each pointer parameter
saying what to allocate (Buffer you allocate for uint32 *: Buffer.alloc(4) per element), a @remarks line when the call is asynchronous or raises a callback,
and an @see link to the matching page on partner.steamgames.com.
The generator maps each C parameter and return type as follows.
C type in steam_api.json
|
TypeScript | What you pass or get |
|---|---|---|
const char * (parameter) |
string |
A JS string. koffi copies it. |
SteamParamStringArray_t * |
SteamParamStringArrayJs |
stringArray(['a', 'b']) |
HServerListRequest |
unknown |
An opaque pointer. Pass it straight back. |
any other * or & parameter |
Buffer | null |
You allocate the bytes (in or out). |
bool |
boolean |
|
char, int8, uint8, int16, uint16, int32, uint32, int
|
number |
|
float, double
|
number |
|
| any enum type | number |
int32. Use flat.EResult.k_EResultOK and friends. |
int64, uint64, size_t, intptr_t, CSteamID, CGameID (parameter) |
bigint | number |
1234n or 1234. Above 2^53 you must use bigint. |
void return |
void |
|
const char * return |
string |
koffi copies the C string into a JS string. |
| any other pointer return | unknown |
An opaque handle. Only useful as an argument to another flat function. |
| 64-bit scalar return | bigint |
Always bigint, never number. |
Typedefs are followed to their base type, so AppId_t is number,
PublishedFileId_t is bigint | number as a parameter and bigint as a return,
and UGCQueryHandle_t is the same.
Note the asymmetry: 64-bit values are accepted as bigint | number but always
returned as bigint. Do not compare a returned handle to a number literal.
const state = steam.ugc.GetItemState(3786319531n); // number, a flag field
const owner: bigint = steam.apps.GetAppOwner(); // CSteamID, always bigint
if (owner !== steam.steamId()) throw new Error('not the owner');Every pointer parameter that is not const char * arrives as Buffer | null.
The flat API never allocates for you, so you allocate a Buffer of the right size,
pass it, and read it back. Steam does not tell you the size at runtime, so it
comes from the function's documented buffer-size argument or from a struct
layout.
Pass the buffer and its size, then cut at the first NUL byte.
const buf = Buffer.alloc(1024);
const len = steam.apps.GetAppInstallDir(1158310, buf, buf.length);
const dir = buf.toString('utf8', 0, Math.max(buf.indexOf(0), 0));buf.indexOf(0) returns -1 when Steam filled the buffer completely without a
terminator, so the Math.max guard is not decoration. The return value of
GetAppInstallDir is the length Steam wrote, but many other functions return
boolean and give you no length at all, so read to the NUL either way.
Allocate 8 bytes per value and read with readBigUInt64LE (or
readBigInt64LE for a signed field).
const processed = Buffer.alloc(8);
const total = Buffer.alloc(8);
const status = steam.ugc.GetItemUpdateProgress(updateHandle, processed, total);
console.log(status, processed.readBigUInt64LE(0), total.readBigUInt64LE(0));Four bytes each, readUInt32LE or readInt32LE. A bool * out parameter is one
byte: read it with readUInt8(0) !== 0.
const width = Buffer.alloc(4);
const height = Buffer.alloc(4);
if (steam.utils.GetImageSize(imageIndex, width, height)) {
console.log(width.readUInt32LE(0), height.readUInt32LE(0));
}Size the buffer from the generated layout, then decode it with decodeStruct.
layoutOf picks the offset table for the current platform.
import { decodeStruct, flat } from 'steamwand.js';
const layout = flat.layoutOf('SteamUGCDetails_t');
const buf = Buffer.alloc(layout.size);
if (steam.ugc.GetQueryUGCResult(queryHandle, 0, buf)) {
const details = decodeStruct<flat.SteamUGCDetails_t>(buf, layout);
console.log(details.m_rgchTitle, details.m_ulSteamIDOwner);
}layoutOf throws steamwand: no generated layout for struct X for a name it
does not know, including the union structs listed under
Limits. decodeStruct throws if the buffer is smaller than
layout.size.
Fixed char[N] fields decode to string, nested structs and non-char arrays
decode to Buffer, and every 64-bit field decodes to bigint.
A counted array is one buffer of count * elementSize bytes, read with a stride.
const count = steam.ugc.GetNumSubscribedItems(false);
const buf = Buffer.alloc(count * 8); // PublishedFileId_t is 8 bytes
const written = steam.ugc.GetSubscribedItems(buf, count, false);
const ids: bigint[] = [];
for (let i = 0; i < written; i++) ids.push(buf.readBigUInt64LE(i * 8));The same buffer works as an in parameter. CreateQueryUGCDetailsRequest takes an
array of file ids you fill yourself:
const ids = [3786319531n, 2504470131n];
const inBuf = Buffer.alloc(ids.length * 8);
ids.forEach((id, i) => inBuf.writeBigUInt64LE(id, i * 8));
const handle = steam.ugc.CreateQueryUGCDetailsRequest(inBuf, ids.length);SteamParamStringArray_t * is the one struct parameter with a helper. Pass the
object that stringArray() returns and koffi marshals the char ** for you.
import { stringArray } from 'steamwand.js';
steam.ugc.AddRequiredTagGroup(queryHandle, stringArray(['gameplay', 'ui']));A method whose JSDoc says "Returns an API call handle" is asynchronous. It gives
you a bigint handle, not a value, and the remark names the struct to await:
@remarks Returns an API call handle. Await it with
`steam.dispatch.callResultStruct<SteamUGCQueryCompleted_t>(handle, layoutOf('SteamUGCQueryCompleted_t'))`.
In your own code that is one await, with layoutOf reached through flat:
const call = steam.userStats.RequestUserStats(steam.steamId());
const r = await steam.dispatch.callResultStruct<flat.UserStatsReceived_t>(
call,
flat.layoutOf('UserStatsReceived_t'),
);
if (r.m_eResult !== flat.EResult.k_EResultOK) {
throw new Error(`RequestUserStats: ${r.m_eResult}`);
}76 of the 801 functions are call results. The struct name in the remark is also
the layout name, so layoutOf(name) and the flat.<name> TypeScript interface
always line up.
callResult(handle) gives you the raw bytes instead, which is what you want when
the struct has no generated layout:
const bytes: Buffer = await steam.dispatch.callResult(call);Both reject with SteamApiCallError when the handle is 0n (Steam refused the
call), when SteamAPI_ManualDispatch_GetAPICallResult fails, or when Steam
reports an IO failure for that call. They reject with a plain Error if
steam.close() runs while the call is still in flight.
k_uAPICallInvalid is 0n, so if (call === flat.k_uAPICallInvalid) is a valid
early check before awaiting.
Callbacks are not tied to one call. A method that raises one says so in its
JSDoc ("Fires the SteamInputConfigurationLoaded_t callback"), but most
callbacks arrive on their own. Subscribe by struct name and steamwand looks the
id up in callbacksById and decodes with the layout for this platform.
const off = steam.on<flat.ItemInstalled_t>('ItemInstalled_t', (data) => {
console.log(data.m_unAppID, data.m_nPublishedFileId);
});
off(); // unsubscribeAn unknown name throws steamwand: unknown callback struct 'X' at subscribe
time. 191 callback structs have ids and layouts.
For raw bytes, or for an id with no generated layout, subscribe on the dispatch object directly:
const off = steam.dispatch.on(flat.callbackId.ItemInstalled_t, (bytes) => {
console.log(bytes.length);
});flat.callbackId maps struct name to id; flat.callbacksById maps id back to
{ id, name, win64, posix }.
Enums are as const objects with Valve's own member names, so they are values,
not TypeScript enum types. Enum parameters are plain number.
steam.friends.GetFriendCount(flat.EFriendFlags.k_EFriendFlagImmediate);
const h = http.CreateHTTPRequest(flat.EHTTPMethod.k_EHTTPMethodGET, url);116 enums and 97 consts are generated. Consts keep their C names and their C
type: integer consts are number, 64-bit consts are bigint.
flat.k_cchPublishedDocumentTitleMax; // 129
flat.k_uAPICallInvalid; // 0n
flat.k_PublishedFileIdInvalid; // 0n25 interfaces, in the order they are exported from src/generated/index.ts.
"Getter" is the cached property on Steam; the rest need
new flat.X(steam.native).
| Class | Accessor | Getter | Functions |
|---|---|---|---|
ISteamUser |
SteamAPI_SteamUser_v023 |
steam.user |
33 |
ISteamFriends |
SteamAPI_SteamFriends_v018 |
steam.friends |
78 |
ISteamUtils |
SteamAPI_SteamUtils_v011 |
steam.utils |
38 |
ISteamMatchmaking |
SteamAPI_SteamMatchmaking_v009 |
38 | |
ISteamMatchmakingServers |
SteamAPI_SteamMatchmakingServers_v003 |
18 | |
ISteamParties |
SteamAPI_SteamParties_v002 |
11 | |
ISteamRemoteStorage |
SteamAPI_SteamRemoteStorage_v016 |
59 | |
ISteamUserStats |
SteamAPI_SteamUserStats_v013 |
steam.userStats |
44 |
ISteamApps |
SteamAPI_SteamApps_v009 |
steam.apps |
35 |
ISteamNetworking |
SteamAPI_SteamNetworking_v006 |
20 | |
ISteamScreenshots |
SteamAPI_SteamScreenshots_v003 |
9 | |
ISteamMusic |
SteamAPI_SteamMusic_v001 |
9 | |
ISteamHTTP |
SteamAPI_SteamHTTP_v003 |
25 | |
ISteamInput |
SteamAPI_SteamInput_v007 |
44 | |
ISteamController |
SteamAPI_SteamController_v008 |
31 | |
ISteamUGC |
SteamAPI_SteamUGC_v021 |
steam.ugc |
99 |
ISteamHTMLSurface |
SteamAPI_SteamHTMLSurface_v005 |
37 | |
ISteamInventory |
SteamAPI_SteamInventory_v003 |
38 | |
ISteamTimeline |
SteamAPI_SteamTimeline_v004 |
18 | |
ISteamVideo |
SteamAPI_SteamVideo_v007 |
4 | |
ISteamParentalSettings |
SteamAPI_SteamParentalSettings_v001 |
6 | |
ISteamRemotePlay |
SteamAPI_SteamRemotePlay_v004 |
20 | |
ISteamNetworkingMessages |
SteamAPI_SteamNetworkingMessages_SteamAPI_v002 |
6 | |
ISteamNetworkingSockets |
SteamAPI_SteamNetworkingSockets_SteamAPI_v013 |
47 | |
ISteamNetworkingUtils |
SteamAPI_SteamNetworkingUtils_SteamAPI_v004 |
34 |
The accessor version is baked into the generated class, so an SDK bump is a regeneration and the diff shows you which versions moved. See Regenerating.
ISteamNetworkingUtils is the only class built from a global accessor; the
other 24 use the user accessor. Interfaces with no accessor in
steam_api.json are not generated at all: ISteamClient, ISteamGameServer,
ISteamGameServerStats, ISteamNetworkingFakeUDPPort, and the five
ISteamMatchmaking*Response callback interfaces.
18 functions are skipped. The generator drops a function when a parameter or return type has no FFI mapping, and prints every skip when it runs. The current set:
- C function pointer parameters (11):
ISteamUtils::SetWarningMessageHook,ISteamInput::EnableActionEventCallbacks,ISteamNetworkingUtils::SetDebugOutputFunction, and the sixISteamNetworkingUtils::SetGlobalCallback_*setters. - Structs passed or returned by value (7):
GetDigitalActionData,GetAnalogActionData, andGetMotionDataon bothISteamInputandISteamController;ISteamParties::GetBeaconLocationData; andISteamNetworking::CreateListenSocket/CreateConnectionSocket, which take aSteamIPAddress_tby value.
There is no workaround inside steamwand for these. For the networking global
callbacks, subscribe to the matching callback struct with steam.on() instead
of installing a C function pointer. For the Steam Input action data, no
alternative exists in the flat API.
Ten structs have no layout. steam_api.json cannot express a C union, and a
layout computed from its field list would decode garbage, so the generator
excludes those structs and everything that embeds them:
SteamNetworkingIdentity, SteamNetworkingIPAddr, SteamNetworkingMessage_t,
SteamInputActionEvent_t, SteamNetConnectionInfo_t,
SteamDatagramGameCoordinatorServerLogin,
SteamNetworkingMessagesSessionRequest_t,
SteamNetworkingMessagesSessionFailed_t,
SteamNetConnectionStatusChangedCallback_t, and
SteamNetworkingFakeIPResult_t.
layoutOf() on any of these throws, and steam.on() cannot decode the four that
are callbacks. Functions that take one as a pointer still work: you get
Buffer | null and you own the bytes. Read them with readUInt32LE and friends
against the field offsets in Valve's steamnetworkingtypes.h, or subscribe with
steam.dispatch.on(id, cb) and parse the raw bytes yourself.
An FFI mistake aborts the process. A wrong buffer size or a null pointer
where Steam expected memory is a segfault inside the Steam DLL, not a JavaScript
exception. Nothing in try/catch helps. See
How It Works for the child-process
recommendation, and Troubleshooting.
Game server APIs are not wired up. ISteamGameServer and
ISteamGameServerStats carry no accessor in steam_api.json, so no class is
generated for them, and SteamNative loads SteamAPI_InitFlat only, not the
game server init entry point. Their callback structs (GSClientApprove_t and
the rest) are still generated, but nothing raises them.