-
Notifications
You must be signed in to change notification settings - Fork 0
Cloud
Since v0.3.0. steam.cloud is the curated layer over ISteamRemoteStorage:
read, write, list and delete Steam Cloud files for the current user and app.
Reads and writes go through Valve's async path (FileWriteAsync,
FileReadAsync), so a slow disk or a large file never blocks the Node event
loop. Those two are async, and so are share and
writeBatch; everything else here is a local read or a one-line
synchronous call. A non-OK EResult becomes a SteamResultError, and
a flat call that returns false becomes a plain Error. See
Errors.
A file only reaches Valve's servers when cloud sync is on for both the account and the app. Check that with isEnabledForAccount and isEnabledForApp; local reads and writes work either way.
The Cloud instance is created lazily and cached on the Steam object. The
workshop lives in Workshop, not here: the legacy
publish-to-workshop calls that also sit on ISteamRemoteStorage are deprecated
and are not wrapped.
writeFile(name: string, data: Buffer | string): Promise<void>Writes one file, replacing it when it already exists.
| Parameter | Type | Meaning |
|---|---|---|
name |
string |
File name, for example save01.json. Forward slashes make subfolders. |
data |
Buffer | string |
The contents. A string is encoded as UTF-8. |
Steam rejects an empty write and caps one file at 100 MB. The promise resolving means the file was written locally; Steam syncs it afterwards, so use isPersisted to find out whether the cloud copy is current.
await steam.cloud.writeFile('save01.json', JSON.stringify({ level: 3 }));Throws SteamResultError with operation: 'FileWriteAsync' on a non-OK result,
for example k_EResultLimitExceeded when the quota is full.
Since v0.6.0.
writeBatch<T>(fn: () => Promise<T> | T): Promise<T>Groups several writes into one save, so Steam syncs them together. A save game spread over more than one file is only consistent as a set: inside the callback Steam holds the sync back, and it starts one sync for everything written once the callback finished.
await steam.cloud.writeBatch(async () => {
await steam.cloud.writeFile('save01/meta.json', JSON.stringify({ level: 3 }));
await steam.cloud.writeFile('save01/world.bin', worldBytes);
});The callback is awaited, so it may be async, and its return value comes back
from writeBatch. Anything it does that is not a cloud write is allowed; it
just delays the sync. The batch is always closed, even when the callback throws,
and the callback's own error is the one that reaches the caller.
Throws Error: steamwand: BeginFileWriteBatch returned false (...) when Steam
refuses to open the batch, for example because one is already open.
readFile(name: string): Promise<Buffer>Reads one whole file. Three flat calls in a row: GetFileSize for the length,
FileReadAsync to queue the read, then FileReadAsyncComplete to copy the
bytes Steam holds into a buffer sized to what Steam reports it actually read,
not to what GetFileSize said. Steam frees its own copy in that last call, so
it runs even for a short read.
An empty file gives an empty buffer without any async call at all.
const save = JSON.parse((await steam.cloud.readFile('save01.json')).toString('utf8'));
console.log(save.level);Throws:
-
Error: steamwand: cloud file does not exist: <name>before any native call. -
SteamResultErrorwithoperation: 'FileReadAsync', for examplek_EResultFileNotFound. -
Error: steamwand: FileReadAsyncComplete returned false (...)when Steam refuses the read handle.
deleteFile(name: string): voidDeletes the file from the cloud and from disk. The file is gone everywhere and stops counting against the quota. This is the one a "delete save game" button wants. Compare forgetFile, which sounds similar and does something quite different.
Throws Error: steamwand: FileDelete returned false (...), which usually means
the file does not exist.
forgetFile(name: string): voidStops syncing the file without deleting anything. The local copy stays on this machine, the cloud copy stays on Valve's servers, and the two stop tracking each other, so the file is not downloaded onto the user's other machines. Use it for a save the user wants on one machine only.
A forgotten file never becomes persisted again, so
isPersisted stays false for it.
Throws Error: steamwand: FileForget returned false (...) when the file does
not exist.
exists(name: string): booleanTrue when the file is in this app's cloud storage, locally or in the cloud.
isPersisted(name: string): booleanTrue when the cloud copy is up to date. Expect false right after a write and
true once the sync finished.
await steam.cloud.writeFile('save01.json', data);
console.log(steam.cloud.isPersisted('save01.json')); // usually false, brieflylistFiles(): CloudFile[]Every file in this app's cloud storage for the current user, in Steam's own order. Empty when the app has none.
for (const f of steam.cloud.listFiles()) console.log(f.name, f.sizeBytes);getFileInfo(name: string): CloudFileInfo | nullSize and modification time without reading the contents. null when the file
does not exist, so this is also a way to ask "how big is it" and "is it there"
in one call.
const info = steam.cloud.getFileInfo('save01.json');
if (info) console.log(info.sizeBytes, new Date(Number(info.timestamp) * 1000));Since v0.6.0.
share(name: string): Promise<bigint>Publishes a cloud file and returns the UGCHandle_t that points at it. The
handle is public: anybody who has it can download the file, so only share what
the user meant to share.
That handle is what a leaderboard entry carries through attachUgc, which is how a score gets a replay attached to it.
await steam.cloud.writeFile('replay01.bin', replayBytes);
const ugc = await steam.cloud.share('replay01.bin');
const board = await steam.leaderboards.find('Feet Traveled');
if (board) await steam.leaderboards.attachUgc(board.handle, ugc);Throws SteamResultError with operation: 'FileShare', for example
k_EResultFileNotFound.
Since v0.6.0.
syncPlatforms(name: string): numberWhich platforms the file syncs to, as an ERemoteStoragePlatform bit mask: 1
Windows, 2 macOS, 8 Linux, 16 Switch, 32 Android, 64 iOS, and -1 for all.
Since v0.6.0.
setSyncPlatforms(name: string, platforms: number): voidLimits a file to certain platforms. Use it for a file that only makes sense on one, for example key bindings a controller-only build cannot read. Steam keeps the file either way, it just stops downloading it elsewhere. Or the bits together for several platforms.
const desktop =
flat.ERemoteStoragePlatform.k_ERemoteStoragePlatformWindows |
flat.ERemoteStoragePlatform.k_ERemoteStoragePlatformLinux;
steam.cloud.setSyncPlatforms('bindings.cfg', desktop);Throws Error: steamwand: SetSyncPlatforms returned false (...), which usually
means the file does not exist.
quota(): CloudQuotaHow much cloud storage this app may use for this user, and how much is left.
Both counts are bigint. A write larger than availableBytes fails with
k_EResultLimitExceeded, so check this before writing something big.
const { totalBytes, availableBytes } = steam.cloud.quota();
console.log(`${availableBytes} of ${totalBytes} bytes free`);Throws Error: steamwand: GetQuota returned false (...) when the quota is not
known yet.
isEnabledForAccount(): booleanWhether the user turned cloud sync on for their whole account. They set this in the Steam client settings and the app cannot change it. Files still write locally when it is off, they just never sync.
isEnabledForApp(): booleanWhether cloud sync is on for this app. Steam takes the starting value from the user's per-app setting.
setEnabledForApp(enabled: boolean): voidTurns cloud sync on or off for this app, for this session. Meant for an in-game "sync my saves" option. It does not change the user's Steam client setting, and it cannot switch sync on while the account has it off. Steam gives no result here, so it cannot fail from JavaScript.
Since v0.6.0.
onLocalFileChange(listener: () => void): () => voidSubscribes to the "Steam just changed files on disk" notice and returns an unsubscribe function. Steam fires it once after a sync changed files while the app was running, for example because the user played on another machine and came back. The callback carries nothing; listLocalChanges says which files.
const off = steam.cloud.onLocalFileChange(() => {
for (const c of steam.cloud.listLocalChanges()) console.log(c.name, c.change);
});
// later: off();Since v0.6.0.
listLocalChanges(): { name: string; change: 'updated' | 'deleted'; pathType: 'absolute' | 'apiFilename' }[]The files a Steam sync changed underneath the running app. Steam clears the list on the next sync, so read it from an onLocalFileChange listener rather than polling for it.
A game that keeps a save open in memory uses this to notice that the copy on disk moved on, and to offer the player a reload instead of overwriting it.
| Field | Type | Meaning |
|---|---|---|
name |
string |
The file, named the way pathType says. |
change |
'updated' | 'deleted' |
From ERemoteStorageLocalFileChange. |
pathType |
'absolute' | 'apiFilename' |
From ERemoteStorageFilePathType. apiFilename names it the way the rest of this layer does; absolute is a full path on disk. |
Empty when nothing changed.
One entry from listFiles.
| Field | Type | Meaning |
|---|---|---|
name |
string |
File name, the same name readFile and deleteFile take. |
sizeBytes |
number |
Size in bytes. Steam caps one file at 100 MB, so a number is enough. |
Returned by getFileInfo.
| Field | Type | Meaning |
|---|---|---|
sizeBytes |
number |
Size in bytes. |
timestamp |
bigint |
Last write time, Unix seconds. 64-bit, so a bigint. |
Returned by quota.
| Field | Type | Meaning |
|---|---|---|
totalBytes |
bigint |
Total bytes Steam grants this app for this user. |
availableBytes |
bigint |
Bytes still free. |
| Shape | When |
|---|---|
SteamResultError |
FileWriteAsync, FileReadAsync or FileShare completed with a non-OK EResult. Common values: k_EResultLimitExceeded (quota full), k_EResultFileNotFound, k_EResultDiskFull. |
Error: steamwand: cloud file does not exist: <name> |
readFile, before it calls anything native. |
Error: steamwand: <call> returned false (invalid handle or argument?) |
FileDelete, FileForget, FileReadAsyncComplete, SetSyncPlatforms, BeginFileWriteBatch or GetQuota returned false. |
SteamApiCallError |
The read or the write never produced a usable result, or completed carrying a different callback struct. See How It Works. |
The rest of ISteamRemoteStorage is on the raw generated steam.remoteStorage:
- The synchronous
FileWriteandFileRead, which block on disk. The async pair is wrapped instead, on purpose. - Streaming writes:
FileWriteStreamOpen,...WriteChunk,...Close,...Cancel, for a file assembled in pieces. - Downloading somebody else's shared file:
UGCDownload,UGCRead,GetUGCDetailsand the cached-UGC calls, which all take the handle share hands out. - The whole deprecated
PublishWorkshopFilefamily. Use Workshop instead.
Those calls take raw Buffer out params and return flat booleans or call
handles. Flat API explains the calling convention.