@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 +89 -0
- package/dist/rsdkv3/rsdkv3.manifest.d.ts +4 -0
- package/dist/rsdkv3/rsdkv3.manifest.d.ts.map +1 -0
- package/dist/rsdkv3/rsdkv3.manifest.js +39 -0
- package/dist/rsdkv3/rsdkv3.manifest.js.map +1 -0
- package/dist/rsdkv3/rsdkv3.options.d.ts +22 -0
- package/dist/rsdkv3/rsdkv3.options.d.ts.map +1 -0
- package/dist/rsdkv3/rsdkv3.options.js +45 -0
- package/dist/rsdkv3/rsdkv3.options.js.map +1 -0
- package/dist/rsdkv3/rsdkv3.sdk.d.ts +45 -0
- package/dist/rsdkv3/rsdkv3.sdk.d.ts.map +1 -0
- package/dist/rsdkv3/rsdkv3.sdk.js +226 -0
- package/dist/rsdkv3/rsdkv3.sdk.js.map +1 -0
- package/dist/rsdkv3/wasm-specs.d.ts +71 -0
- package/dist/rsdkv3/wasm-specs.d.ts.map +1 -0
- package/dist/rsdkv3/wasm-specs.js +2 -0
- package/dist/rsdkv3/wasm-specs.js.map +1 -0
- package/package.json +52 -0
- package/src/rsdkv3.manifest.ts +44 -0
- package/src/rsdkv3.options.ts +68 -0
- package/src/rsdkv3.sdk.ts +258 -0
- package/src/wasm-specs.ts +72 -0
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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|