@patterkit/runtime 0.12.1 → 0.14.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.
- package/CHANGELOG.md +48 -0
- package/README.md +25 -6
- package/dist/index.cjs +275 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -27
- package/dist/index.d.ts +90 -27
- package/dist/index.js +272 -122
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -72,7 +72,21 @@ function buildTagIndex(bundle) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// src/engine.ts
|
|
75
|
+
var import_model2 = require("@patterkit/model");
|
|
75
76
|
var astCache = /* @__PURE__ */ new WeakMap();
|
|
77
|
+
var OWNER = "Patter";
|
|
78
|
+
var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
|
|
79
|
+
var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
|
|
80
|
+
var keys = {
|
|
81
|
+
/** A scene's SHARED `@scene` props (one bag per scene, every flow's). */
|
|
82
|
+
stage: (sceneId) => `patter/scene/${esc(sceneId)}`,
|
|
83
|
+
/** Everything one flow registers starts with this. */
|
|
84
|
+
flow: (flowId) => `patter/flow/${esc(flowId)}/`,
|
|
85
|
+
/** A flow's NOT-shared `@patter` globals. */
|
|
86
|
+
flowGlobals: (flowId) => `patter/flow/${esc(flowId)}/patter`,
|
|
87
|
+
/** A flow's NOT-shared `@scene` props for one scene. */
|
|
88
|
+
flowScene: (flowId, sceneId) => `patter/flow/${esc(flowId)}/scene/${esc(sceneId)}`
|
|
89
|
+
};
|
|
76
90
|
var Engine = class _Engine {
|
|
77
91
|
host;
|
|
78
92
|
defaultSeed;
|
|
@@ -132,18 +146,32 @@ var Engine = class _Engine {
|
|
|
132
146
|
const patterSharedDecls = props.filter((p) => p.shared ?? true).map(toDecl);
|
|
133
147
|
const patterLocalDecls = props.filter((p) => !(p.shared ?? true)).map(toDecl);
|
|
134
148
|
const patterSharedNames = new Set(patterSharedDecls.map((d) => d.name.toLowerCase()));
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
const registry = options.registry ?? new import_scoperegistry.ScopeRegistry();
|
|
150
|
+
const ownsRegistry = !options.registry || options[OWNS_REGISTRY] === true;
|
|
151
|
+
const patterBag = new import_scoperegistry2.PropertyBag(patterSharedDecls, { pathPrefix: "@patter." });
|
|
152
|
+
const hostScopes = [];
|
|
153
|
+
const registered = [];
|
|
154
|
+
try {
|
|
155
|
+
registry.mountOwned("patter", patterBag, { owner: OWNER });
|
|
156
|
+
registered.push("patter");
|
|
157
|
+
const hostBound = /* @__PURE__ */ new Set();
|
|
158
|
+
if (options.world) {
|
|
159
|
+
const worldSpec = bundle.scopeRegistry?.scopes.find((s) => s.token === "world");
|
|
160
|
+
const decls = (worldSpec?.declarations ?? []).map(toForeignDecl);
|
|
161
|
+
registry.defineForeign("world", options.world, decls, { writable: worldSpec?.writable ?? true, owner: OWNER });
|
|
162
|
+
registered.push("world");
|
|
163
|
+
hostBound.add("world");
|
|
164
|
+
}
|
|
165
|
+
for (const spec of ownsRegistry ? bundle.scopeRegistry?.scopes ?? [] : []) {
|
|
166
|
+
if (hostBound.has(spec.token) || registry.has(spec.token)) continue;
|
|
167
|
+
const decls = (spec.declarations ?? []).map((d) => selfBackedDecl(d, spec.writable));
|
|
168
|
+
registry.defineOwned(spec.token, decls, { owner: OWNER });
|
|
169
|
+
registered.push(spec.token);
|
|
170
|
+
hostScopes.push(spec.token);
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
for (const k of registered) registry.remove(k, { keep: true });
|
|
174
|
+
throw e;
|
|
147
175
|
}
|
|
148
176
|
const sceneSharedNames = /* @__PURE__ */ new Map();
|
|
149
177
|
for (const [sceneId, scene] of Object.entries(bundle.scenes)) {
|
|
@@ -165,7 +193,10 @@ var Engine = class _Engine {
|
|
|
165
193
|
blockGameIdToId: this.blockGameIdToId,
|
|
166
194
|
// same instances the engine resolves with
|
|
167
195
|
tagIndex: buildTagIndex(bundle),
|
|
168
|
-
|
|
196
|
+
registry,
|
|
197
|
+
ownsRegistry,
|
|
198
|
+
patterBag,
|
|
199
|
+
hostScopes,
|
|
169
200
|
patterSharedDecls,
|
|
170
201
|
patterLocalDecls,
|
|
171
202
|
patterSharedNames,
|
|
@@ -182,7 +213,8 @@ var Engine = class _Engine {
|
|
|
182
213
|
captionClose: (bundle.closedCaptions ?? import_model.DEFAULT_CAPTION_DELIMITERS).close,
|
|
183
214
|
captionCharacter: bundle.closedCaptions?.character || import_model.DEFAULT_CAPTION_CHARACTER,
|
|
184
215
|
// absent/empty -> SFX
|
|
185
|
-
refSplitCache: /* @__PURE__ */ new Map()
|
|
216
|
+
refSplitCache: /* @__PURE__ */ new Map(),
|
|
217
|
+
refSplitRevision: registry.revision
|
|
186
218
|
};
|
|
187
219
|
}
|
|
188
220
|
/** The active locale (string + character-name lookups resolve in it). */
|
|
@@ -227,10 +259,12 @@ var Engine = class _Engine {
|
|
|
227
259
|
* content-drift policy (§9.8) resolves edits under the cursor: stack frames re-find their next
|
|
228
260
|
* child by id, drifted options drop, a vanished snippet is skipped.
|
|
229
261
|
*
|
|
230
|
-
* Returns the REPLACEMENT engine
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
262
|
+
* Returns the REPLACEMENT engine, on the same registry. This one hands its bags over (each is
|
|
263
|
+
* removed from the registry with its values kept, and the replacement claims them as it
|
|
264
|
+
* registers), its flows are closed, and it should be discarded. Hosts re-bind their flow handles
|
|
265
|
+
* via `next.getFlow(id)`. If the restore throws (defensive - §9.8 makes this unreachable for
|
|
266
|
+
* ordinary edits), the swap falls back to a fresh engine with each saved flow restarted from the
|
|
267
|
+
* top of the scene it was in; the shared properties carry over.
|
|
234
268
|
*/
|
|
235
269
|
hotSwap(bundle) {
|
|
236
270
|
const snapshot = this.saveGame();
|
|
@@ -239,12 +273,19 @@ var Engine = class _Engine {
|
|
|
239
273
|
next2.setClosedCaptions(this.host.captionsOn);
|
|
240
274
|
return next2;
|
|
241
275
|
};
|
|
242
|
-
const
|
|
276
|
+
const make = () => new _Engine(bundle, {
|
|
277
|
+
...this.creationOptions,
|
|
278
|
+
registry: this.host.registry,
|
|
279
|
+
[OWNS_REGISTRY]: this.host.ownsRegistry
|
|
280
|
+
});
|
|
281
|
+
this.release(true);
|
|
282
|
+
const next = make();
|
|
243
283
|
try {
|
|
244
284
|
next.loadGame(snapshot);
|
|
245
285
|
return carryOver(next);
|
|
246
286
|
} catch {
|
|
247
|
-
|
|
287
|
+
next.release(true);
|
|
288
|
+
const fresh = make();
|
|
248
289
|
for (const [id, f] of Object.entries(snapshot.flows)) {
|
|
249
290
|
const sceneId = f.cursor.currentSceneId;
|
|
250
291
|
try {
|
|
@@ -283,6 +324,7 @@ var Engine = class _Engine {
|
|
|
283
324
|
* call to reach for when you want a speaker's variation state to carry on.
|
|
284
325
|
*/
|
|
285
326
|
openFlow(id, opts = {}) {
|
|
327
|
+
this.assertExternalScopes();
|
|
286
328
|
const sceneId = this.resolveSceneRef(opts.scene);
|
|
287
329
|
const blockId = this.resolveBlockRef(sceneId, opts.block);
|
|
288
330
|
this.flowsById.get(id)?.close();
|
|
@@ -322,6 +364,17 @@ var Engine = class _Engine {
|
|
|
322
364
|
}
|
|
323
365
|
return existing.advanceToStop().played;
|
|
324
366
|
}
|
|
367
|
+
/** Content that names another engine's scope (`@story.act`) runs only where that engine is on this
|
|
368
|
+
* registry: without it every read would answer false and every write fail, so the flow is refused as
|
|
369
|
+
* it opens (or the save as it loads), before anything changes. By then a game has built all of its
|
|
370
|
+
* engines, whatever order it built them in. The same message on every runtime. */
|
|
371
|
+
assertExternalScopes() {
|
|
372
|
+
for (const token of this.host.bundle.externalScopes ?? []) {
|
|
373
|
+
if (!this.host.registry.has(token)) {
|
|
374
|
+
throw new Error(`this content names @${token}, which no engine on this registry registered: give every engine the game's one registry`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
325
378
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
326
379
|
resolveSceneRef(ref) {
|
|
327
380
|
if (ref == null) return void 0;
|
|
@@ -507,7 +560,7 @@ var Engine = class _Engine {
|
|
|
507
560
|
* loadGame() replaces every bag, so re-enumerate after a load.
|
|
508
561
|
*/
|
|
509
562
|
listBags() {
|
|
510
|
-
const mounts = [{ bag: this.host.
|
|
563
|
+
const mounts = [{ bag: this.host.patterBag }];
|
|
511
564
|
for (const [sceneId, bag] of this.host.stageBags) mounts.push({ bag, pathPrefix: `@scene:${sceneId}.` });
|
|
512
565
|
return mounts;
|
|
513
566
|
}
|
|
@@ -526,20 +579,42 @@ var Engine = class _Engine {
|
|
|
526
579
|
reset() {
|
|
527
580
|
for (const flow of this.flowsById.values()) flow.close();
|
|
528
581
|
this.flowsById.clear();
|
|
529
|
-
this.host.
|
|
582
|
+
this.host.patterBag.reseed(this.host.patterSharedDecls);
|
|
530
583
|
this.host.sharedVisits.clear();
|
|
531
584
|
this.host.sharedSelectors.clear();
|
|
585
|
+
for (const s of this.host.stageBags.keys()) this.host.registry.remove(keys.stage(s));
|
|
586
|
+
this.host.stageBags.clear();
|
|
587
|
+
this.host.registry.discardParked("patter/");
|
|
588
|
+
}
|
|
589
|
+
/** Remove every bag this engine registered, keeping the values parked when `keep` (a live reload
|
|
590
|
+
* handing its state to a replacement), and close its flows. The engine is inert afterwards. */
|
|
591
|
+
release(keep) {
|
|
592
|
+
for (const flow of this.flowsById.values()) {
|
|
593
|
+
flow.releaseBags(keep);
|
|
594
|
+
flow.close();
|
|
595
|
+
}
|
|
596
|
+
this.flowsById.clear();
|
|
597
|
+
const reg = this.host.registry;
|
|
598
|
+
for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep });
|
|
532
599
|
this.host.stageBags.clear();
|
|
600
|
+
for (const t of ["patter", ...this.host.hostScopes]) if (reg.has(t)) reg.remove(t, { keep });
|
|
601
|
+
if (this.creationOptions.world && reg.has("world")) reg.remove("world");
|
|
533
602
|
}
|
|
534
603
|
/** Read a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level). */
|
|
535
604
|
getProperty(ref) {
|
|
536
605
|
const { scope, name } = this.splitShared(ref);
|
|
537
|
-
return this.host.
|
|
606
|
+
return this.host.registry.get(scope, name);
|
|
538
607
|
}
|
|
539
|
-
/** Write a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level).
|
|
608
|
+
/** Write a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level).
|
|
609
|
+
*
|
|
610
|
+
* This is the GAME's surface, so it writes with host authority: a host-scope declaration's
|
|
611
|
+
* `writable: false` is the story's promise not to write the value, never a lock on the game that
|
|
612
|
+
* owns it. The story's own writes come through `Flow.runEffects`, which never claims it. (Ruled
|
|
613
|
+
* across the family 2026-09-05, after a Storylets venue clock and this project's coverage driver
|
|
614
|
+
* were each refused the one property they existed to move.) */
|
|
540
615
|
setProperty(ref, value) {
|
|
541
616
|
const { scope, name } = this.splitShared(ref);
|
|
542
|
-
this.host.
|
|
617
|
+
this.host.registry.set(scope, name, value, { host: true });
|
|
543
618
|
}
|
|
544
619
|
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
545
620
|
* value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
|
|
@@ -586,54 +661,66 @@ var Engine = class _Engine {
|
|
|
586
661
|
// @scene is scene-namespaced and needs a flow's current scene - silently
|
|
587
662
|
// routing it into the shared bag (as a junk "scene.x" key) was a trap.
|
|
588
663
|
splitShared(ref) {
|
|
589
|
-
|
|
590
|
-
if (!split) {
|
|
591
|
-
split = (0, import_dialect.splitRef)(ref, (t) => t === "scene" || this.host.shared.has(t));
|
|
592
|
-
this.host.refSplitCache.set(ref, split);
|
|
593
|
-
}
|
|
664
|
+
const split = splitHostRef(this.host, ref);
|
|
594
665
|
if (split.scope === "scene") {
|
|
595
666
|
throw new Error(`'${ref}': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);
|
|
596
667
|
}
|
|
597
668
|
return split;
|
|
598
669
|
}
|
|
599
|
-
/**
|
|
670
|
+
/** The shared `@patter` values alone, as `{ patter: {...} }`.
|
|
671
|
+
* @deprecated The registry holds every property now: a game saves `registry.save()` once, and a
|
|
672
|
+
* standalone engine's `saveGame()` carries its registry's values. Removed at the next breaking release. */
|
|
600
673
|
save() {
|
|
601
|
-
return this.host.
|
|
674
|
+
return { patter: this.host.patterBag.save() };
|
|
602
675
|
}
|
|
603
|
-
/**
|
|
676
|
+
/** Lay a `{ patter: {...} }` section over the shared `@patter` values; anything else is ignored.
|
|
677
|
+
* @deprecated See `save`. Removed at the next breaking release. */
|
|
604
678
|
load(blob) {
|
|
605
|
-
this.host.
|
|
679
|
+
if (blob.patter) this.host.patterBag.load(blob.patter);
|
|
606
680
|
}
|
|
607
|
-
/** Snapshot the whole game
|
|
681
|
+
/** Snapshot the whole game's NON-property state: visit counts, shared selector cursors, and every
|
|
682
|
+
* live flow's cursor and PRNG. The property values are the registry's: a standalone engine (one that
|
|
683
|
+
* made its own registry) carries them here under `registry`; a game that passed a registry saves it
|
|
684
|
+
* once itself, beside each engine's `saveGame()`. */
|
|
608
685
|
saveGame() {
|
|
609
686
|
const flows = {};
|
|
610
687
|
for (const [id, flow] of this.flowsById) flows[id] = flow.snapshot();
|
|
611
688
|
return {
|
|
612
|
-
version:
|
|
613
|
-
|
|
689
|
+
version: import_model2.SAVE_VERSION,
|
|
690
|
+
...this.host.ownsRegistry ? { registry: this.host.registry.save() } : {},
|
|
614
691
|
sharedVisits: Object.fromEntries(this.host.sharedVisits),
|
|
615
692
|
sharedSelectors: serialiseSelectors(this.host.sharedSelectors),
|
|
616
|
-
stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, bag.save()])),
|
|
617
693
|
flows
|
|
618
694
|
};
|
|
619
695
|
}
|
|
620
|
-
/**
|
|
696
|
+
/**
|
|
697
|
+
* Restore a `saveGame()`: visit counts, shared selector cursors, and every flow. Property values come
|
|
698
|
+
* from the registry. A save that carries them (a standalone engine's, or a version 2 save from before
|
|
699
|
+
* the registry held them) has them moved into the registry here; otherwise the game loads its
|
|
700
|
+
* registry itself, before or after this call. Either order works: this engine's bags are handed back
|
|
701
|
+
* to the registry (values kept) and the restored flows claim them as they register.
|
|
702
|
+
*/
|
|
621
703
|
loadGame(save) {
|
|
622
|
-
|
|
623
|
-
|
|
704
|
+
const version = save.version;
|
|
705
|
+
if (version !== 2 && version !== import_model2.SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
|
|
706
|
+
this.assertExternalScopes();
|
|
707
|
+
const reg = this.host.registry;
|
|
708
|
+
for (const [id, flow] of this.flowsById) {
|
|
709
|
+
flow.releaseBags(id in save.flows);
|
|
710
|
+
flow.close();
|
|
711
|
+
}
|
|
712
|
+
this.flowsById.clear();
|
|
713
|
+
for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep: true });
|
|
714
|
+
this.host.stageBags.clear();
|
|
715
|
+
const values = save.version === 2 ? sectionsFromV2(save) : save.registry;
|
|
716
|
+
if (values) {
|
|
717
|
+
if (this.host.ownsRegistry) reg.load(values);
|
|
718
|
+
else reg.load(values, { keepParked: true });
|
|
719
|
+
}
|
|
624
720
|
this.host.sharedVisits.clear();
|
|
625
721
|
for (const [id, n] of Object.entries(save.sharedVisits ?? {})) this.host.sharedVisits.set(id, n);
|
|
626
722
|
this.host.sharedSelectors.clear();
|
|
627
723
|
for (const [id, st] of deserialiseSelectors(save.sharedSelectors)) this.host.sharedSelectors.set(id, st);
|
|
628
|
-
this.host.stageBags.clear();
|
|
629
|
-
for (const [s, values] of Object.entries(save.stageBags ?? {})) {
|
|
630
|
-
const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
|
|
631
|
-
const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => shared.has(d.name.toLowerCase()));
|
|
632
|
-
const bag = new import_scoperegistry2.PropertyBag(decls);
|
|
633
|
-
bag.load(values);
|
|
634
|
-
this.host.stageBags.set(s, bag);
|
|
635
|
-
}
|
|
636
|
-
this.flowsById.clear();
|
|
637
724
|
for (const [id, snap] of Object.entries(save.flows)) {
|
|
638
725
|
const flow = new Flow(id, this.host, this.defaultSeed);
|
|
639
726
|
flow.restore(snap);
|
|
@@ -645,7 +732,9 @@ var Flow = class {
|
|
|
645
732
|
id;
|
|
646
733
|
host;
|
|
647
734
|
local;
|
|
648
|
-
//
|
|
735
|
+
// the NOT-shared `@patter` globals (this flow's copy), keys.flowGlobals
|
|
736
|
+
/** The registry keys this flow has registered (its globals and each scene bag). */
|
|
737
|
+
registered = /* @__PURE__ */ new Set();
|
|
649
738
|
rngState;
|
|
650
739
|
// Execution cursor. The `stack` is the continuation stack: each frame is a
|
|
651
740
|
// position within a block's children (the top frame is the active block run;
|
|
@@ -670,40 +759,44 @@ var Flow = class {
|
|
|
670
759
|
/** Per-node entry counts for this flow (node id -> times entered). */
|
|
671
760
|
visitCounts = /* @__PURE__ */ new Map();
|
|
672
761
|
// Per-flow halves of the two scopes. The NOT-shared `@patter` globals live in
|
|
673
|
-
// `local
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
// resolver presents one merged scope, routing each property
|
|
677
|
-
// declared `shared` flag
|
|
762
|
+
// `local`; the NOT-shared `@scene` props live in `sceneBags` (namespaced per
|
|
763
|
+
// scene; they PERSIST across re-entries, spec §7). The SHARED halves live on the
|
|
764
|
+
// host (`host.patterBag` / `host.stageBags`). All of them are registered in the
|
|
765
|
+
// game's registry. Each resolver presents one merged scope, routing each property
|
|
766
|
+
// to its half by the declared `shared` flag: that split is why `@patter` and
|
|
767
|
+
// `@scene` are composed here rather than aliased to a single registry key.
|
|
678
768
|
/** This flow's per-scene LOCAL scene props; see FlowHost.stageBags. */
|
|
679
769
|
sceneBags = /* @__PURE__ */ new Map();
|
|
680
770
|
patterResolver = {
|
|
681
|
-
get: (n) => this.host.patterSharedNames.has(n) ? this.host.
|
|
771
|
+
get: (n) => this.host.patterSharedNames.has(n) ? this.host.patterBag.get(n) : this.local.get(n),
|
|
682
772
|
set: (n, v) => {
|
|
683
|
-
if (this.host.patterSharedNames.has(n)) this.host.
|
|
684
|
-
else this.local.set(
|
|
773
|
+
if (this.host.patterSharedNames.has(n)) this.host.registry.set("patter", n, v);
|
|
774
|
+
else this.local.set(n, v);
|
|
685
775
|
}
|
|
686
776
|
};
|
|
687
777
|
sceneResolver = {
|
|
688
778
|
get: (n) => {
|
|
689
779
|
const s = this.currentSceneId;
|
|
690
780
|
if (s === null) return void 0;
|
|
691
|
-
|
|
692
|
-
return bag?.get(n);
|
|
781
|
+
return this.sceneBagFor(s, n)?.get(n);
|
|
693
782
|
},
|
|
694
783
|
set: (n, v) => {
|
|
695
784
|
const s = this.currentSceneId;
|
|
696
785
|
if (s === null) return;
|
|
697
|
-
const bag = this.
|
|
786
|
+
const bag = this.sceneBagFor(s, n);
|
|
698
787
|
if (bag) bag.set(n, v);
|
|
699
788
|
}
|
|
700
789
|
};
|
|
701
|
-
// The eval context is built
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
790
|
+
// The eval context is built once and REFRESHED only when the registry's set of
|
|
791
|
+
// scopes moves (its `revision`): every constituent resolves live state at call
|
|
792
|
+
// time (bags mutate in place per scoperegistry's contract; patter/scene route
|
|
793
|
+
// through this flow's resolvers, which read the current `local`/`sceneBags`/
|
|
794
|
+
// `currentSceneId`; the host callbacks read current flow fields), but another
|
|
795
|
+
// engine registering `@story` after this flow opened must still be readable.
|
|
796
|
+
// Rebuilding it per evaluation was the engine's hottest allocation.
|
|
706
797
|
evalCtx;
|
|
798
|
+
ctxRevision = -1;
|
|
799
|
+
registryQualities;
|
|
707
800
|
flowLog = [];
|
|
708
801
|
/** Monotonic across the flow's life; survives clearLog so two reads agree on order. */
|
|
709
802
|
flowSeq = 0;
|
|
@@ -711,12 +804,9 @@ var Flow = class {
|
|
|
711
804
|
this.id = id;
|
|
712
805
|
this.host = host;
|
|
713
806
|
this.rngState = (0, import_expr.toUint32)(seed);
|
|
714
|
-
this.local = this.
|
|
715
|
-
const scopes = { ...host.shared.toEvalContext().scopes };
|
|
716
|
-
scopes["patter"] = this.patterResolver;
|
|
717
|
-
scopes["scene"] = this.sceneResolver;
|
|
807
|
+
this.local = this.newLocal();
|
|
718
808
|
this.evalCtx = {
|
|
719
|
-
scopes,
|
|
809
|
+
scopes: {},
|
|
720
810
|
host: {
|
|
721
811
|
nextRandom: this.rng,
|
|
722
812
|
visits: (id2) => this.visitCounts.get(id2) ?? 0,
|
|
@@ -729,6 +819,21 @@ var Flow = class {
|
|
|
729
819
|
qualities: (scope, name) => this.stagesFor(scope, name)
|
|
730
820
|
};
|
|
731
821
|
}
|
|
822
|
+
/** The eval context, its scopes refreshed if the registry's set of scopes has moved since. */
|
|
823
|
+
context() {
|
|
824
|
+
const reg = this.host.registry;
|
|
825
|
+
if (reg.revision !== this.ctxRevision) {
|
|
826
|
+
const base = reg.toEvalContext();
|
|
827
|
+
const scopes = this.evalCtx.scopes;
|
|
828
|
+
for (const k of Object.keys(scopes)) delete scopes[k];
|
|
829
|
+
Object.assign(scopes, base.scopes);
|
|
830
|
+
scopes["patter"] = this.patterResolver;
|
|
831
|
+
scopes["scene"] = this.sceneResolver;
|
|
832
|
+
this.registryQualities = base.qualities;
|
|
833
|
+
this.ctxRevision = reg.revision;
|
|
834
|
+
}
|
|
835
|
+
return this.evalCtx;
|
|
836
|
+
}
|
|
732
837
|
/** The stage ladder of `@scope.name` when it is a declared quality, else undefined. Names compare
|
|
733
838
|
* lowercase, as the compiler emits references (the selfBackedResolver lesson). */
|
|
734
839
|
stagesFor(scope, name) {
|
|
@@ -741,13 +846,14 @@ var Flow = class {
|
|
|
741
846
|
const scene = this.currentSceneId != null ? this.host.bundle.scenes[this.currentSceneId] : void 0;
|
|
742
847
|
return fromDecls(scene?.sceneProps);
|
|
743
848
|
}
|
|
744
|
-
return fromDecls(this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations);
|
|
849
|
+
return this.registryQualities?.(scope, name) ?? fromDecls(this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations);
|
|
745
850
|
}
|
|
746
851
|
// -- Host API -------------------------------------------------------------
|
|
747
852
|
/** Begin this flow at a scene (and optionally a specific block within it). */
|
|
748
853
|
start(sceneId, blockId) {
|
|
749
|
-
this.
|
|
750
|
-
this.
|
|
854
|
+
this.releaseBags(false);
|
|
855
|
+
this.host.registry.discardParked(keys.flow(this.id));
|
|
856
|
+
this.mountLocal();
|
|
751
857
|
this.selectors.clear();
|
|
752
858
|
this.visitCounts.clear();
|
|
753
859
|
this.stack = [];
|
|
@@ -847,6 +953,7 @@ var Flow = class {
|
|
|
847
953
|
* cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
|
|
848
954
|
*/
|
|
849
955
|
close() {
|
|
956
|
+
this.releaseBags(false);
|
|
850
957
|
this.closed = true;
|
|
851
958
|
this.flowEnded = true;
|
|
852
959
|
this.stack = [];
|
|
@@ -956,7 +1063,7 @@ var Flow = class {
|
|
|
956
1063
|
* halves are the Engine's listBags.
|
|
957
1064
|
*/
|
|
958
1065
|
listBags() {
|
|
959
|
-
const mounts = [{ bag: this.local
|
|
1066
|
+
const mounts = [{ bag: this.local, pathPrefix: `${this.id}/@patter.` }];
|
|
960
1067
|
for (const [sceneId, bag] of this.sceneBags) mounts.push({ bag, pathPrefix: `${this.id}/@scene:${sceneId}.` });
|
|
961
1068
|
return mounts;
|
|
962
1069
|
}
|
|
@@ -1004,10 +1111,17 @@ var Flow = class {
|
|
|
1004
1111
|
const { scope, name } = this.splitRef(ref);
|
|
1005
1112
|
if (scope === "patter") return this.patterResolver.get(name);
|
|
1006
1113
|
if (scope === "scene") return this.sceneResolver.get(name);
|
|
1007
|
-
return this.host.
|
|
1114
|
+
return this.host.registry.get(scope, name);
|
|
1008
1115
|
}
|
|
1009
|
-
/** Write a property by ref (routed by scope, then by the property's `shared` flag).
|
|
1116
|
+
/** Write a property by ref (routed by scope, then by the property's `shared` flag). The GAME's
|
|
1117
|
+
* surface: it writes with host authority, so a `writable: false` host declaration does not refuse
|
|
1118
|
+
* it (see `Engine.setProperty`). The story's writes go through `writeProperty` instead. */
|
|
1010
1119
|
setProperty(ref, value) {
|
|
1120
|
+
this.writeProperty(ref, value, true);
|
|
1121
|
+
}
|
|
1122
|
+
/** The write itself. `host` says WHO is writing, which is the only thing `writable: false` cares
|
|
1123
|
+
* about: the story is refused, the game is not. */
|
|
1124
|
+
writeProperty(ref, value, host) {
|
|
1011
1125
|
const { scope, name } = this.splitRef(ref);
|
|
1012
1126
|
if (scope === "patter") {
|
|
1013
1127
|
this.patterResolver.set(name, value);
|
|
@@ -1015,16 +1129,13 @@ var Flow = class {
|
|
|
1015
1129
|
if (this.currentSceneId === null) throw new Error(`'${ref}': the flow has not entered a scene yet`);
|
|
1016
1130
|
this.sceneResolver.set(name, value);
|
|
1017
1131
|
} else {
|
|
1018
|
-
this.host.
|
|
1132
|
+
this.host.registry.set(scope, name, value, host ? { host: true } : void 0);
|
|
1019
1133
|
}
|
|
1020
1134
|
}
|
|
1021
1135
|
// -- Save / restore (engine-driven) --------------------------------------
|
|
1022
|
-
/** @internal Snapshot this flow's cursor +
|
|
1136
|
+
/** @internal Snapshot this flow's cursor + PRNG + visits. Its properties are the registry's. */
|
|
1023
1137
|
snapshot() {
|
|
1024
1138
|
return {
|
|
1025
|
-
scopes: this.local.save(),
|
|
1026
|
-
// owned scope "patter" = the NOT-shared globals (@scene saved separately)
|
|
1027
|
-
sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, bag.save()])),
|
|
1028
1139
|
rngState: this.rngState,
|
|
1029
1140
|
visits: Object.fromEntries(this.visitCounts),
|
|
1030
1141
|
cursor: {
|
|
@@ -1062,15 +1173,11 @@ var Flow = class {
|
|
|
1062
1173
|
}
|
|
1063
1174
|
return { ...frame };
|
|
1064
1175
|
});
|
|
1065
|
-
this.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return [s, bag];
|
|
1071
|
-
}));
|
|
1072
|
-
this.local = this.freshLocal();
|
|
1073
|
-
this.local.load(snap.scopes);
|
|
1176
|
+
this.releaseBags(false);
|
|
1177
|
+
this.mountLocal();
|
|
1178
|
+
for (const s of /* @__PURE__ */ new Set([c.currentSceneId, ...this.stack.map((f) => f.sceneId)])) {
|
|
1179
|
+
if (s !== null && this.host.bundle.scenes[s]) this.ensureSceneBags(s);
|
|
1180
|
+
}
|
|
1074
1181
|
this.activeSnippet = null;
|
|
1075
1182
|
if (c.activeSnippetId !== null) {
|
|
1076
1183
|
const node = this.host.nodeIndex.get(c.activeSnippetId);
|
|
@@ -1341,7 +1448,7 @@ var Flow = class {
|
|
|
1341
1448
|
* node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
|
|
1342
1449
|
*/
|
|
1343
1450
|
matchedSpec(node, want) {
|
|
1344
|
-
const evalTruthy = (n) => truthy((0, import_expr.evaluate)(n, this.
|
|
1451
|
+
const evalTruthy = (n) => truthy((0, import_expr.evaluate)(n, this.context(), import_dialect.patterDialect));
|
|
1345
1452
|
return (0, import_expr_specificity.matchedSpecificity)(node, evalTruthy, { want });
|
|
1346
1453
|
}
|
|
1347
1454
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
@@ -1359,7 +1466,7 @@ var Flow = class {
|
|
|
1359
1466
|
for (const e of effects ?? []) {
|
|
1360
1467
|
const value = this.evalExpr(e.value);
|
|
1361
1468
|
const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
|
|
1362
|
-
this.
|
|
1469
|
+
this.writeProperty(e.target, value, false);
|
|
1363
1470
|
this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
|
|
1364
1471
|
}
|
|
1365
1472
|
}
|
|
@@ -1368,7 +1475,7 @@ var Flow = class {
|
|
|
1368
1475
|
return truthy(this.evalExpr(node.condition));
|
|
1369
1476
|
}
|
|
1370
1477
|
evalExpr(expr) {
|
|
1371
|
-
return (0, import_expr.evaluate)(this.conditionAst(expr), this.
|
|
1478
|
+
return (0, import_expr.evaluate)(this.conditionAst(expr), this.context(), import_dialect.patterDialect);
|
|
1372
1479
|
}
|
|
1373
1480
|
/** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
|
|
1374
1481
|
* evaluator and the Best-match specificity walker so both work off one parse. */
|
|
@@ -1493,18 +1600,51 @@ var Flow = class {
|
|
|
1493
1600
|
const key = (0, import_model.castStringKey)(character);
|
|
1494
1601
|
return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.castDisplay.get(character);
|
|
1495
1602
|
}
|
|
1496
|
-
/** Split a ref into scope + name. Tokens: `@scene`,
|
|
1603
|
+
/** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
|
|
1497
1604
|
splitRef(ref) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1605
|
+
return splitHostRef(this.host, ref);
|
|
1606
|
+
}
|
|
1607
|
+
/** A fresh, unregistered bag for the NOT-shared `@patter` globals (the shared ones live on the host). */
|
|
1608
|
+
newLocal() {
|
|
1609
|
+
return new import_scoperegistry2.PropertyBag(this.host.patterLocalDecls, { pathPrefix: "@patter." });
|
|
1610
|
+
}
|
|
1611
|
+
/** Register a fresh globals bag under this flow's key; it claims any values waiting there. */
|
|
1612
|
+
mountLocal() {
|
|
1613
|
+
this.local = this.newLocal();
|
|
1614
|
+
const key = keys.flowGlobals(this.id);
|
|
1615
|
+
this.host.registry.mountOwned(key, this.local, { owner: OWNER });
|
|
1616
|
+
this.registered.add(key);
|
|
1617
|
+
}
|
|
1618
|
+
/** @internal Remove every bag this flow registered; with `keep`, their values wait in the registry for
|
|
1619
|
+
* the flow that replaces this one. Engine-driven (close, loadGame, hotSwap). */
|
|
1620
|
+
releaseBags(keep) {
|
|
1621
|
+
for (const key of this.registered) this.host.registry.remove(key, { keep });
|
|
1622
|
+
this.registered.clear();
|
|
1623
|
+
this.sceneBags.clear();
|
|
1504
1624
|
}
|
|
1505
|
-
/** The
|
|
1506
|
-
|
|
1507
|
-
|
|
1625
|
+
/** The bag a `@scene` property of scene `s` lives in (stage or this flow's), made if missing. */
|
|
1626
|
+
sceneBagFor(s, name) {
|
|
1627
|
+
if (!this.host.bundle.scenes[s]) return void 0;
|
|
1628
|
+
this.ensureSceneBags(s);
|
|
1629
|
+
return this.host.sceneSharedNames.get(s)?.has(name.toLowerCase()) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
|
|
1630
|
+
}
|
|
1631
|
+
/** Make (and register) scene `s`'s stage bag and this flow's bag for it, if not made yet. A bag made
|
|
1632
|
+
* here claims whatever values the registry holds for its key: that is how a loaded save reaches it. */
|
|
1633
|
+
ensureSceneBags(s) {
|
|
1634
|
+
const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
|
|
1635
|
+
const props = this.host.bundle.scenes[s]?.sceneProps ?? [];
|
|
1636
|
+
if (!this.sceneBags.has(s)) {
|
|
1637
|
+
const bag = new import_scoperegistry2.PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1638
|
+
const key = keys.flowScene(this.id, s);
|
|
1639
|
+
this.host.registry.mountOwned(key, bag, { owner: OWNER });
|
|
1640
|
+
this.registered.add(key);
|
|
1641
|
+
this.sceneBags.set(s, bag);
|
|
1642
|
+
}
|
|
1643
|
+
if (!this.host.stageBags.has(s)) {
|
|
1644
|
+
const bag = new import_scoperegistry2.PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1645
|
+
this.host.registry.mountOwned(keys.stage(s), bag, { owner: OWNER });
|
|
1646
|
+
this.host.stageBags.set(s, bag);
|
|
1647
|
+
}
|
|
1508
1648
|
}
|
|
1509
1649
|
/**
|
|
1510
1650
|
* Seed a scene's `@scene` props (spec §7). The not-shared props seed THIS flow's
|
|
@@ -1515,13 +1655,7 @@ var Flow = class {
|
|
|
1515
1655
|
*/
|
|
1516
1656
|
seedScene(scene) {
|
|
1517
1657
|
const shared = this.host.sceneSharedNames.get(scene.id) ?? /* @__PURE__ */ new Set();
|
|
1518
|
-
|
|
1519
|
-
if (!this.sceneBags.has(scene.id)) {
|
|
1520
|
-
this.sceneBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase()))));
|
|
1521
|
-
}
|
|
1522
|
-
if (!this.host.stageBags.has(scene.id)) {
|
|
1523
|
-
this.host.stageBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase()))));
|
|
1524
|
-
}
|
|
1658
|
+
this.ensureSceneBags(scene.id);
|
|
1525
1659
|
for (const decl of scene.sceneProps ?? []) {
|
|
1526
1660
|
if (!decl.temporary) continue;
|
|
1527
1661
|
const name = decl.name.toLowerCase();
|
|
@@ -1567,16 +1701,32 @@ function toDecl(decl) {
|
|
|
1567
1701
|
function toForeignDecl(decl) {
|
|
1568
1702
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
|
|
1569
1703
|
}
|
|
1570
|
-
function
|
|
1571
|
-
const
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1704
|
+
function selfBackedDecl(decl, scopeWritable) {
|
|
1705
|
+
const d = toForeignDecl(decl);
|
|
1706
|
+
const writable = decl.writable ?? scopeWritable;
|
|
1707
|
+
return writable === void 0 ? d : { ...d, writable };
|
|
1708
|
+
}
|
|
1709
|
+
function splitHostRef(host, ref) {
|
|
1710
|
+
if (host.refSplitRevision !== host.registry.revision) {
|
|
1711
|
+
host.refSplitCache.clear();
|
|
1712
|
+
host.refSplitRevision = host.registry.revision;
|
|
1713
|
+
}
|
|
1714
|
+
let hit = host.refSplitCache.get(ref);
|
|
1715
|
+
if (!hit) {
|
|
1716
|
+
hit = (0, import_dialect.splitRef)(ref, (t) => t === "scene" || host.registry.has(t) || (host.bundle.externalScopes?.includes(t) ?? false));
|
|
1717
|
+
host.refSplitCache.set(ref, hit);
|
|
1718
|
+
}
|
|
1719
|
+
return hit;
|
|
1720
|
+
}
|
|
1721
|
+
function sectionsFromV2(save) {
|
|
1722
|
+
const out = {};
|
|
1723
|
+
if (save.shared?.patter) out["patter"] = save.shared.patter;
|
|
1724
|
+
for (const [s, v] of Object.entries(save.stageBags ?? {})) out[keys.stage(s)] = v;
|
|
1725
|
+
for (const [id, f] of Object.entries(save.flows ?? {})) {
|
|
1726
|
+
if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
|
|
1727
|
+
for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
|
|
1728
|
+
}
|
|
1729
|
+
return out;
|
|
1580
1730
|
}
|
|
1581
1731
|
function truthy(v) {
|
|
1582
1732
|
if (typeof v === "boolean") return v;
|
|
@@ -1586,7 +1736,7 @@ function truthy(v) {
|
|
|
1586
1736
|
}
|
|
1587
1737
|
|
|
1588
1738
|
// src/describe.ts
|
|
1589
|
-
var
|
|
1739
|
+
var import_model3 = require("@patterkit/model");
|
|
1590
1740
|
var isShared = (d, scopeDefault) => d.shared ?? scopeDefault;
|
|
1591
1741
|
function summariseProperty(d, scopeDefault) {
|
|
1592
1742
|
return {
|
|
@@ -1639,11 +1789,11 @@ function describeBundle(bundle) {
|
|
|
1639
1789
|
const sceneProps = [];
|
|
1640
1790
|
for (const scene of Object.values(bundle.scenes)) {
|
|
1641
1791
|
counts.scenes++;
|
|
1642
|
-
const gameId = (0,
|
|
1792
|
+
const gameId = (0, import_model3.effectiveGameId)(scene);
|
|
1643
1793
|
addresses.push({
|
|
1644
1794
|
gameId,
|
|
1645
1795
|
name: scene.name,
|
|
1646
|
-
blocks: scene.blocks.map((b) => ({ gameId: (0,
|
|
1796
|
+
blocks: scene.blocks.map((b) => ({ gameId: (0, import_model3.effectiveGameId)(b), name: b.name }))
|
|
1647
1797
|
});
|
|
1648
1798
|
for (const block of scene.blocks) countBlock(block, counts);
|
|
1649
1799
|
if (scene.sceneProps?.length) {
|