@wasm-gaming/rsdkv3-wasm 0.1.4 → 0.2.0

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.
@@ -0,0 +1,382 @@
1
+ // ---- Introspectable property definitions ----
2
+ /**
3
+ * The listener bookkeeping, shipped rather than described — the one piece of runtime
4
+ * code in this package.
5
+ *
6
+ * It is a **convenience, not a requirement**: the contract stays structural, so an
7
+ * engine that already owns an emitter satisfies {@link EngineSurface} without this.
8
+ * What it saves is the part everyone gets subtly wrong — iterating a copy, so a
9
+ * listener that unsubscribes mid-emit does not corrupt the loop; de-duplicating a
10
+ * listener registered twice; and keeping one throwing listener from silencing the
11
+ * rest.
12
+ *
13
+ * Hold it **privately** and hand the same instance to every carrier. That is what
14
+ * makes `sdk.on(…)` and `play.on(…)` one channel rather than two, lets a host
15
+ * subscribe before the engine exists, and keeps `emit` out of the public API — a host
16
+ * must not be able to forge an engine event.
17
+ */
18
+ export class EventEmitter {
19
+ #listeners = new Map();
20
+ /** @param listeners Registered immediately — what `EngineInit.on` carries. */
21
+ constructor(listeners) {
22
+ for (const type of Object.keys(listeners ?? {})) {
23
+ const listener = listeners?.[type];
24
+ if (listener)
25
+ this.on(type, listener);
26
+ }
27
+ }
28
+ /** Registering the same listener twice registers it once, as `EventTarget` does. */
29
+ on(type, listener) {
30
+ let set = this.#listeners.get(type);
31
+ if (!set)
32
+ this.#listeners.set(type, (set = new Set()));
33
+ set.add(listener);
34
+ return this;
35
+ }
36
+ /**
37
+ * The listener is required, as in `EventTarget`. A convenience `off(type)` dropping
38
+ * every listener for a type is a footgun on a page where two components subscribed
39
+ * independently: one unsubscribing would silence the other.
40
+ */
41
+ off(type, listener) {
42
+ const set = this.#listeners.get(type);
43
+ if (set?.delete(listener) && set.size === 0) {
44
+ this.#listeners.delete(type);
45
+ }
46
+ return this;
47
+ }
48
+ /**
49
+ * Takes the **detail**, and wraps it. The rest tuple is what lets an event with no
50
+ * detail be emitted as `emit('exit')` while still checking the ones that have one.
51
+ */
52
+ emit(type, ...[detail]) {
53
+ const set = this.#listeners.get(type);
54
+ if (!set)
55
+ return;
56
+ // Built once and frozen: every listener gets this same object, so a mutable one
57
+ // would let the first rewrite what the rest read.
58
+ const event = Object.freeze({ type, detail: detail });
59
+ // A copy: a listener may call `off()` — on itself or another — while this runs.
60
+ for (const listener of [...set]) {
61
+ try {
62
+ listener(event);
63
+ }
64
+ catch (thrown) {
65
+ // One bad listener must neither silence the rest nor vanish. Rethrowing on a
66
+ // fresh task puts it in front of `window.onerror` with its stack intact.
67
+ queueMicrotask(() => {
68
+ throw thrown;
69
+ });
70
+ }
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Wrap a promise of a live engine in the chainable surface.
76
+ *
77
+ * Exported because it is the single most duplicated function in an engine, and an
78
+ * engine that does not extend {@link EnginePlayBase} still needs exactly this.
79
+ *
80
+ * `on` and `off` do **not** go through the chain: they reach the channel now, so a
81
+ * listener is live before the promise settles. Routing them through it would bind
82
+ * after the session opened and lose whatever was emitted while it was opening.
83
+ */
84
+ export function hydratePlay(inner, events) {
85
+ const chain = (step) => hydratePlay(inner.then(async (play) => {
86
+ await step(play);
87
+ return play;
88
+ }), events);
89
+ const hydrated = {
90
+ then: (onfulfilled, onrejected) => inner.then(onfulfilled, onrejected),
91
+ catch: (onrejected) => inner.catch(onrejected),
92
+ finally: (onfinally) => inner.finally(onfinally),
93
+ [Symbol.toStringTag]: 'PromisePlay',
94
+ config: ((patch) => patch === undefined
95
+ ? inner.then((play) => play.config())
96
+ : chain((play) => play.config(patch))),
97
+ options: ((patch) => patch === undefined
98
+ ? inner.then((play) => play.options())
99
+ : chain((play) => play.options(patch))),
100
+ assets: ((patch) => patch === undefined
101
+ ? inner.then((play) => play.assets())
102
+ : chain((play) => play.assets(patch))),
103
+ storage: ((patch) => patch === undefined
104
+ ? inner.then((play) => play.storage())
105
+ : chain((play) => play.storage(patch))),
106
+ mount: (target) => chain((play) => play.mount(target)),
107
+ on: (type, listener) => {
108
+ events.on(type, listener);
109
+ return hydrated;
110
+ },
111
+ off: (type, listener) => {
112
+ events.off(type, listener);
113
+ return hydrated;
114
+ },
115
+ restart: (options) => chain((play) => play.restart(options)),
116
+ destroy: () => inner.then((play) => play.destroy()),
117
+ };
118
+ return hydrated;
119
+ }
120
+ /**
121
+ * The live engine, with everything the contract prescribes already written: state,
122
+ * the cumulative merge, the descriptors, the chainable promise, and the channel.
123
+ *
124
+ * A subclass supplies what only it can know — {@link EnginePlayBase.open} and
125
+ * {@link EnginePlayBase.close}, optionally {@link EnginePlayBase.mounted} and
126
+ * {@link EnginePlayBase.patched} — and its properties as an {@link EngineSpec}.
127
+ *
128
+ * The spec arrives as a **constructor argument**, not an abstract field, and that is
129
+ * not a style choice: a subclass field initialiser runs *after* `super()`, so a
130
+ * `protected abstract spec` would still be `undefined` while this constructor seeded
131
+ * state from it. Passing it up makes the ordering trap impossible rather than merely
132
+ * avoided.
133
+ */
134
+ export class EnginePlayBase {
135
+ #spec;
136
+ #state;
137
+ #events;
138
+ #target;
139
+ /** The target {@link EnginePlayBase.mounted} has already been told about. */
140
+ #attached;
141
+ /**
142
+ * @param events The factory's channel. Omitted only when a play is built on its
143
+ * own; the factory always passes its own, which is what makes `sdk.on(…)` and
144
+ * `play.on(…)` one channel rather than two.
145
+ */
146
+ constructor(spec, init, events) {
147
+ this.#spec = spec;
148
+ this.#events = events ?? new EventEmitter(init?.on);
149
+ this.#state = {
150
+ config: seed(spec.config, init?.config),
151
+ options: seed(spec.options, init?.options),
152
+ assets: seed(spec.assets, init?.assets),
153
+ storage: seed(spec.storage, init?.storage),
154
+ };
155
+ // Recorded, not applied. `mounted()` is overridable, and a subclass's own fields
156
+ // are still uninitialised while this constructor runs — so the hook is deferred to
157
+ // the first `restart()`, by which point the subclass is whole.
158
+ this.#target = init?.mount;
159
+ }
160
+ /** The element is already recorded when this runs. */
161
+ mounted(target) {
162
+ void target;
163
+ }
164
+ /** A payload changed on a running engine — apply what is `live`, ignore the rest. */
165
+ patched(kind, patch) {
166
+ void kind;
167
+ void patch;
168
+ }
169
+ /**
170
+ * Emit one of this engine's own events.
171
+ *
172
+ * `protected`, so a typed host cannot forge an engine event — but be exact about
173
+ * what that buys: TypeScript erases `protected`, so this is a compile-time guard,
174
+ * not a capability boundary. Plain JavaScript can still reach it. Only the `#`
175
+ * fields below — the channel and the state — are unreachable at runtime.
176
+ */
177
+ emit(type, ...detail) {
178
+ this.#events.emit(type, ...detail);
179
+ }
180
+ /** Where the picture is, for a subclass that needs the element after `mounted()`. */
181
+ get target() {
182
+ return this.#target;
183
+ }
184
+ config(patch) {
185
+ // `arguments.length`, not `patch === undefined`: `config(undefined)` is a write of
186
+ // nothing, not a read.
187
+ if (arguments.length === 0)
188
+ return Promise.resolve(this.#describe('config'));
189
+ return this.#patch('config', patch);
190
+ }
191
+ options(patch) {
192
+ if (arguments.length === 0)
193
+ return Promise.resolve(this.#describe('options'));
194
+ return this.#patch('options', patch);
195
+ }
196
+ assets(patch) {
197
+ if (arguments.length === 0)
198
+ return Promise.resolve(this.#describe('assets'));
199
+ return this.#patch('assets', patch);
200
+ }
201
+ storage(patch) {
202
+ if (arguments.length === 0)
203
+ return Promise.resolve(this.#describe('storage'));
204
+ return this.#patch('storage', patch);
205
+ }
206
+ mount(target) {
207
+ this.#target = target;
208
+ return this.#run(() => this.#attach());
209
+ }
210
+ on(type, listener) {
211
+ this.#events.on(type, listener);
212
+ return hydratePlay(Promise.resolve(this), this.#events);
213
+ }
214
+ off(type, listener) {
215
+ this.#events.off(type, listener);
216
+ return hydratePlay(Promise.resolve(this), this.#events);
217
+ }
218
+ restart(options) {
219
+ return this.#run(async () => {
220
+ if (options)
221
+ Object.assign(this.#state.options, options);
222
+ // A target supplied to the constructor lands here, which is what makes
223
+ // `new Sdk({ mount: el }).start()` and `new Sdk().mount(el).start()` the same
224
+ // call — the rule the contract states for everything on the surface.
225
+ await this.#attach();
226
+ const opened = { ...this.#state.options };
227
+ await this.open(opened);
228
+ this.#emitCore('start', opened);
229
+ });
230
+ }
231
+ async destroy() {
232
+ await this.#guard(async () => {
233
+ await this.close();
234
+ this.#target = undefined;
235
+ this.#attached = undefined;
236
+ this.#emitCore('exit', undefined);
237
+ });
238
+ }
239
+ /** Tell the engine where it is, once per distinct target. Reparenting is legal. */
240
+ async #attach() {
241
+ if (!this.#target || this.#target === this.#attached)
242
+ return;
243
+ this.#attached = this.#target;
244
+ await this.mounted(this.#target);
245
+ }
246
+ // ---- Shared machinery ----
247
+ /** One getter's answer: the spec, with the current value written into each entry. */
248
+ #describe(kind) {
249
+ const spec = this.#spec[kind];
250
+ const state = this.#state[kind];
251
+ const out = {};
252
+ for (const key of Object.keys(spec)) {
253
+ out[key] = { ...spec[key], value: state[key] };
254
+ }
255
+ return out;
256
+ }
257
+ /** Cumulative, merged flat, one level — `undefined` is a value, not "leave alone". */
258
+ #patch(kind, patch) {
259
+ Object.assign(this.#state[kind], patch);
260
+ return this.#run(async () => {
261
+ await this.patched(kind, patch ?? {});
262
+ });
263
+ }
264
+ /** A step, as a link in the chain — and as an `error` event if it throws. */
265
+ #run(step) {
266
+ return hydratePlay(this.#guard(step), this.#events);
267
+ }
268
+ /**
269
+ * The rule that errors reach a host **both ways**: the promise rejects *and*
270
+ * `error` is emitted. Written once here rather than at every throw site.
271
+ */
272
+ async #guard(step) {
273
+ try {
274
+ await step();
275
+ return this;
276
+ }
277
+ catch (thrown) {
278
+ const error = thrown instanceof Error ? thrown : new Error(String(thrown));
279
+ this.#emitCore('error', error);
280
+ throw error;
281
+ }
282
+ }
283
+ /**
284
+ * The core events, emitted from here rather than by a subclass.
285
+ *
286
+ * The cast is the one thing TypeScript will not check here: `emit` takes a rest
287
+ * tuple that is conditional on the detail type, and that conditional stays
288
+ * unresolved while `P` is still a type parameter. It is sound — `EventsOf<P>`
289
+ * always contains `CoreEvents<P>` by construction — and confining it to this one
290
+ * method is why the three call sites above read plainly.
291
+ */
292
+ #emitCore(type, detail) {
293
+ // Cast the channel, not the method: pulling `emit` out into a variable would call
294
+ // it unbound, and it reads its listeners off `this`.
295
+ const events = this.#events;
296
+ events.emit(type, detail);
297
+ }
298
+ }
299
+ /** Starting state: every `default` the spec declares, then whatever `init` overrides. */
300
+ function seed(spec, patch) {
301
+ const state = {};
302
+ for (const [key, property] of Object.entries(spec)) {
303
+ if (property.default !== undefined)
304
+ state[key] = property.default;
305
+ }
306
+ return Object.assign(state, patch);
307
+ }
308
+ /**
309
+ * The factory, with everything the contract prescribes already written: the
310
+ * cumulative init, the synchronous chain, the channel that outlives every session,
311
+ * and one live engine per factory.
312
+ *
313
+ * A subclass supplies its static identity and {@link EngineSDKBase.createPlay}.
314
+ */
315
+ export class EngineSDKBase {
316
+ #init;
317
+ #play;
318
+ /**
319
+ * Created before any engine exists and handed to every carrier downstream. That is
320
+ * what lets a host subscribe to an error thrown while the first session opens.
321
+ */
322
+ events;
323
+ constructor(init) {
324
+ this.#init = { ...init };
325
+ this.events = new EventEmitter(init?.on);
326
+ }
327
+ config(patch) {
328
+ if (arguments.length === 0)
329
+ return this.#pending().config();
330
+ return this.#patch('config', patch);
331
+ }
332
+ options(patch) {
333
+ if (arguments.length === 0)
334
+ return this.#pending().options();
335
+ return this.#patch('options', patch);
336
+ }
337
+ assets(patch) {
338
+ if (arguments.length === 0)
339
+ return this.#pending().assets();
340
+ return this.#patch('assets', patch);
341
+ }
342
+ storage(patch) {
343
+ if (arguments.length === 0)
344
+ return this.#pending().storage();
345
+ return this.#patch('storage', patch);
346
+ }
347
+ /** Recorded now, applied when the engine exists — same call as `new Sdk({ mount })`. */
348
+ mount(target) {
349
+ this.#init.mount = target;
350
+ this.#play?.mount(target);
351
+ return this;
352
+ }
353
+ on(type, listener) {
354
+ this.events.on(type, listener);
355
+ return this;
356
+ }
357
+ off(type, listener) {
358
+ this.events.off(type, listener);
359
+ return this;
360
+ }
361
+ /**
362
+ * The only way in. On a live engine it reopens the session rather than building a
363
+ * second one: a factory governs at most one engine.
364
+ */
365
+ start(options) {
366
+ this.#play ??= this.createPlay(this.#init, this.events);
367
+ return this.#play.restart(options);
368
+ }
369
+ /** Answers a read before there is an engine: the settings the next one will use. */
370
+ #pending() {
371
+ return this.#play ?? this.createPlay(this.#init, this.events);
372
+ }
373
+ #patch(kind, patch) {
374
+ this.#init[kind] = { ...this.#init[kind], ...patch };
375
+ // A live engine gets it too, so the factory does not silently describe a state
376
+ // the engine is not in.
377
+ if (this.#play && patch)
378
+ this.#play[kind](patch);
379
+ return this;
380
+ }
381
+ }
382
+ //# sourceMappingURL=engine-specs.js.map
@@ -1,22 +1,70 @@
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
- */
1
+ /**
2
+ * What RSDKv3 accepts, as types and as data.
3
+ *
4
+ * The four payloads of the wasm-gaming engine contract, closed for this engine, and
5
+ * {@link RSDKV3_SPEC} — the same thing as data the contract's base class reads:
6
+ * every default lives there and nowhere else.
7
+ */
8
+ import type { EnginePayloads, EngineSpec } from '@wasm-gaming/engine-specs';
9
+ /** Bytes, or a thunk that produces them only when the engine actually needs them. */
10
+ export type Rsdkv3Asset = Uint8Array | ArrayBuffer | null | (() => Uint8Array | ArrayBuffer | null | Promise<Uint8Array | ArrayBuffer | null>);
11
+ /** The engine, written into the generated `settings.ini`. Fixed for the life of the wasm. */
12
+ export interface Rsdkv3Config {
13
+ /** RSDKv3's native in-canvas Dev Menu. A host draws its own from `play.devMenu`. */
8
14
  devMenu?: boolean;
9
- /** RSDKv3 EngineDebugMode — enables the web devmenu embind bridge hooks. */
15
+ /** RSDKv3 `EngineDebugMode`. */
10
16
  engineDebugMode?: boolean;
11
- /** VSync setting in the generated settings.ini. */
12
17
  vsync?: boolean;
13
- /** 0 = JP/EU soundtrack, 1 = US soundtrack. */
14
- soundtrack?: number;
15
- /** 0 = English. Matches RSDKv3 language numeric value. */
18
+ /** Sonic CD soundtrack: 0 = JP/EU, 1 = US. */
19
+ soundtrack?: 0 | 1;
20
+ /** RSDKv3 language index (0 = English). */
16
21
  language?: number;
17
- /** Toggle touch controls in the generated settings.ini. */
18
22
  disableTouchControls?: boolean;
23
+ /** Emscripten glue URL. Defaults to `./rsdkv3.js` next to the SDK. */
24
+ jsUrl?: string;
25
+ /** WASM URL. Defaults to `./rsdkv3.wasm` next to the SDK. */
26
+ wasmUrl?: string;
27
+ }
28
+ /** The session. Nothing per-session yet. */
29
+ export type Rsdkv3Options = Record<string, never>;
30
+ export interface Rsdkv3Assets {
31
+ /** The game's `Data.rsdk` (Sonic CD). Required to start. */
32
+ data?: Rsdkv3Asset;
33
+ /** A ready-made `settings.ini`; omitted, one is generated from `config`. */
34
+ settings?: Rsdkv3Asset;
35
+ }
36
+ /** Where this engine's files live, and whether they outlive the page. */
37
+ export interface Rsdkv3Storage {
38
+ /**
39
+ * Per-game folder, relative to the engine's own: files land in `rsdkv3/<namespace>/`
40
+ * (WASMFS `/data/rsdkv3/<namespace>`, and the OPFS folder the save is mirrored to).
41
+ * Segments are cleaned (`.`/`..` dropped, odd characters → `_`); a leading
42
+ * `rsdkv3/` is stripped once. `play.storagePath` gives the result.
43
+ *
44
+ * @defaultValue `"default"`
45
+ */
46
+ namespace?: string;
47
+ /** `null` keeps nothing; otherwise `SData.bin` is mirrored to OPFS. */
48
+ persist?: 'opfs' | null;
49
+ }
50
+ export interface Rsdkv3Events {
51
+ /** `SData.bin` changed and was written to `rsdkv3/<namespace>/`. */
52
+ saves: {
53
+ files: string[];
54
+ };
55
+ }
56
+ export interface Rsdkv3Payloads extends EnginePayloads {
57
+ config: Rsdkv3Config;
58
+ options: Rsdkv3Options;
59
+ assets: Rsdkv3Assets;
60
+ storage: Rsdkv3Storage;
61
+ events: Rsdkv3Events;
19
62
  }
20
- export declare const DEFAULT_RSDKV3_OPTIONS: Required<Rsdkv3Options>;
21
- export declare const RSDKV3_OPTIONS_SCHEMA: JSONSchema;
63
+ /** The native picture, which the SDK scales to fit its mount. */
64
+ export declare const RSDKV3_VIDEO: {
65
+ readonly width: 426;
66
+ readonly height: 240;
67
+ readonly aspect: "16:9";
68
+ };
69
+ export declare const RSDKV3_SPEC: EngineSpec<Rsdkv3Payloads>;
22
70
  //# sourceMappingURL=rsdkv3.options.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"rsdkv3.options.d.ts","sourceRoot":"","sources":["../../src/rsdkv3.options.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5E,qFAAqF;AACrF,MAAM,MAAM,WAAW,GACnB,UAAU,GACV,WAAW,GACX,IAAI,GACJ,CAAC,MAAM,UAAU,GAAG,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,UAAU,GAAG,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC;AAEvF,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,oFAAoF;IACpF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gCAAgC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,4CAA4C;AAC5C,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAElD,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,oEAAoE;IACpE,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAC5B;AAED,MAAM,WAAW,cAAe,SAAQ,cAAc;IACpD,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,iEAAiE;AACjE,eAAO,MAAM,YAAY;;;;CAAuD,CAAC;AAEjF,eAAO,MAAM,WAAW,EAAE,UAAU,CAAC,cAAc,CAwClD,CAAC"}
@@ -1,45 +1,44 @@
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: {
1
+ /** The native picture, which the SDK scales to fit its mount. */
2
+ export const RSDKV3_VIDEO = { width: 426, height: 240, aspect: '16:9' };
3
+ export const RSDKV3_SPEC = {
4
+ config: {
23
5
  devMenu: {
24
- type: 'boolean',
25
6
  default: false,
26
- description: "RSDKv3's native in-canvas Dev Menu. The launcher provides its own overlay via instance.devMenu, so keep this off.",
7
+ live: false,
8
+ description: "RSDKv3's native Dev Menu; keep it off and draw your own from play.devMenu.",
27
9
  },
28
- engineDebugMode: {
29
- type: 'boolean',
30
- default: false,
31
- description: 'Enables the web devmenu embind bridge (stage list / warp / pause).',
10
+ engineDebugMode: { default: false, live: false, description: 'RSDKv3 EngineDebugMode.' },
11
+ vsync: { default: false, live: false, description: 'VSync in the generated settings.ini.' },
12
+ soundtrack: { default: 0, enum: [0, 1], live: false, description: 'Sonic CD soundtrack: 0 = JP/EU, 1 = US.' },
13
+ language: { default: 0, live: false, description: 'RSDKv3 language index (0 = English).' },
14
+ disableTouchControls: { default: true, live: false, description: 'Hide the touch controls.' },
15
+ jsUrl: { live: false, description: 'Emscripten glue URL. Defaults to ./rsdkv3.js.' },
16
+ wasmUrl: { live: false, description: 'WASM URL. Defaults to ./rsdkv3.wasm.' },
17
+ },
18
+ options: {},
19
+ assets: {
20
+ data: {
21
+ allowedTypes: ['.rsdk'],
22
+ live: false,
23
+ description: 'The RSDKv3 game data pack (Sonic CD). Required to start.',
24
+ },
25
+ settings: {
26
+ allowedTypes: ['.ini'],
27
+ live: false,
28
+ description: 'A ready-made settings.ini. Omitted, one is generated from config.',
29
+ },
30
+ },
31
+ storage: {
32
+ namespace: {
33
+ default: 'default',
34
+ live: false,
35
+ description: "Folder for this game's save, inside the engine's own rsdkv3/ folder (OPFS rsdkv3/<namespace>/).",
32
36
  },
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.',
37
+ persist: {
38
+ enum: ['opfs', null],
39
+ live: false,
40
+ description: 'null keeps nothing; otherwise SData.bin is mirrored to OPFS.',
40
41
  },
41
- language: { type: 'integer', minimum: 0, default: 0 },
42
- disableTouchControls: { type: 'boolean', default: true },
43
42
  },
44
43
  };
45
44
  //# sourceMappingURL=rsdkv3.options.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"rsdkv3.options.js","sourceRoot":"","sources":["../../src/rsdkv3.options.ts"],"names":[],"mappings":"AAwEA,iEAAiE;AACjE,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAW,CAAC;AAEjF,MAAM,CAAC,MAAM,WAAW,GAA+B;IACrD,MAAM,EAAE;QACN,OAAO,EAAE;YACP,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,KAAK;YACX,WAAW,EAAE,4EAA4E;SAC1F;QACD,eAAe,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,yBAAyB,EAAE;QACxF,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,sCAAsC,EAAE;QAC3F,UAAU,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,yCAAyC,EAAE;QAC7G,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,sCAAsC,EAAE;QAC1F,oBAAoB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,0BAA0B,EAAE;QAC7F,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,+CAA+C,EAAE;QACpF,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,sCAAsC,EAAE;KAC9E;IACD,OAAO,EAAE,EAAE;IACX,MAAM,EAAE;QACN,IAAI,EAAE;YACJ,YAAY,EAAE,CAAC,OAAO,CAAC;YACvB,IAAI,EAAE,KAAK;YACX,WAAW,EAAE,0DAA0D;SACxE;QACD,QAAQ,EAAE;YACR,YAAY,EAAE,CAAC,MAAM,CAAC;YACtB,IAAI,EAAE,KAAK;YACX,WAAW,EAAE,mEAAmE;SACjF;KACF;IACD,OAAO,EAAE;QACP,SAAS,EAAE;YACT,OAAO,EAAE,SAAS;YAClB,IAAI,EAAE,KAAK;YACX,WAAW,EAAE,iGAAiG;SAC/G;QACD,OAAO,EAAE;YACP,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC;YACpB,IAAI,EAAE,KAAK;YACX,WAAW,EAAE,8DAA8D;SAC5E;KACF;CACF,CAAC"}