Skip to content
Joël Deffner edited this page Sep 4, 2026 · 2 revisions

DLC

Since v0.5.0. steam.dlc is the curated layer over the DLC half of ISteamApps: what the app has, what the user owns, what is installed, and installing the rest. It is called dlc because the generated ISteamApps class already owns steam.apps.

The layer is deliberately small. listDlc is an index loop over a call with three out-buffers, where getting the name buffer wrong reads garbage, and install is a two step flow: start the download, then wait for Steam's DlcInstalled_t. The ownership and install checks are one-line reads that come along because a DLC gate reads all four in the same breath.

The install, build, beta and launch facts of the running app are on System, not here: this layer answers questions about that app's DLC.

The Apps instance is created lazily and cached on the Steam object. Its class name is Apps, not Dlc, because it wraps ISteamApps; only the getter is renamed.

listDlc

listDlc(): DlcInfo[]

The DLC of the running app, one entry per DLC, in Steam's order. Empty when the app has none.

This is a local read against the Steam client, so it needs no round trip. The list is what Steam knows about the app, not what the user owns. Check ownership separately with steam.apps.BIsSubscribedApp(appId), and installed state with steam.apps.BIsDlcInstalled(appId).

import { init } from 'steamwand.js';

const steam = init({ appId: 480 });
for (const dlc of steam.dlc.listDlc()) {
  console.log(dlc.appId, dlc.name, steam.dlc.isInstalled(dlc.appId));
}
steam.close();

Names are read into a 128-byte buffer, which is what Valve's own samples use. An index Steam refuses is skipped rather than pushed as a half-filled entry, so the returned array can be shorter than GetDLCCount().

Owned DLC only

There is no flag for it. Filter the list yourself:

const owned = steam.dlc.listDlc().filter((d) => d.available && steam.dlc.isOwned(d.appId));

available and owned are different questions: available is Steam's flag for "still on the store and not hidden", which a DLC the user already owns can lose.

isOwned and isInstalled

Since v0.6.0.

isOwned(appId: number): boolean
isInstalled(appId: number): boolean

isOwned is whether the user has a license, whether or not the files are anywhere. isInstalled is whether the files are on disk. They are different questions: a DLC the user just bought is owned long before Steam has finished downloading it, so a content gate that only checks ownership loads files that are not there yet.

if (steam.dlc.isOwned(1234) && !steam.dlc.isInstalled(1234)) {
  await steam.dlc.install(1234);
}

install

Since v0.6.0.

install(appId: number): Promise<void>

Installs a DLC and resolves once Steam reports it installed, so the resolved promise means the files are on disk.

Returns at once when the DLC is already installed. Otherwise Steam queues the download and confirms with DlcInstalled_t, which this waits for. The download is Steam's, so this takes as long as the DLC is large; watch it with downloadProgress.

The user must own the DLC. Steam ignores an install request for a DLC without a license, and no callback follows, so the promise never settles. Check isOwned first.

const [dlc] = steam.dlc.listDlc();
if (dlc && steam.dlc.isOwned(dlc.appId)) await steam.dlc.install(dlc.appId);

uninstall

Since v0.6.0.

uninstall(appId: number): void

Uninstalls a DLC and frees its disk space. Steam has no result for this, so it cannot fail from JavaScript, and it returns before the files are gone.

downloadProgress

Since v0.6.0.

downloadProgress(appId: number): { bytesDownloaded: bigint; bytesTotal: bigint } | null

How far a DLC download has come, or null when that DLC is not downloading, which includes a finished one. Byte counts are 64-bit, so bigint.

const p = steam.dlc.downloadProgress(1234);
if (p) console.log(Number((p.bytesDownloaded * 100n) / p.bytesTotal), '%');

onInstalled

Since v0.6.0.

onInstalled(listener: (appId: number) => void): () => void

Subscribes to DLC finishing installation, and returns an unsubscribe function.

It fires for every DLC Steam installs while the app runs, including one the user bought from the store or the overlay, so this is the hook that turns on the content without a restart. install already waits for its own DLC, so this is for the ones the app did not start.

const off = steam.dlc.onInstalled((appId) => console.log('installed', appId));
// later: off();

Types

DlcInfo

One entry from listDlc.

Field Type Meaning
appId number App id of the DLC.
available boolean True while the DLC is available for purchase or install, so neither hidden nor removed.
name string Display name, in the Steam client language.

App ids are 32-bit, so number and not bigint.

Errors

Shape When
never settles install was called for a DLC the user does not own. Steam drops the request and sends no callback.

Nothing here throws. listDlc skips a DLC entry Steam refuses and gives an empty array for an app with no DLC; if the array is empty and you expected entries, the app id is wrong or Steam has not finished loading the app's data. The ownership and install reads answer false rather than failing.

What this layer does not do

The rest of ISteamApps is either on System or on the raw generated steam.apps:

On System

On the raw steam.apps

  • BIsSubscribed, BIsSubscribedFromFreeWeekend, GetEarliestPurchaseUnixTime(appId), BIsVACBanned, BIsCybercafe, BIsLowViolence.
  • SetDlcContext(appId), which tells Steam which DLC the app is currently acting as.
  • GetInstalledDepots, MarkContentCorrupt, GetFileDetails, GetAvailableGameLanguages.
  • SetGamePerformanceSetting and SetGameRenderResolution, the Steam Deck performance hints.
  • RequestAppProofOfPurchaseKey and RequestAllProofOfPurchaseKeys, the CD key retrieval for apps that still have keys.

Flat API explains the calling convention.

Next: System for the client and machine facts, and for everything ISteamApps says about the running app itself.

Clone this wiki locally