@w3booster/sdk
v4.9.0
Published
One SDK for realtime W3Booster match data in overlays and web apps.
Downloads
2,845
Maintainers
Readme
@w3booster/sdk
SDK 4.8.1 uses protocol 4.0. Build with cumulative player statistics, economy history, rolling loss windows and configurable setting shortcuts. Optional state additions stay forward-compatible. See what changed and the compatibility policy.
Browser SDK for realtime W3Booster match data in applications and overlays. It handles app authorization, transport selection, reconnects, protocol validation, patches, and immutable hydrated state.
Find the right guide
| Build | Start here | | --- | --- | | Debug a blank or unstable overlay (humans and coding agents) | Debugging guide and runnable fixtures | | Your first working app | Starter tutorial | | Economy graphs, item events and loss comparisons | Analytics | | Understand available fields and self-play limits | Data availability | | User settings and desktop shortcuts | Settings guide | | Smooth health, mana and progress | Interpolation | | Interactive in-game regions | Overlay input | | Match-specific names, costs and icons | Catalogs | | Update an existing app | Upgrade guide | | Look up a type or method | Generated reference |
Quick start
Create an application
New project? Start with the runnable TypeScript starter and first-app tutorial. It renders a dashboard immediately with demo data; no registered app or Warcraft III installation is needed.
npx --yes --package=github:W3Booster/app-starter w3booster-create my-app
cd my-app
npm ci
npm run devOpen http://localhost:5173/. The minimal starter contains one dashboard, complete lifecycle cleanup, and demo scenarios for missing data and no active match. Independent example apps cover resources, transparent overlays, and SDK-synced settings. For a complete Angular starting point, fork Match Vision.
Adding the SDK to an existing app: create a private application in W3Booster, copy its public client ID, then run these commands from your project directory:
npm install @w3booster/sdk
npx w3booster-settings init app_your_id --endpoint https://api.w3booster.comimport { w3boosterApp } from './w3booster.generated';
const runtime = w3boosterApp.createRuntime({ retry: true });
runtime.lifecycle.subscribe(snapshot => {
renderConnection(snapshot.status, snapshot.error);
render(snapshot.state, snapshot.settings, snapshot.host);
});
await runtime.start();
// When the page or component is disposed:
await runtime.stop();The client ID is the public, immutable identifier generated when an app is created in W3Booster. It is not a secret. The one-time init command creates a checked-in typed binding for that application definition. The managed runtime publishes connection status, synchronized state, resolved settings, and host capabilities atomically; normal applications do not pass connection URLs, credentials, or scopes.
Use demo: true when W3Booster is not running. Demo support is loaded on demand and stays out of normal production startup:
const runtime = w3boosterApp.createRuntime({ demo: true });
await runtime.start();The built-in demo includes representative players, resources, heroes, upgrades, statistics, control groups, overlay runtime, and application metadata. Supply typed settings without constructing a complete state:
const client = await startClient<Settings>({
clientId: 'your_app_id',
demo: { settings: { layout: 'wide' } }
});Set interval: 0 for a static deterministic fixture, or pass state to replace the complete demo state.
For real data during development, run the app on localhost and use Apps → Developer → My apps → Test locally. The temporary session supplies real credentials and replaces only your app surfaces. Application code remains unchanged.
See the guides, searchable API reference, and troubleshooting guide. Documentation and generated API references describe the published SDK release.
W3Booster reserves the URL fragment for its short-lived launch credential; opening a client consumes it and cleans the visible address. Use normal History API paths or query parameters for application routing instead of hash routing.
W3Booster Cloud is the default backend. A platform-provided ?backend=local or ?backend=cloud launch parameter is handled by the SDK automatically; application code must not parse or forward it. Platform developers can force the local API explicitly:
const client = await startClient({
clientId: 'your_app_id',
backend: 'local'
});backend: 'auto' tries local and then cloud. A complete HTTPS URL in backendUrl selects another platform environment. Keeping URL selection separate from backend: 'local' | 'cloud' | 'auto' lets TypeScript catch misspelled standard backends. Remote HTTP and WebSocket endpoints must use HTTPS/WSS; unencrypted HTTP/WS is accepted only for localhost.
Platform integrations that explicitly provide a tokenProvider should return the current credential. The SDK calls it for every broker ticket request, including reconnects, so refreshed credentials are used automatically. Normally launched applications do not need this option.
State lifecycle
start() and top-level startClient() are the canonical long-lived frontend entry points and resolve with synchronized state by default. open() and top-level openClient() explicitly stop at transport readiness. Use whenReady() when work only needs any hydrated snapshot, including preserved state during reconnect. Use whenSynchronized() when rendering or an action must wait for a fresh snapshot from the current connection:
const initialState = await client.whenReady(); // 10-second default timeout
const freshState = await client.whenSynchronized();Use createClient() when the UI needs to observe the complete lifecycle, including the initial connecting and retry states. Attach listeners first, then connect:
import { createClient } from '@w3booster/sdk';
const lifetime = new AbortController();
const { signal } = lifetime;
const client = createClient({ clientId: 'your_app_id', retry: true, signal });
client.lifecycle.subscribe(snapshot => {
renderConnectionStatus(snapshot.status);
render(snapshot.state, { fresh: snapshot.isSynchronized });
if (snapshot.error) report(snapshot.error);
}, { signal });
await client.start({ signal });start() defaults to synchronized state with no timeout and rejects if synchronization becomes permanently impossible. Its optional signal cancels transport opening and readiness together and closes an incomplete startup. Pass { until: 'connected' } for a transport-only startup or an explicit timeout when the UI wants a bounded wait. Runtime construction is intentionally rejected; use openClient(), startClient(), or createClient() so internal mutable stores and transports remain encapsulated behind frozen read-only facades.
Generated application runtimes share transport opening, but each concurrent runtime.start() call keeps its own readiness milestone, timeout, and cancellation. Cancelling one runtime startup wait does not disconnect another caller or the runtime lifetime; call runtime.stop() for shared teardown.
client.lifecycle publishes connection status, current state, freshness, and the current connection/synchronization error as one snapshot. Non-fatal recorder and consumer-listener problems—including listeners attached to a generated application runtime—are published as structured issue events without turning healthy match data into a connection failure. The lifecycle store is the preferred UI integration point when its values feed one view model. The narrower state.subscribe(), subscribeStatus(), and event APIs remain useful when a feature needs only one stream.
client.state is the authoritative source of current data:
get()returns the current state ornullbefore the first snapshot.isSynchronizedreports whether that state has a complete baseline from the current connection.player(id)returns one current player ornull.subscribe(listener)runs immediately and after every update, reset, or synchronization-freshness change; its value isnullwhile no hydrated state is available. A freshness-only notification retains the same state identity, so readclient.state.isSynchronizedfrom the store when handling it.watch(selector, listener)reevaluates immediately, including while state is unavailable, and after those same transitions; the listener runs whenObject.is()detects a new selection. Pass{ equals }for value-based comparison. A selector may readclient.state.isSynchronizedwhen freshness is its selected value.- Every subscription method returns an unsubscribe function.
Hydrated state is recursively immutable. Patches and recorder updates use structural sharing, so untouched matches, players, heroes, settings, and extension branches keep their object identity. Consumers may use referential equality to avoid unnecessary work while continuing to identify rendered entities by their domain IDs. Allocation-heavy SDK selectors also retain their result identity when called again with the same frozen SDK collections.
The initial snapshot emits state.ready and state.changed. Match, player, and hero domain events describe changes after that snapshot; they are not a replacement for rendering initial state. In particular, an already-running match does not synthesize match.started when the app opens.
const stopClock = client.state.watch(
state => state?.match.gameTime ?? null,
(seconds, previousSeconds) => drawClock(seconds, previousSeconds)
);
client.on('match.started', ({ match }) => showNewMatch(match));
client.on('player.resources.changed', ({ player, resources }) => updateEconomy(player.id, resources));
client.on('hero.changed', ({ player, hero }) => updateHero(player.id, hero));
client.on('issue', issue => console.warn(issue.source, issue.recoverable, issue.error));client.status is the current connection state. subscribeStatus() immediately reports it and then reports transitions such as reconnecting, connected, and error; the lower-level status event reports transitions only. Automatic network reconnects preserve hydrated state for continuity, set state.isSynchronized to false, and hold patches until a fresh snapshot re-establishes a complete baseline. A complete forward snapshot can establish that baseline directly across a sequence gap; the SDK emits stream.gap but does not request a redundant resync. An explicit disconnect() closes transports, rejects pending readiness waits, and clears state and diagnostics; existing subscriptions remain registered if the same client is connected again.
Resilient frontend lifecycle
Generated application bindings provide the shortest framework integration. Their managed runtime combines connection state, synchronization freshness, resolved settings, and reactive host capabilities into one immediate snapshot and owns teardown:
import { canUseHostCapability } from '@w3booster/sdk';
import { w3boosterApp } from './w3booster.generated';
const runtime = w3boosterApp.createRuntime({ retry: true });
runtime.lifecycle.subscribe(snapshot => {
renderConnection(snapshot.status, snapshot.error);
render(snapshot.state, snapshot.settings);
toggleWindowAction(canUseHostCapability(snapshot.host, 'window:open'));
});
await runtime.start();
// Angular destroy, React cleanup, Vue unmount, or page teardown:
await runtime.stop();Use the lower-level client lifecycle when a project is not database-bound or deliberately needs to manage subscriptions separately.
One AbortController can own the connection, readiness wait, and subscriptions for a component or application. Aborting it cancels connection/retry work, disconnects an established client, settles pending readiness waits, and removes subscriptions:
const controller = new AbortController();
const { signal } = controller;
const client = createClient({
clientId: 'your_app_id',
retry: true,
signal
});
client.state.subscribe(state => renderConnectionState(state), { signal });
client.on('issue', issue => report(issue.error, issue), { signal });
await client.open();
await client.whenSynchronized({ signal });
// React effect cleanup, Angular destroy, Vue unmount, or page teardown:
controller.abort();retry: true retries transient initial connection failures with capped exponential backoff. An unlimited policy requires either the connection lifetime signal or the signal supplied to start() for that incomplete startup. Calling open() without either signal rejects before connecting. Authorization, configuration, and protocol failures fail immediately. Initial retries remain in connecting; reconnecting is reserved for a previously established transport. During an initial retry, client.lifecycle.get().retry exposes its attempt, configured attempt limit (null means unlimited), last transient error, and next delay (null while the attempt is underway), so a frontend can explain progress without parsing errors. It returns to null after success, terminal failure, or cancellation. Use retry: { maxAttempts: 5, initialDelay: 250, maxDelay: 5000 } for a bounded policy that does not require a signal. The default remains one initial attempt so command-line tools and explicit error screens fail promptly.
After an established broker socket closes, transient reconnects use their own policy and permanent configuration, authorization, and protocol failures transition to error. The default is unlimited capped reconnects. Pass reconnect: false to fail immediately or reconnect: { maxAttempts: 5, initialDelay: 500, maxDelay: 10000 } for a bounded policy.
Concurrent open() calls on the same client share one attempt. All cancellation rejects with the standard AbortError name. subscribe(), subscribeStatus(), watch(), on(), and once() still return explicit unsubscribe functions when a signal is not convenient.
Scopes and capabilities
An application requests scopes in its W3Booster metadata. The server filters every snapshot to the granted scopes. Passing scopes when opening a client can only request a smaller subset.
| Scope | Capability | Conditional state |
| --- | --- | --- |
| match:read | match | Match lifecycle, time, map, mode, realm, and broadcaster IDs |
| players:read | players | Player identity, race, team, color, and position |
| stats:read | stats | Ranking statistics and main-account data |
| units:read | units | Ordinary unit instances, health, and observed mana |
| buildings:read | buildings | Building instances, health, and observed mana |
| production:read | production | Building identity and production queues |
| heroes:read | heroes | Heroes, health, mana, abilities, and inventory |
| upgrades:read | upgrades | Completed, active, and researching upgrades |
| resources:read | resources | Gold, lumber, supply, and worker supply |
| controlgroups:read | controlgroups | Control-group front units and sizes |
state.gameContext is shared context delivered to every authorized app, including
apps with an empty scope list. It contains hudScale (always present; default 1),
optional chatbarOpen, optional teamColors, and optional
heroBarLastOccupiedSlot for the last occupied native hero slot
(including gaps and shared portraits). Unknown boolean values stay
absent. The SDK gameContext(state) selector also supplies a scale-1 fallback
before hydration. These values do not require a data scope or plan.
overlay:read and the old overlay runtime branches are removed. Use
gameContext directly. Private recorder discovery follows actual live-data grants
and is stripped before snapshots reach applications.
Match Vision's wins/losses counter lives in state.application.data.matchScore.
The API delivers it only to Match Vision, regardless of scopes. It is separate
from state.application.settings and is not a score reported by Warcraft.
There is no score alias in the current platform snapshot;
third-party apps cannot obtain that counter by requesting overlay:read.
Observer resources require only match:read, players:read, and resources:read.
A local recorder can be used even with a production API; browser local-network
permission is separate from W3Booster data scopes.
A granted scope does not guarantee that data exists in every match or account context. Check state.capabilities and keep conditional fields optional:
client.state.subscribe(state => {
if (!state) return;
const canShowResources = state.capabilities.includes('resources');
const resources = canShowResources ? state.players[0]?.resources : undefined;
renderResources(resources);
});Events
Useful events include:
state.ready,state.changedmatch.started,match.changed,match.endedplayer.added,player.changed,player.removedplayer.resources.changed,player.stats.changed,player.upgrades.changedhero.added,hero.changed,hero.removedhero.inventory.changed,hero.abilities.changedapplication.settings.changedstatus,issue,stream.gap
on() and once() return unsubscribe functions. Listener failures are isolated and forwarded to the structured issue event so one application callback cannot interrupt state delivery.
Event payloads are immutable and shared safely between listeners. Async listener promises are observed for rejection but do not delay or serialize later events; applications that require ordered asynchronous work should queue it explicitly inside the listener.
Typed application settings
Settings defined in application metadata are delivered as state.application.settings. Type the settings once and the state, events, and store inherit that type:
interface Settings {
layout: 'compact' | 'wide';
showHeroes?: boolean;
}
const client = await startClient<Settings>('your_app_id');
client.state.subscribe(state => {
const settings = state?.application?.settings;
if (settings) setLayout(settings.layout);
});Settings and extension models are checked recursively for JSON compatibility. Date, Map, functions, class instances, and similar values are rejected by TypeScript at client creation as well as by runtime ingress validation.
client.host.setSetting(path, value) resolves with the complete saved settings only after W3Booster confirms persistence. It rejects with HostActionError when the platform rejects the write, or ConnectionError when the host is unavailable or does not answer. The normal application.settings.changed event still updates every open surface.
With typed settings, setting paths—including nested dot paths—and their values are checked by TypeScript:
interface Settings {
observer?: { layout: 'compact' | 'wide'; showHeroes?: boolean };
}
const client = await startClient<Settings>('your_app_id');
const saved = await client.host.setSetting('observer.layout', 'wide');
// client.host.setSetting('observer.layout', 'large'); // TypeScript errorThe event's settings and previousSettings values can be undefined when application state is added or removed. Hydrated state and settings are recursively read-only in TypeScript because the SDK freezes delivered state at runtime.
Default settings shortcuts
The Windows desktop can toggle a public boolean setting with a global shortcut.
In the developer settings editor, set Default shortcut, or include the optional
defaultHotkey in that field's schema:
{ "key": "showPanel", "label": "Show panel", "type": "boolean", "default": true,
"group": "layout", "defaultHotkey": "Control+Alt+H" }Use canonical Control, Alt, Shift order followed by a letter, digit or
F1–F24; F12 is reserved. Letters and digits require Control or Alt. Defaults must
be unique within your app and cannot belong to internal fields. Users can change,
clear or restore defaults. The desktop resolves conflicts across apps and reports
keys unavailable at the OS level. Your app receives ordinary settings changes;
it does not register keyboard listeners or receive other apps' key events.
This metadata requires a desktop/API release that supports settings hotkeys.
Errors and troubleshooting
Handle initial connection failures around startClient() and later non-fatal stream or listener problems with the structured issue event:
import {
startClient,
ConnectionError,
HostActionError,
isAbortError,
PermissionRequiredError,
ProtocolError
} from '@w3booster/sdk';
try {
const client = await startClient('your_app_id');
client.on('issue', ({ error, source, recoverable }) => {
console.warn({ source, recoverable });
if (error instanceof ProtocolError) console.error(error.code, error.details);
else console.error(error);
});
} catch (error) {
if (error instanceof PermissionRequiredError) {
showMessage('Enable and open this app from W3Booster.');
} else if (error instanceof ConnectionError) {
console.error(error.code, error.message, error.causes);
} else if (error instanceof HostActionError) {
showMessage(error.message);
} else if (isAbortError(error)) {
// Normal component or page teardown.
} else {
throw error;
}
}Common causes:
- Permission required: the URL was opened directly, the app is disabled, or its temporary development session expired.
- Outdated generated binding: regenerate and redeploy the application when the broker reports
APPLICATION_DEFINITION_MISMATCH. - No initial state:
whenReady()timed out before the platform supplied a snapshot. - Local connection failure: trust the W3Booster localhost certificate and verify the local backend is running.
- Protocol error: inspect
ProtocolError.code; recoverable invalid state or patch data requests a resync. Unsupported protocol versions and application mismatches are permanent and close the active transport. A complete forward snapshot is accepted as the new baseline across an ordinary sequence gap. - Missing fields: verify the application scope, the matching capability, and whether that data exists for the current match.
ConnectionError.code is stable for programmatic handling (UNAVAILABLE, CONFIGURATION, APPLICATION_DEFINITION_MISMATCH, MISSING_BROWSER_API, BROKER_TIMEOUT, STARTUP_TIMEOUT, STATE_TIMEOUT, HOST_UNAVAILABLE, or HOST_TIMEOUT). STARTUP_TIMEOUT means a managed runtime did not reach its requested lifecycle milestone within the caller's whole-startup deadline; STATE_TIMEOUT means an established client did not receive the requested state in time. Configuration and application-definition mismatch failures are never retried; their optional status carries the HTTP status. isAbortError() recognizes lifecycle cancellation, and isW3BoosterError() recognizes every SDK error class. HostActionError represents an acknowledged platform rejection rather than a transport failure.
Frontends that prefer one discriminated branch can use classifyW3BoosterError(error). It returns a stable kind, code, and original error, plus status or authorizeUrl when available, without imposing application-specific user-facing copy.
The package targets Chrome/Chromium 92+, Edge 92+, Firefox 90+, and Safari 15.4+ and requires native ESM, fetch, WebSocket, and AbortController. OBS browser sources must embed Chromium 92 or newer. Node.js is supported for tooling and tests; realtime Node usage must provide an appropriate WebSocket environment or a testing transport.
Low-latency recorder data
During active observer and replay matches, the SDK automatically consumes the local recorder socket advertised by authenticated platform state. The platform remains authoritative for identity, permissions, settings, capabilities, and the initial snapshot; the local socket overlays volatile match values such as game time, HUD scale, resources, heroes, and upgrades without discarding platform-owned or additive metadata. If a socket does not open promptly, the SDK rotates through the advertised URLs and keeps retrying. If an active socket disconnects, cached recorder values stop overriding authenticated platform snapshots until the recorder reconnects and sends fresh data.
Recorder bursts are deduplicated and published at most once per display frame. Repeated values do not publish a new state, and unchanged state branches retain their references.
Numeric and coordinate conventions
match.gameTimeis elapsed in-game time in whole seconds and excludes paused time.match.mapis a human-readable, display-ready name; the SDK decodes producer transport escaping once at snapshot/patch ingress.gameContext.hudScaleis the applied Warcraft HUD scale multiplier from0.5through1.0, forwarded unchanged by W3Booster. Apply it as a scale/zoom value; it is not a percentage.player.startPositionuses Warcraft III map coordinates, not pixels. It is suitable for relative map placement and player ordering; transforming it onto an image depends on that map's bounds.- Gold, lumber, supply, and worker supply are already normalized player-facing values; applications do not divide recorder values themselves.
match.realmidentifies the recorder realm:Reforged,W3Champions,W3Champions@EU, orW3Champions@NAare current examples.match.isReforgeddescribes graphics, not the stats provider.stats.*.mmr, when present, is the provider-reported matchmaking rating for that record. Missing MMR stays absent; level and rank are separate values. W3Champions ratings are race-specific.stats.*.winRateis a percentage from0through100, ready to display with a percent sign.hero.abilities[].lastActivation, when present, is a positive millisecond timestamp on the match game-time clock; absence means the ability has not activated. PreferstandardGameCooldowns.abilityCooldown()instead of interpreting it directly.- Upgrade
gametimeis the Unix timestamp in milliseconds when W3Booster observed the upgrade. Research start/finish values are ISO-8601 timestamps.
match.started and match.ended events include observedAt, an ISO-8601 client observation time. It is explicit receipt/transition timing, not an authoritative match timestamp; prefer match.startedAt and match.endedAt when the platform supplies them. When an active match with the same ID becomes finished, an ended event exposes that completed snapshot as event.match, so event.match.endedAt is directly available; previousMatch retains the active snapshot.
Use client.subscribeMatchLifecycle() when a feature needs both the match that is already active at hydration and later transitions. It reports { phase, initial, observedAt, match, state }; initial distinguishes hydration/re-hydration from a later semantic transition. By default, a finished match in the first snapshot remains a baseline rather than a new event. History and audit consumers can pass { includeCurrentFinished: true } to receive it once as { phase: 'ended', initial: true }.
Only loopback and private-network socket addresses are accepted. Capabilities still control all exposed fields. Set localRecorder: false only when an application deliberately needs to disable this behavior. client.diagnostics.localTransport is recorder-local while it is active.
Finished match results
A finished match can include an optional recorder-confirmed result:
// Inside your application runtime setup; runtime.signal owns the subscription.
runtime.client.state.subscribe(state => {
const match = state?.match;
if (match?.status !== 'finished' || !match.result) return;
const { playerId, outcome } = match.result;
console.log(match.id, playerId, outcome); // outcome: 'won' | 'lost'
}, { signal: runtime.signal });match.result requires match:read, with no additional scope or plan gate.
playerId identifies the actual local player within that match; joining its player
record requires players:read. The field is absent for unknown results, observer
games and replays. It is not a full winner/loser list or the result of an observer's
selected player. Missing data must never be interpreted as a loss.
The outcome can arrive after the first finished snapshot and match.ended event,
so result consumers must also observe state hydration. The platform retains the
current terminal match until the next match starts, including across reconnects;
it does not provide a historical result log. match.endedAt is the platform's
completion timestamp when supplied; observedAt is a client observation time.
Display updates can run repeatedly. Persistent scoring requires app-owned rules and atomic deduplication by match ID, including across windows, reloads and retries. The SDK does not choose exclusions, count session wins/losses, or reset scores.
See the developer guide for a complete display example.
Selectors
Pure state helpers live in @w3booster/sdk/selectors:
import {
broadcasterFirstTeams,
broadcasterPlayer,
groupPlayersByTeam,
headToHeadPair,
heroInventory,
inventorySlotIdentity,
gameContext,
playerHeroes,
playerDisplayIdentity,
playerResources,
playerResourcesOrZero,
playerRelationship
} from '@w3booster/sdk/selectors';
const broadcaster = broadcasterPlayer(state.match, state.players);
const teams = groupPlayersByTeam(state.players);
const observerPlayers = headToHeadPair(state.players); // typed pair, or null until both players are scoped
const broadcasterTeams = broadcasterFirstTeams(state.players, state.match);
const relation = playerRelationship(state.players[0], state.match, state.players);
const inventory = heroInventory(playerHeroes(broadcaster)[0]);
const context = gameContext(state); // always includes hudScale; no scope required
const heroes = playerHeroes(broadcaster);
const identity = playerDisplayIdentity(broadcaster, {
stripBattleTagDiscriminator: true
}); // account/in-game names plus hasAlias
const resources = playerResources(broadcaster); // undefined without scoped resource data
const resourcesForDisplay = playerResourcesOrZero(broadcaster); // explicit stable zero fallback
const itemKey = inventorySlotIdentity(0, inventory[0]);Also available: isActiveMatch(), battleTagName(), upgradeIdentity(), and currentUpgrades(). Grouping, broadcaster, pair, and team-ordering selectors preserve input subtypes, so lightweight history and view-model records do not need application-side casts or regrouping. playerDisplayIdentity() centralizes account versus in-game naming semantics and offers an explicit numeric BattleTag-discriminator policy while applications retain localized formatting. Selectors keep common protocol-derived identity, grouping, and relationship logic out of individual applications. Standard-game presentation rules such as observer ordering and simplified team colors live in the separate namespace below.
Additive overlay branches can remain strictly typed without a catch-all index signature. Supply the extension model as the client's second generic; it is deeply read-only and propagates through state, lifecycle, and event payloads. The normalization-owned names runtime, misc, and settings are reserved and rejected as extension branches:
interface TournamentOverlay {
readonly tournament: { readonly round: number };
}
const client = createClient<Settings, TournamentOverlay>({ clientId: 'your_app_id' });
const round = client.state.get()?.overlay?.tournament.round;
// Generated bindings expose the same generic:
const runtime = w3boosterApp.createRuntime<TournamentOverlay>();Warcraft III game data
Load the exact generated catalog advertised by the current match. No unit tables or
artwork are bundled with the SDK. The optional /game-data entry fetches and verifies
the catalog; images remain hosted and load when displayed.
import { loadGameData, abilityCooldownsForState } from '@w3booster/sdk/game-data';
import * as standardGame from '@w3booster/sdk/standard-game';
const data = await loadGameData(state.match.gameDataId);
const type = data.units.get(unit.typeId);
const gold = type?.cost.gold;
const supply = type?.supply.used;
const icon = data.assets.unitIcon(unit.typeId, { graphics: 'classic' });
const upgrade = data.upgrades.get(research.typeId)?.levels[research.level - 1];
const upgradeIcon = data.assets.upgradeIcon(research.typeId, {
graphics: state.match.isReforged ? 'reforged' : 'classic', level: research.level
});
const cooldowns = abilityCooldownsForState(state, data);
const clock = standardGame.dayNightState(state.match.gameTime);
const progress = standardGame.heroExperienceState(hero.experience);Rawcodes are case-sensitive and identify types; unit instance IDs identify individual
live units. Abilities and upgrades use typeId and a separate level. Missing types
or artwork return undefined. Catalog load failures never substitute another patch.
Static base stats do not replace observed HP, mana or production progress. The catalog
covers current standard melee definitions; custom map modifications remain outside its scope.
See GAME_DATA.md for the API, generated source policy and SDK 4 migration.
Lightweight presentation rules, race localization keys, colors, team ordering and
statistics selection remain in /standard-game. Upgrade categories and ability
cooldowns come from the generated catalog.
Shared asset URLs
Country flags and provider badges use the independent @w3booster/sdk/assets helpers:
import { countryFlagUrl, resolveAssetBaseUrl } from '@w3booster/sdk/assets';
const flag = countryFlagUrl(player.mainAccount?.country, {
baseUrl: resolveAssetBaseUrl()
});Country identifiers are trimmed and normalized to lowercase. Flags default to the immutable https://static.w3booster.com/assets/country-flags/v1/ catalog. resolveAssetBaseUrl() safely consumes the host-owned assetBaseUrl launch parameter only when W3Booster selects the local backend and advertises a loopback mirror; an explicit validated baseUrl still takes precedence. The artwork remains outside the npm package.
Battle.net badges use their own versioned catalog and division IDs (0–7), independent of W3Champions league IDs:
import { bnetLeagueIconUrl, bnetLeagueManifestUrl, resolveAssetBaseUrl } from '@w3booster/sdk/assets';
const options = { baseUrl: resolveAssetBaseUrl() };
const badge = bnetLeagueIconUrl(6, options); // Gladiator, compact original artwork
const largeBadge = bnetLeagueIconUrl(6, { ...options, variant: 'standard' });
const manifest = bnetLeagueManifestUrl(options);The divisions are Unplaced, Combatant, Challenger, Rival, Duelist, Elite, Gladiator and Champion. Unknown IDs return undefined; unplaced uses its standard badge because no simplified original exists. The catalog retains eight standard and seven simplified originals, with source paths, dimensions and checksums. Use an actual Battle.net division value; the helper does not derive it from MMR. Local Battle.net fetching and arranged-team attribution are still under development; this helper does not fetch player statistics.
Host actions
Embedded surfaces can ask W3Booster to perform supported host actions:
await client.host.openWindow({ path: '?view=compact', width: 520, height: 620 });
await client.host.closeWindow();
const settings = await client.host.setSetting('observer.layout', 'wide');
const accepted = await client.host.command('application.preview', undefined, {
parse(value) {
if (!value || typeof value !== 'object' || value.accepted !== true) {
throw new TypeError('Invalid preview acknowledgement');
}
return value.accepted;
}
});Every asynchronous host method accepts action options at its final argument, so pending acknowledgements can share a feature or component lifetime and use a shorter timeout when appropriate:
const feature = new AbortController();
await client.host.openWindow({ path: '?view=compact' }, { signal: feature.signal, timeout: 3000 });
feature.abort(); // cancels any later pending feature actionsclient.host.available becomes true after an authenticated connection inside a captured W3Booster application launch. Browser responses must come from the launch's embedding origin; Electron windows use the injected host bridge. Every action waits for a host acknowledgement and rejects when delivery, execution, cancellation, or timeout fails; use void client.host.openWindow(...) only when a view deliberately does not need to await it. Named window actions resolve with void. Generic command() results remain unknown unless a parser validates and transforms the host acknowledgement. A capability refresh starts automatically after authentication. host.can(capability) is a synchronous read for imperative code. Reactive UIs should subscribe to host.lifecycle and call canUseHostCapability(snapshot, capability), or consume snapshot.host from a generated application runtime, so controls update when discovery completes. Both require successful discovery and explicit advertised support; missing or invalid capability responses leave actions unavailable. host.capabilityStatus, capabilities, supports(), and subscribeCapabilities() expose lower-level reads. Call refreshCapabilities() only when an explicit refresh is needed. Settings writes are serialized globally because parent and child paths may overlap; setSetting() resolves to the complete persisted settings. Embedded application surfaces automatically report their document height. Pass autoResize: false when an application deliberately manages its host height itself.
Settings definitions
Applications and their settings are configured in W3Booster. Their client IDs, settings schemas, defaults, and requested scopes are public app metadata. The SDK generates either a TypeScript module containing exact settings types or a plain JavaScript ESM module containing the same immutable runtime metadata. Versioned connection, startup, and settings-resolution behavior stays in @w3booster/sdk/app behind the generated w3boosterApp binding.
Install the SDK once:
npm install @w3booster/sdkBind the project once with the client ID shown in the Application tab:
npx w3booster-settings init app_your_id --endpoint https://api.w3booster.comThis creates src/w3booster.generated.ts, stores the public app binding in package.json, and adds explicit w3booster:sync and w3booster:check scripts. This keeps ordinary installs, starts, and builds deterministic and offline-friendly. Pass --install-hooks only when a project deliberately wants synchronization after dependency installation and before its existing dev, start, and build scripts. Existing lifecycle commands are preserved and run after synchronization. Installed hooks invoke w3booster-settings directly from the lifecycle PATH, so they do not assume npm, pnpm, Yarn, or Bun.
Plain-JavaScript applications use the same canonical binding by selecting a JavaScript output:
npx w3booster-settings init app_your_id --endpoint https://api.w3booster.com --output src/w3booster.generated.js.js, .mjs, and .jsx outputs contain parseable plain ESM with no TypeScript-only syntax. TypeScript remains the default because it also generates exact settings aliases.
Use --output when the generated file should live somewhere else or to select the plain-JavaScript format by extension. --endpoint is persisted in the project binding for non-default platform environments; connected CI can instead provide W3BOOSTER_SETTINGS_URL. Synchronization does not rewrite an unchanged file. During ordinary development, a checked-in binding remains usable when the public endpoint is temporarily unavailable; npm run w3booster:check remains deliberately strict for CI.
Use the generated helper in the frontend. The managed runtime completes partial delivered settings over the database defaults and publishes them atomically with state and host capability changes. Generated W3BoosterAppClient, W3BoosterAppRuntime, and W3BoosterAppRuntimeSnapshot aliases preserve the delivered-versus-resolved settings distinction without consumer-side generic reconstruction:
import { w3boosterApp } from './w3booster.generated';
const runtime = w3boosterApp.createRuntime({ retry: true });
runtime.lifecycle.subscribe(snapshot => {
renderLayout(snapshot.settings);
renderMatch(snapshot.state, { fresh: snapshot.isSynchronized });
});
await runtime.start();
await runtime.stop(); // when this application surface is disposedFor a pure selector that already receives state, w3boosterApp.settingsFor(state) returns the same resolved, memoized settings value. resolveSettings(partial) remains available when only a delivered settings object is at hand.
Commit w3booster.generated.ts so editors and offline builds retain full types and schema changes remain visible in reviews. Generated runtimes send the binding revision during broker negotiation; an outdated bundle fails once with APPLICATION_DEFINITION_MISMATCH instead of running against incompatible defaults or types. Current user values are not part of the public definition; they arrive only through the authenticated state.application.settings stream.
The lower-level validateSettingsSchema(), settingsDefaults(), resolveSettings(), and generateSettingsBinding() utilities remain available from @w3booster/sdk/settings for tooling that needs to inspect definitions directly. Runtime validation alone cannot derive new compile-time field names; use the generated binding when frontend type safety matters.
Framework integration
Framework-neutral derived state is available without allocating a new result on every unrelated lifecycle update. Subscriber promise rejections are observed. Stores derived from SDK lifecycle/state stores inherit their structured client issue channel automatically; pass onError to override reporting for an application-owned source:
import { createSelectorStore } from '@w3booster/sdk/store';
const connectionStatus = createSelectorStore(runtime.lifecycle, snapshot => snapshot.status);
connectionStatus.subscribe(status => renderStatus(status), { signal: lifetime.signal });For application selectors that allocate arrays or view models, createMemoizedSelector() uses a weak cache for each first-argument object identity and retains a bounded set of Object.is option variants. Interleaved list and multi-view calls therefore keep their identities. The caller owns invalidation: inputs and returned results must be treated as immutable, and mutation does not invalidate a cached result. This matches recursively frozen SDK snapshots and the identity rules used by reactive frontend stores.
Allocation-heavy public SDK selectors cache recursively frozen SDK inputs, but recompute mutable frontend-owned arrays and records. Deserialized history and other local mutable data therefore cannot receive stale derived results.
React consumers can use the dependency-free adapter with useSyncExternalStore. Supplying a deterministic server snapshot keeps SSR and hydration explicit:
import { useSyncExternalStore } from 'react';
import { createReactStore } from '@w3booster/sdk/react';
const lifecycleStore = createReactStore(client.lifecycle, {
getServerSnapshot: () => ({ status: 'idle', state: null, isSynchronized: false, error: null })
});
const lifecycle = useSyncExternalStore(
lifecycleStore.subscribe,
lifecycleStore.getSnapshot,
lifecycleStore.getServerSnapshot
);Angular signals can consume the standard dependency-free subscribable adapter without manual teardown bookkeeping or an SDK dependency on Angular:
import { toSignal } from '@angular/core/rxjs-interop';
import { createSubscribable } from '@w3booster/sdk/store';
const lifecycle = toSignal(createSubscribable(client.lifecycle), {
initialValue: client.lifecycle.get()
});The same observer/unsubscribe contract also maps directly to an RxJS observable:
const lifecycle$ = new Observable(subscriber => {
const subscription = createSubscribable(client.lifecycle).subscribe(subscriber);
return () => subscription.unsubscribe();
});Advanced subpaths
@w3booster/sdk/assetscontains versioned URL helpers for shared hosted assets such as country flags.@w3booster/sdk/appowns typed application bindings created from generated public metadata.@w3booster/sdk/game-dataloads versioned generated types, cooldowns and artwork references on demand.@w3booster/sdk/compositorcontains browser-source composition APIs used by W3Booster's platform compositor. Its watcher renews expired browser-source sessions and reauthorizes reconnects automatically. Returned child launch URLs require HTTPS except for exact loopback development hosts and may not contain URL user information, because their fragments can carry launch credentials. Ordinary applications do not import it.@w3booster/sdk/settingscontains settings-schema types, validation, default derivation, and database-definition code generation.@w3booster/sdk/testingcontainscreateDemoTransportand custom transport types for SDK and integration tests. Application demo mode normally usesstartClient({ demo: true })instead.@w3booster/sdk/reactadapts immediately-publishing SDK stores to React's completeuseSyncExternalStorecontract without adding React as a package dependency.@w3booster/sdk/storecreates stable framework-neutral derived stores with configurable equality and signal-owned subscriptions.
Custom transports are intentionally isolated to the testing namespace:
import { openClient } from '@w3booster/sdk';
import { createDemoTransport, type TestingConnectOptions } from '@w3booster/sdk/testing';
const options: TestingConnectOptions = {
clientId: 'your_app_id',
transport: createDemoTransport()
};
const client = await openClient(options);The complete public data model is exported from @w3booster/sdk. See COMPATIBILITY.md for the Semantic Versioning and protocol policy and CHANGELOG.md for release changes.
You can keep an existing SDK version when the API adds optional state attributes. Unknown safe JSON fields are preserved in immutable snapshots and patches without being interpreted; using new typed helpers may require an upgrade. Changes to existing field types, required fields, or closed enum values are breaking changes. See additive API compatibility.
SDK 3 unit instances
Players expose three mutually exclusive instance maps: player.units,
player.heroes, and player.buildings. id is the complete opaque engine
instance identity within match.id; typeId is the Warcraft rawcode used for
metadata/artwork. Two units of the same type have different IDs. A transformed
hero retains its instance ID and reports its current typeId. Do not parse IDs or
use them across matches without the match ID.
import { playerUnits, playerHeroes, playerBuildings } from '@w3booster/sdk/selectors';
const heroes = playerHeroes(player); // Real heroes, in ascending instance-ID order
const allHeroes = playerHeroes(player, { includeIllusions: true });
const copies = allHeroes.filter(hero => hero.isIllusion);
const hero = player.heroes?.[instanceId];
const hp = hero?.hitpoints; // { current, max }, already in game HP
const buildings = playerBuildings(player);
const queue = buildings[0]?.production?.queue;Every observed unit, hero and building has a required isIllusion: boolean.
Raw instance maps retain illusions in their normal type collection with their
own IDs; there is no separate illusion model or collection. playerUnits,
playerHeroes and playerBuildings exclude illusions by default. Pass
{ includeIllusions: true } to any of them when you need every instance. Selector
results remain immutable and memoized; they do not modify the raw maps.
All three selectors return ascending full instance-ID order, including when illusions are included. This order is independent of snapshot insertion order and health/XP changes. It is deterministic, not a guarantee of spawn order or Warcraft's native hero-panel order. Do not convert the 64-bit IDs to JavaScript numbers to sort them, as that can lose precision.
For Warcraft's hero ordering, use optional hero.heroOrder. Lower keys come
first within a player. This is an engine ordering key, not a spawn timestamp or
an immutable F-key slot; ownership and lifecycle changes can reassign it. Missing
means not observed. Keys can have gaps (illusions can consume keys too); do not
use a key directly as an array index or F-key number. Sort a copy, keep missing
keys last, and break ties by full ID:
const orderedHeroes = [...playerHeroes(player)].sort((a, b) =>
(a.heroOrder ?? Number.MAX_SAFE_INTEGER) - (b.heroOrder ?? Number.MAX_SAFE_INTEGER)
|| (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));The reader always supplies the boolean. Invalid/missing classifications do not
create public observations. When constructing typed demo/test units yourself,
include isIllusion: false or true. Existing SDK versions can still consume
the extended API snapshots, but only updated selectors provide the new defaults.
Pools, hero XP/level, and production can arrive independently. Missing data is
unknown; never substitute zero. A missing production object means unavailable;
an empty queue means production was observed idle. Queue entries contain
position, typeId, and the shared TimedProgress fields. They have no future
unit ID or stable job ID, and repeated typeIds are retained. Construction has its
own optional construction: TimedProgress, based on the engine's construction
component rather than HP. Missing construction means no component is observed;
it is not a fabricated 100%. Building upgrades are not yet exposed as construction.
Every timed activity uses the same fields:
interface TimedProgress {
progress: number | null; // completed fraction, 0..1
totalSeconds: number | null; // positive game-time duration
remainingSeconds: number | null; // game seconds until completion
}Timers are either both known or both null. Waiting/uninitialized/unreadable
production has null timers; waiting slots have zero progress. Render seconds
with Math.ceil(remainingSeconds), percentage with progress * 100, or elapsed
with totalSeconds - remainingSeconds, after checking for null. Use snapshots;
never advance these timers on a wall clock through game pauses or speed changes.
abilityCooldown() and abilityCooldownsForState() return this shape plus
active, replacing their former total, remaining, and elapsed fields.
Cooldowns remain standard-game estimates from activation and the game clock;
construction and production use observed engine timers. Researching upgrades
also use this shape instead of start/finish wall-clock timestamps. The current
native research list does not provide active research timing; unavailable fields
stay null rather than using a guessed duration.
Heroes expose position: { x, y } with changed values sampled every 200 ms.
Structures expose their first successfully observed position for the instance
and retain it if moved; this is not necessarily a spawn location. Ordinary units
omit position. Inventory remains an ordered array of rawcodes, with empty
strings and repeated items representing actual slots.
Collections are observations, not authoritative unit/building counts. No completeness flag is emitted without a reliable producer. Missing collections mean outside scope or unavailable; empty collections contain no observed entries. Ordinary games expose the current player's units; observer/replay collection covers participating players and excludes neutrals. Hero data retains its own permission and entitlement gate. A production-only grant supplies building identity and queues without granting building health. Construction requires both building and production capabilities. Production, construction, heroes and research require PRO in self-play; observing and replaying are free. Native collection and API capabilities enforce this before the SDK receives the data.
Unit.mana exposes observed current and max values for ordinary units as well
as heroes. Missing mana means unavailable, not zero; max: 0 is an observed unit
without mana capacity. For a caster group's average, use observed members with
mana.max > 0 and keep unknown members distinct from empty mana pools.
SDK processing adds no poller. Ordinary health, mana and queues follow the native 200 ms cycle; hero HP/mana follows the XP loop. Unchanged observations do not publish a new snapshot, and untouched instance objects retain identity.
SDK 4 uses protocol 4 and rejects earlier protocol majors. Migrate Hero.id rawcode lookups to
Hero.typeId, replace array access with selectors or instance-map lookup, and
keep DOM keys based on instance IDs. API, native recorder, desktop relay, and
consumer SDK releases need a coordinated rollout. See the SDK 4 migration guide.
Ladder records
player.stats.records contains explicit provider, gameMode, queue, optional
season/race, and provider-reported mmr, rank, league and win/loss values.
Arranged records carry team.id and team.members; they are never collapsed into
a player's random-team rating. Unknown values remain absent.
standardGame.statsForMode(player, mode, { provider, queue, teamId }) selects one exact
record and returns undefined for unavailable or ambiguous ladders. There is no
fallback to another mode or race. preferredStats and the old solo/team slots
are removed in SDK 3 and later.
APM, mana recovery, combat and local economy (SDK 4.2)
Player.apm is Warcraft’s average APM under resources:read.
player.apm.changed reports changes; zero is valid and absence means unavailable.
Self-play resources and APM belong to the real local player, independently of
the selected player; observer/replay access covers participating players.
Missing economy remains unavailable until gold, lumber, supply and supply cap
have all been observed; worker supply is optional. A worker-count packet alone
does not imply zero economy. SDK 4.3 recovers partial local feeds without
fabricating missing values. Actual observed zero remains valid.
Unit.mana.regenerationPerSecond is an optional observed net rate per game
second. It may be zero or negative; only a positive reading supports a recovery
estimate. Use the current catalog ability level’s mana cost. Do not extrapolate
actual mana or run a wall-clock countdown through a paused replay.
Hero.combat carries cumulative damage dealt, self-damage, damage received and
healing dealt. Self-damage is included in both damage totals. Healing includes
self-healing and excludes ordinary regeneration; a separate healing-to-others
total is not available. Treat these as observations for the hero instance, not
a guarantee of full-match coverage when recording starts late.
Since SDK 4.3, supporting recorders also provide hero.combat.damage: a summon-inclusive
total, unit contributions in breakdown, and a complete coverage flag.
The original damageDealt remains Warcraft's raw hero counter for compatibility.
const damage = hero.combat?.damage;
if (damage) {
for (const contribution of damage.breakdown) {
// Sum damageDealt once per contribution, not once per listed unit type.
renderContribution(contribution.units, contribution.damageDealt);
}
renderTotal(damage.total, { complete: damage.complete });
}Each contribution lists { typeId, isIllusion } unit types. Repeated summons of
one type are grouped; illusions are separate from real units of the same type.
Each type/illusion pair appears in only one contribution.
For example, a Keeper and treants can have separate contributions. A Water
Elemental redirects its damage to the Archmage: their shared contribution lists
both types. Multiple types mean a shared amount, not an individual amount for
each type. The SDK never labels that merged counter as the hero's direct damage,
nor reports a redirected summon as having dealt zero. Contributions always sum
to total. There is no ability breakdown.
Accounting retains observed damage after summons disappear. Standard melee
summoning relationships provide a fallback only for a unique eligible same-owner
hero. Duplicate casters or unsupported custom relationships can remain unresolved.
complete: false reports known gaps such as late recording, unresolved sources,
lost final reads or incomplete enumeration. Doom/Dark Arrow can briefly credit
the dying enemy instead of their summon; those unrecoverable hits also make the
summary incomplete. A partial total is a lower bound, not an exact all-summon
lifetime total. Missing damage means the recorder
does not provide this summary. Never label the legacy fallback as summon-inclusive
or add current summon damage to the corrected total again.
PlayerStatsCollection.source and observedAt identify live/cache results and
the original Unix-millisecond observation time. Older producers may omit them.
See the SDK 4.2 guide for access and
lifecycle details, checked examples and the changes since the SDK 3 announcement.
Health, mana and known active progress values can now advance between observations using the engine clock. See interpolation behavior for heartbeat frequency, stale-stream handling and coordinated release requirements.
Observer/replay consumers can use explicit participant outcomes, cumulative item
and economy counters, and bounded loss windows through the optional
@w3booster/sdk/analytics entry. Requires the corresponding recorder
and platform version; unavailable observations remain absent.
In-game mouse interaction
Mark UI containers with w3-interactive. The optional input bridge supports
block/conditional routing, live region updates and pointer-based dragging.
See the overlay input guide for host requirements, examples
and hang behavior. Requires SDK 4.4 and the matching desktop host/compositor release.
Neutral map points of interest
Applications granted match:read and pois:read can read state.pois or use
pointsOfInterest(state, { kind: 'goblin-merchant' }) from
@w3booster/sdk/selectors. Entries contain building positions and optional offers,
stock and timing data when supplied by the recorder. Missing fields mean unknown.
In self-play, state.poiMode === 'initial': building locations and starting
inventories are captured once at game start, without visibility filtering, and
never updated during the match. Do not advance their timers. Observer/replay
snapshots may use poiMode === 'live'. If the game-start capture was missed, the
initial collection is unavailable. Catalog sale lists and stock defaults describe
standard object configuration, not current inventory or custom-map overrides.
See the SDK 4.3 guide for native coverage, stock timers and examples.
Stats for the current platform
W3C records can also be available during Battle.net matches. Every ladder record
retains its provider; results from both providers coexist without replacing
one another. Use the recorder realm to select the current platform:
import { currentStatsProvider, currentPlatformStats } from '@w3booster/sdk/selectors';
import { statsForMode } from '@w3booster/sdk/standard-game';
const records = currentPlatformStats(state.match, player);
const provider = currentStatsProvider(state.match);
const rating = provider ? statsForMode(player, state.match.mode, { provider }) : undefined;Reforged/Battle.net select bnet; W3Champions regional realms select
w3champions. Unknown realms return no provider. These selectors never fall back
to another platform's stats. W3C lookup requires an exact full BattleTag; aliases
are not guessed. Existing scopes and identity redaction still apply.
Collection source/observedAt, when present in a mixed collection, describe the
Battle.net desktop observation, not the freshness of W3C records.
