@wasm-gaming/rsdkv3-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,89 @@
1
+ # @wasm-gaming/rsdkv3-wasm
2
+
3
+ RSDKv3 (Retro Software Development Kit v3, Sonic CD 2011 decompilation) compiled
4
+ to WebAssembly, wrapped in a JS SDK conforming to the
5
+ [wasm-gaming engine contract](https://github.com/wasm-gaming/engine-specs).
6
+
7
+ The engine does not bake game data with `--preload-file`. Hosts provide
8
+ `Data.rsdk` at runtime, so the same `rsdkv3.wasm` can be reused across installs.
9
+
10
+ ## Contract surface
11
+
12
+ ```js
13
+ import { manifest, load } from '@wasm-gaming/rsdkv3-wasm';
14
+
15
+ const engine = await load({
16
+ canvas,
17
+ assets: { data: dataRsdkBytes /*, settings: iniBytes */ },
18
+ onEvent: (e) => { /* 'ready' | 'error' */ },
19
+ // Optional override when artifacts are hosted externally:
20
+ // jsUrl, wasmUrl,
21
+ });
22
+
23
+ engine.pause();
24
+ engine.resume();
25
+ engine.setInput('rsdkv4');
26
+ engine.destroy();
27
+ ```
28
+
29
+ - `manifest` defines required `data` (`/data/Data.rsdk`) and optional `settings`
30
+ (`/data/settings.ini`) assets.
31
+ - `load(config)` returns an `EngineInstance` and exposes `devMenu` bridge helpers:
32
+ `getStageList()`, `loadStage(category, stage)`, `setPaused(bool)`.
33
+
34
+ ## Options
35
+
36
+ Engine-specific options are declared in
37
+ [src/rsdkv3.options.ts](src/rsdkv3.options.ts) and mirrored into `manifest.options`:
38
+
39
+ - `devMenu`
40
+ - `engineDebugMode`
41
+ - `vsync`
42
+ - `soundtrack` (`0` JP/EU, `1` US)
43
+ - `language`
44
+ - `disableTouchControls`
45
+
46
+ If `assets.settings` is omitted, the SDK generates `settings.ini` from these
47
+ options.
48
+
49
+ ## Build
50
+
51
+ All build logic is in [Makefile](Makefile):
52
+
53
+ ```bash
54
+ make build # SDK (TypeScript) + WASM
55
+ make build-sdk # SDK only
56
+ make build-wasm # WASM via Docker wrapper
57
+ make preview # serves dist/ at http://localhost:8080
58
+ ```
59
+
60
+ - `scripts/build.sh` clones `Rubberduckycooly/Sonic-CD-11-Decompilation`, applies
61
+ known Emscripten patches, vendors `libtheora`, and outputs:
62
+ - `dist/rsdkv3/rsdkv3.js`
63
+ - `dist/rsdkv3/rsdkv3.wasm`
64
+ - `scripts/emit-manifest.mjs` writes `dist/manifest.json` from typed manifest.
65
+ - `scripts/seed-settings.mjs` seeds `dist/settings.ini` from
66
+ `src/settings.default.ini` when absent.
67
+
68
+ ## Try locally
69
+
70
+ ```bash
71
+ make build
72
+ cp /path/to/Data.rsdk dist/
73
+ make preview
74
+ # open http://localhost:8080/
75
+ ```
76
+
77
+ The demo attempts `./Data.rsdk`; if missing, it shows a file picker and supports
78
+ drag-and-drop.
79
+
80
+ ## Status
81
+
82
+ - ✅ TypeScript SDK + manifest build
83
+ - ✅ CI workflow scaffold for SDK/WASM/release/pages
84
+ - ⏳ Browser runtime verification with a real `Data.rsdk`
85
+ - ⏳ Full WASM build verification (`make build-wasm`) on this new subproject
86
+
87
+ ## License
88
+
89
+ MIT for this wrapper. Upstream engine and game data keep their own licenses.
@@ -0,0 +1,4 @@
1
+ import type { EngineManifest } from '@wasm-gaming/engine-specs';
2
+ export declare const manifest: EngineManifest;
3
+ export default manifest;
4
+ //# sourceMappingURL=rsdkv3.manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rsdkv3.manifest.d.ts","sourceRoot":"","sources":["../../src/rsdkv3.manifest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,eAAO,MAAM,QAAQ,EAAE,cAkCtB,CAAC;AAEF,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,39 @@
1
+ // The rsdkv3 EngineManifest — typed against the contract, so a drift from
2
+ // @wasm-gaming/engine-specs is a compile error here. `npm run build:manifest`
3
+ // serializes this to dist/manifest.json (the artifact CI attaches to a Release).
4
+ import { RSDKV3_OPTIONS_SCHEMA } from './rsdkv3.options.js';
5
+ export const manifest = {
6
+ id: 'rsdkv3',
7
+ version: '0.1.0',
8
+ name: 'Retro Software Development Kit v3',
9
+ artifacts: {
10
+ // Relative to the manifest (dist/manifest.json); the engine files live in
11
+ // the dist/rsdkv3/ subfolder.
12
+ wasm: 'rsdkv3/rsdkv3.wasm',
13
+ js: 'rsdkv3/rsdkv3.js',
14
+ },
15
+ assets: [
16
+ {
17
+ key: 'data',
18
+ // Lives under the OPFS-backed (persistent) working dir; the engine reads it
19
+ // via CWD (booted with the UsingCWD arg).
20
+ mountPath: '/data/Data.rsdk',
21
+ required: true,
22
+ accept: ['.rsdk'],
23
+ description: 'Sonic CD 2011 game data pack (Data.rsdk).',
24
+ },
25
+ {
26
+ key: 'settings',
27
+ mountPath: '/data/settings.ini',
28
+ required: false,
29
+ accept: ['.ini'],
30
+ description: 'Engine settings. Omitted → the SDK generates one from config.options.',
31
+ },
32
+ ],
33
+ input: 'rsdkv4',
34
+ video: { baseWidth: 426, baseHeight: 240, aspect: '16:9' },
35
+ options: RSDKV3_OPTIONS_SCHEMA,
36
+ capabilities: { saveStates: false, sram: false, coreSelectable: false },
37
+ };
38
+ export default manifest;
39
+ //# sourceMappingURL=rsdkv3.manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rsdkv3.manifest.js","sourceRoot":"","sources":["../../src/rsdkv3.manifest.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,8EAA8E;AAC9E,iFAAiF;AAGjF,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,CAAC,MAAM,QAAQ,GAAmB;IACtC,EAAE,EAAE,QAAQ;IACZ,OAAO,EAAE,OAAO;IAChB,IAAI,EAAE,mCAAmC;IACzC,SAAS,EAAE;QACT,0EAA0E;QAC1E,8BAA8B;QAC9B,IAAI,EAAE,oBAAoB;QAC1B,EAAE,EAAE,kBAAkB;KACvB;IACD,MAAM,EAAE;QACN;YACE,GAAG,EAAE,MAAM;YACX,4EAA4E;YAC5E,0CAA0C;YAC1C,SAAS,EAAE,iBAAiB;YAC5B,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,CAAC,OAAO,CAAC;YACjB,WAAW,EACT,2CAA2C;SAC9C;QACD;YACE,GAAG,EAAE,UAAU;YACf,SAAS,EAAE,oBAAoB;YAC/B,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,CAAC,MAAM,CAAC;YAChB,WAAW,EACT,uEAAuE;SAC1E;KACF;IACD,KAAK,EAAE,QAAQ;IACf,KAAK,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE;IAC1D,OAAO,EAAE,qBAAqB;IAC9B,YAAY,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE;CACxE,CAAC;AAEF,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { JSONSchema } from '@wasm-gaming/engine-specs';
2
+ export interface Rsdkv3Options {
3
+ /**
4
+ * RSDKv3's native in-canvas Dev Menu. Kept off by default: the launcher owns
5
+ * the debug/stage-select UI (via `instance.devMenu`), and leaving this on has
6
+ * historically produced a native menu-like screen at boot.
7
+ */
8
+ devMenu?: boolean;
9
+ /** RSDKv3 EngineDebugMode — enables the web devmenu embind bridge hooks. */
10
+ engineDebugMode?: boolean;
11
+ /** VSync setting in the generated settings.ini. */
12
+ vsync?: boolean;
13
+ /** 0 = JP/EU soundtrack, 1 = US soundtrack. */
14
+ soundtrack?: number;
15
+ /** 0 = English. Matches RSDKv3 language numeric value. */
16
+ language?: number;
17
+ /** Toggle touch controls in the generated settings.ini. */
18
+ disableTouchControls?: boolean;
19
+ }
20
+ export declare const DEFAULT_RSDKV3_OPTIONS: Required<Rsdkv3Options>;
21
+ export declare const RSDKV3_OPTIONS_SCHEMA: JSONSchema;
22
+ //# sourceMappingURL=rsdkv3.options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rsdkv3.options.d.ts","sourceRoot":"","sources":["../../src/rsdkv3.options.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,aAAa,CAO1D,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,UA0BnC,CAAC"}
@@ -0,0 +1,45 @@
1
+ // Engine-specific options for RSDKv3.
2
+ //
3
+ // This is the authoritative description of what `EngineConfig.options` accepts
4
+ // for this engine. It provides both:
5
+ // - `Rsdkv3Options` — compile-time type for TS hosts
6
+ // - `RSDKV3_OPTIONS_SCHEMA` — JSON Schema mirrored into manifest.json
7
+ // - `DEFAULT_RSDKV3_OPTIONS` — defaults used when no settings.ini is supplied
8
+ //
9
+ // At runtime these options are serialized into Sonic CD's `settings.ini` format
10
+ // when the host does not pass an explicit `settings` asset.
11
+ export const DEFAULT_RSDKV3_OPTIONS = {
12
+ devMenu: false,
13
+ engineDebugMode: false,
14
+ vsync: false,
15
+ soundtrack: 0,
16
+ language: 0,
17
+ disableTouchControls: true,
18
+ };
19
+ export const RSDKV3_OPTIONS_SCHEMA = {
20
+ type: 'object',
21
+ additionalProperties: false,
22
+ properties: {
23
+ devMenu: {
24
+ type: 'boolean',
25
+ default: false,
26
+ description: "RSDKv3's native in-canvas Dev Menu. The launcher provides its own overlay via instance.devMenu, so keep this off.",
27
+ },
28
+ engineDebugMode: {
29
+ type: 'boolean',
30
+ default: false,
31
+ description: 'Enables the web devmenu embind bridge (stage list / warp / pause).',
32
+ },
33
+ vsync: { type: 'boolean', default: false },
34
+ soundtrack: {
35
+ type: 'integer',
36
+ minimum: 0,
37
+ maximum: 1,
38
+ default: 0,
39
+ description: 'Sonic CD soundtrack selector: 0=JP/EU, 1=US.',
40
+ },
41
+ language: { type: 'integer', minimum: 0, default: 0 },
42
+ disableTouchControls: { type: 'boolean', default: true },
43
+ },
44
+ };
45
+ //# sourceMappingURL=rsdkv3.options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rsdkv3.options.js","sourceRoot":"","sources":["../../src/rsdkv3.options.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,+EAA+E;AAC/E,qCAAqC;AACrC,gEAAgE;AAChE,yEAAyE;AACzE,gFAAgF;AAChF,EAAE;AACF,gFAAgF;AAChF,4DAA4D;AAuB5D,MAAM,CAAC,MAAM,sBAAsB,GAA4B;IAC7D,OAAO,EAAE,KAAK;IACd,eAAe,EAAE,KAAK;IACtB,KAAK,EAAE,KAAK;IACZ,UAAU,EAAE,CAAC;IACb,QAAQ,EAAE,CAAC;IACX,oBAAoB,EAAE,IAAI;CAC3B,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAe;IAC/C,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,KAAK;IAC3B,UAAU,EAAE;QACV,OAAO,EAAE;YACP,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,KAAK;YACd,WAAW,EACT,mHAAmH;SACtH;QACD,eAAe,EAAE;YACf,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,KAAK;YACd,WAAW,EAAE,oEAAoE;SAClF;QACD,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;QAC1C,UAAU,EAAE;YACV,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,CAAC;YACV,WAAW,EAAE,8CAA8C;SAC5D;QACD,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE;QACrD,oBAAoB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;KACzD;CACF,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { EngineConfig, EngineInstance, AssetData } from '@wasm-gaming/engine-specs';
2
+ import { manifest } from './rsdkv3.manifest.js';
3
+ export { manifest };
4
+ /** RSDKv3-specific bridge for the launcher's debug/stage-select UI. */
5
+ export interface RsdkDevMenuBridge {
6
+ getStageList(): Array<{
7
+ name: string;
8
+ stages: Array<{
9
+ name: string;
10
+ }>;
11
+ }>;
12
+ loadStage(categoryIdx: number, stageIdx: number): void;
13
+ setPaused(paused: boolean): void;
14
+ }
15
+ export type Rsdkv3Instance = EngineInstance & {
16
+ devMenu: RsdkDevMenuBridge;
17
+ /** Always false in the current rsdkv3 build (in-memory FS backend). */
18
+ persistent: boolean;
19
+ /** Relative storage namespace used under /data (e.g. "sonic-cd"). */
20
+ storageNamespace: string;
21
+ /** Remove files for this namespace only. */
22
+ purgeStorage(): {
23
+ data: boolean;
24
+ settings: boolean;
25
+ };
26
+ };
27
+ /**
28
+ * Extra (engine-specific) config on top of the contract's EngineConfig: lazy asset
29
+ * providers, invoked only on a cache miss — e.g. to skip a large Data.rsdk fetch
30
+ * when it's already persisted in OPFS.
31
+ */
32
+ export type Rsdkv3LoadConfig = EngineConfig & {
33
+ dataProvider?: () => Promise<AssetData> | AssetData;
34
+ settingsProvider?: () => Promise<AssetData> | AssetData;
35
+ /** Per-game storage folder under /data used for the in-memory filesystem. */
36
+ storageNamespace?: string;
37
+ };
38
+ /** Boot the RSDKv3 engine. */
39
+ export declare function load(config: Rsdkv3LoadConfig): Promise<Rsdkv3Instance>;
40
+ declare const _default: {
41
+ manifest: import("@wasm-gaming/engine-specs").EngineManifest;
42
+ load: typeof load;
43
+ };
44
+ export default _default;
45
+ //# sourceMappingURL=rsdkv3.sdk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rsdkv3.sdk.d.ts","sourceRoot":"","sources":["../../src/rsdkv3.sdk.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAuB,MAAM,2BAA2B,CAAC;AAC9G,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhD,OAAO,EAAE,QAAQ,EAAE,CAAC;AA0FpB,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAChC,YAAY,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IACzE,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACvD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG;IAC5C,OAAO,EAAE,iBAAiB,CAAC;IAC3B,uEAAuE;IACvE,UAAU,EAAE,OAAO,CAAC;IACpB,qEAAqE;IACrE,gBAAgB,EAAE,MAAM,CAAC;IACzB,4CAA4C;IAC5C,YAAY,IAAI;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;CACtD,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG;IAC5C,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;IACpD,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;IACxD,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,8BAA8B;AAC9B,wBAAsB,IAAI,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CA2H5E;;;;;AAED,wBAAkC"}
@@ -0,0 +1,226 @@
1
+ // @wasm-gaming/engine-rsdkv3 — SDK entry point.
2
+ //
3
+ // Conforms to the wasm-gaming engine contract (github.com/wasm-gaming/engine-specs):
4
+ // exports `manifest` (declarative) and `load(config)` (imperative).
5
+ //
6
+ // The engine binary is game-agnostic for RSDKv3. Sonic CD is selected by
7
+ // `Data.rsdk`, mounted at runtime into the Emscripten filesystem.
8
+ import { manifest } from './rsdkv3.manifest.js';
9
+ import { DEFAULT_RSDKV3_OPTIONS } from './rsdkv3.options.js';
10
+ export { manifest };
11
+ const WORK_DIR = '/data';
12
+ const DEFAULT_STORAGE_NAMESPACE = 'default';
13
+ /** Serialize engine options into RSDKv3's settings.ini format. */
14
+ function buildSettingsIni(options = {}) {
15
+ const o = { ...DEFAULT_RSDKV3_OPTIONS, ...options };
16
+ return [
17
+ '[Dev]',
18
+ `EngineDebugMode=${o.engineDebugMode ? 'true' : 'false'}`,
19
+ `DevMenu=${o.devMenu ? 'true' : 'false'}`,
20
+ '',
21
+ '[Game]',
22
+ `Language=${o.language | 0}`,
23
+ `DisableTouchControls=${o.disableTouchControls ? 'true' : 'false'}`,
24
+ '',
25
+ '[Video]',
26
+ 'Windowed=true',
27
+ 'Borderless=false',
28
+ 'ExclusiveFS=false',
29
+ `VSync=${o.vsync ? 'true' : 'false'}`,
30
+ 'WindowScale=2',
31
+ 'ScreenWidth=426',
32
+ 'ScreenHeight=240',
33
+ 'RefreshRate=60',
34
+ '',
35
+ '[Audio]',
36
+ 'BGMVolume=1.000000',
37
+ 'SFXVolume=1.000000',
38
+ `Soundtrack=${o.soundtrack | 0}`,
39
+ '',
40
+ ].join('\n');
41
+ }
42
+ function toUint8(x) {
43
+ if (x == null)
44
+ return null;
45
+ if (typeof x === 'string')
46
+ return new TextEncoder().encode(x);
47
+ if (x instanceof Uint8Array)
48
+ return x;
49
+ if (x instanceof ArrayBuffer)
50
+ return new Uint8Array(x);
51
+ if (ArrayBuffer.isView(x))
52
+ return new Uint8Array(x.buffer, x.byteOffset, x.byteLength);
53
+ throw new TypeError('asset must be Uint8Array | ArrayBuffer | string');
54
+ }
55
+ /** Normalize a user-provided storage namespace into a safe relative path. */
56
+ function normalizeStorageNamespace(namespace) {
57
+ if (typeof namespace !== 'string' || !namespace.trim())
58
+ return DEFAULT_STORAGE_NAMESPACE;
59
+ const cleaned = namespace
60
+ .split('/')
61
+ .map((segment) => segment.trim())
62
+ .filter(Boolean)
63
+ .map((segment) => segment.replace(/[^A-Za-z0-9._-]/g, '_'))
64
+ .filter(Boolean)
65
+ .join('/');
66
+ return cleaned || DEFAULT_STORAGE_NAMESPACE;
67
+ }
68
+ /** Best-effort mkdir -p for the Emscripten FS layer. */
69
+ function ensureDir(Module, path) {
70
+ const parts = path.split('/').filter(Boolean);
71
+ let current = '';
72
+ for (const part of parts) {
73
+ current += `/${part}`;
74
+ try {
75
+ Module.FS.mkdir(current);
76
+ }
77
+ catch {
78
+ /* already exists */
79
+ }
80
+ }
81
+ }
82
+ /** Ensure the game working directory exists in the in-memory filesystem. */
83
+ function mountWorkingDir(Module, storageNamespace) {
84
+ const workDir = `${WORK_DIR}/${storageNamespace}`;
85
+ ensureDir(Module, workDir);
86
+ return { persistent: false, workDir };
87
+ }
88
+ /** True if `path` exists in the (mounted) filesystem. */
89
+ function fileExists(Module, path) {
90
+ try {
91
+ Module.FS.stat(path);
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ /** Boot the RSDKv3 engine. */
99
+ export async function load(config) {
100
+ const { canvas, assets, onEvent } = config;
101
+ const options = config.options;
102
+ if (!canvas)
103
+ throw new Error('rsdkv3: config.canvas is required');
104
+ // Emscripten's SDL2 port locates the canvas via document.querySelector('#canvas').
105
+ if (canvas.id !== 'canvas')
106
+ canvas.id = 'canvas';
107
+ const emit = (e) => {
108
+ try {
109
+ onEvent?.(e);
110
+ }
111
+ catch { /* host handler must not break us */ }
112
+ };
113
+ const jsUrl = config.jsUrl ?? new URL('./rsdkv3.js', import.meta.url).href;
114
+ const wasmUrl = config.wasmUrl ?? new URL('./rsdkv3.wasm', import.meta.url).href;
115
+ // RSDKv3 uses the same key map preset used by the shared launcher script.
116
+ if (typeof window !== 'undefined')
117
+ window.__gamepadKeyMap = manifest.input;
118
+ const mod = await import(/* @vite-ignore */ jsUrl);
119
+ const createRSDKv3 = mod.default;
120
+ const Module = await createRSDKv3({
121
+ canvas,
122
+ noInitialRun: true, // built with -sINVOKE_RUN=0; we mount data before main()
123
+ locateFile: (path) => (path.endsWith('.wasm') ? wasmUrl : path),
124
+ print: (...a) => console.log('[rsdkv3]', ...a),
125
+ printErr: (...a) => console.error('[rsdkv3]', ...a),
126
+ onAbort: (reason) => emit({ type: 'error', error: new Error(`rsdkv3 aborted: ${reason}`) }),
127
+ });
128
+ const storageNamespace = normalizeStorageNamespace(config.storageNamespace);
129
+ const { persistent, workDir } = mountWorkingDir(Module, storageNamespace);
130
+ const dataPath = `${workDir}/Data.rsdk`;
131
+ const settingsPath = `${workDir}/settings.ini`;
132
+ // Data.rsdk — precedence: explicit asset > lazy provider > existing file.
133
+ let dataBytes = toUint8(assets?.data);
134
+ if (!dataBytes && config.dataProvider) {
135
+ dataBytes = toUint8(await config.dataProvider());
136
+ }
137
+ if (dataBytes) {
138
+ Module.FS.writeFile(dataPath, dataBytes);
139
+ }
140
+ else if (!fileExists(Module, dataPath)) {
141
+ throw new Error('rsdkv3: no Data.rsdk — provide assets.data or dataProvider');
142
+ }
143
+ // settings.ini — explicit asset > lazy provider > generated from options.
144
+ let settingsBytes = toUint8(assets?.settings);
145
+ if (!settingsBytes && config.settingsProvider) {
146
+ settingsBytes = toUint8(await config.settingsProvider());
147
+ }
148
+ if (settingsBytes) {
149
+ Module.FS.writeFile(settingsPath, settingsBytes);
150
+ }
151
+ else if (!fileExists(Module, settingsPath)) {
152
+ Module.FS.writeFile(settingsPath, new TextEncoder().encode(buildSettingsIni(options)));
153
+ }
154
+ // else: keep existing settings.ini in memory FS
155
+ Module.FS.chdir(workDir);
156
+ const setPaused = (paused) => {
157
+ if (typeof Module.web_devmenu_set_paused === 'function')
158
+ Module.web_devmenu_set_paused(paused);
159
+ };
160
+ const devMenu = {
161
+ getStageList() {
162
+ if (typeof Module.web_devmenu_get_stage_list !== 'function')
163
+ return [];
164
+ try {
165
+ return JSON.parse(Module.web_devmenu_get_stage_list());
166
+ }
167
+ catch (e) {
168
+ console.error('[rsdkv3] getStageList failed', e);
169
+ return [];
170
+ }
171
+ },
172
+ loadStage(categoryIdx, stageIdx) {
173
+ if (typeof Module.web_devmenu_load_stage === 'function') {
174
+ Module.web_devmenu_load_stage(categoryIdx | 0, stageIdx | 0);
175
+ }
176
+ },
177
+ setPaused,
178
+ };
179
+ // Run main() from the mounted working dir. simulate_infinite_loop schedules
180
+ // the rAF loop and returns via a benign unwind Emscripten swallows.
181
+ Module.callMain(['UsingCWD']);
182
+ emit({ type: 'ready' });
183
+ return {
184
+ start() { },
185
+ pause() { setPaused(true); },
186
+ resume() { setPaused(false); },
187
+ reset() {
188
+ throw new Error('rsdkv3: reset() is not supported — destroy() and load() again');
189
+ },
190
+ setInput(map) {
191
+ if (typeof window !== 'undefined') {
192
+ window.__gamepadKeyMap = map;
193
+ }
194
+ },
195
+ destroy() {
196
+ try {
197
+ Module.pauseMainLoop?.();
198
+ }
199
+ catch { /* noop */ }
200
+ try {
201
+ setPaused(true);
202
+ }
203
+ catch { /* noop */ }
204
+ },
205
+ devMenu,
206
+ persistent,
207
+ storageNamespace,
208
+ purgeStorage() {
209
+ const deleteFileIfExists = (path) => {
210
+ try {
211
+ Module.FS.unlink(path);
212
+ return true;
213
+ }
214
+ catch {
215
+ return false;
216
+ }
217
+ };
218
+ return {
219
+ data: deleteFileIfExists(dataPath),
220
+ settings: deleteFileIfExists(settingsPath),
221
+ };
222
+ },
223
+ };
224
+ }
225
+ export default { manifest, load };
226
+ //# sourceMappingURL=rsdkv3.sdk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rsdkv3.sdk.js","sourceRoot":"","sources":["../../src/rsdkv3.sdk.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ export type AssetData = Uint8Array | ArrayBuffer | string;
2
+ export interface EngineEventError {
3
+ message?: string;
4
+ }
5
+ export type EngineEvent = {
6
+ type: 'ready';
7
+ } | {
8
+ type: 'error';
9
+ error: Error;
10
+ } | {
11
+ type: string;
12
+ [key: string]: unknown;
13
+ };
14
+ export interface EngineConfig {
15
+ canvas?: HTMLCanvasElement;
16
+ assets?: {
17
+ data?: AssetData;
18
+ settings?: AssetData;
19
+ };
20
+ options?: unknown;
21
+ onEvent?: (event: EngineEvent) => void;
22
+ jsUrl?: string;
23
+ wasmUrl?: string;
24
+ persist?: 'opfs' | 'idbfs' | null;
25
+ }
26
+ export interface EngineInstance {
27
+ start(): void;
28
+ pause(): void;
29
+ resume(): void;
30
+ reset(): void;
31
+ setInput(preset?: string): void;
32
+ destroy(): void;
33
+ }
34
+ export interface JSONSchema {
35
+ type?: string;
36
+ additionalProperties?: boolean;
37
+ properties?: Record<string, unknown>;
38
+ }
39
+ export interface EngineArtifacts {
40
+ wasm: string;
41
+ js: string;
42
+ }
43
+ export interface AssetSpec {
44
+ key: string;
45
+ mountPath: string;
46
+ required: boolean;
47
+ accept: string[];
48
+ description?: string;
49
+ }
50
+ export interface VideoSpec {
51
+ baseWidth: number;
52
+ baseHeight: number;
53
+ aspect: string;
54
+ }
55
+ export interface EngineCapabilities {
56
+ saveStates: boolean;
57
+ sram: boolean;
58
+ coreSelectable: boolean;
59
+ }
60
+ export interface EngineManifest {
61
+ id: string;
62
+ version: string;
63
+ name: string;
64
+ artifacts: EngineArtifacts;
65
+ assets: AssetSpec[];
66
+ input: string;
67
+ video: VideoSpec;
68
+ options: JSONSchema;
69
+ capabilities: EngineCapabilities;
70
+ }
71
+ //# sourceMappingURL=wasm-specs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-specs.d.ts","sourceRoot":"","sources":["../../src/wasm-specs.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,SAAS,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC;IACpD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,eAAe,CAAC;IAC3B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;IACpB,YAAY,EAAE,kBAAkB,CAAC;CAClC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=wasm-specs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-specs.js","sourceRoot":"","sources":["../../src/wasm-specs.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@wasm-gaming/rsdkv3-wasm",
3
+ "version": "0.1.2",
4
+ "description": "RSDKv3 (Retro Software Development Kit v3) compiled to WebAssembly, with a JS SDK conforming to the wasm-gaming engine contract. Runs Sonic CD by loading Data.rsdk at runtime.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/wasm-gaming/rsdkv3-wasm.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/wasm-gaming/rsdkv3-wasm/issues"
11
+ },
12
+ "homepage": "https://github.com/wasm-gaming/rsdkv3-wasm#readme",
13
+ "type": "module",
14
+ "main": "./dist/rsdkv3/rsdkv3.sdk.js",
15
+ "types": "./dist/rsdkv3/rsdkv3.sdk.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/rsdkv3/rsdkv3.sdk.d.ts",
19
+ "import": "./dist/rsdkv3/rsdkv3.sdk.js"
20
+ },
21
+ "./options": {
22
+ "types": "./dist/rsdkv3/rsdkv3.options.d.ts",
23
+ "import": "./dist/rsdkv3/rsdkv3.options.js"
24
+ },
25
+ "./manifest": "./dist/manifest.json"
26
+ },
27
+ "files": [
28
+ "dist/rsdkv3/",
29
+ "dist/manifest.json",
30
+ "src/rsdkv3.manifest.ts",
31
+ "src/rsdkv3.options.ts",
32
+ "src/rsdkv3.sdk.ts",
33
+ "src/wasm-specs.ts",
34
+ "README.md"
35
+ ],
36
+ "comment:scripts": "Build logic lives in the Makefile, not here (make build / build-sdk / build-wasm / preview).",
37
+ "scripts": {},
38
+ "peerDependencies": {
39
+ "@wasm-gaming/engine-specs": "*"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@wasm-gaming/engine-specs": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "@wasm-gaming/engine-specs": "^0.1.0",
48
+ "typescript": "^5.5.0"
49
+ },
50
+ "engineContract": "https://github.com/wasm-gaming/engine-specs",
51
+ "license": "MIT"
52
+ }
@@ -0,0 +1,44 @@
1
+ // The rsdkv3 EngineManifest — typed against the contract, so a drift from
2
+ // @wasm-gaming/engine-specs is a compile error here. `npm run build:manifest`
3
+ // serializes this to dist/manifest.json (the artifact CI attaches to a Release).
4
+
5
+ import type { EngineManifest } from '@wasm-gaming/engine-specs';
6
+ import { RSDKV3_OPTIONS_SCHEMA } from './rsdkv3.options.js';
7
+
8
+ export const manifest: EngineManifest = {
9
+ id: 'rsdkv3',
10
+ version: '0.1.0',
11
+ name: 'Retro Software Development Kit v3',
12
+ artifacts: {
13
+ // Relative to the manifest (dist/manifest.json); the engine files live in
14
+ // the dist/rsdkv3/ subfolder.
15
+ wasm: 'rsdkv3/rsdkv3.wasm',
16
+ js: 'rsdkv3/rsdkv3.js',
17
+ },
18
+ assets: [
19
+ {
20
+ key: 'data',
21
+ // Lives under the OPFS-backed (persistent) working dir; the engine reads it
22
+ // via CWD (booted with the UsingCWD arg).
23
+ mountPath: '/data/Data.rsdk',
24
+ required: true,
25
+ accept: ['.rsdk'],
26
+ description:
27
+ 'Sonic CD 2011 game data pack (Data.rsdk).',
28
+ },
29
+ {
30
+ key: 'settings',
31
+ mountPath: '/data/settings.ini',
32
+ required: false,
33
+ accept: ['.ini'],
34
+ description:
35
+ 'Engine settings. Omitted → the SDK generates one from config.options.',
36
+ },
37
+ ],
38
+ input: 'rsdkv4',
39
+ video: { baseWidth: 426, baseHeight: 240, aspect: '16:9' },
40
+ options: RSDKV3_OPTIONS_SCHEMA,
41
+ capabilities: { saveStates: false, sram: false, coreSelectable: false },
42
+ };
43
+
44
+ export default manifest;
@@ -0,0 +1,68 @@
1
+ // Engine-specific options for RSDKv3.
2
+ //
3
+ // This is the authoritative description of what `EngineConfig.options` accepts
4
+ // for this engine. It provides both:
5
+ // - `Rsdkv3Options` — compile-time type for TS hosts
6
+ // - `RSDKV3_OPTIONS_SCHEMA` — JSON Schema mirrored into manifest.json
7
+ // - `DEFAULT_RSDKV3_OPTIONS` — defaults used when no settings.ini is supplied
8
+ //
9
+ // At runtime these options are serialized into Sonic CD's `settings.ini` format
10
+ // when the host does not pass an explicit `settings` asset.
11
+
12
+ import type { JSONSchema } from '@wasm-gaming/engine-specs';
13
+
14
+ export interface Rsdkv3Options {
15
+ /**
16
+ * RSDKv3's native in-canvas Dev Menu. Kept off by default: the launcher owns
17
+ * the debug/stage-select UI (via `instance.devMenu`), and leaving this on has
18
+ * historically produced a native menu-like screen at boot.
19
+ */
20
+ devMenu?: boolean;
21
+ /** RSDKv3 EngineDebugMode — enables the web devmenu embind bridge hooks. */
22
+ engineDebugMode?: boolean;
23
+ /** VSync setting in the generated settings.ini. */
24
+ vsync?: boolean;
25
+ /** 0 = JP/EU soundtrack, 1 = US soundtrack. */
26
+ soundtrack?: number;
27
+ /** 0 = English. Matches RSDKv3 language numeric value. */
28
+ language?: number;
29
+ /** Toggle touch controls in the generated settings.ini. */
30
+ disableTouchControls?: boolean;
31
+ }
32
+
33
+ export const DEFAULT_RSDKV3_OPTIONS: Required<Rsdkv3Options> = {
34
+ devMenu: false,
35
+ engineDebugMode: false,
36
+ vsync: false,
37
+ soundtrack: 0,
38
+ language: 0,
39
+ disableTouchControls: true,
40
+ };
41
+
42
+ export const RSDKV3_OPTIONS_SCHEMA: JSONSchema = {
43
+ type: 'object',
44
+ additionalProperties: false,
45
+ properties: {
46
+ devMenu: {
47
+ type: 'boolean',
48
+ default: false,
49
+ description:
50
+ "RSDKv3's native in-canvas Dev Menu. The launcher provides its own overlay via instance.devMenu, so keep this off.",
51
+ },
52
+ engineDebugMode: {
53
+ type: 'boolean',
54
+ default: false,
55
+ description: 'Enables the web devmenu embind bridge (stage list / warp / pause).',
56
+ },
57
+ vsync: { type: 'boolean', default: false },
58
+ soundtrack: {
59
+ type: 'integer',
60
+ minimum: 0,
61
+ maximum: 1,
62
+ default: 0,
63
+ description: 'Sonic CD soundtrack selector: 0=JP/EU, 1=US.',
64
+ },
65
+ language: { type: 'integer', minimum: 0, default: 0 },
66
+ disableTouchControls: { type: 'boolean', default: true },
67
+ },
68
+ };
@@ -0,0 +1,258 @@
1
+ // @wasm-gaming/rsdkv3-wasm — SDK entry point.
2
+ //
3
+ // Conforms to the wasm-gaming engine contract (github.com/wasm-gaming/engine-specs):
4
+ // exports `manifest` (declarative) and `load(config)` (imperative).
5
+ //
6
+ // The engine binary is game-agnostic for RSDKv3. Sonic CD is selected by
7
+ // `Data.rsdk`, mounted at runtime into the Emscripten filesystem.
8
+
9
+ import type { EngineConfig, EngineInstance, AssetData, InputPreset, KeyMap } from '@wasm-gaming/engine-specs';
10
+ import { manifest } from './rsdkv3.manifest.js';
11
+ import { DEFAULT_RSDKV3_OPTIONS, type Rsdkv3Options } from './rsdkv3.options.js';
12
+
13
+ export { manifest };
14
+
15
+ const WORK_DIR = '/data';
16
+ const DEFAULT_STORAGE_NAMESPACE = 'default';
17
+
18
+ /** Serialize engine options into RSDKv3's settings.ini format. */
19
+ function buildSettingsIni(options: Rsdkv3Options = {}): string {
20
+ const o = { ...DEFAULT_RSDKV3_OPTIONS, ...options };
21
+ return [
22
+ '[Dev]',
23
+ `EngineDebugMode=${o.engineDebugMode ? 'true' : 'false'}`,
24
+ `DevMenu=${o.devMenu ? 'true' : 'false'}`,
25
+ '',
26
+ '[Game]',
27
+ `Language=${o.language | 0}`,
28
+ `DisableTouchControls=${o.disableTouchControls ? 'true' : 'false'}`,
29
+ '',
30
+ '[Video]',
31
+ 'Windowed=true',
32
+ 'Borderless=false',
33
+ 'ExclusiveFS=false',
34
+ `VSync=${o.vsync ? 'true' : 'false'}`,
35
+ 'WindowScale=2',
36
+ 'ScreenWidth=426',
37
+ 'ScreenHeight=240',
38
+ 'RefreshRate=60',
39
+ '',
40
+ '[Audio]',
41
+ 'BGMVolume=1.000000',
42
+ 'SFXVolume=1.000000',
43
+ `Soundtrack=${o.soundtrack | 0}`,
44
+ '',
45
+ ].join('\n');
46
+ }
47
+
48
+ function toUint8(x: unknown): Uint8Array | null {
49
+ if (x == null) return null;
50
+ if (typeof x === 'string') return new TextEncoder().encode(x);
51
+ if (x instanceof Uint8Array) return x;
52
+ if (x instanceof ArrayBuffer) return new Uint8Array(x);
53
+ if (ArrayBuffer.isView(x)) return new Uint8Array(x.buffer, x.byteOffset, x.byteLength);
54
+ throw new TypeError('asset must be Uint8Array | ArrayBuffer | string');
55
+ }
56
+
57
+ /** Normalize a user-provided storage namespace into a safe relative path. */
58
+ function normalizeStorageNamespace(namespace: unknown): string {
59
+ if (typeof namespace !== 'string' || !namespace.trim()) return DEFAULT_STORAGE_NAMESPACE;
60
+
61
+ const cleaned = namespace
62
+ .split('/')
63
+ .map((segment) => segment.trim())
64
+ .filter(Boolean)
65
+ .map((segment) => segment.replace(/[^A-Za-z0-9._-]/g, '_'))
66
+ .filter(Boolean)
67
+ .join('/');
68
+
69
+ return cleaned || DEFAULT_STORAGE_NAMESPACE;
70
+ }
71
+
72
+ /** Best-effort mkdir -p for the Emscripten FS layer. */
73
+ function ensureDir(Module: any, path: string): void {
74
+ const parts = path.split('/').filter(Boolean);
75
+ let current = '';
76
+ for (const part of parts) {
77
+ current += `/${part}`;
78
+ try {
79
+ Module.FS.mkdir(current);
80
+ } catch {
81
+ /* already exists */
82
+ }
83
+ }
84
+ }
85
+
86
+ /** Ensure the game working directory exists in the in-memory filesystem. */
87
+ function mountWorkingDir(Module: any, storageNamespace: string): { persistent: boolean; workDir: string } {
88
+ const workDir = `${WORK_DIR}/${storageNamespace}`;
89
+ ensureDir(Module, workDir);
90
+ return { persistent: false, workDir };
91
+ }
92
+
93
+ /** True if `path` exists in the (mounted) filesystem. */
94
+ function fileExists(Module: any, path: string): boolean {
95
+ try {
96
+ Module.FS.stat(path);
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /** RSDKv3-specific bridge for the launcher's debug/stage-select UI. */
104
+ export interface RsdkDevMenuBridge {
105
+ getStageList(): Array<{ name: string; stages: Array<{ name: string }> }>;
106
+ loadStage(categoryIdx: number, stageIdx: number): void;
107
+ setPaused(paused: boolean): void;
108
+ }
109
+
110
+ export type Rsdkv3Instance = EngineInstance & {
111
+ devMenu: RsdkDevMenuBridge;
112
+ /** Always false in the current rsdkv3 build (in-memory FS backend). */
113
+ persistent: boolean;
114
+ /** Relative storage namespace used under /data (e.g. "sonic-cd"). */
115
+ storageNamespace: string;
116
+ /** Remove files for this namespace only. */
117
+ purgeStorage(): { data: boolean; settings: boolean };
118
+ };
119
+
120
+ /**
121
+ * Extra (engine-specific) config on top of the contract's EngineConfig: lazy asset
122
+ * providers, invoked only on a cache miss — e.g. to skip a large Data.rsdk fetch
123
+ * when it's already persisted in OPFS.
124
+ */
125
+ export type Rsdkv3LoadConfig = EngineConfig & {
126
+ dataProvider?: () => Promise<AssetData> | AssetData;
127
+ settingsProvider?: () => Promise<AssetData> | AssetData;
128
+ /** Per-game storage folder under /data used for the in-memory filesystem. */
129
+ storageNamespace?: string;
130
+ };
131
+
132
+ /** Boot the RSDKv3 engine. */
133
+ export async function load(config: Rsdkv3LoadConfig): Promise<Rsdkv3Instance> {
134
+ const { canvas, assets, onEvent } = config;
135
+ const options = config.options as Rsdkv3Options | undefined;
136
+ if (!canvas) throw new Error('rsdkv3: config.canvas is required');
137
+
138
+ // Emscripten's SDL2 port locates the canvas via document.querySelector('#canvas').
139
+ if (canvas.id !== 'canvas') canvas.id = 'canvas';
140
+
141
+ const emit = (e: Parameters<NonNullable<EngineConfig['onEvent']>>[0]) => {
142
+ try { onEvent?.(e); } catch { /* host handler must not break us */ }
143
+ };
144
+
145
+ const jsUrl = config.jsUrl ?? new URL('./rsdkv3.js', import.meta.url).href;
146
+ const wasmUrl = config.wasmUrl ?? new URL('./rsdkv3.wasm', import.meta.url).href;
147
+
148
+ // RSDKv3 uses the same key map preset used by the shared launcher script.
149
+ if (typeof window !== 'undefined') (window as any).__gamepadKeyMap = manifest.input;
150
+
151
+ const mod: any = await import(/* @vite-ignore */ jsUrl);
152
+ const createRSDKv3 = mod.default;
153
+
154
+ const Module: any = await createRSDKv3({
155
+ canvas,
156
+ noInitialRun: true, // built with -sINVOKE_RUN=0; we mount data before main()
157
+ locateFile: (path: string) => (path.endsWith('.wasm') ? wasmUrl : path),
158
+ print: (...a: unknown[]) => console.log('[rsdkv3]', ...a),
159
+ printErr: (...a: unknown[]) => console.error('[rsdkv3]', ...a),
160
+ onAbort: (reason: unknown) =>
161
+ emit({ type: 'error', error: new Error(`rsdkv3 aborted: ${reason}`) }),
162
+ });
163
+
164
+ const storageNamespace = normalizeStorageNamespace(config.storageNamespace);
165
+ const { persistent, workDir } = mountWorkingDir(Module, storageNamespace);
166
+ const dataPath = `${workDir}/Data.rsdk`;
167
+ const settingsPath = `${workDir}/settings.ini`;
168
+
169
+ // Data.rsdk — precedence: explicit asset > lazy provider > existing file.
170
+ let dataBytes = toUint8(assets?.data);
171
+ if (!dataBytes && config.dataProvider) {
172
+ dataBytes = toUint8(await config.dataProvider());
173
+ }
174
+ if (dataBytes) {
175
+ Module.FS.writeFile(dataPath, dataBytes);
176
+ } else if (!fileExists(Module, dataPath)) {
177
+ throw new Error('rsdkv3: no Data.rsdk — provide assets.data or dataProvider');
178
+ }
179
+
180
+ // settings.ini — explicit asset > lazy provider > generated from options.
181
+ let settingsBytes = toUint8(assets?.settings);
182
+ if (!settingsBytes && config.settingsProvider) {
183
+ settingsBytes = toUint8(await config.settingsProvider());
184
+ }
185
+ if (settingsBytes) {
186
+ Module.FS.writeFile(settingsPath, settingsBytes);
187
+ } else if (!fileExists(Module, settingsPath)) {
188
+ Module.FS.writeFile(settingsPath, new TextEncoder().encode(buildSettingsIni(options)));
189
+ }
190
+ // else: keep existing settings.ini in memory FS
191
+
192
+ Module.FS.chdir(workDir);
193
+
194
+ const setPaused = (paused: boolean) => {
195
+ if (typeof Module.web_devmenu_set_paused === 'function') Module.web_devmenu_set_paused(paused);
196
+ };
197
+
198
+ const devMenu: RsdkDevMenuBridge = {
199
+ getStageList() {
200
+ if (typeof Module.web_devmenu_get_stage_list !== 'function') return [];
201
+ try {
202
+ return JSON.parse(Module.web_devmenu_get_stage_list());
203
+ } catch (e) {
204
+ console.error('[rsdkv3] getStageList failed', e);
205
+ return [];
206
+ }
207
+ },
208
+ loadStage(categoryIdx, stageIdx) {
209
+ if (typeof Module.web_devmenu_load_stage === 'function') {
210
+ Module.web_devmenu_load_stage(categoryIdx | 0, stageIdx | 0);
211
+ }
212
+ },
213
+ setPaused,
214
+ };
215
+
216
+ // Run main() from the mounted working dir. simulate_infinite_loop schedules
217
+ // the rAF loop and returns via a benign unwind Emscripten swallows.
218
+ Module.callMain(['UsingCWD']);
219
+ emit({ type: 'ready' });
220
+
221
+ return {
222
+ start() {},
223
+ pause() { setPaused(true); },
224
+ resume() { setPaused(false); },
225
+ reset() {
226
+ throw new Error('rsdkv3: reset() is not supported — destroy() and load() again');
227
+ },
228
+ setInput(map: InputPreset | KeyMap) {
229
+ if (typeof window !== 'undefined') {
230
+ (window as any).__gamepadKeyMap = map;
231
+ }
232
+ },
233
+ destroy() {
234
+ try { Module.pauseMainLoop?.(); } catch { /* noop */ }
235
+ try { setPaused(true); } catch { /* noop */ }
236
+ },
237
+ devMenu,
238
+ persistent,
239
+ storageNamespace,
240
+ purgeStorage() {
241
+ const deleteFileIfExists = (path: string): boolean => {
242
+ try {
243
+ Module.FS.unlink(path);
244
+ return true;
245
+ } catch {
246
+ return false;
247
+ }
248
+ };
249
+
250
+ return {
251
+ data: deleteFileIfExists(dataPath),
252
+ settings: deleteFileIfExists(settingsPath),
253
+ };
254
+ },
255
+ };
256
+ }
257
+
258
+ export default { manifest, load };
@@ -0,0 +1,72 @@
1
+ export type AssetData = Uint8Array | ArrayBuffer | string;
2
+
3
+ export interface EngineEventError {
4
+ message?: string;
5
+ }
6
+
7
+ export type EngineEvent =
8
+ | { type: 'ready' }
9
+ | { type: 'error'; error: Error }
10
+ | { type: string; [key: string]: unknown };
11
+
12
+ export interface EngineConfig {
13
+ canvas?: HTMLCanvasElement;
14
+ assets?: { data?: AssetData; settings?: AssetData };
15
+ options?: unknown;
16
+ onEvent?: (event: EngineEvent) => void;
17
+ jsUrl?: string;
18
+ wasmUrl?: string;
19
+ persist?: 'opfs' | 'idbfs' | null;
20
+ }
21
+
22
+ export interface EngineInstance {
23
+ start(): void;
24
+ pause(): void;
25
+ resume(): void;
26
+ reset(): void;
27
+ setInput(preset?: string): void;
28
+ destroy(): void;
29
+ }
30
+
31
+ export interface JSONSchema {
32
+ type?: string;
33
+ additionalProperties?: boolean;
34
+ properties?: Record<string, unknown>;
35
+ }
36
+
37
+ export interface EngineArtifacts {
38
+ wasm: string;
39
+ js: string;
40
+ }
41
+
42
+ export interface AssetSpec {
43
+ key: string;
44
+ mountPath: string;
45
+ required: boolean;
46
+ accept: string[];
47
+ description?: string;
48
+ }
49
+
50
+ export interface VideoSpec {
51
+ baseWidth: number;
52
+ baseHeight: number;
53
+ aspect: string;
54
+ }
55
+
56
+ export interface EngineCapabilities {
57
+ saveStates: boolean;
58
+ sram: boolean;
59
+ coreSelectable: boolean;
60
+ }
61
+
62
+ export interface EngineManifest {
63
+ id: string;
64
+ version: string;
65
+ name: string;
66
+ artifacts: EngineArtifacts;
67
+ assets: AssetSpec[];
68
+ input: string;
69
+ video: VideoSpec;
70
+ options: JSONSchema;
71
+ capabilities: EngineCapabilities;
72
+ }