stovesdk.js 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 firejune
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,206 @@
1
- # Temporary Holding Version
1
+ # stovesdk.js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Unofficial Node.js / Electron bindings for the **STOVE PC SDK** (N-API).
4
+
5
+ The STOVE PC SDK ships official bindings for C, C#, and C++ only. This project wraps the native SDK so Electron and Node.js games can use it the way `steamworks.js` wraps Steamworks.
6
+
7
+ > **Status: alpha.** Install from npm as `stovesdk.js`; the Windows x64 prebuild ships inside the package and is also attached to each [GitHub release](https://github.com/firejune/stovesdk.js/releases). See *Install* and *Build* below.
8
+
9
+ ## Scope
10
+
11
+ A thin wrapper around the SDK. No game policy.
12
+
13
+ - SDK initialize / uninitialize
14
+ - Callback pump (`RunCallback`) for the host event loop
15
+ - Ownership query
16
+ - Achievements and stats
17
+ - Login identity reads (user, country/language)
18
+
19
+ What a game does when a check fails (blocking, demo modes, messaging, quitting) belongs in the game, not in this module.
20
+
21
+ ## Platform
22
+
23
+ Windows x64 only, which matches the STOVE PC SDK itself. The addon targets N-API (version 8), so one build works across Node.js and Electron versions without a rebuild for every Electron ABI.
24
+
25
+ ## Bring your own SDK
26
+
27
+ The STOVE PC SDK is **not** included in this repository and is not redistributed by this package.
28
+
29
+ 1. Download the STOVE PC SDK from the STOVE developer portal under your own developer account.
30
+ 2. Place it under [`sdk/`](sdk/README.md), or point `STOVE_PCSDK_DIR` at your copy. The expected layout is documented in `sdk/README.md`; `npm run check-sdk` tells you what is missing.
31
+
32
+ Your use of the SDK is governed by STOVE's own terms.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ npm install stovesdk.js
38
+ ```
39
+
40
+ The package carries the Windows x64 prebuild at `prebuilds/win32-x64/stovesdk.node` (with its `SHA256SUMS`), which is the last place `load()` looks, so the binary needs no further step. Installing compiles nothing: `package.json` sets `"gypfile": false`, so npm does not run the implicit `node-gyp rebuild` that a root `binding.gyp` would otherwise trigger, and the install succeeds on any platform — the JS entry works everywhere, and off Windows `load()` refuses at runtime with code `STOVE_PCSDK_UNSUPPORTED_PLATFORM`.
41
+
42
+ What you still provide is the SDK: `BaseSDK.dll`, `OwnershipSDK.dll` and `GameSupportSDK.dll` next to the `.node` or next to your executable (see *Build*). They are part of the SDK, not of this package.
43
+
44
+ ### Where `load()` looks
45
+
46
+ In order; the first file that exists wins:
47
+
48
+ | Location | How |
49
+ | --- | --- |
50
+ | Any path | `load('C:/path/to/stovesdk.node')` |
51
+ | Any path | `STOVE_PCSDK_ADDON=C:/path/to/stovesdk.node` in the environment |
52
+ | `node_modules/stovesdk.js/build/Release/` | where `npm run build` puts its output |
53
+ | `node_modules/stovesdk.js/prebuilds/win32-x64/` | where the npm package ships the prebuild |
54
+
55
+ An Electron app usually ships the `.node` in its own resources, next to the three SDK DLLs it has to ship anyway, and calls `load(path)` with that location. When none of the four places has a file, `load()` throws with code `STOVE_PCSDK_ADDON_NOT_FOUND` and lists the paths it tried.
56
+
57
+ ### Installing from a git tag instead
58
+
59
+ ```sh
60
+ npm install github:firejune/stovesdk.js#vX.Y.Z
61
+ ```
62
+
63
+ installs the same package without the prebuild (binaries are not tracked in git). Download `stovesdk.node` and `SHA256SUMS` from the [GitHub release](https://github.com/firejune/stovesdk.js/releases) of the same version, check the sum (`sha256sum -c SHA256SUMS`, or `CertUtil -hashfile stovesdk.node SHA256`), and put the binary in one of the places above. (v0.1.0 still runs `node-gyp rebuild` on install; add `--ignore-scripts` for that one.)
64
+
65
+ Or build the binary yourself, below.
66
+
67
+ ## Build
68
+
69
+ On Windows x64 with Visual Studio Build Tools (C++ workload) and Python installed. The pinned build tool (node-gyp 13) declares Node `^22.22.2 || ^24.15.0 || >=26` — that is a requirement of the build, not of the addon, whose runtime floor stays Node 18:
70
+
71
+ ```sh
72
+ npm ci # dev dependencies; nothing is compiled on install
73
+ npm run check-sdk # verifies sdk/ or STOVE_PCSDK_DIR
74
+ npm run build # node-gyp rebuild → build/Release/stovesdk.node
75
+ for m in BaseSDK OwnershipSDK GameSupportSDK; do
76
+ cp "$(node tools/sdk-dir.js)/$m/Deploy/Bin/x64/Release/$m.dll" build/Release/
77
+ done # the smoke loads the .node, which needs the three SDK DLLs beside it
78
+ npm run smoke # loads the addon and exercises the paths that need no STOVE client
79
+ ```
80
+
81
+ The `.node` hard-imports `BaseSDK.dll`, `OwnershipSDK.dll` and `GameSupportSDK.dll`. Ship those three DLLs next to the `.node` (or next to your executable) — they are part of the SDK, not of this package. When one is in neither place, `load()` throws with code `STOVE_PCSDK_DLL_NOT_FOUND` naming the missing files and the two directories it checked (see *`load()` errors*).
82
+
83
+ On other platforms the JS side still works for development, but there is nothing to compile: `npm ci`, then `npm test` and `npm run typecheck` run as they do in CI; `tools/syntax-check.sh` can parse the C++ against your SDK headers with clang as a pre-flight.
84
+
85
+ ## Usage
86
+
87
+ ```js
88
+ const { load, ErrorCode } = require('stovesdk.js')
89
+
90
+ const stove = load() // throws off Windows, when no build/prebuild exists, or when an SDK DLL is missing (see `load()` errors)
91
+
92
+ // The SDK delivers every asynchronous result through its callback pump.
93
+ // Run it on the thread that initializes the SDK (the Electron main process), ~16 ms.
94
+ const pump = setInterval(() => stove.runCallbacks(), stove.getAddonInfo().pumpIntervalMs)
95
+
96
+ async function main() {
97
+ const init = await stove.initialize({
98
+ environment: 'LIVE', // as configured for your title in STOVE Studio
99
+ gameId: 'YOUR_GAME_ID',
100
+ applicationKey: 'YOUR_APPLICATION_KEY', // never the Application Secret
101
+ })
102
+ if (init.status === 'restart') {
103
+ // The SDK asked the STOVE client to relaunch the app through the launcher.
104
+ // The vendor guide expects the app to exit here — that decision is yours.
105
+ return
106
+ }
107
+
108
+ const { OwnershipCode, OwnershipGameCode } = stove.constants
109
+ const ownerships = await stove.getOwnershipList()
110
+ const ownsGame = ownerships.some(
111
+ o => o.ownershipCode === OwnershipCode.ACQUIRE && o.gameCode === OwnershipGameCode.BASIC
112
+ )
113
+
114
+ // Achievements on STOVE are driven by stats: an achievement is a stat plus a goal,
115
+ // so "unlocking" one means modifying its stat.
116
+ await stove.setStat('STAT_ID', 1)
117
+ const achievement = await stove.getAchievement('ACHIEVEMENT_ID')
118
+ console.log(ownsGame, achievement.status)
119
+ }
120
+
121
+ main()
122
+ .catch(err => {
123
+ // Every rejection is an Error with { step, sdk, method, code, externalError }.
124
+ if (err.code === ErrorCode.NOT_INITIALIZED) console.error('call initialize() first')
125
+ else console.error(err.step, err.code, err.message)
126
+ })
127
+ .finally(() => {
128
+ clearInterval(pump)
129
+ stove.uninitialize()
130
+ })
131
+ ```
132
+
133
+ ## API
134
+
135
+ All functions live on the object returned by `load()`. Full types: [`index.d.ts`](index.d.ts).
136
+
137
+ | Function | SDK call | Returns |
138
+ | --- | --- | --- |
139
+ | `initialize({ environment, gameId, applicationKey, restartWaitMs? })` | `Base_RestartAppIfNecessaryAsync` → `Base_Initialize` | `Promise<{ status: 'ok', result } \| { status: 'restart' }>` |
140
+ | `runCallbacks()` | `Base_RunCallback` | `void` — call periodically; Promises settle from here |
141
+ | `isInitialized()` | — | `boolean` |
142
+ | `uninitialize()` | `GameSupport_UnInitialize`, `Ownership_UnInitialize`, `Base_UnInitialize` (those that were initialized) | `{ gameSupport, ownership, base }` — pending Promises reject with `ErrorCode.ABORTED` |
143
+ | `getOwnershipList()` | `Ownership_OwnershipList` (`Ownership_Initialize` lazily) | `Promise<Ownership[]>` — raw list; interpret with `constants` |
144
+ | `getUser()` | `Base_GetUser` | `{ ok, memberNumber, gameUserId, nickname }` (sync) |
145
+ | `getGds()` | `Base_GetGds` | `{ ok, nation, isDefault, language }` (sync) |
146
+ | `getAchievement(id)` | `GameSupport_Achievement` | `Promise<Achievement>` (a query — completes nothing) |
147
+ | `getAllAchievements()` | `GameSupport_AllAchievement` | `Promise<Achievement[]>` |
148
+ | `getStat(statId)` | `GameSupport_Stat` | `Promise<Stat>` |
149
+ | `setStat(statId, int32)` | `GameSupport_ModifyStat` | `Promise<ModifyStatResult>` |
150
+ | `getVersion()` | `Base_GetVersion` | `{ ok, version }` (sync, informational) |
151
+ | `getAddonInfo()` | — | `{ sdkVersion, napiVersion, arch, platform, pumpIntervalMs }` |
152
+ | `constants` | SDK enums | `OwnershipCode`, `OwnershipGameCode`, `ErrorCode` |
153
+
154
+ Notes:
155
+
156
+ - `GameSupport_Initialize` runs lazily on the first achievement/stat call; `Ownership_Initialize` on the first `getOwnershipList()`. Both are torn down by `uninitialize()`, Base last.
157
+ - 64-bit values (`memberNumber`, `purchaseDate`, `updatedAt`) cross as decimal strings so no precision is lost.
158
+ - Synchronous reads (`getUser`, `getGds`) never throw: before `initialize()` they answer `{ ok: false, code: ErrorCode.NOT_INITIALIZED, reason }`.
159
+ - Argument errors throw synchronously (`TypeError` / `RangeError`) before anything reaches the SDK.
160
+ - A stat that has never been written reports `currentValue` as `-2147483648`. That is the SDK's own `int32` value, passed through unchanged.
161
+ - Leave a moment between `uninitialize()` and the next `initialize()`. The STOVE client re-creates its side of the connection after a disconnect, and an immediate re-initialize can answer `{ status: 'restart' }`.
162
+
163
+ ### Error codes
164
+
165
+ Rejected Promises carry `step`, `sdk`, `method`, `code` and `externalError`. `code` is the SDK's own result code (unsigned) when the SDK answered, or one of these negative addon codes when the addon refused the call:
166
+
167
+ | `ErrorCode` | Value | Meaning |
168
+ | --- | --- | --- |
169
+ | `NOT_INITIALIZED` | `-1` | `initialize()` has not succeeded, or `uninitialize()` has run |
170
+ | `ALREADY_INITIALIZED` | `-2` | `initialize()` called while the SDK is up |
171
+ | `IN_PROGRESS` | `-3` | the same kind of call is already in flight |
172
+ | `ABORTED` | `-4` | a pending Promise was discarded by `uninitialize()` |
173
+
174
+ ### `load()` errors
175
+
176
+ `load()` throws synchronously; each error carries a string `code`:
177
+
178
+ | `code` | When |
179
+ | --- | --- |
180
+ | `STOVE_PCSDK_UNSUPPORTED_PLATFORM` | not Windows |
181
+ | `STOVE_PCSDK_ADDON_NOT_FOUND` | no `stovesdk.node` at the path given, in `STOVE_PCSDK_ADDON`, or in `build/Release/` / `prebuilds/win32-x64/`; the message lists the paths tried |
182
+ | `STOVE_PCSDK_DLL_NOT_FOUND` | the addon exists but one of `BaseSDK.dll`, `OwnershipSDK.dll`, `GameSupportSDK.dll` is next to neither the addon nor the host executable; `missing` and `searched` list the files and directories, `cause` is Node's `ERR_DLOPEN_FAILED` |
183
+
184
+ When all three DLLs are present and the load still fails, the original error is thrown unchanged — the cause is then outside what this module can see, such as the DLLs' own imports (the SDK requires the VC++ redistributable) or a binary built for another architecture.
185
+
186
+ ## Threading model
187
+
188
+ SDK callbacks are plain C function pointers with no user-data slot, and the vendor docs do not state which thread they fire on. The addon therefore never touches N-API inside a callback: callbacks copy their result into a mutex-guarded queue, and `runCallbacks()` drains that queue on the JS thread, where it advances the init chain, issues follow-up SDK calls and settles Promises.
189
+
190
+ ## Prebuilds, publishing and CI
191
+
192
+ [`.github/workflows/prebuild.yml`](.github/workflows/prebuild.yml) is a manually triggered Windows x64 build that expects the SDK to be present on the runner at `STOVE_PCSDK_DIR`. Because the SDK is not public, the workflow does not download it; how CI gets the SDK is an open design question — see the workflow header for the options under consideration. Until it is settled, the prebuild is built by the maintainer and attached to the GitHub release.
193
+
194
+ [`.github/workflows/publish.yml`](.github/workflows/publish.yml) publishes a tagged release to npm, by manual dispatch once its prebuild is attached: it takes `stovesdk.node` from the GitHub release, checks it against `SHA256SUMS`, checks that the tarball carries that one binary and nothing from the SDK, and publishes over OIDC trusted publishing with provenance. [RELEASING.md](RELEASING.md) has the sequence.
195
+
196
+ ## Contributing and releases
197
+
198
+ [CONTRIBUTING.md](CONTRIBUTING.md) covers issues, the checks a pull request has to clear and the commit conventions; [RELEASING.md](RELEASING.md) covers how a version is cut and how prebuilds reach a release; [ROADMAP.md](ROADMAP.md) says what comes next.
199
+
200
+ ## Disclaimer
201
+
202
+ This is an independent project. It is not affiliated with, endorsed by, or supported by Smilegate or STOVE. "STOVE" is a trademark of its respective owner.
203
+
204
+ ## License
205
+
206
+ [MIT](LICENSE)
package/binding.gyp ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ # stovesdk.js — N-API addon build (Windows x64 only).
3
+ #
4
+ # Why node-gyp (and not cmake-js / napi-rs):
5
+ # - The SDK ships as MSVC C++ headers + import libs + DLLs, and its structs have their
6
+ # ctors/dtors inside the DLLs. Wrapping that from C++ with node-addon-api is a direct call.
7
+ # - node-gyp finds MSVC via vswhere on its own, needs no CMake, and injects the
8
+ # win_delay_load_hook that lets a module built against Node headers load inside electron.exe.
9
+ # N-API (NAPI_VERSION=8) is the ABI-stable surface, so one .node loads in Node and Electron.
10
+ #
11
+ # The SDK is never part of this repository: tools/sdk-dir.js resolves it from STOVE_PCSDK_DIR
12
+ # or ./sdk and fails with a readable message when the layout is incomplete.
13
+ 'variables': {
14
+ 'stove_pcsdk_dir%': '<!(node tools/sdk-dir.js)',
15
+ 'stove_pcsdk_sdk_version%': '<!(node tools/sdk-dir.js version)',
16
+ },
17
+ 'targets': [
18
+ {
19
+ 'target_name': 'stovesdk',
20
+ 'conditions': [
21
+ ['OS=="win"', {
22
+ 'sources': ['src/stovesdk.cc'],
23
+ 'include_dirs': [
24
+ "<!@(node -p \"require('node-addon-api').include\")",
25
+ # Root first: OwnershipSDK/GameSupportSDK headers include
26
+ # "BaseSDK/Deploy/Include/Misc/Structures.h" relative to this root.
27
+ '<(stove_pcsdk_dir)',
28
+ '<(stove_pcsdk_dir)/BaseSDK/Deploy/Include',
29
+ '<(stove_pcsdk_dir)/OwnershipSDK/Deploy/Include',
30
+ '<(stove_pcsdk_dir)/GameSupportSDK/Deploy/Include',
31
+ ],
32
+ 'libraries': [
33
+ # Import libraries: the .node then hard-imports BaseSDK.dll / OwnershipSDK.dll /
34
+ # GameSupportSDK.dll. Intentional — a missing DLL fails at require() time as a
35
+ # catchable JS error instead of an SEH crash on first call that /DELAYLOAD would give.
36
+ '<(stove_pcsdk_dir)/BaseSDK/Deploy/Lib/x64/Release/BaseSDK.lib',
37
+ '<(stove_pcsdk_dir)/OwnershipSDK/Deploy/Lib/x64/Release/OwnershipSDK.lib',
38
+ '<(stove_pcsdk_dir)/GameSupportSDK/Deploy/Lib/x64/Release/GameSupportSDK.lib',
39
+ ],
40
+ 'defines': [
41
+ 'NAPI_VERSION=8',
42
+ 'NAPI_DISABLE_CPP_EXCEPTIONS',
43
+ 'NOMINMAX',
44
+ 'WIN32_LEAN_AND_MEAN',
45
+ 'UNICODE',
46
+ '_UNICODE',
47
+ 'STOVE_PCSDK_SDK_VERSION="<(stove_pcsdk_sdk_version)"',
48
+ ],
49
+ 'msvs_settings': {
50
+ 'VCCLCompilerTool': {
51
+ 'ExceptionHandling': 1,
52
+ 'AdditionalOptions': ['/std:c++17', '/utf-8'],
53
+ },
54
+ },
55
+ 'configurations': {
56
+ 'Release': {
57
+ 'msvs_settings': {
58
+ 'VCCLCompilerTool': {
59
+ # /MD (dynamic CRT): the SDK DLLs themselves require the VC++ redistributable
60
+ # (MSVCP140 / VCRUNTIME140), so matching them adds no new runtime dependency.
61
+ 'RuntimeLibrary': 2,
62
+ },
63
+ },
64
+ },
65
+ },
66
+ }, {
67
+ # Nothing to build off Windows; keep the target so `node-gyp configure` still succeeds.
68
+ 'type': 'none',
69
+ }],
70
+ ],
71
+ },
72
+ ],
73
+ }
package/index.d.ts ADDED
@@ -0,0 +1,241 @@
1
+ // Type definitions for stovesdk.js — unofficial Node.js / Electron bindings for the STOVE PC SDK.
2
+
3
+ /** Result of a synchronous SDK call. */
4
+ export interface SdkResult {
5
+ /** `true` when the SDK reported success. */
6
+ ok: boolean
7
+ /** Which SDK module answered (e.g. "BaseSDK"). Empty when the addon refused the call itself. */
8
+ sdk: string
9
+ /** SDK method code. */
10
+ method: number
11
+ /** SDK result code (unsigned), or a negative {@link ErrorCode} when the addon refused the call. */
12
+ code: number
13
+ }
14
+
15
+ /** Result delivered through an SDK callback — carries the SDK's own message as well. */
16
+ export interface SdkCallbackResult extends SdkResult {
17
+ message: string
18
+ externalError: number
19
+ }
20
+
21
+ /** A synchronous read the addon did not forward to the SDK (it is not initialized yet). */
22
+ export interface NotAskedResult extends SdkResult {
23
+ ok: false
24
+ reason: string
25
+ }
26
+
27
+ /**
28
+ * Error shape for every rejected Promise. `code` is the SDK result code, or one of
29
+ * {@link ErrorCode} (negative) when the addon itself refused the call.
30
+ */
31
+ export interface SdkError extends Error {
32
+ /** Which step failed: "restartAppIfNecessary" | "initialize" | "ownershipInitialize" | "ownershipList" | "gameSupportInitialize" | "achievement" | "allAchievements" | "stat" | "modifyStat" | "uninitialize". */
33
+ step: string
34
+ sdk: string
35
+ method: number
36
+ code: number
37
+ externalError: number
38
+ }
39
+
40
+ /** Addon-level error codes (negative so they never collide with SDK result codes). */
41
+ export declare const ErrorCode: Readonly<{
42
+ /** `initialize()` has not succeeded (or `uninitialize()` has run). */
43
+ NOT_INITIALIZED: -1
44
+ /** `initialize()` was called while the SDK is already initialized. */
45
+ ALREADY_INITIALIZED: -2
46
+ /** The same kind of call is already in flight. */
47
+ IN_PROGRESS: -3
48
+ /** A pending Promise was discarded by `uninitialize()`. */
49
+ ABORTED: -4
50
+ }>
51
+
52
+ export interface InitializeOptions {
53
+ /** STOVE environment string as given by STOVE Studio for your title (e.g. "LIVE"). */
54
+ environment: string
55
+ /** Game ID issued by STOVE Studio. */
56
+ gameId: string
57
+ /** Application Key issued by STOVE Studio. Never the Application Secret. */
58
+ applicationKey: string
59
+ /** How long the SDK may wait for the STOVE client when checking whether a relaunch is needed. Default 60000. */
60
+ restartWaitMs?: number
61
+ }
62
+
63
+ export type InitializeResult =
64
+ /** The SDK is initialized; start pumping `runCallbacks()` for as long as it stays up. */
65
+ | { status: 'ok'; result: SdkCallbackResult }
66
+ /**
67
+ * The SDK could not attach to a running STOVE client and has asked the client to relaunch the
68
+ * application. The SDK is NOT initialized. The vendor guide expects the application to exit now.
69
+ */
70
+ | { status: 'restart' }
71
+
72
+ export interface Ownership {
73
+ gameId: string
74
+ /** One of {@link constants.OwnershipGameCode}. */
75
+ gameCode: number
76
+ /** One of {@link constants.OwnershipCode}. */
77
+ ownershipCode: number
78
+ /** 64-bit timestamp as a decimal string. */
79
+ purchaseDate: string
80
+ /** 64-bit member number as a decimal string. */
81
+ memberNumber: string
82
+ }
83
+
84
+ export interface UserInfo extends SdkResult {
85
+ memberNumber: string
86
+ gameUserId: string
87
+ nickname: string
88
+ }
89
+
90
+ export interface GdsInfo extends SdkResult {
91
+ /** Country code of the login. */
92
+ nation: string
93
+ /** `true` when the country could not be determined and `nation` is STOVE's default instead. */
94
+ isDefault: boolean
95
+ language: string
96
+ }
97
+
98
+ export interface AchievementCondition {
99
+ valueOperation: string
100
+ type: string
101
+ goalValue: number
102
+ }
103
+
104
+ export interface Achievement {
105
+ achievementId: string
106
+ name: string
107
+ description: string
108
+ /** Status string as reported by the SDK (e.g. "ONGOING", "ACHIEVED"). */
109
+ status: string
110
+ value: number
111
+ defaultImageUrl: string
112
+ achievedImageUrl: string
113
+ condition: AchievementCondition | null
114
+ result: SdkCallbackResult
115
+ }
116
+
117
+ export interface Stat {
118
+ statId: string
119
+ gameId: string
120
+ memberNumber: string
121
+ /** 64-bit timestamp as a decimal string. */
122
+ updatedAt: string
123
+ currentValue: number
124
+ result: SdkCallbackResult
125
+ }
126
+
127
+ export interface ModifyStatResult {
128
+ statId: string
129
+ /** The value that was sent. */
130
+ value: number
131
+ /** The stat's value after the SDK applied the change. */
132
+ currentValue: number
133
+ updated: boolean
134
+ errorMessage: string
135
+ result: SdkCallbackResult
136
+ }
137
+
138
+ export interface UninitializeResult {
139
+ gameSupport: SdkResult | null
140
+ ownership: SdkResult | null
141
+ base: SdkResult | null
142
+ }
143
+
144
+ export interface AddonInfo {
145
+ /** Contents of `<sdk>/VERSION` at build time, or "unknown". */
146
+ sdkVersion: string
147
+ napiVersion: number
148
+ arch: 'x64'
149
+ platform: 'win32'
150
+ /** Suggested `runCallbacks()` interval (vendor guide). */
151
+ pumpIntervalMs: number
152
+ }
153
+
154
+ export interface Constants {
155
+ OwnershipCode: Readonly<{ NONE: number; ACQUIRE: number; LOSE: number }>
156
+ OwnershipGameCode: Readonly<{ NONE: number; BASIC: number; DEMO: number; DLC: number }>
157
+ ErrorCode: typeof ErrorCode
158
+ }
159
+
160
+ /** The native addon surface. */
161
+ export interface StovePcSdk {
162
+ getAddonInfo(): AddonInfo
163
+ /** Base_GetVersion. Synchronous, informational; may report `ok: false` before initialization. */
164
+ getVersion(): SdkResult & { version: string }
165
+
166
+ /**
167
+ * Base_RestartAppIfNecessaryAsync followed by Base_Initialize. Settles from a later
168
+ * `runCallbacks()` call, so start the pump before awaiting this.
169
+ */
170
+ initialize(options: InitializeOptions): Promise<InitializeResult>
171
+ /** Base_RunCallback plus Promise settlement. Call periodically (see `getAddonInfo().pumpIntervalMs`) on the thread that called `initialize()`. */
172
+ runCallbacks(): void
173
+ isInitialized(): boolean
174
+ /** Uninitializes every module that was initialized (GameSupport, Ownership, then Base). Rejects pending Promises with `ErrorCode.ABORTED`. Safe to call twice. */
175
+ uninitialize(): UninitializeResult
176
+
177
+ /** Ownership_OwnershipList (Ownership_Initialize is called lazily). Returns the raw list; interpret it with `constants`. */
178
+ getOwnershipList(): Promise<Ownership[]>
179
+
180
+ /** Base_GetUser. Synchronous; answers `{ ok: false, code: ErrorCode.NOT_INITIALIZED }` before initialization instead of throwing. */
181
+ getUser(): UserInfo | NotAskedResult
182
+ /** Base_GetGds. Synchronous; same refusal shape as `getUser()`. */
183
+ getGds(): GdsInfo | NotAskedResult
184
+
185
+ /** GameSupport_Achievement — a query. Achievements are completed by modifying their stat (`setStat`). */
186
+ getAchievement(achievementId: string): Promise<Achievement>
187
+ /** GameSupport_AllAchievement. */
188
+ getAllAchievements(): Promise<Achievement[]>
189
+ /** GameSupport_Stat. */
190
+ getStat(statId: string): Promise<Stat>
191
+ /** GameSupport_ModifyStat. `value` must be an int32 (throws RangeError otherwise). */
192
+ setStat(statId: string, value: number): Promise<ModifyStatResult>
193
+
194
+ readonly constants: Constants
195
+ }
196
+
197
+ /** File name of the compiled addon. */
198
+ export declare const ADDON_FILE: 'stovesdk.node'
199
+
200
+ /** The SDK DLLs the addon imports: `BaseSDK.dll`, `OwnershipSDK.dll`, `GameSupportSDK.dll`. They ship with the SDK, not with this package. */
201
+ export declare const SDK_DLLS: readonly string[]
202
+
203
+ /** Paths `load()` tries, in order. */
204
+ export declare function candidateAddonPaths(env?: NodeJS.ProcessEnv, baseDir?: string): string[]
205
+
206
+ /** First existing candidate path; throws (code `STOVE_PCSDK_ADDON_NOT_FOUND`) when none exists. */
207
+ export declare function resolveAddonPath(env?: NodeJS.ProcessEnv, baseDir?: string, exists?: (p: string) => boolean): string
208
+
209
+ /**
210
+ * Error `load()` throws when the addon file exists but cannot be loaded because one or more of
211
+ * {@link SDK_DLLS} is next to neither the addon nor the host executable.
212
+ */
213
+ export interface DllNotFoundError extends Error {
214
+ code: 'STOVE_PCSDK_DLL_NOT_FOUND'
215
+ /** The DLL file names that were not found. */
216
+ missing: string[]
217
+ /** The directories that were checked: the addon's and the host executable's. */
218
+ searched: string[]
219
+ /** Node's own `ERR_DLOPEN_FAILED` error. */
220
+ cause: Error
221
+ }
222
+
223
+ /**
224
+ * Which of {@link SDK_DLLS} are found neither in the directory of `addonPath` nor in that of
225
+ * `execPath` (default `process.execPath`). Empty when all three are present.
226
+ */
227
+ export declare function missingSdkDlls(addonPath: string, execPath?: string, exists?: (p: string) => boolean): string[]
228
+
229
+ /**
230
+ * The error `load()` throws when `require(addonPath)` threw `err`: a {@link DllNotFoundError}
231
+ * when `err` is `ERR_DLOPEN_FAILED` and an SDK DLL is missing, otherwise `err` itself.
232
+ */
233
+ export declare function explainLoadError(err: unknown, addonPath: string, execPath?: string, exists?: (p: string) => boolean): unknown
234
+
235
+ /**
236
+ * Loads the native addon (cached after the first call). Throws with code
237
+ * `STOVE_PCSDK_UNSUPPORTED_PLATFORM` off Windows, `STOVE_PCSDK_ADDON_NOT_FOUND` when no build
238
+ * exists, and `STOVE_PCSDK_DLL_NOT_FOUND` ({@link DllNotFoundError}) when the addon is there but
239
+ * an SDK DLL is not. Any other load failure is rethrown as Node reported it.
240
+ */
241
+ export declare function load(addonPath?: string): StovePcSdk
package/index.js ADDED
@@ -0,0 +1,117 @@
1
+ 'use strict'
2
+
3
+ // Entry point: locates the compiled addon and loads it.
4
+ //
5
+ // Search order (first hit wins):
6
+ // 1. STOVE_PCSDK_ADDON — explicit path to a .node file
7
+ // 2. ./build/Release/stovesdk.node — local `npm run build`
8
+ // 3. ./prebuilds/win32-x64/stovesdk.node — CI prebuild dropped into the package
9
+ //
10
+ // DLL lookup: the .node hard-imports BaseSDK.dll / OwnershipSDK.dll / GameSupportSDK.dll. Node
11
+ // loads addons with LOAD_WITH_ALTERED_SEARCH_PATH, so DLLs placed next to the .node are found
12
+ // first; the directory of the host executable is the other place the Windows loader always looks.
13
+ // When the require() fails with ERR_DLOPEN_FAILED, `load()` checks those two directories for the
14
+ // three DLLs and, if any is absent from both, rethrows with code STOVE_PCSDK_DLL_NOT_FOUND naming
15
+ // the files — Node's own message names neither the DLL nor the directory, and reads as if the
16
+ // .node itself were missing.
17
+ //
18
+ // Nothing here imports Electron, so the same file works from plain Node and the Electron main
19
+ // process. The load is lazy and explicit (`load()`) so merely requiring this module on a
20
+ // non-Windows machine (tests, tooling) does not throw.
21
+
22
+ const fs = require('fs')
23
+ const path = require('path')
24
+ const { SDK_MODULES } = require('./lib/sdk-dir')
25
+
26
+ const ADDON_FILE = 'stovesdk.node'
27
+ const SDK_DLLS = Object.freeze(SDK_MODULES.map(mod => `${mod}.dll`))
28
+
29
+ const ErrorCode = Object.freeze({
30
+ NOT_INITIALIZED: -1,
31
+ ALREADY_INITIALIZED: -2,
32
+ IN_PROGRESS: -3,
33
+ ABORTED: -4,
34
+ })
35
+
36
+ function candidateAddonPaths(env = process.env, baseDir = __dirname) {
37
+ const list = []
38
+ if (env.STOVE_PCSDK_ADDON && env.STOVE_PCSDK_ADDON.trim()) list.push(path.resolve(env.STOVE_PCSDK_ADDON.trim()))
39
+ list.push(path.join(baseDir, 'build', 'Release', ADDON_FILE))
40
+ list.push(path.join(baseDir, 'prebuilds', 'win32-x64', ADDON_FILE))
41
+ return list
42
+ }
43
+
44
+ function resolveAddonPath(env = process.env, baseDir = __dirname, exists = fs.existsSync) {
45
+ const candidates = candidateAddonPaths(env, baseDir)
46
+ const found = candidates.find(exists)
47
+ if (found) return found
48
+ const err = new Error(
49
+ `[stovesdk] compiled addon not found. Looked in:\n${candidates.map(c => ` - ${c}`).join('\n')}\n` +
50
+ 'Build it with `npm run build` on Windows x64 (with the SDK under sdk/ or STOVE_PCSDK_DIR), ' +
51
+ 'or set STOVE_PCSDK_ADDON to a prebuilt .node file.'
52
+ )
53
+ err.code = 'STOVE_PCSDK_ADDON_NOT_FOUND'
54
+ throw err
55
+ }
56
+
57
+ // The directories this module can vouch for: the addon's own and the host executable's. The
58
+ // Windows loader also searches the system directories and PATH, but a consumer does not put
59
+ // the SDK there, so a DLL absent from these two is reported as not found.
60
+ function sdkDllSearchDirs(addonPath, execPath = process.execPath) {
61
+ return [...new Set([path.dirname(path.resolve(addonPath)), path.dirname(path.resolve(execPath))])]
62
+ }
63
+
64
+ // The SDK DLLs found in none of those directories. Empty when all three are present — then a
65
+ // failed load has another cause (a DLL built for another architecture, its own imports such as
66
+ // the VC++ runtime missing), and the original error says more than this module can.
67
+ function missingSdkDlls(addonPath, execPath = process.execPath, exists = fs.existsSync) {
68
+ const dirs = sdkDllSearchDirs(addonPath, execPath)
69
+ return SDK_DLLS.filter(dll => !dirs.some(dir => exists(path.join(dir, dll))))
70
+ }
71
+
72
+ // The error `load()` throws when `require(addonPath)` threw `err`: a coded error naming the
73
+ // missing SDK DLLs when that is what happened, otherwise `err` itself, untouched.
74
+ function explainLoadError(err, addonPath, execPath = process.execPath, exists = fs.existsSync) {
75
+ if (!err || err.code !== 'ERR_DLOPEN_FAILED') return err
76
+ const missing = missingSdkDlls(addonPath, execPath, exists)
77
+ if (!missing.length) return err
78
+ const searched = sdkDllSearchDirs(addonPath, execPath)
79
+ const explained = new Error(
80
+ `[stovesdk] the addon imports ${SDK_DLLS.join(', ')}; ` +
81
+ `${missing.join(', ')} ${missing.length === 1 ? 'was' : 'were'} not found next to the addon or next to the host executable. Looked in:\n` +
82
+ `${searched.map(dir => ` - ${dir}`).join('\n')}\n` +
83
+ `Copy the SDK's <Module>/Deploy/Bin/x64/Release/<Module>.dll files beside ${path.resolve(addonPath)} ` +
84
+ 'or beside your executable. They are part of the STOVE PC SDK, not of this package.',
85
+ { cause: err }
86
+ )
87
+ explained.code = 'STOVE_PCSDK_DLL_NOT_FOUND'
88
+ explained.missing = missing
89
+ explained.searched = searched
90
+ return explained
91
+ }
92
+
93
+ let cached = null
94
+
95
+ function load(addonPath) {
96
+ if (cached) return cached
97
+ if (process.platform !== 'win32') {
98
+ const err = new Error(`[stovesdk] the STOVE PC SDK is Windows-only (platform: ${process.platform})`)
99
+ err.code = 'STOVE_PCSDK_UNSUPPORTED_PLATFORM'
100
+ throw err
101
+ }
102
+ const file = addonPath ? path.resolve(addonPath) : resolveAddonPath()
103
+ if (!fs.existsSync(file)) {
104
+ const err = new Error(`[stovesdk] addon not found: ${file}`)
105
+ err.code = 'STOVE_PCSDK_ADDON_NOT_FOUND'
106
+ throw err
107
+ }
108
+ try {
109
+ // eslint-disable-next-line global-require -- native module, resolved at runtime by design
110
+ cached = require(file)
111
+ } catch (err) {
112
+ throw explainLoadError(err, file)
113
+ }
114
+ return cached
115
+ }
116
+
117
+ module.exports = { ADDON_FILE, SDK_DLLS, ErrorCode, candidateAddonPaths, resolveAddonPath, missingSdkDlls, explainLoadError, load }