@wasm-gaming/engine-specs 0.3.1 → 0.3.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 +88 -3
- package/demo/app.html +28 -6
- package/demo/demo.sdk.js +104 -322
- package/dist/engine-specs.d.ts +254 -0
- package/dist/engine-specs.d.ts.map +1 -1
- package/dist/engine-specs.js +380 -1
- package/dist/engine-specs.js.map +1 -1
- package/package.json +1 -1
- package/src/engine-specs.ts +598 -0
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@ The **engine contract** for the wasm-gaming ecosystem: one shared JS API that ev
|
|
|
4
4
|
WASM game/emulator engine implements and every host drives. Define it once here,
|
|
5
5
|
depend on it everywhere, and engines can't drift.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
The types, plus the contract implemented once so twelve engines inherit it instead of
|
|
8
|
+
re-deriving it — [src/engine-specs.ts](src/engine-specs.ts) is the whole package. No
|
|
9
|
+
runtime dependencies, because twelve engines depend on this.
|
|
9
10
|
|
|
10
11
|
## The shape
|
|
11
12
|
|
|
@@ -55,6 +56,85 @@ without asking the engine anything, and without the host knowing a single key na
|
|
|
55
56
|
`restart()`. The descriptor shape is never accepted on write: that asymmetry is what
|
|
56
57
|
keeps a write unambiguous the day a value is itself an object.
|
|
57
58
|
|
|
59
|
+
## Listening
|
|
60
|
+
|
|
61
|
+
`on` and `off` are on the same fluent surface, so they chain wherever the setters do —
|
|
62
|
+
and **registration is synchronous on every carrier**, including the promise `start()`
|
|
63
|
+
returns. The chain is what the return value continues, never what the subscription
|
|
64
|
+
waits for; otherwise `start().on('error', …)` would bind after the session opened and
|
|
65
|
+
miss an error thrown while it was opening.
|
|
66
|
+
|
|
67
|
+
A listener receives an **event**, not the payload: what it carries is `detail`, as
|
|
68
|
+
`CustomEvent` does it. The envelope is what lets an event grow a field without every
|
|
69
|
+
listener signature changing — and what survives `postMessage` intact when the engine
|
|
70
|
+
lives in a worker, which is where this ecosystem is going.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const play = await new Rsdkv4SDK({ on: { error: report } }) // in the constructor
|
|
74
|
+
.on('start', (event) => console.log('session', event.detail))
|
|
75
|
+
.assets({ pack })
|
|
76
|
+
.start();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It is deliberately **not** a DOM event: two members, frozen, no bubbling, no
|
|
80
|
+
`preventDefault()`, no `target`. Half-implementing a familiar interface is worse than
|
|
81
|
+
offering a different one.
|
|
82
|
+
|
|
83
|
+
The factory and the engine it opens are **one channel**, so subscribing before there is
|
|
84
|
+
an engine is the normal way to do it. Every engine emits `start`, `error`, `exit`,
|
|
85
|
+
`pause` and `resume`; an engine that has more of its own declares them in its payload
|
|
86
|
+
bundle and they type-check alongside the core set:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
interface Rsdkv4Payloads extends EnginePayloads {
|
|
90
|
+
// …the four payloads
|
|
91
|
+
events: { levelLoaded: { zone: string } }; // optional; omit it for the core set alone
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`error` is emitted **and** the promise rejects — both, always — so a host that does not
|
|
96
|
+
`await` is not left blind, and one that does is not forced to subscribe.
|
|
97
|
+
|
|
98
|
+
## Writing an engine
|
|
99
|
+
|
|
100
|
+
None of the above has to be implemented by hand. `EnginePlayBase` and `EngineSDKBase`
|
|
101
|
+
carry the state, the descriptors, the cumulative merge, the chainable promise and the
|
|
102
|
+
channel; an engine declares its properties as data and fills in what only it knows.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import { EnginePlayBase, EngineSDKBase } from '@wasm-gaming/engine-specs';
|
|
106
|
+
|
|
107
|
+
const spec = {
|
|
108
|
+
config: { region: { default: 'us', enum: ['us', 'eu', 'jp'], live: false } },
|
|
109
|
+
options: { slot: { default: 0, live: true } },
|
|
110
|
+
assets: { pack: { allowedTypes: ['.bin'], live: false } },
|
|
111
|
+
storage: { namespace: { default: 'rsdkv4', live: false } },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
class Rsdkv4Play extends EnginePlayBase {
|
|
115
|
+
constructor(init, events) { super(spec, init, events); }
|
|
116
|
+
async open(options) { /* start or reopen a session */ }
|
|
117
|
+
async close() { /* stop and release */ }
|
|
118
|
+
mounted(target) { /* the picture goes here */ }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class Rsdkv4SDK extends EngineSDKBase {
|
|
122
|
+
static id = 'rsdkv4';
|
|
123
|
+
static version = '1.0.0';
|
|
124
|
+
createPlay(init, events) { return new Rsdkv4Play(init, events); }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`default` is declared in the spec and **nowhere else** — the base seeds its starting
|
|
129
|
+
state from it, so the initial value and the advertised default cannot drift apart.
|
|
130
|
+
`start`, `exit` and `error` are emitted for you, including the rule that a failing hook
|
|
131
|
+
both rejects the chain and emits `error`; `this.emit(…)` is for the engine's own
|
|
132
|
+
events.
|
|
133
|
+
|
|
134
|
+
Adoption is optional: the contract is structural, so an engine with its own emitter and
|
|
135
|
+
its own state satisfies `EngineSDK` without extending anything. `EventEmitter` and
|
|
136
|
+
`hydratePlay` are exported for engines that want the pieces rather than the whole.
|
|
137
|
+
|
|
58
138
|
## Closing the types
|
|
59
139
|
|
|
60
140
|
One type parameter carries all four payloads, so there is no order to get wrong:
|
|
@@ -85,10 +165,15 @@ make preview # serve site/ on :8020 (PORT= to change it)
|
|
|
85
165
|
Two engines implement the contract in this repo, and both are type-checked in CI:
|
|
86
166
|
[tests/engine.conformance.ts](tests/engine.conformance.ts), the reference engine, and
|
|
87
167
|
[demo/demo.sdk.js](demo/demo.sdk.js), which drives the [demo
|
|
88
|
-
page](https://wasm-gaming.github.io/engine-specs/demo/).
|
|
168
|
+
page](https://wasm-gaming.github.io/engine-specs/demo/). Between them they cover both
|
|
169
|
+
branches of the event map — the reference engine declares one of its own, the demo
|
|
170
|
+
omits `events` and gets the core set alone. The negative cases live in
|
|
89
171
|
[tests/types.test-d.ts](tests/types.test-d.ts), guarded by `@ts-expect-error` — which
|
|
90
172
|
fails if a rejection ever stops happening.
|
|
91
173
|
|
|
174
|
+
The emitter is the one part with behaviour to run rather than compile, so it has
|
|
175
|
+
tests that run: [tests/event-emitter.test.mjs](tests/event-emitter.test.mjs).
|
|
176
|
+
|
|
92
177
|
## License
|
|
93
178
|
|
|
94
179
|
MIT.
|
package/demo/app.html
CHANGED
|
@@ -71,6 +71,27 @@
|
|
|
71
71
|
calls = [line, ...calls].slice(0, 40);
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The only place the factory is built, so the page subscribes exactly once — and
|
|
76
|
+
* does it in the constructor, which is the same call as `.on(type, listener)`.
|
|
77
|
+
*
|
|
78
|
+
* The handlers outlive every engine this factory opens: `start` fires again on each
|
|
79
|
+
* `restart()`, `exit` on `destroy()`, and `pause`/`resume` come from methods the
|
|
80
|
+
* contract does not know about. Nothing below re-subscribes.
|
|
81
|
+
*
|
|
82
|
+
* Each one receives an event, not the payload: what it carries is `detail`.
|
|
83
|
+
*/
|
|
84
|
+
const engine = () =>
|
|
85
|
+
(sdk ??= new DemoEngineSDK({
|
|
86
|
+
on: {
|
|
87
|
+
start: ({ detail }) => say(`▸ start fileName: ${detail.fileName ?? 'unset'}`),
|
|
88
|
+
pause: ({ detail }) => say(`▸ pause owner: ${detail.owner}`),
|
|
89
|
+
resume: ({ detail }) => say(`▸ resume owner: ${detail.owner}`),
|
|
90
|
+
error: ({ detail }) => say(`▸ error ${detail.message}`),
|
|
91
|
+
exit: ({ type }) => say(`▸ ${type}`),
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
|
|
74
95
|
const show = (value) => {
|
|
75
96
|
if (value === undefined) return 'unset';
|
|
76
97
|
if (value instanceof Uint8Array) return `${value.byteLength} bytes`;
|
|
@@ -92,7 +113,7 @@
|
|
|
92
113
|
|
|
93
114
|
/** Reads the live engine — or the factory, when there is not one yet. */
|
|
94
115
|
const refresh = async () => {
|
|
95
|
-
const source = play ?? (
|
|
116
|
+
const source = play ?? engine();
|
|
96
117
|
const [config, options, assetProps, storage] = await Promise.all([
|
|
97
118
|
source.config(),
|
|
98
119
|
source.options(),
|
|
@@ -121,7 +142,7 @@
|
|
|
121
142
|
const onSelect = async ({ key, file }) => {
|
|
122
143
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
123
144
|
picked[key] = file.name;
|
|
124
|
-
|
|
145
|
+
engine();
|
|
125
146
|
say(`sdk.assets({ ${key}: Uint8Array(${bytes.byteLength}) })`);
|
|
126
147
|
sdk.assets({ [key]: bytes });
|
|
127
148
|
if (key === 'rom') {
|
|
@@ -139,7 +160,7 @@
|
|
|
139
160
|
};
|
|
140
161
|
|
|
141
162
|
const onStart = async () => {
|
|
142
|
-
|
|
163
|
+
engine();
|
|
143
164
|
say('play = await sdk.mount(stage).start()');
|
|
144
165
|
// The whole chain in one expression, exactly as the contract advertises it.
|
|
145
166
|
play = await sdk.mount(document.querySelector('#stage')).start();
|
|
@@ -152,15 +173,16 @@
|
|
|
152
173
|
await refresh();
|
|
153
174
|
};
|
|
154
175
|
|
|
155
|
-
//
|
|
176
|
+
// The methods are this engine's own; the `pause`/`resume` they emit are the
|
|
177
|
+
// contract's — which is why the log shows a ▸ line the page never wrote here.
|
|
156
178
|
const onPause = async () => {
|
|
157
|
-
say('play.pause() // engine-specific,
|
|
179
|
+
say('play.pause() // engine-specific method, contract event');
|
|
158
180
|
play.pause();
|
|
159
181
|
await refresh();
|
|
160
182
|
};
|
|
161
183
|
|
|
162
184
|
const onResume = async () => {
|
|
163
|
-
say('play.resume() // engine-specific,
|
|
185
|
+
say('play.resume() // engine-specific method, contract event');
|
|
164
186
|
play.resume();
|
|
165
187
|
await refresh();
|
|
166
188
|
};
|