@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 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
+ }