@cyberart-io/engine 0.0.2 → 0.0.4
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 +200 -9
- package/dist/headless.d.ts +805 -0
- package/dist/headless.js +8 -0
- package/dist/index.d.ts +832 -64
- package/dist/index.js +1 -1
- package/docs/asset-resolver.md +155 -0
- package/docs/capability-manifest.md +84 -0
- package/docs/events.md +20 -8
- package/docs/headless-harness.md +58 -16
- package/docs/normalized-geometry.md +88 -0
- package/docs/presentation-adapter.md +150 -0
- package/docs/presentation-cue.md +82 -0
- package/docs/runtime-group.md +120 -0
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ This guide is the path from `npm i` to a piece on screen, then a short map of th
|
|
|
24
24
|
npm i @cyberart-io/engine
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The package is minified JS plus
|
|
27
|
+
The package is minified JS plus rolled-up `.d.ts` for two entrypoints: the browser runtime (`.`) and Node/jsdom helpers (`./headless`). It does not include animation carts. Tone.js is an optional peer — skip it until a cart declares `audio: 'tone'`.
|
|
28
28
|
|
|
29
29
|
## Quick start
|
|
30
30
|
|
|
@@ -87,9 +87,15 @@ window.addEventListener('pagehide', () => {
|
|
|
87
87
|
|
|
88
88
|
Full API for the event router, deterministic replay, and CI harness (so agents can use every export):
|
|
89
89
|
|
|
90
|
-
- [Events and router](docs/events.md) — mailbox, envelope, `createEventRouter`, hops, idempotency, rejections
|
|
90
|
+
- [Events and router](docs/events.md) — mailbox, envelope, typed contracts, `createEventRouter`, hops, idempotency, rejections
|
|
91
91
|
- [Deterministic mode](docs/deterministic-mode.md) — `step` / `schedule`, clocks, `ScriptedAction`, replay diffs
|
|
92
|
-
- [Headless harness](docs/headless-harness.md) — `createHeadlessHarness`, jsdom canvas, inspect / screenshot
|
|
92
|
+
- [Headless harness](docs/headless-harness.md) — `createHeadlessHarness` from `@cyberart-io/engine/headless`, jsdom canvas, inspect / screenshot
|
|
93
|
+
- [Presentation adapter](docs/presentation-adapter.md) — host-owned render model, intents, loading / error / unsupported
|
|
94
|
+
- [Asset resolver](docs/asset-resolver.md) — host-pluggable images/audio/fonts/spritesheets, cache, preload, fallbacks
|
|
95
|
+
- [Presentation cue](docs/presentation-cue.md) — deterministic `createPresentationTimeline`, duplicate policy, reduced-motion, lifecycle events
|
|
96
|
+
- [Capability manifest](docs/capability-manifest.md) — versioned JSON for runtime features, phases, managers, assets, events, permissions, integrations
|
|
97
|
+
- [Normalized geometry](docs/normalized-geometry.md) — coordinate spaces, contain/cover/crop layout, landmarks, hit regions, debug overlay
|
|
98
|
+
- [Runtime group](docs/runtime-group.md) — `createRuntimeGroup`, shared router attach, lockstep clock; `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless`
|
|
93
99
|
|
|
94
100
|
## Write a cart
|
|
95
101
|
|
|
@@ -130,6 +136,10 @@ const cart = runtime.mount(artProject, {
|
|
|
130
136
|
});
|
|
131
137
|
```
|
|
132
138
|
|
|
139
|
+
`runtime.hostChannel` is the mailbox for this instance. Attach it to `createEventRouter` from the host; carts never receive the router.
|
|
140
|
+
|
|
141
|
+
`runtime.assets` is the preloader when `createRuntime({ assets: { resolver } })` was set. Logical refs and fallbacks: [asset resolver](docs/asset-resolver.md).
|
|
142
|
+
|
|
133
143
|
`createRuntime` options:
|
|
134
144
|
|
|
135
145
|
| Option | Default | Meaning |
|
|
@@ -139,6 +149,7 @@ const cart = runtime.mount(artProject, {
|
|
|
139
149
|
| `captureKeyboard` | `false` | When true, the cart listens for `window` keydown. Full-page players pass `true`. Off in deterministic mode. |
|
|
140
150
|
| `audio` | none | Libraries to unlock if `unlockAudio()` runs before `mount`. After mount, the cart’s `metadata.audio` wins. |
|
|
141
151
|
| `deterministic` | off | Host-controlled clock, `step`/`advance`, and scripted input/assets. Leave unset for live kaleidoscope / Art Blocks. |
|
|
152
|
+
| `assets` | off | Host `AssetResolver` plus engine cache/preload. Carts keep logical refs. Leave unset when the piece has no media. |
|
|
142
153
|
|
|
143
154
|
`CartHandle` (what `mount` returns):
|
|
144
155
|
|
|
@@ -162,7 +173,7 @@ const cart = runtime.mount(artProject, {
|
|
|
162
173
|
|
|
163
174
|
`runtime.destroy()` tears down the runtime. `runtime.unlockAudio()` is for an early click before `mount`. `runtime.onError` receives frame errors (`phase`, `consecutive`, `stopped`).
|
|
164
175
|
|
|
165
|
-
One cart per runtime. A second `mount` unloads the first. Several pieces on a page means several `createRuntime()` calls.
|
|
176
|
+
One cart per runtime. A second `mount` unloads the first. Several pieces on a page means several `createRuntime()` calls, or one `createRuntimeGroup()` that attaches each mailbox to a shared router and locksteps a deterministic clock.
|
|
166
177
|
|
|
167
178
|
## Audio
|
|
168
179
|
|
|
@@ -228,12 +239,17 @@ const replay = await cart.getReplayMetadata();
|
|
|
228
239
|
|
|
229
240
|
## Headless harness
|
|
230
241
|
|
|
231
|
-
CI and agents should drive the **same** `createRuntime({ deterministic })` path.
|
|
242
|
+
CI and agents should drive the **same** `createRuntime({ deterministic })` path. Import the harness from **`@cyberart-io/engine/headless`** so browser production builds never walk `node:fs/promises`.
|
|
243
|
+
|
|
244
|
+
- Carts and production hosts: `import { createRuntime } from '@cyberart-io/engine'`
|
|
245
|
+
- Tests, CI, and frame capture: `import { createHeadlessHarness, createHeadlessMultiCartHarness } from '@cyberart-io/engine/headless'`
|
|
246
|
+
|
|
247
|
+
`@cyberart-io/engine` does not re-export the harness. `installHeadlessCanvas` is the documented jsdom install (test-only). `createHeadlessHarness` sizes a container, mounts, and wraps step / input / inspect / snapshot.
|
|
232
248
|
|
|
233
249
|
Full options, `click` clock rule, Node-only `captureFrame`, remount, and the reproduce command: [headless harness](docs/headless-harness.md).
|
|
234
250
|
|
|
235
251
|
```ts
|
|
236
|
-
import { createHeadlessHarness } from '@cyberart-io/engine';
|
|
252
|
+
import { createHeadlessHarness } from '@cyberart-io/engine/headless';
|
|
237
253
|
|
|
238
254
|
const harness = createHeadlessHarness({ cart: artProject, seed: 42 });
|
|
239
255
|
await harness.step(5);
|
|
@@ -245,7 +261,7 @@ harness.destroy();
|
|
|
245
261
|
|
|
246
262
|
## Events
|
|
247
263
|
|
|
248
|
-
Default path: per-runtime **mailbox** (`dispatch` / `consume` / `emit`). Multi-cart hosts attach each `HostChannel` to `createEventRouter` — carts never see the router.
|
|
264
|
+
Default path: per-runtime **mailbox** (`dispatch` / `consume` / `emit`). Multi-cart hosts should prefer `createRuntimeGroup()` (it calls `router.attach` for each mailbox). You can still attach each `HostChannel` to `createEventRouter` yourself — carts never see the router.
|
|
249
265
|
|
|
250
266
|
```ts
|
|
251
267
|
const cart = runtime.mount(artProject, {
|
|
@@ -258,7 +274,182 @@ const cart = runtime.mount(artProject, {
|
|
|
258
274
|
cart.dispatch({ type: 'art-project.theme', payload: 'dusk' });
|
|
259
275
|
```
|
|
260
276
|
|
|
261
|
-
Envelope, `attach` / `detach` / `publish` / `turn`, hops, idempotency, budgets, and rejection reasons: [events and router](docs/events.md).
|
|
277
|
+
Envelope, contracts (`defineIntent` / `defineStateEvent` / `defineDiagnostic`), `attach` / `detach` / `publish` / `turn`, hops, idempotency, budgets, and rejection reasons: [events and router](docs/events.md).
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import {
|
|
281
|
+
defineIntent,
|
|
282
|
+
defineStateEvent,
|
|
283
|
+
createContractRegistry,
|
|
284
|
+
deriveAttachOptions,
|
|
285
|
+
createEventRouter,
|
|
286
|
+
} from '@cyberart-io/engine';
|
|
287
|
+
|
|
288
|
+
const exit = defineIntent('adventure.intent.exit-requested', {
|
|
289
|
+
version: 1,
|
|
290
|
+
fields: { exitId: { type: 'string' } },
|
|
291
|
+
});
|
|
292
|
+
const room = defineStateEvent('adventure.state.room-changed', {
|
|
293
|
+
version: 1,
|
|
294
|
+
fields: { roomId: { type: 'string' } },
|
|
295
|
+
});
|
|
296
|
+
if (!exit.ok || !room.ok) throw new Error('contracts');
|
|
297
|
+
const contracts = [exit.contract, room.contract];
|
|
298
|
+
const registry = createContractRegistry(contracts);
|
|
299
|
+
const router = createEventRouter({
|
|
300
|
+
validate: registry.asRouterValidate,
|
|
301
|
+
});
|
|
302
|
+
router.attach('presentation', runtime.hostChannel, deriveAttachOptions(contracts, 'cart'));
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Capability manifest
|
|
306
|
+
|
|
307
|
+
JSON for what a cart needs from the host. `validateCapabilityManifest` returns structured diagnostics instead of throwing.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import {
|
|
311
|
+
defineCapabilityManifest,
|
|
312
|
+
validateCapabilityManifest,
|
|
313
|
+
} from '@cyberart-io/engine';
|
|
314
|
+
|
|
315
|
+
const defined = defineCapabilityManifest({
|
|
316
|
+
id: 'adventure.presentation',
|
|
317
|
+
runtime: { minContractVersion: 1, features: ['router'] },
|
|
318
|
+
phases: ['loading', 'ready', 'error', 'unsupported'],
|
|
319
|
+
managers: ['pointer', 'hostChannel'],
|
|
320
|
+
assets: { kinds: ['image'], declarations: [{ id: 'room-bg', kind: 'image' }] },
|
|
321
|
+
acceptedEvents: ['adventure.state.*'],
|
|
322
|
+
emittedEvents: ['adventure.intent.exit-requested'],
|
|
323
|
+
permissions: { emit: ['adventure.intent.*'], subscribe: ['adventure.state.*'] },
|
|
324
|
+
integrations: ['tone'],
|
|
325
|
+
});
|
|
326
|
+
if (!defined.ok) throw new Error(defined.errors.map((e) => e.detail).join('; '));
|
|
327
|
+
validateCapabilityManifest(defined.manifest, {
|
|
328
|
+
contractVersion: 1,
|
|
329
|
+
features: ['router'],
|
|
330
|
+
integrations: ['tone'],
|
|
331
|
+
emit: ['adventure.intent.*'],
|
|
332
|
+
subscribe: ['adventure.state.*'],
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Fields, host allowlists, and diagnostic codes: [capability manifest](docs/capability-manifest.md).
|
|
337
|
+
|
|
338
|
+
## Runtime group
|
|
339
|
+
|
|
340
|
+
Several production carts, one router, one lockstep clock. Use this instead of intercepting each `onEvent` and republishing by hand.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { createRuntimeGroup } from '@cyberart-io/engine';
|
|
344
|
+
|
|
345
|
+
const group = createRuntimeGroup({
|
|
346
|
+
origin: 0,
|
|
347
|
+
participants: [
|
|
348
|
+
{ id: 'effects', cart: effectsCart, emit: ['ambience.intent.*'], subscribe: ['adventure.state.*'] },
|
|
349
|
+
{ id: 'ambience', cart: ambienceCart, subscribe: ['ambience.intent.*'] },
|
|
350
|
+
],
|
|
351
|
+
});
|
|
352
|
+
group.publish({ type: 'adventure.state.loon-whistle', kind: 'state', payload: { habitat: 'pond' } });
|
|
353
|
+
await group.step(2);
|
|
354
|
+
const { trace } = await group.inspect();
|
|
355
|
+
group.destroy();
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Headless / CI: `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless` is the same handle after `installHeadlessCanvas()`. Full options: [runtime group](docs/runtime-group.md).
|
|
359
|
+
|
|
360
|
+
## Normalized geometry
|
|
361
|
+
|
|
362
|
+
Shared 0–1 content-box coordinates, landmarks, and hit regions. Pixel `PresentationRegion` on the presentation adapter is unchanged.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
import {
|
|
366
|
+
createPresentationLayout,
|
|
367
|
+
pointerToRegion,
|
|
368
|
+
ROUND_TRIP_TOLERANCE,
|
|
369
|
+
} from '@cyberart-io/engine';
|
|
370
|
+
|
|
371
|
+
const layout = createPresentationLayout({
|
|
372
|
+
contentWidth: 1920,
|
|
373
|
+
contentHeight: 1080,
|
|
374
|
+
viewportWidth: 1280,
|
|
375
|
+
viewportHeight: 800,
|
|
376
|
+
mode: 'contain',
|
|
377
|
+
});
|
|
378
|
+
const hit = pointerToRegion({ x: canvasX, y: canvasY }, layout, doc);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Spaces, contain/cover/crop, debug overlay, and the reproduce command: [normalized geometry](docs/normalized-geometry.md).
|
|
382
|
+
|
|
383
|
+
## Presentation adapter
|
|
384
|
+
|
|
385
|
+
Host-owned render model in, interaction intents out. Cyberart does not mutate the canonical world. `gameManager` is not this contract.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import {
|
|
389
|
+
attachPresentationAdapter,
|
|
390
|
+
createReferencePresentationCart,
|
|
391
|
+
} from '@cyberart-io/engine';
|
|
392
|
+
import { createHeadlessHarness } from '@cyberart-io/engine/headless';
|
|
393
|
+
|
|
394
|
+
const harness = createHeadlessHarness({
|
|
395
|
+
cart: createReferencePresentationCart(),
|
|
396
|
+
seed: 42,
|
|
397
|
+
onEvent: (event) => hostReduce(event),
|
|
398
|
+
});
|
|
399
|
+
const adapter = attachPresentationAdapter(harness);
|
|
400
|
+
adapter.present({
|
|
401
|
+
contractVersion: 1,
|
|
402
|
+
phase: 'ready',
|
|
403
|
+
view: { title: 'Joiner Brook', regions: [] },
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`start()` maps to `cart.start()`. Phases: `loading` / `ready` / `error` / `unsupported`. Full contract, router attach (`runtime.hostChannel`), and the reproduce command: [presentation adapter](docs/presentation-adapter.md).
|
|
408
|
+
|
|
409
|
+
## Assets
|
|
410
|
+
|
|
411
|
+
Hosts resolve logical refs (Library IDs, Moltazine posts, `world:asset/…`, ordinary URLs). The engine caches, dedupes, reports progress, and applies timeouts / CORS-shaped failures / silent fallbacks. Policy stays out of authored cart code.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
import {
|
|
415
|
+
createRuntime,
|
|
416
|
+
createFixtureAssetResolver,
|
|
417
|
+
createHostedAssetResolver,
|
|
418
|
+
} from '@cyberart-io/engine';
|
|
419
|
+
|
|
420
|
+
const roomAssets = [
|
|
421
|
+
{ id: 'room-bg', ref: 'moltazine:post/porch-1#primary-image', type: 'image' as const },
|
|
422
|
+
{
|
|
423
|
+
id: 'ambience',
|
|
424
|
+
ref: 'world:asset/stream-loop',
|
|
425
|
+
type: 'audio' as const,
|
|
426
|
+
fallback: 'silent' as const,
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
const local = createRuntime({
|
|
431
|
+
container,
|
|
432
|
+
assets: {
|
|
433
|
+
resolver: createFixtureAssetResolver({
|
|
434
|
+
'moltazine:post/porch-1#primary-image': { url: 'fixture://images/porch.png' },
|
|
435
|
+
'world:asset/stream-loop': { url: 'fixture://audio/stream-loop.ogg' },
|
|
436
|
+
}),
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
await local.assets!.preload(roomAssets);
|
|
440
|
+
|
|
441
|
+
const hosted = createRuntime({
|
|
442
|
+
container,
|
|
443
|
+
assets: {
|
|
444
|
+
resolver: createHostedAssetResolver({
|
|
445
|
+
cdnBase: 'https://cdn.example/library/',
|
|
446
|
+
failures: { 'world:asset/stream-loop': 'cors' },
|
|
447
|
+
}),
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
The same declarations produce fixture URLs locally and CDN URLs (plus a typed CORS failure and silent audio fallback) when hosted. Live preload dispatches `ASSET_READY_EVENT` / `ASSET_FAILED_EVENT`. Deterministic mode leaves those events to scripted `{ type: 'asset' }` actions so tests do not wait on wall-clock fetch. Full API and the reproduce command: [asset resolver](docs/asset-resolver.md).
|
|
262
453
|
|
|
263
454
|
## Save and load
|
|
264
455
|
|
|
@@ -333,7 +524,7 @@ A canvas the host adopted is left in place on destroy; a canvas the engine creat
|
|
|
333
524
|
|
|
334
525
|
## Publishing this package (maintainers)
|
|
335
526
|
|
|
336
|
-
Not part of writing a cart. Engine source lives in `packages/engine/src/` (not mixed into the site). The npm tarball is built from `packages/engine/src/index.ts` and contains minified `dist/index.js`, rolled-up
|
|
527
|
+
Not part of writing a cart. Engine source lives in `packages/engine/src/` (not mixed into the site). The npm tarball is built from `packages/engine/src/index.ts` and `packages/engine/src/headless.ts` and contains minified `dist/index.js` + `dist/headless.js`, rolled-up `.d.ts` for both, `LICENSE`, `README.md`, `docs/` (event / deterministic / harness / presentation-adapter / asset-resolver / presentation-cue API), and `package.json`. Site code that still imports `src/ui/lib/...` hits thin re-export shims so those paths keep working.
|
|
337
528
|
|
|
338
529
|
```bash
|
|
339
530
|
pnpm run pack:engine
|