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 +21 -0
- package/README.md +205 -2
- package/binding.gyp +73 -0
- package/index.d.ts +241 -0
- package/index.js +117 -0
- package/lib/sdk-dir.js +58 -0
- package/package.json +48 -4
- package/prebuilds/win32-x64/SHA256SUMS +1 -0
- package/prebuilds/win32-x64/stovesdk.node +0 -0
- package/src/stovesdk.cc +904 -0
- package/tools/check-tarball.js +60 -0
- package/tools/sdk-dir.js +36 -0
- package/tools/smoke.js +102 -0
- package/tools/syntax-check.sh +34 -0
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
|
-
#
|
|
1
|
+
# stovesdk.js
|
|
2
2
|
|
|
3
|
-
|
|
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 }
|