@cyberart-io/engine 0.0.3 → 0.0.5
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 +96 -12
- package/dist/headless.d.ts +602 -4
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +835 -7
- package/dist/index.js +1 -1
- package/docs/asset-resolver.md +4 -4
- package/docs/browser-harness.md +126 -0
- package/docs/capability-manifest.md +95 -0
- package/docs/compositor.md +103 -0
- package/docs/events.md +27 -22
- package/docs/headless-harness.md +46 -16
- package/docs/normalized-geometry.md +88 -0
- package/docs/presentation-adapter.md +10 -10
- package/docs/presentation-cue.md +1 -1
- package/docs/replay-inspector.md +88 -0
- package/docs/runtime-group.md +122 -0
- package/package.json +2 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Normalized geometry
|
|
2
|
+
|
|
3
|
+
Shared 0–1 content-box coordinates, anchors, hit regions, and resize-stable
|
|
4
|
+
contain/cover/crop layouts. Pixel `PresentationRegion` on the presentation
|
|
5
|
+
adapter is unchanged; hosts can adopt this contract later.
|
|
6
|
+
|
|
7
|
+
Back to the [package README](../README.md).
|
|
8
|
+
|
|
9
|
+
## One-command reproduce (this repo)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm exec vitest run packages/engine/src/canvas/cyb-60-normalized-geometry.repro.spec.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Contract (`GEOMETRY_CONTRACT_VERSION = 1`)
|
|
16
|
+
|
|
17
|
+
| Field | Meaning |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `version` | Always `1`. Other versions fail `validateGeometry`. |
|
|
20
|
+
| `landmarks` | Named anchors (`id` + normalized point + optional origin keyword). |
|
|
21
|
+
| `regions` | Hitboxes: axis-aligned `rect` and/or `polygon` in normalized space. |
|
|
22
|
+
| `padding` | Normalized insets `{ top, right, bottom, left }`. |
|
|
23
|
+
| `safeArea` | Same inset shape; authoring hint, not applied by the layout math. |
|
|
24
|
+
|
|
25
|
+
Documents are JSON-serializable (`JSON.parse(JSON.stringify(doc))` round-trips). Import from **`@cyberart-io/engine`** (`createPresentationLayout`, `pointerToRegion`, `serializeGeometry`, …).
|
|
26
|
+
|
|
27
|
+
If a region sets both `rect` and `polygon`, hit-testing uses the polygon and overlay/AABB helpers use the rect. Prefer one shape per region.
|
|
28
|
+
|
|
29
|
+
## Coordinate spaces
|
|
30
|
+
|
|
31
|
+
Normalized is **0–1 of the content box** (intrinsic artwork), not the letterboxed viewport.
|
|
32
|
+
|
|
33
|
+
| Space | Unit | Box |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `normalized` | 0–1 | Full content (`contentWidth` × `contentHeight`) |
|
|
36
|
+
| `asset` | px | Same content, intrinsic pixels |
|
|
37
|
+
| `css` / `viewport` | CSS px | Layout (`viewportWidth` × `viewportHeight`) |
|
|
38
|
+
| `canvas` | buffer px | `css * devicePixelRatio` (default dpr `1`) |
|
|
39
|
+
|
|
40
|
+
`pointerToRegion` defaults to **canvas** space (same as `PointerManager` / harness `click`). Pass `{ space: 'css' }` for layout pixels.
|
|
41
|
+
|
|
42
|
+
## Layout (`createPresentationLayout`)
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createPresentationLayout } from '@cyberart-io/engine';
|
|
46
|
+
|
|
47
|
+
const layout = createPresentationLayout({
|
|
48
|
+
contentWidth: 1920,
|
|
49
|
+
contentHeight: 1080,
|
|
50
|
+
viewportWidth: 1280,
|
|
51
|
+
viewportHeight: 800,
|
|
52
|
+
mode: 'contain', // or 'cover' | 'crop'
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Mode | Scale | Offsets |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `contain` | `min(vw/cw, vh/ch)` | Letterbox ≥ 0 |
|
|
59
|
+
| `cover` | `max(vw/cw, vh/ch)` | Crop ≤ 0 (centered) |
|
|
60
|
+
| `crop` | Same as `cover` | Same as `cover` |
|
|
61
|
+
|
|
62
|
+
Resize-stable under **contain**: a normalized point stays on the same content location when only the viewport changes. Cover/crop keep the content centered and report `visibleNormalizedRect` for the uncropped slice.
|
|
63
|
+
|
|
64
|
+
## Round-trip tolerance
|
|
65
|
+
|
|
66
|
+
| Constant | Value | Use |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `ROUND_TRIP_TOLERANCE` | `1e-6` | Max \|Δ\| in normalized units after canvas/css round-trip |
|
|
69
|
+
| `ROUND_TRIP_TOLERANCE_CANVAS_PX` | `0.5` | Max \|Δ\| in canvas pixels after normalized round-trip |
|
|
70
|
+
|
|
71
|
+
## Helpers
|
|
72
|
+
|
|
73
|
+
| Export | Role |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `normalizedToCanvas` / `canvasToNormalized` | Content box ↔ drawing buffer |
|
|
76
|
+
| `normalizedToCss` / `cssToNormalized` | Content box ↔ layout pixels |
|
|
77
|
+
| `normalizedToAsset` / `assetToNormalized` | Content box ↔ intrinsic pixels |
|
|
78
|
+
| `pointerToRegion(pointer, layout, doc)` | First matching region (document order) |
|
|
79
|
+
| `regionToViewport(region, layout)` | CSS rect (polygon AABB) |
|
|
80
|
+
| `createLandmarkRegistry()` | `register` / `get` / `list`; duplicate ids → `{ ok: false, error: 'duplicate-id' }` |
|
|
81
|
+
| `validateGeometry(doc)` | Overlap, out-of-bounds, duplicate id, invalid shape; diagnostics include region ids |
|
|
82
|
+
| `drawGeometryDebug(ctx, layout, doc, diagnostics)` | Canvas2D `fillRect` / `strokeRect` / `fillText` when present; always returns recorded calls with region ids |
|
|
83
|
+
|
|
84
|
+
Origin keywords: `center`, `top-left`, `top-right`, `bottom-left`, `bottom-right`, `top`, `bottom`, `left`, `right`. `pointFromOrigin` yields the matching unit-square point.
|
|
85
|
+
|
|
86
|
+
## Fixture (CYB-60)
|
|
87
|
+
|
|
88
|
+
Content **1920×1080**. Hotspot point `(0.25, 0.4)` inside region `hotspot` `{ x: 0.2, y: 0.35, width: 0.1, height: 0.1 }`. Contain viewports **1280×800** and **390×844**.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Presentation adapter
|
|
2
2
|
|
|
3
|
-
Host-owned render models in, interaction intents out. Cyberart is not authoritative for game rules. Depends on [events](events.md). CI: [headless harness](headless-harness.md).
|
|
3
|
+
Host-owned render models in, interaction intents out. Cyberart is not authoritative for game rules. Depends on [events](events.md). CI: [headless harness](headless-harness.md). Normalized 0–1 regions: [normalized geometry](normalized-geometry.md).
|
|
4
4
|
|
|
5
5
|
Back to the [package README](../README.md).
|
|
6
6
|
|
|
@@ -46,17 +46,17 @@ const model: PresentationModel = {
|
|
|
46
46
|
reason: undefined, // string when not ready
|
|
47
47
|
view: {
|
|
48
48
|
background: '#1b3a4a',
|
|
49
|
-
title: '
|
|
49
|
+
title: 'Overlook',
|
|
50
50
|
regions: [
|
|
51
51
|
{
|
|
52
|
-
id: 'north
|
|
52
|
+
id: 'region-north',
|
|
53
53
|
x: 0,
|
|
54
54
|
y: 0,
|
|
55
55
|
width: 320,
|
|
56
56
|
height: 45,
|
|
57
57
|
intent: {
|
|
58
|
-
type: '
|
|
59
|
-
payload: { exitId: '
|
|
58
|
+
type: 'host.intent.exit-requested',
|
|
59
|
+
payload: { exitId: 'beta' },
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
62
|
],
|
|
@@ -64,7 +64,7 @@ const model: PresentationModel = {
|
|
|
64
64
|
};
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`view` is host-owned. The reference cart hit-tests `regions` and paints `background`. `title` is inspectable state, not drawn. Extra fields (characters, exits, inventory) are ignored — map clickable things onto `regions`. Coordinates are drawing-space pixels, the same space as `PointerManager` and harness `click`.
|
|
67
|
+
`view` is host-owned. The reference cart hit-tests `regions` and paints `background`. `title` is inspectable state, not drawn. Extra fields (characters, exits, inventory) are ignored — map clickable things onto `regions`. Coordinates on `PresentationRegion` are drawing-space pixels, the same space as `PointerManager` and harness `click`. Convert 0–1 content-box geometry with `createPresentationLayout` / `pointerToRegion` from [normalized geometry](normalized-geometry.md).
|
|
68
68
|
|
|
69
69
|
`adapter.model` / `adapter.phase` are what this session last presented (or the boot model). The getter returns a **copy**. After `step`, `inspect().state.model` is what the cart applied.
|
|
70
70
|
|
|
@@ -81,9 +81,9 @@ The reference cart never writes the host’s world. An unknown `contractVersion`
|
|
|
81
81
|
|
|
82
82
|
### Intents
|
|
83
83
|
|
|
84
|
-
Region `intent.type` should be routable: `*.intent.*` (for example `
|
|
84
|
+
Region `intent.type` should be routable: `*.intent.*` (for example `host.intent.exit-requested`). The reference cart always sets `kind: 'intent'` on clicks (a hotspot cannot emit `state`).
|
|
85
85
|
|
|
86
|
-
Default router `emit` is `['*.intent.*']`, which does **not** match `room.exit.requested`. Prefer `
|
|
86
|
+
Default router `emit` is `['*.intent.*']`, which does **not** match `room.exit.requested`. Prefer `host.intent.*`.
|
|
87
87
|
|
|
88
88
|
## Reference cart + session
|
|
89
89
|
|
|
@@ -121,7 +121,7 @@ const adapter = mountPresentationAdapter(runtime, {
|
|
|
121
121
|
await adapter.start();
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
A host with a custom view shape passes `cart:` into `mountPresentationAdapter` and still `present()`s the same event. Do not import
|
|
124
|
+
A host with a custom view shape passes `cart:` into `mountPresentationAdapter` and still `present()`s the same event. Do not import the host types into engine carts.
|
|
125
125
|
|
|
126
126
|
## Router
|
|
127
127
|
|
|
@@ -131,7 +131,7 @@ router.attach('presentation', runtime.hostChannel, {
|
|
|
131
131
|
subscribe: [...PRESENTATION_SUBSCRIBE_PATTERNS],
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
router.subscribe(['
|
|
134
|
+
router.subscribe(['host.intent.*', 'presentation.intent.unsupported'], (event) => {
|
|
135
135
|
const next = reduce(world, event); // host validates; ignore invalid intents
|
|
136
136
|
if (next) {
|
|
137
137
|
// Do not pass `{ cause: event }` — a second `presentation.state.model`
|
package/docs/presentation-cue.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Presentation cue / timeline
|
|
2
2
|
|
|
3
|
-
Deterministic cue/effect primitive. Carts call `step(frames)` with the same clock they use for
|
|
3
|
+
Deterministic cue/effect primitive. Carts call `step(frames)` with the same clock they use for deterministic mode; there are no `setTimeout` / rAF timers. Overlay pulses, scene transitions, and NPC beats should share this lifecycle instead of ad-hoc frame counters.
|
|
4
4
|
|
|
5
5
|
Back to the [package README](../README.md). Related: [deterministic mode](deterministic-mode.md), [events](events.md) (routed contracts vs local `cue.*` names).
|
|
6
6
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Replay inspector
|
|
2
|
+
|
|
3
|
+
Embeddable debug API for multi-cart routing. It records router decisions, builds a causation tree, redacts configured payload keys, bounds retention, and exports a JSON tape that the headless harness can replay. There is no Player UI.
|
|
4
|
+
|
|
5
|
+
Related: [events](events.md), [runtime group](runtime-group.md), [headless harness](headless-harness.md), [deterministic mode](deterministic-mode.md).
|
|
6
|
+
|
|
7
|
+
Back to the [package README](../README.md).
|
|
8
|
+
|
|
9
|
+
## One-command reproduce (this repo)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm exec vitest run packages/engine/src/canvas/cyb-65-replay-inspector.repro.spec.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
| Surface | Import | Use |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Production host | `createReplayInspector` from `@cyberart-io/engine` | Attach to `createEventRouter` or `createRuntimeGroup`. |
|
|
20
|
+
| Vitest / CI | same API, or from `@cyberart-io/engine/headless` | `replayExportedTrace` against `createHeadlessMultiCartHarness`. |
|
|
21
|
+
|
|
22
|
+
Hosts attach this inspector to a router or runtime group. Event types stay host-defined.
|
|
23
|
+
|
|
24
|
+
## `createReplayInspector(options?)`
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createReplayInspector } from '@cyberart-io/engine';
|
|
28
|
+
import { createHeadlessMultiCartHarness } from '@cyberart-io/engine/headless';
|
|
29
|
+
|
|
30
|
+
const group = createHeadlessMultiCartHarness({
|
|
31
|
+
origin: 0,
|
|
32
|
+
participants: [
|
|
33
|
+
{ id: 'effects', cart: effectsCart, emit: ['overlay.intent.*'], subscribe: ['host.state.*'] },
|
|
34
|
+
{ id: 'ambience', cart: ambienceCart, subscribe: ['overlay.intent.*'] },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const inspector = createReplayInspector({
|
|
39
|
+
redactedKeys: ['token'],
|
|
40
|
+
maxRecords: 512,
|
|
41
|
+
});
|
|
42
|
+
const session = inspector.bind(group);
|
|
43
|
+
session.publish({ type: 'host.state.accepted', kind: 'state', payload: { token: 'secret' } });
|
|
44
|
+
await session.step(2);
|
|
45
|
+
const exported = await session.exportTrace();
|
|
46
|
+
inspector.destroy();
|
|
47
|
+
group.destroy();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`host.state.accepted` delivered to `effects` causes `overlay.intent.play` for `ambience`. The inspector tree is that chain plus any later rejects.
|
|
51
|
+
|
|
52
|
+
### Options (`CreateReplayInspectorOptions`)
|
|
53
|
+
|
|
54
|
+
| Option | Default | Meaning |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `redactedKeys` | `[]` | Object keys replaced with `REDACTED_VALUE` in payloads and cart state summaries. |
|
|
57
|
+
| `maxRecords` | `512` | Oldest records drop; `report().dropped` counts them. |
|
|
58
|
+
| `registry` | none | `ContractRegistry.get` annotates `payloadSchemaVersion`. |
|
|
59
|
+
|
|
60
|
+
### Bind vs watch
|
|
61
|
+
|
|
62
|
+
| Method | Meaning |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `watchRouter(router)` | Copy existing `inspectDecisions()`, then subscribe. |
|
|
65
|
+
| `bind(group)` | Watch the group router and record a `publish` / `dispatch` / `step` / asset tape. `publish` extras (`cause`) are stored and replayed. `unbind()` drops the decision subscription. |
|
|
66
|
+
| `importTrace(json)` | Load an export for inspection without replaying. |
|
|
67
|
+
| `exportTrace(filter?)` | JSON: participants, tape, redacted records, snapshots. |
|
|
68
|
+
| `report(filter?)` | Machine-readable CI report: participants, records, trees, dropped. |
|
|
69
|
+
| `causationTree(correlationId?)` | Forest of accepted/rejected nodes. Duplicates are omitted. |
|
|
70
|
+
| `replayExportedTrace(export, group)` | Play the tape on a fresh group; compare with `compareReplayTraces`. |
|
|
71
|
+
|
|
72
|
+
`dispatch` of `cyberart.asset.ready` / `cyberart.asset.failed` is stored as tape `kind: 'asset'`.
|
|
73
|
+
|
|
74
|
+
Cart summaries include `emit` / `subscribe` / `authoritative`, clock, redacted state, and `errorCount` / `lastError` (message only).
|
|
75
|
+
|
|
76
|
+
## Router traces
|
|
77
|
+
|
|
78
|
+
`createEventRouter` now also exposes:
|
|
79
|
+
|
|
80
|
+
| Method | Meaning |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `inspectParticipants()` | id, emit, subscribe, authoritative. |
|
|
83
|
+
| `inspectDecisions()` | Bounded decision log (`maxDecisions`, default 256). |
|
|
84
|
+
| `subscribeDecision(listener)` | Live accepted / rejected / duplicate records. |
|
|
85
|
+
|
|
86
|
+
`RuntimeGroup.inspectParticipants()` adds `kind`. Group `inspect()` includes the same attach fields. `maxTrace` bounds the accepted-event envelope log (default 1024).
|
|
87
|
+
|
|
88
|
+
Duplicate idempotency hits are `outcome: 'duplicate'` / `reason: 'duplicate'` and do not fan out. Permission failures stay `unauthorized`; missing targets `unknown-target`; spent TTL `hop-limit`.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Runtime group
|
|
2
|
+
|
|
3
|
+
Host helper that mounts several production `createRuntime` carts, attaches each mailbox with `router.attach(id, runtime.hostChannel, attachOptions)`, and locksteps one shared deterministic clock. Carts never receive the router. Not a second engine. Do not call the headless wrapper from Player or kaleidoscope.
|
|
4
|
+
|
|
5
|
+
Capability-manifest integration is optional and structural (`capability?: { emit, subscribe, authoritative }`). This module does not import the manifest.
|
|
6
|
+
|
|
7
|
+
Related: [events](events.md), [headless harness](headless-harness.md), [deterministic mode](deterministic-mode.md), [compositor](compositor.md), [replay inspector](replay-inspector.md).
|
|
8
|
+
|
|
9
|
+
Back to the [package README](../README.md).
|
|
10
|
+
|
|
11
|
+
## One-command reproduce (this repo)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm exec vitest run packages/engine/src/canvas/cyb-61-runtime-group.repro.spec.ts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## When to use which
|
|
18
|
+
|
|
19
|
+
| Surface | Import | Use |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Production host | `createRuntimeGroup` from `@cyberart-io/engine` | Several carts in one page; host injects containers or lets the group create them. |
|
|
22
|
+
| Vitest / jsdom | `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless` | Same group API after `installHeadlessCanvas()`. |
|
|
23
|
+
|
|
24
|
+
`createRuntime` still does **not** attach a router by itself. The group is the attach + lockstep layer.
|
|
25
|
+
|
|
26
|
+
## `createRuntimeGroup(options)`
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { createRuntimeGroup } from '@cyberart-io/engine';
|
|
30
|
+
|
|
31
|
+
const group = createRuntimeGroup({
|
|
32
|
+
origin: 0,
|
|
33
|
+
createId: (() => {
|
|
34
|
+
let n = 0;
|
|
35
|
+
return () => `g-${++n}`;
|
|
36
|
+
})(),
|
|
37
|
+
participants: [
|
|
38
|
+
{
|
|
39
|
+
id: 'effects',
|
|
40
|
+
cart: effectsCart,
|
|
41
|
+
seed: `0x${'61'.repeat(32)}`,
|
|
42
|
+
emit: ['ambience.intent.*'],
|
|
43
|
+
subscribe: ['host.state.*'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'ambience',
|
|
47
|
+
cart: ambienceCart,
|
|
48
|
+
kind: 'render',
|
|
49
|
+
seed: `0x${'62'.repeat(32)}`,
|
|
50
|
+
subscribe: ['ambience.intent.*'],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
group.publish({
|
|
56
|
+
type: 'host.state.accepted',
|
|
57
|
+
kind: 'state',
|
|
58
|
+
payload: { id: 'north' },
|
|
59
|
+
});
|
|
60
|
+
await group.step(2);
|
|
61
|
+
const { participants, trace, diagnostics } = await group.inspect();
|
|
62
|
+
group.destroy();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Example mapping: an accepted `host.state.accepted` is consumed by the effects cart, which emits `ambience.intent.play`; the router delivers that cue to the ambience cart. No project-specific bridge.
|
|
66
|
+
|
|
67
|
+
### Options (`CreateRuntimeGroupOptions`)
|
|
68
|
+
|
|
69
|
+
| Option | Default | Meaning |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `participants` | required | One entry per cart. Ids must be unique. Sorted lexicographically for lockstep. |
|
|
72
|
+
| `origin` | `0` | Passed to every `createRuntime({ deterministic: { origin } })`. |
|
|
73
|
+
| `width` / `height` | `320` / `180` | Used when the group creates a container. |
|
|
74
|
+
| `createId` / `now` | `evt-1`… / first cart clock | Injected into the **one** shared router. |
|
|
75
|
+
| `validate` | none | Router `validate` for cart-originated events. |
|
|
76
|
+
| `router` | `{}` | Extra `createEventRouter` options (`maxHops`, `maxPerTurn`, …). |
|
|
77
|
+
| `maxTrace` | `1024` | Bound on the accepted-event inspect trace. |
|
|
78
|
+
|
|
79
|
+
### Participant config
|
|
80
|
+
|
|
81
|
+
| Field | Meaning |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `id` | Router participant id. Reserved: `host`, `router`. |
|
|
84
|
+
| `cart` | `AnimationCart` mounted through `createRuntime`. |
|
|
85
|
+
| `kind` | `'render'` (default) or `'calculation'` (no-op/minimal render; still a real cart). |
|
|
86
|
+
| `seed` | `0x` + 64 hex, or any seed `createRuntime` accepts. Default: derived from `id`. |
|
|
87
|
+
| `container` | Injected mount node. If omitted, the group creates and owns a sized `div`. |
|
|
88
|
+
| `emit` / `subscribe` / `authoritative` | Passed to `router.attach`. |
|
|
89
|
+
| `capability` | Structural fallback for those three fields when the explicit ones are omitted. |
|
|
90
|
+
| `initialState` / `gameManager` / `onEvent` | `mount` options. The group still records outbound events. |
|
|
91
|
+
|
|
92
|
+
### Handle (`RuntimeGroup`)
|
|
93
|
+
|
|
94
|
+
| Member | Meaning |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `router` | The shared `EventRouter`. Do not create a second one. |
|
|
97
|
+
| `step(n)` | For each frame: `router.turn()` once, then `await cart.step(1)` in sorted id order. No-op while paused. Throws after `destroy()`. |
|
|
98
|
+
| `pause` / `resume` | Group flag plus every cart. `step` does not advance while paused. |
|
|
99
|
+
| `reset` | Remounts every cart to its initial state, clears traces / logs, `router.turn()`. Does not change `paused`; call `resume()` if you need lockstep after a paused reset. |
|
|
100
|
+
| `dispatch(id, event)` | Mailbox inbound on that participant. |
|
|
101
|
+
| `publish(event)` | Host `router.publish`. Unknown `target` → `unknown-target` rejection. |
|
|
102
|
+
| `inspect()` | `{ participants: Record<id, { state, events, errors, kind, clock, emit, subscribe, authoritative }>, trace, diagnostics }`. |
|
|
103
|
+
| `inspectParticipants()` | Sync attach view (`id`, `kind`, `emit`, `subscribe`, `authoritative`). |
|
|
104
|
+
| `destroy()` | Detach router listeners, `runtime.destroy()` each cart, remove owned containers. Idempotent. |
|
|
105
|
+
|
|
106
|
+
Idempotency, correlation, causation, unauthorized emit, hop/loop checks are the existing router. The group only attaches and locksteps. For “why did this effect not fire?”, attach [`createReplayInspector`](replay-inspector.md).
|
|
107
|
+
|
|
108
|
+
## `createHeadlessMultiCartHarness(options)`
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { createHeadlessMultiCartHarness } from '@cyberart-io/engine/headless';
|
|
112
|
+
|
|
113
|
+
const harness = createHeadlessMultiCartHarness({
|
|
114
|
+
participants: [effects, ambience, telemetry],
|
|
115
|
+
});
|
|
116
|
+
await harness.step(2);
|
|
117
|
+
harness.destroy();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Calls `installHeadlessCanvas()` then `createRuntimeGroup`. Same handle. Three-cart CI fixture: effects + ambience + a calculation telemetry cart.
|
|
121
|
+
|
|
122
|
+
Cleanup: `afterEach(() => group.destroy())` so a failed assertion does not leak nodes. After `destroy`, `step` throws and owned containers are gone from `document.body`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyberart-io/engine",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "CyberArt host engine: mount
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "CyberArt host engine: mount carts, events, capability manifests, geometry, runtime groups, and a Node/jsdom headless entry.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"files": [
|