@wasm-gaming/rsdkv4-wasm 0.1.2
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/README.md +229 -0
- package/dist/manifest.json +75 -0
- package/dist/rsdkv4/rsdkv4.js +9268 -0
- package/dist/rsdkv4/rsdkv4.manifest.d.ts +4 -0
- package/dist/rsdkv4/rsdkv4.manifest.d.ts.map +1 -0
- package/dist/rsdkv4/rsdkv4.manifest.js +39 -0
- package/dist/rsdkv4/rsdkv4.manifest.js.map +1 -0
- package/dist/rsdkv4/rsdkv4.options.d.ts +22 -0
- package/dist/rsdkv4/rsdkv4.options.d.ts.map +1 -0
- package/dist/rsdkv4/rsdkv4.options.js +41 -0
- package/dist/rsdkv4/rsdkv4.options.js.map +1 -0
- package/dist/rsdkv4/rsdkv4.sdk.d.ts +48 -0
- package/dist/rsdkv4/rsdkv4.sdk.d.ts.map +1 -0
- package/dist/rsdkv4/rsdkv4.sdk.js +257 -0
- package/dist/rsdkv4/rsdkv4.sdk.js.map +1 -0
- package/dist/rsdkv4/rsdkv4.wasm +0 -0
- package/package.json +51 -0
- package/src/rsdkv4.manifest.ts +44 -0
- package/src/rsdkv4.options.ts +64 -0
- package/src/rsdkv4.sdk.ts +294 -0
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# @wasm-gaming/rsdkv4-wasm
|
|
2
|
+
|
|
3
|
+
RSDKv4 (Retro Software Development Kit v4, Rubberduckycooly/mattConn decompilation)
|
|
4
|
+
compiled to WebAssembly, wrapped in a small JS SDK that conforms to the
|
|
5
|
+
[wasm-gaming engine contract](https://github.com/wasm-gaming/engine-specs).
|
|
6
|
+
|
|
7
|
+
**One WASM, two games.** The engine binary is game-agnostic. `Data.rsdk` is loaded
|
|
8
|
+
into the Emscripten filesystem *at runtime*, so the same `rsdkv4.wasm` runs both
|
|
9
|
+
**Sonic the Hedgehog** and **Sonic the Hedgehog 2** — the `Data.rsdk` you hand it
|
|
10
|
+
is what picks the game. (The old build baked `Data.rsdk` in with `--preload-file`
|
|
11
|
+
and had to be recompiled per game; this doesn't.)
|
|
12
|
+
|
|
13
|
+
## Contract surface
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { manifest, load } from '@wasm-gaming/rsdkv4-wasm';
|
|
17
|
+
|
|
18
|
+
const engine = await load({
|
|
19
|
+
canvas, // an <canvas id="canvas">
|
|
20
|
+
assets: { data: dataRsdkBytes /*, settings: iniBytes */ },
|
|
21
|
+
storageNamespace: 'sonic1', // carpeta por juego bajo /data (OPFS/WASMFS)
|
|
22
|
+
onEvent: (e) => { /* 'ready' | 'error' | 'exit' */ },
|
|
23
|
+
// Where the app placed the CI-built artifacts (defaults to package-relative):
|
|
24
|
+
// jsUrl, wasmUrl,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
engine.pause();
|
|
28
|
+
engine.resume();
|
|
29
|
+
engine.setInput('rsdkv4');
|
|
30
|
+
engine.purgeStorage(); // borra Data.rsdk/settings.ini de este namespace
|
|
31
|
+
engine.destroy();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- `manifest` — declarative `EngineManifest` (see [src/rsdkv4.manifest.ts](src/rsdkv4.manifest.ts)):
|
|
35
|
+
required `data` asset (`/Data.rsdk`), optional `settings` asset (`/settings.ini`),
|
|
36
|
+
`input: "rsdkv4"` preset, 16:9 video, no save-states.
|
|
37
|
+
- `load(config)` → `EngineInstance` (`start`/`pause`/`resume`/`reset`/`setInput`/`destroy`).
|
|
38
|
+
`reset()` is unsupported by RSDKv4 — `destroy()` and `load()` again.
|
|
39
|
+
|
|
40
|
+
### Dev menu — the launcher owns the UI
|
|
41
|
+
The debug/stage-select **UI is the launcher's responsibility**, not the engine's.
|
|
42
|
+
The engine only exposes the raw bridge as `instance.devMenu` (backed by the
|
|
43
|
+
`web_devmenu_*` embind functions compiled into the WASM):
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const engine = await load({ canvas, assets: { data } });
|
|
47
|
+
engine.pause(); // freeze while a menu is open
|
|
48
|
+
const categories = engine.devMenu.getStageList(); // [{ name, stages: [{ name }] }]
|
|
49
|
+
engine.devMenu.loadStage(0, 2); // warp to category 0 / stage 2
|
|
50
|
+
engine.resume();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The launcher decides how Escape behaves and draws its own overlay. It should
|
|
54
|
+
intercept Escape in the **capture phase** if it wants to repurpose it (the engine
|
|
55
|
+
ships with `DevMenu=false`, so the native in-canvas menu stays suppressed).
|
|
56
|
+
|
|
57
|
+
### Options
|
|
58
|
+
Engine-specific options are described in [src/rsdkv4.options.ts](src/rsdkv4.options.ts)
|
|
59
|
+
(`Rsdkv4Options` type + `RSDKV4_OPTIONS_SCHEMA`, mirrored into
|
|
60
|
+
[src/rsdkv4.manifest.ts](src/rsdkv4.manifest.ts)'s `options`). When the host doesn't pass an explicit
|
|
61
|
+
`settings` asset, the SDK **generates `settings.ini` from `config.options`**
|
|
62
|
+
(`devMenu`, `engineDebugMode`, `vsync`, `startingCategory/Scene/Player`).
|
|
63
|
+
|
|
64
|
+
### Other niceties
|
|
65
|
+
- **Canvas id guard** — forces `canvas.id = "canvas"` because Emscripten's SDL2
|
|
66
|
+
port locates the canvas via `querySelector('#canvas')`.
|
|
67
|
+
|
|
68
|
+
> Audio autoplay-unlock and the gamepad→keyboard translator are cross-cutting host
|
|
69
|
+
> concerns (the app's shared input script); the SDK only exposes the `input`
|
|
70
|
+
> preset name via the manifest and `setInput()`.
|
|
71
|
+
|
|
72
|
+
## Filesystem (WASMFS / OPFS)
|
|
73
|
+
|
|
74
|
+
Built with **`-sWASMFS`** — Emscripten's modern filesystem, replacing MEMFS. The
|
|
75
|
+
game working dir `/data` is mounted on **OPFS** (persistent) via the `WebFS.cpp`
|
|
76
|
+
`web_mount_opfs` helper when the page is **cross-origin isolated**; otherwise it
|
|
77
|
+
falls back to the WASMFS in-memory backend so the engine still boots. `Data.rsdk`
|
|
78
|
+
and the generated `settings.ini` are written there and read via CWD (the engine
|
|
79
|
+
is booted with the `UsingCWD` arg).
|
|
80
|
+
|
|
81
|
+
### Backend selection — `config.persist`
|
|
82
|
+
- `undefined` (default) — **auto**: OPFS when cross-origin isolated, else in-memory.
|
|
83
|
+
- `'opfs'` — **force** OPFS; warn + fall back to in-memory if unavailable.
|
|
84
|
+
- `null` — **force the in-memory** WASMFS backend (the MEMFS-equivalent fallback;
|
|
85
|
+
non-persistent).
|
|
86
|
+
|
|
87
|
+
(`'idbfs'` isn't supported under WASMFS and is treated as in-memory.)
|
|
88
|
+
|
|
89
|
+
### Per-game storage namespace — `config.storageNamespace`
|
|
90
|
+
- `undefined` (default) — uses `/data/default`.
|
|
91
|
+
- `'sonic1'` / `'sonic2'` (recommended) — keeps each game in its own folder,
|
|
92
|
+
e.g. `/data/sonic1` and `/data/sonic2`.
|
|
93
|
+
- Supports nested paths like `'sonic1/profile-a'`.
|
|
94
|
+
|
|
95
|
+
This prevents cross-game reuse collisions and allows purging one game's files
|
|
96
|
+
without touching the others.
|
|
97
|
+
|
|
98
|
+
### Skip re-fetch when already persisted
|
|
99
|
+
When the working dir is OPFS-backed, the SDK checks whether `/data/Data.rsdk`
|
|
100
|
+
(and `settings.ini`) already exist and **reuses them instead of re-downloading**.
|
|
101
|
+
Precedence for `Data.rsdk`: explicit `assets.data` → persisted OPFS copy → lazy
|
|
102
|
+
`dataProvider()` (called only on a miss). So a host can pass a `dataProvider` that
|
|
103
|
+
fetches the ROM and it will only run on first load / non-isolated pages:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
await load({
|
|
107
|
+
canvas,
|
|
108
|
+
assets: {}, // no eager 40 MB download
|
|
109
|
+
storageNamespace: 'sonic1', // namespace-specific cache
|
|
110
|
+
dataProvider: () => fetch('/Data.rsdk').then(r => r.arrayBuffer()),
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
To purge only one game's persisted files:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
const sonic2 = await load({
|
|
118
|
+
canvas,
|
|
119
|
+
assets: {},
|
|
120
|
+
storageNamespace: 'sonic2',
|
|
121
|
+
dataProvider: () => fetch('/sonic2/Data.rsdk').then(r => r.arrayBuffer()),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Removes /data/sonic2/Data.rsdk and /data/sonic2/settings.ini only.
|
|
125
|
+
const purged = sonic2.purgeStorage();
|
|
126
|
+
console.log(purged); // { data: true|false, settings: true|false }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> Persistence only actually engages under cross-origin isolation (see the OPFS
|
|
130
|
+
> caveat below), so the skip-fetch benefit needs COOP/COEP + `-pthread` to be real.
|
|
131
|
+
|
|
132
|
+
> ⚠️ **OPFS caveat (unverified):** OPFS sync-access handles only exist in Workers,
|
|
133
|
+
> so Emscripten proxies them to a thread — reliable OPFS needs **`-pthread`** in
|
|
134
|
+
> the build plus **COOP/COEP** headers on the host. This build is single-threaded,
|
|
135
|
+
> so today OPFS persistence is best-effort and gated on `crossOriginIsolated`.
|
|
136
|
+
> Adding `-pthread` interacts with SDL2 + `emscripten_set_main_loop` and must be
|
|
137
|
+
> validated with a real build.
|
|
138
|
+
|
|
139
|
+
## Build
|
|
140
|
+
|
|
141
|
+
All build logic lives in the **Makefile** (`package.json` has no scripts). WASM +
|
|
142
|
+
dist are built in CI and attached to a Release — **not** committed.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
make build # TypeScript (make build-sdk) + WASM (make build-wasm) → dist/
|
|
146
|
+
make build-sdk # TypeScript only → dist/ (fast; no Docker)
|
|
147
|
+
make build-wasm # local: runs scripts/build.sh inside emscripten/emsdk (Docker)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- `build-lib` compiles the SDK/options/manifest (`.js` + `.d.ts`) → `dist/rsdkv4/`.
|
|
151
|
+
- `build-manifest` serializes the typed manifest to `dist/manifest.json`.
|
|
152
|
+
- `build-demo` compiles `src/demo/demo.ts` → `dist/demo.js`, copies
|
|
153
|
+
`src/demo/index.html` → `dist/index.html`, seeds `dist/settings.ini`.
|
|
154
|
+
- **`scripts/build.sh`** does not call Docker — it runs the WASM build steps
|
|
155
|
+
directly and expects an Emscripten SDK on PATH. In CI it runs inside an
|
|
156
|
+
`emscripten/emsdk` container job; locally, **`scripts/build-docker.sh`** (what
|
|
157
|
+
`make build-wasm` invokes) runs it inside that container for you. build.sh clones
|
|
158
|
+
`mattConn/Sonic-Decompilation-WASM`, applies the engine
|
|
159
|
+
patches (init order, audio init, controller init, decoupled 120/30Hz loop, the
|
|
160
|
+
`WebDevMenu` embind bridge, the `WebFS` OPFS helper) and links with
|
|
161
|
+
`-sWASMFS -sINVOKE_RUN=0 -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createRSDKv4`
|
|
162
|
+
+ `FS`/`callMain`/`ccall` exported, **without** `--preload-file`.
|
|
163
|
+
|
|
164
|
+
Contract types are resolved from the npm package `@wasm-gaming/engine-specs`
|
|
165
|
+
(installed via this project's devDependencies).
|
|
166
|
+
|
|
167
|
+
### dist/ layout
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
dist/
|
|
171
|
+
├── rsdkv4/ # the engine package (all rsdkv4.* artifacts)
|
|
172
|
+
│ ├── rsdkv4.js # Emscripten ES6 factory
|
|
173
|
+
│ ├── rsdkv4.wasm
|
|
174
|
+
│ ├── rsdkv4.sdk.js (+ .d.ts)
|
|
175
|
+
│ ├── rsdkv4.options.js (+ .d.ts)
|
|
176
|
+
│ └── rsdkv4.manifest.js(+ .d.ts)
|
|
177
|
+
├── manifest.json # declarative manifest (artifacts → rsdkv4/rsdkv4.*)
|
|
178
|
+
├── index.html # demo shell (import map → ./rsdkv4/rsdkv4.sdk.js)
|
|
179
|
+
├── demo.js
|
|
180
|
+
├── settings.ini # seeded from src/settings.default.ini if absent
|
|
181
|
+
└── Data.rsdk # dev-provided (git-ignored)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Try it locally
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
make build # produce dist/ (TypeScript + WASM)
|
|
188
|
+
cp /path/to/Data.rsdk dist/ # git-ignored dev file the demo auto-loads
|
|
189
|
+
cp /path/to/settings.ini dist/ # OPTIONAL — else seeded/generated
|
|
190
|
+
make preview # serves dist/ on :8080
|
|
191
|
+
# open http://localhost:8080/
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The demo fetches `./Data.rsdk` (required) and, if present, `./settings.ini`
|
|
195
|
+
(optional) from `dist/`, mounting both into WASMFS. Without a `Data.rsdk` file the
|
|
196
|
+
demo shows a pick / drag-and-drop prompt; without a `settings.ini` file the SDK
|
|
197
|
+
synthesizes one from `config.options`.
|
|
198
|
+
|
|
199
|
+
## Live demo (GitHub Pages)
|
|
200
|
+
|
|
201
|
+
The `pages` job in [.github/workflows/build.yml](.github/workflows/build.yml)
|
|
202
|
+
publishes `dist/` to GitHub Pages on pushes to `main` (and manual runs). Since
|
|
203
|
+
`Data.rsdk` is git-ignored, the live demo opens on the pick / drag-and-drop
|
|
204
|
+
prompt — drop a `Data.rsdk` to boot.
|
|
205
|
+
|
|
206
|
+
- **One-time setup:** repo *Settings → Pages → Build and deployment → Source =
|
|
207
|
+
GitHub Actions*. The workflow also calls `actions/configure-pages` with
|
|
208
|
+
`enablement: true`, so first deploy can bootstrap Pages automatically when the
|
|
209
|
+
repo-level Pages site does not exist yet.
|
|
210
|
+
- **OPFS note:** GitHub Pages can't set COOP/COEP headers, so the page isn't
|
|
211
|
+
cross-origin isolated — OPFS stays off and the SDK uses the in-memory WASMFS
|
|
212
|
+
backend (see the filesystem caveat above).
|
|
213
|
+
|
|
214
|
+
## Status
|
|
215
|
+
|
|
216
|
+
- ✅ **TS build** — compiles clean; manifest validates against the contract.
|
|
217
|
+
- ✅ **WASM build** (`make build-wasm`) — produces a valid ~10 MB `dist/rsdkv4/rsdkv4.wasm`
|
|
218
|
+
+ ES6-factory glue with the intended flags/symbols (`-sWASMFS`, `web_mount_opfs`,
|
|
219
|
+
the `web_devmenu_*` embind bridge, `callMain`/`FS`/`ccall`, no `--preload-file`).
|
|
220
|
+
- ⏳ **End-to-end runtime in a browser** (boot from a real `Data.rsdk`) — not yet
|
|
221
|
+
verified.
|
|
222
|
+
- ⏳ **OPFS persistence** — gated on `crossOriginIsolated`; almost certainly needs
|
|
223
|
+
a `-pthread` build + COOP/COEP to actually engage (see the caveat above). Until
|
|
224
|
+
then the SDK falls back to the in-memory WASMFS backend.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT for this wrapper. The RSDKv4 decompilation and Sonic game data have their own
|
|
229
|
+
licenses; game data (`Data.rsdk`) is user-provided and never distributed here.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "rsdkv4",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"name": "Retro Software Development Kit v4",
|
|
5
|
+
"artifacts": {
|
|
6
|
+
"wasm": "rsdkv4/rsdkv4.wasm",
|
|
7
|
+
"js": "rsdkv4/rsdkv4.js"
|
|
8
|
+
},
|
|
9
|
+
"assets": [
|
|
10
|
+
{
|
|
11
|
+
"key": "data",
|
|
12
|
+
"mountPath": "/data/Data.rsdk",
|
|
13
|
+
"required": true,
|
|
14
|
+
"accept": [
|
|
15
|
+
".rsdk"
|
|
16
|
+
],
|
|
17
|
+
"description": "RSDKv4 game data pack. Differs per game (Sonic 1 vs Sonic 2); selects the game."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"key": "settings",
|
|
21
|
+
"mountPath": "/data/settings.ini",
|
|
22
|
+
"required": false,
|
|
23
|
+
"accept": [
|
|
24
|
+
".ini"
|
|
25
|
+
],
|
|
26
|
+
"description": "Engine settings. Omitted → the SDK generates one from config.options."
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"input": "rsdkv4",
|
|
30
|
+
"video": {
|
|
31
|
+
"baseWidth": 424,
|
|
32
|
+
"baseHeight": 240,
|
|
33
|
+
"aspect": "16:9"
|
|
34
|
+
},
|
|
35
|
+
"options": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"properties": {
|
|
39
|
+
"devMenu": {
|
|
40
|
+
"type": "boolean",
|
|
41
|
+
"default": false,
|
|
42
|
+
"description": "RSDKv4's native in-canvas Dev Menu. The launcher provides its own overlay via instance.devMenu, so keep this off."
|
|
43
|
+
},
|
|
44
|
+
"engineDebugMode": {
|
|
45
|
+
"type": "boolean",
|
|
46
|
+
"default": true,
|
|
47
|
+
"description": "Enables the web devmenu embind bridge (stage list / warp / pause)."
|
|
48
|
+
},
|
|
49
|
+
"vsync": {
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"default": true
|
|
52
|
+
},
|
|
53
|
+
"startingCategory": {
|
|
54
|
+
"type": "integer",
|
|
55
|
+
"minimum": 0,
|
|
56
|
+
"default": 0
|
|
57
|
+
},
|
|
58
|
+
"startingScene": {
|
|
59
|
+
"type": "integer",
|
|
60
|
+
"minimum": 0,
|
|
61
|
+
"default": 0
|
|
62
|
+
},
|
|
63
|
+
"startingPlayer": {
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"minimum": 0,
|
|
66
|
+
"default": 0
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"capabilities": {
|
|
71
|
+
"saveStates": false,
|
|
72
|
+
"sram": false,
|
|
73
|
+
"coreSelectable": false
|
|
74
|
+
}
|
|
75
|
+
}
|