@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.js
CHANGED
|
@@ -40,7 +40,21 @@ function buildTagIndex(bundle) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// src/engine.ts
|
|
43
|
+
import { SAVE_VERSION } from "@patterkit/model";
|
|
43
44
|
var astCache = /* @__PURE__ */ new WeakMap();
|
|
45
|
+
var OWNER = "Patter";
|
|
46
|
+
var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
|
|
47
|
+
var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
|
|
48
|
+
var keys = {
|
|
49
|
+
/** A scene's SHARED `@scene` props (one bag per scene, every flow's). */
|
|
50
|
+
stage: (sceneId) => `patter/scene/${esc(sceneId)}`,
|
|
51
|
+
/** Everything one flow registers starts with this. */
|
|
52
|
+
flow: (flowId) => `patter/flow/${esc(flowId)}/`,
|
|
53
|
+
/** A flow's NOT-shared `@patter` globals. */
|
|
54
|
+
flowGlobals: (flowId) => `patter/flow/${esc(flowId)}/patter`,
|
|
55
|
+
/** A flow's NOT-shared `@scene` props for one scene. */
|
|
56
|
+
flowScene: (flowId, sceneId) => `patter/flow/${esc(flowId)}/scene/${esc(sceneId)}`
|
|
57
|
+
};
|
|
44
58
|
var Engine = class _Engine {
|
|
45
59
|
host;
|
|
46
60
|
defaultSeed;
|
|
@@ -100,18 +114,32 @@ var Engine = class _Engine {
|
|
|
100
114
|
const patterSharedDecls = props.filter((p) => p.shared ?? true).map(toDecl);
|
|
101
115
|
const patterLocalDecls = props.filter((p) => !(p.shared ?? true)).map(toDecl);
|
|
102
116
|
const patterSharedNames = new Set(patterSharedDecls.map((d) => d.name.toLowerCase()));
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
const registry = options.registry ?? new ScopeRegistry();
|
|
118
|
+
const ownsRegistry = !options.registry || options[OWNS_REGISTRY] === true;
|
|
119
|
+
const patterBag = new PropertyBag(patterSharedDecls, { pathPrefix: "@patter." });
|
|
120
|
+
const hostScopes = [];
|
|
121
|
+
const registered = [];
|
|
122
|
+
try {
|
|
123
|
+
registry.mountOwned("patter", patterBag, { owner: OWNER });
|
|
124
|
+
registered.push("patter");
|
|
125
|
+
const hostBound = /* @__PURE__ */ new Set();
|
|
126
|
+
if (options.world) {
|
|
127
|
+
const worldSpec = bundle.scopeRegistry?.scopes.find((s) => s.token === "world");
|
|
128
|
+
const decls = (worldSpec?.declarations ?? []).map(toForeignDecl);
|
|
129
|
+
registry.defineForeign("world", options.world, decls, { writable: worldSpec?.writable ?? true, owner: OWNER });
|
|
130
|
+
registered.push("world");
|
|
131
|
+
hostBound.add("world");
|
|
132
|
+
}
|
|
133
|
+
for (const spec of ownsRegistry ? bundle.scopeRegistry?.scopes ?? [] : []) {
|
|
134
|
+
if (hostBound.has(spec.token) || registry.has(spec.token)) continue;
|
|
135
|
+
const decls = (spec.declarations ?? []).map((d) => selfBackedDecl(d, spec.writable));
|
|
136
|
+
registry.defineOwned(spec.token, decls, { owner: OWNER });
|
|
137
|
+
registered.push(spec.token);
|
|
138
|
+
hostScopes.push(spec.token);
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
for (const k of registered) registry.remove(k, { keep: true });
|
|
142
|
+
throw e;
|
|
115
143
|
}
|
|
116
144
|
const sceneSharedNames = /* @__PURE__ */ new Map();
|
|
117
145
|
for (const [sceneId, scene] of Object.entries(bundle.scenes)) {
|
|
@@ -133,7 +161,10 @@ var Engine = class _Engine {
|
|
|
133
161
|
blockGameIdToId: this.blockGameIdToId,
|
|
134
162
|
// same instances the engine resolves with
|
|
135
163
|
tagIndex: buildTagIndex(bundle),
|
|
136
|
-
|
|
164
|
+
registry,
|
|
165
|
+
ownsRegistry,
|
|
166
|
+
patterBag,
|
|
167
|
+
hostScopes,
|
|
137
168
|
patterSharedDecls,
|
|
138
169
|
patterLocalDecls,
|
|
139
170
|
patterSharedNames,
|
|
@@ -150,7 +181,8 @@ var Engine = class _Engine {
|
|
|
150
181
|
captionClose: (bundle.closedCaptions ?? DEFAULT_CAPTION_DELIMITERS).close,
|
|
151
182
|
captionCharacter: bundle.closedCaptions?.character || DEFAULT_CAPTION_CHARACTER,
|
|
152
183
|
// absent/empty -> SFX
|
|
153
|
-
refSplitCache: /* @__PURE__ */ new Map()
|
|
184
|
+
refSplitCache: /* @__PURE__ */ new Map(),
|
|
185
|
+
refSplitRevision: registry.revision
|
|
154
186
|
};
|
|
155
187
|
}
|
|
156
188
|
/** The active locale (string + character-name lookups resolve in it). */
|
|
@@ -195,10 +227,12 @@ var Engine = class _Engine {
|
|
|
195
227
|
* content-drift policy (§9.8) resolves edits under the cursor: stack frames re-find their next
|
|
196
228
|
* child by id, drifted options drop, a vanished snippet is skipped.
|
|
197
229
|
*
|
|
198
|
-
* Returns the REPLACEMENT engine
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
230
|
+
* Returns the REPLACEMENT engine, on the same registry. This one hands its bags over (each is
|
|
231
|
+
* removed from the registry with its values kept, and the replacement claims them as it
|
|
232
|
+
* registers), its flows are closed, and it should be discarded. Hosts re-bind their flow handles
|
|
233
|
+
* via `next.getFlow(id)`. If the restore throws (defensive - §9.8 makes this unreachable for
|
|
234
|
+
* ordinary edits), the swap falls back to a fresh engine with each saved flow restarted from the
|
|
235
|
+
* top of the scene it was in; the shared properties carry over.
|
|
202
236
|
*/
|
|
203
237
|
hotSwap(bundle) {
|
|
204
238
|
const snapshot = this.saveGame();
|
|
@@ -207,12 +241,19 @@ var Engine = class _Engine {
|
|
|
207
241
|
next2.setClosedCaptions(this.host.captionsOn);
|
|
208
242
|
return next2;
|
|
209
243
|
};
|
|
210
|
-
const
|
|
244
|
+
const make = () => new _Engine(bundle, {
|
|
245
|
+
...this.creationOptions,
|
|
246
|
+
registry: this.host.registry,
|
|
247
|
+
[OWNS_REGISTRY]: this.host.ownsRegistry
|
|
248
|
+
});
|
|
249
|
+
this.release(true);
|
|
250
|
+
const next = make();
|
|
211
251
|
try {
|
|
212
252
|
next.loadGame(snapshot);
|
|
213
253
|
return carryOver(next);
|
|
214
254
|
} catch {
|
|
215
|
-
|
|
255
|
+
next.release(true);
|
|
256
|
+
const fresh = make();
|
|
216
257
|
for (const [id, f] of Object.entries(snapshot.flows)) {
|
|
217
258
|
const sceneId = f.cursor.currentSceneId;
|
|
218
259
|
try {
|
|
@@ -251,6 +292,7 @@ var Engine = class _Engine {
|
|
|
251
292
|
* call to reach for when you want a speaker's variation state to carry on.
|
|
252
293
|
*/
|
|
253
294
|
openFlow(id, opts = {}) {
|
|
295
|
+
this.assertExternalScopes();
|
|
254
296
|
const sceneId = this.resolveSceneRef(opts.scene);
|
|
255
297
|
const blockId = this.resolveBlockRef(sceneId, opts.block);
|
|
256
298
|
this.flowsById.get(id)?.close();
|
|
@@ -290,6 +332,17 @@ var Engine = class _Engine {
|
|
|
290
332
|
}
|
|
291
333
|
return existing.advanceToStop().played;
|
|
292
334
|
}
|
|
335
|
+
/** Content that names another engine's scope (`@story.act`) runs only where that engine is on this
|
|
336
|
+
* registry: without it every read would answer false and every write fail, so the flow is refused as
|
|
337
|
+
* it opens (or the save as it loads), before anything changes. By then a game has built all of its
|
|
338
|
+
* engines, whatever order it built them in. The same message on every runtime. */
|
|
339
|
+
assertExternalScopes() {
|
|
340
|
+
for (const token of this.host.bundle.externalScopes ?? []) {
|
|
341
|
+
if (!this.host.registry.has(token)) {
|
|
342
|
+
throw new Error(`this content names @${token}, which no engine on this registry registered: give every engine the game's one registry`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
293
346
|
/** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
|
|
294
347
|
resolveSceneRef(ref) {
|
|
295
348
|
if (ref == null) return void 0;
|
|
@@ -475,7 +528,7 @@ var Engine = class _Engine {
|
|
|
475
528
|
* loadGame() replaces every bag, so re-enumerate after a load.
|
|
476
529
|
*/
|
|
477
530
|
listBags() {
|
|
478
|
-
const mounts = [{ bag: this.host.
|
|
531
|
+
const mounts = [{ bag: this.host.patterBag }];
|
|
479
532
|
for (const [sceneId, bag] of this.host.stageBags) mounts.push({ bag, pathPrefix: `@scene:${sceneId}.` });
|
|
480
533
|
return mounts;
|
|
481
534
|
}
|
|
@@ -494,20 +547,42 @@ var Engine = class _Engine {
|
|
|
494
547
|
reset() {
|
|
495
548
|
for (const flow of this.flowsById.values()) flow.close();
|
|
496
549
|
this.flowsById.clear();
|
|
497
|
-
this.host.
|
|
550
|
+
this.host.patterBag.reseed(this.host.patterSharedDecls);
|
|
498
551
|
this.host.sharedVisits.clear();
|
|
499
552
|
this.host.sharedSelectors.clear();
|
|
553
|
+
for (const s of this.host.stageBags.keys()) this.host.registry.remove(keys.stage(s));
|
|
554
|
+
this.host.stageBags.clear();
|
|
555
|
+
this.host.registry.discardParked("patter/");
|
|
556
|
+
}
|
|
557
|
+
/** Remove every bag this engine registered, keeping the values parked when `keep` (a live reload
|
|
558
|
+
* handing its state to a replacement), and close its flows. The engine is inert afterwards. */
|
|
559
|
+
release(keep) {
|
|
560
|
+
for (const flow of this.flowsById.values()) {
|
|
561
|
+
flow.releaseBags(keep);
|
|
562
|
+
flow.close();
|
|
563
|
+
}
|
|
564
|
+
this.flowsById.clear();
|
|
565
|
+
const reg = this.host.registry;
|
|
566
|
+
for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep });
|
|
500
567
|
this.host.stageBags.clear();
|
|
568
|
+
for (const t of ["patter", ...this.host.hostScopes]) if (reg.has(t)) reg.remove(t, { keep });
|
|
569
|
+
if (this.creationOptions.world && reg.has("world")) reg.remove("world");
|
|
501
570
|
}
|
|
502
571
|
/** Read a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level). */
|
|
503
572
|
getProperty(ref) {
|
|
504
573
|
const { scope, name } = this.splitShared(ref);
|
|
505
|
-
return this.host.
|
|
574
|
+
return this.host.registry.get(scope, name);
|
|
506
575
|
}
|
|
507
|
-
/** Write a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level).
|
|
576
|
+
/** Write a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level).
|
|
577
|
+
*
|
|
578
|
+
* This is the GAME's surface, so it writes with host authority: a host-scope declaration's
|
|
579
|
+
* `writable: false` is the story's promise not to write the value, never a lock on the game that
|
|
580
|
+
* owns it. The story's own writes come through `Flow.runEffects`, which never claims it. (Ruled
|
|
581
|
+
* across the family 2026-09-05, after a Storylets venue clock and this project's coverage driver
|
|
582
|
+
* were each refused the one property they existed to move.) */
|
|
508
583
|
setProperty(ref, value) {
|
|
509
584
|
const { scope, name } = this.splitShared(ref);
|
|
510
|
-
this.host.
|
|
585
|
+
this.host.registry.set(scope, name, value, { host: true });
|
|
511
586
|
}
|
|
512
587
|
/** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
|
|
513
588
|
* value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
|
|
@@ -554,54 +629,66 @@ var Engine = class _Engine {
|
|
|
554
629
|
// @scene is scene-namespaced and needs a flow's current scene - silently
|
|
555
630
|
// routing it into the shared bag (as a junk "scene.x" key) was a trap.
|
|
556
631
|
splitShared(ref) {
|
|
557
|
-
|
|
558
|
-
if (!split) {
|
|
559
|
-
split = splitRef(ref, (t) => t === "scene" || this.host.shared.has(t));
|
|
560
|
-
this.host.refSplitCache.set(ref, split);
|
|
561
|
-
}
|
|
632
|
+
const split = splitHostRef(this.host, ref);
|
|
562
633
|
if (split.scope === "scene") {
|
|
563
634
|
throw new Error(`'${ref}': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);
|
|
564
635
|
}
|
|
565
636
|
return split;
|
|
566
637
|
}
|
|
567
|
-
/**
|
|
638
|
+
/** The shared `@patter` values alone, as `{ patter: {...} }`.
|
|
639
|
+
* @deprecated The registry holds every property now: a game saves `registry.save()` once, and a
|
|
640
|
+
* standalone engine's `saveGame()` carries its registry's values. Removed at the next breaking release. */
|
|
568
641
|
save() {
|
|
569
|
-
return this.host.
|
|
642
|
+
return { patter: this.host.patterBag.save() };
|
|
570
643
|
}
|
|
571
|
-
/**
|
|
644
|
+
/** Lay a `{ patter: {...} }` section over the shared `@patter` values; anything else is ignored.
|
|
645
|
+
* @deprecated See `save`. Removed at the next breaking release. */
|
|
572
646
|
load(blob) {
|
|
573
|
-
this.host.
|
|
647
|
+
if (blob.patter) this.host.patterBag.load(blob.patter);
|
|
574
648
|
}
|
|
575
|
-
/** Snapshot the whole game
|
|
649
|
+
/** Snapshot the whole game's NON-property state: visit counts, shared selector cursors, and every
|
|
650
|
+
* live flow's cursor and PRNG. The property values are the registry's: a standalone engine (one that
|
|
651
|
+
* made its own registry) carries them here under `registry`; a game that passed a registry saves it
|
|
652
|
+
* once itself, beside each engine's `saveGame()`. */
|
|
576
653
|
saveGame() {
|
|
577
654
|
const flows = {};
|
|
578
655
|
for (const [id, flow] of this.flowsById) flows[id] = flow.snapshot();
|
|
579
656
|
return {
|
|
580
|
-
version:
|
|
581
|
-
|
|
657
|
+
version: SAVE_VERSION,
|
|
658
|
+
...this.host.ownsRegistry ? { registry: this.host.registry.save() } : {},
|
|
582
659
|
sharedVisits: Object.fromEntries(this.host.sharedVisits),
|
|
583
660
|
sharedSelectors: serialiseSelectors(this.host.sharedSelectors),
|
|
584
|
-
stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, bag.save()])),
|
|
585
661
|
flows
|
|
586
662
|
};
|
|
587
663
|
}
|
|
588
|
-
/**
|
|
664
|
+
/**
|
|
665
|
+
* Restore a `saveGame()`: visit counts, shared selector cursors, and every flow. Property values come
|
|
666
|
+
* from the registry. A save that carries them (a standalone engine's, or a version 2 save from before
|
|
667
|
+
* the registry held them) has them moved into the registry here; otherwise the game loads its
|
|
668
|
+
* registry itself, before or after this call. Either order works: this engine's bags are handed back
|
|
669
|
+
* to the registry (values kept) and the restored flows claim them as they register.
|
|
670
|
+
*/
|
|
589
671
|
loadGame(save) {
|
|
590
|
-
|
|
591
|
-
|
|
672
|
+
const version = save.version;
|
|
673
|
+
if (version !== 2 && version !== SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
|
|
674
|
+
this.assertExternalScopes();
|
|
675
|
+
const reg = this.host.registry;
|
|
676
|
+
for (const [id, flow] of this.flowsById) {
|
|
677
|
+
flow.releaseBags(id in save.flows);
|
|
678
|
+
flow.close();
|
|
679
|
+
}
|
|
680
|
+
this.flowsById.clear();
|
|
681
|
+
for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep: true });
|
|
682
|
+
this.host.stageBags.clear();
|
|
683
|
+
const values = save.version === 2 ? sectionsFromV2(save) : save.registry;
|
|
684
|
+
if (values) {
|
|
685
|
+
if (this.host.ownsRegistry) reg.load(values);
|
|
686
|
+
else reg.load(values, { keepParked: true });
|
|
687
|
+
}
|
|
592
688
|
this.host.sharedVisits.clear();
|
|
593
689
|
for (const [id, n] of Object.entries(save.sharedVisits ?? {})) this.host.sharedVisits.set(id, n);
|
|
594
690
|
this.host.sharedSelectors.clear();
|
|
595
691
|
for (const [id, st] of deserialiseSelectors(save.sharedSelectors)) this.host.sharedSelectors.set(id, st);
|
|
596
|
-
this.host.stageBags.clear();
|
|
597
|
-
for (const [s, values] of Object.entries(save.stageBags ?? {})) {
|
|
598
|
-
const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
|
|
599
|
-
const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => shared.has(d.name.toLowerCase()));
|
|
600
|
-
const bag = new PropertyBag(decls);
|
|
601
|
-
bag.load(values);
|
|
602
|
-
this.host.stageBags.set(s, bag);
|
|
603
|
-
}
|
|
604
|
-
this.flowsById.clear();
|
|
605
692
|
for (const [id, snap] of Object.entries(save.flows)) {
|
|
606
693
|
const flow = new Flow(id, this.host, this.defaultSeed);
|
|
607
694
|
flow.restore(snap);
|
|
@@ -613,7 +700,9 @@ var Flow = class {
|
|
|
613
700
|
id;
|
|
614
701
|
host;
|
|
615
702
|
local;
|
|
616
|
-
//
|
|
703
|
+
// the NOT-shared `@patter` globals (this flow's copy), keys.flowGlobals
|
|
704
|
+
/** The registry keys this flow has registered (its globals and each scene bag). */
|
|
705
|
+
registered = /* @__PURE__ */ new Set();
|
|
617
706
|
rngState;
|
|
618
707
|
// Execution cursor. The `stack` is the continuation stack: each frame is a
|
|
619
708
|
// position within a block's children (the top frame is the active block run;
|
|
@@ -638,40 +727,44 @@ var Flow = class {
|
|
|
638
727
|
/** Per-node entry counts for this flow (node id -> times entered). */
|
|
639
728
|
visitCounts = /* @__PURE__ */ new Map();
|
|
640
729
|
// Per-flow halves of the two scopes. The NOT-shared `@patter` globals live in
|
|
641
|
-
// `local
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
// resolver presents one merged scope, routing each property
|
|
645
|
-
// declared `shared` flag
|
|
730
|
+
// `local`; the NOT-shared `@scene` props live in `sceneBags` (namespaced per
|
|
731
|
+
// scene; they PERSIST across re-entries, spec §7). The SHARED halves live on the
|
|
732
|
+
// host (`host.patterBag` / `host.stageBags`). All of them are registered in the
|
|
733
|
+
// game's registry. Each resolver presents one merged scope, routing each property
|
|
734
|
+
// to its half by the declared `shared` flag: that split is why `@patter` and
|
|
735
|
+
// `@scene` are composed here rather than aliased to a single registry key.
|
|
646
736
|
/** This flow's per-scene LOCAL scene props; see FlowHost.stageBags. */
|
|
647
737
|
sceneBags = /* @__PURE__ */ new Map();
|
|
648
738
|
patterResolver = {
|
|
649
|
-
get: (n) => this.host.patterSharedNames.has(n) ? this.host.
|
|
739
|
+
get: (n) => this.host.patterSharedNames.has(n) ? this.host.patterBag.get(n) : this.local.get(n),
|
|
650
740
|
set: (n, v) => {
|
|
651
|
-
if (this.host.patterSharedNames.has(n)) this.host.
|
|
652
|
-
else this.local.set(
|
|
741
|
+
if (this.host.patterSharedNames.has(n)) this.host.registry.set("patter", n, v);
|
|
742
|
+
else this.local.set(n, v);
|
|
653
743
|
}
|
|
654
744
|
};
|
|
655
745
|
sceneResolver = {
|
|
656
746
|
get: (n) => {
|
|
657
747
|
const s = this.currentSceneId;
|
|
658
748
|
if (s === null) return void 0;
|
|
659
|
-
|
|
660
|
-
return bag?.get(n);
|
|
749
|
+
return this.sceneBagFor(s, n)?.get(n);
|
|
661
750
|
},
|
|
662
751
|
set: (n, v) => {
|
|
663
752
|
const s = this.currentSceneId;
|
|
664
753
|
if (s === null) return;
|
|
665
|
-
const bag = this.
|
|
754
|
+
const bag = this.sceneBagFor(s, n);
|
|
666
755
|
if (bag) bag.set(n, v);
|
|
667
756
|
}
|
|
668
757
|
};
|
|
669
|
-
// The eval context is built
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
758
|
+
// The eval context is built once and REFRESHED only when the registry's set of
|
|
759
|
+
// scopes moves (its `revision`): every constituent resolves live state at call
|
|
760
|
+
// time (bags mutate in place per scoperegistry's contract; patter/scene route
|
|
761
|
+
// through this flow's resolvers, which read the current `local`/`sceneBags`/
|
|
762
|
+
// `currentSceneId`; the host callbacks read current flow fields), but another
|
|
763
|
+
// engine registering `@story` after this flow opened must still be readable.
|
|
764
|
+
// Rebuilding it per evaluation was the engine's hottest allocation.
|
|
674
765
|
evalCtx;
|
|
766
|
+
ctxRevision = -1;
|
|
767
|
+
registryQualities;
|
|
675
768
|
flowLog = [];
|
|
676
769
|
/** Monotonic across the flow's life; survives clearLog so two reads agree on order. */
|
|
677
770
|
flowSeq = 0;
|
|
@@ -679,12 +772,9 @@ var Flow = class {
|
|
|
679
772
|
this.id = id;
|
|
680
773
|
this.host = host;
|
|
681
774
|
this.rngState = toUint32(seed);
|
|
682
|
-
this.local = this.
|
|
683
|
-
const scopes = { ...host.shared.toEvalContext().scopes };
|
|
684
|
-
scopes["patter"] = this.patterResolver;
|
|
685
|
-
scopes["scene"] = this.sceneResolver;
|
|
775
|
+
this.local = this.newLocal();
|
|
686
776
|
this.evalCtx = {
|
|
687
|
-
scopes,
|
|
777
|
+
scopes: {},
|
|
688
778
|
host: {
|
|
689
779
|
nextRandom: this.rng,
|
|
690
780
|
visits: (id2) => this.visitCounts.get(id2) ?? 0,
|
|
@@ -697,6 +787,21 @@ var Flow = class {
|
|
|
697
787
|
qualities: (scope, name) => this.stagesFor(scope, name)
|
|
698
788
|
};
|
|
699
789
|
}
|
|
790
|
+
/** The eval context, its scopes refreshed if the registry's set of scopes has moved since. */
|
|
791
|
+
context() {
|
|
792
|
+
const reg = this.host.registry;
|
|
793
|
+
if (reg.revision !== this.ctxRevision) {
|
|
794
|
+
const base = reg.toEvalContext();
|
|
795
|
+
const scopes = this.evalCtx.scopes;
|
|
796
|
+
for (const k of Object.keys(scopes)) delete scopes[k];
|
|
797
|
+
Object.assign(scopes, base.scopes);
|
|
798
|
+
scopes["patter"] = this.patterResolver;
|
|
799
|
+
scopes["scene"] = this.sceneResolver;
|
|
800
|
+
this.registryQualities = base.qualities;
|
|
801
|
+
this.ctxRevision = reg.revision;
|
|
802
|
+
}
|
|
803
|
+
return this.evalCtx;
|
|
804
|
+
}
|
|
700
805
|
/** The stage ladder of `@scope.name` when it is a declared quality, else undefined. Names compare
|
|
701
806
|
* lowercase, as the compiler emits references (the selfBackedResolver lesson). */
|
|
702
807
|
stagesFor(scope, name) {
|
|
@@ -709,13 +814,14 @@ var Flow = class {
|
|
|
709
814
|
const scene = this.currentSceneId != null ? this.host.bundle.scenes[this.currentSceneId] : void 0;
|
|
710
815
|
return fromDecls(scene?.sceneProps);
|
|
711
816
|
}
|
|
712
|
-
return fromDecls(this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations);
|
|
817
|
+
return this.registryQualities?.(scope, name) ?? fromDecls(this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations);
|
|
713
818
|
}
|
|
714
819
|
// -- Host API -------------------------------------------------------------
|
|
715
820
|
/** Begin this flow at a scene (and optionally a specific block within it). */
|
|
716
821
|
start(sceneId, blockId) {
|
|
717
|
-
this.
|
|
718
|
-
this.
|
|
822
|
+
this.releaseBags(false);
|
|
823
|
+
this.host.registry.discardParked(keys.flow(this.id));
|
|
824
|
+
this.mountLocal();
|
|
719
825
|
this.selectors.clear();
|
|
720
826
|
this.visitCounts.clear();
|
|
721
827
|
this.stack = [];
|
|
@@ -815,6 +921,7 @@ var Flow = class {
|
|
|
815
921
|
* cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
|
|
816
922
|
*/
|
|
817
923
|
close() {
|
|
924
|
+
this.releaseBags(false);
|
|
818
925
|
this.closed = true;
|
|
819
926
|
this.flowEnded = true;
|
|
820
927
|
this.stack = [];
|
|
@@ -924,7 +1031,7 @@ var Flow = class {
|
|
|
924
1031
|
* halves are the Engine's listBags.
|
|
925
1032
|
*/
|
|
926
1033
|
listBags() {
|
|
927
|
-
const mounts = [{ bag: this.local
|
|
1034
|
+
const mounts = [{ bag: this.local, pathPrefix: `${this.id}/@patter.` }];
|
|
928
1035
|
for (const [sceneId, bag] of this.sceneBags) mounts.push({ bag, pathPrefix: `${this.id}/@scene:${sceneId}.` });
|
|
929
1036
|
return mounts;
|
|
930
1037
|
}
|
|
@@ -972,10 +1079,17 @@ var Flow = class {
|
|
|
972
1079
|
const { scope, name } = this.splitRef(ref);
|
|
973
1080
|
if (scope === "patter") return this.patterResolver.get(name);
|
|
974
1081
|
if (scope === "scene") return this.sceneResolver.get(name);
|
|
975
|
-
return this.host.
|
|
1082
|
+
return this.host.registry.get(scope, name);
|
|
976
1083
|
}
|
|
977
|
-
/** Write a property by ref (routed by scope, then by the property's `shared` flag).
|
|
1084
|
+
/** Write a property by ref (routed by scope, then by the property's `shared` flag). The GAME's
|
|
1085
|
+
* surface: it writes with host authority, so a `writable: false` host declaration does not refuse
|
|
1086
|
+
* it (see `Engine.setProperty`). The story's writes go through `writeProperty` instead. */
|
|
978
1087
|
setProperty(ref, value) {
|
|
1088
|
+
this.writeProperty(ref, value, true);
|
|
1089
|
+
}
|
|
1090
|
+
/** The write itself. `host` says WHO is writing, which is the only thing `writable: false` cares
|
|
1091
|
+
* about: the story is refused, the game is not. */
|
|
1092
|
+
writeProperty(ref, value, host) {
|
|
979
1093
|
const { scope, name } = this.splitRef(ref);
|
|
980
1094
|
if (scope === "patter") {
|
|
981
1095
|
this.patterResolver.set(name, value);
|
|
@@ -983,16 +1097,13 @@ var Flow = class {
|
|
|
983
1097
|
if (this.currentSceneId === null) throw new Error(`'${ref}': the flow has not entered a scene yet`);
|
|
984
1098
|
this.sceneResolver.set(name, value);
|
|
985
1099
|
} else {
|
|
986
|
-
this.host.
|
|
1100
|
+
this.host.registry.set(scope, name, value, host ? { host: true } : void 0);
|
|
987
1101
|
}
|
|
988
1102
|
}
|
|
989
1103
|
// -- Save / restore (engine-driven) --------------------------------------
|
|
990
|
-
/** @internal Snapshot this flow's cursor +
|
|
1104
|
+
/** @internal Snapshot this flow's cursor + PRNG + visits. Its properties are the registry's. */
|
|
991
1105
|
snapshot() {
|
|
992
1106
|
return {
|
|
993
|
-
scopes: this.local.save(),
|
|
994
|
-
// owned scope "patter" = the NOT-shared globals (@scene saved separately)
|
|
995
|
-
sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, bag.save()])),
|
|
996
1107
|
rngState: this.rngState,
|
|
997
1108
|
visits: Object.fromEntries(this.visitCounts),
|
|
998
1109
|
cursor: {
|
|
@@ -1030,15 +1141,11 @@ var Flow = class {
|
|
|
1030
1141
|
}
|
|
1031
1142
|
return { ...frame };
|
|
1032
1143
|
});
|
|
1033
|
-
this.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
return [s, bag];
|
|
1039
|
-
}));
|
|
1040
|
-
this.local = this.freshLocal();
|
|
1041
|
-
this.local.load(snap.scopes);
|
|
1144
|
+
this.releaseBags(false);
|
|
1145
|
+
this.mountLocal();
|
|
1146
|
+
for (const s of /* @__PURE__ */ new Set([c.currentSceneId, ...this.stack.map((f) => f.sceneId)])) {
|
|
1147
|
+
if (s !== null && this.host.bundle.scenes[s]) this.ensureSceneBags(s);
|
|
1148
|
+
}
|
|
1042
1149
|
this.activeSnippet = null;
|
|
1043
1150
|
if (c.activeSnippetId !== null) {
|
|
1044
1151
|
const node = this.host.nodeIndex.get(c.activeSnippetId);
|
|
@@ -1309,7 +1416,7 @@ var Flow = class {
|
|
|
1309
1416
|
* node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
|
|
1310
1417
|
*/
|
|
1311
1418
|
matchedSpec(node, want) {
|
|
1312
|
-
const evalTruthy = (n) => truthy(evaluate(n, this.
|
|
1419
|
+
const evalTruthy = (n) => truthy(evaluate(n, this.context(), patterDialect));
|
|
1313
1420
|
return scoreSpecificity(node, evalTruthy, { want });
|
|
1314
1421
|
}
|
|
1315
1422
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
@@ -1327,7 +1434,7 @@ var Flow = class {
|
|
|
1327
1434
|
for (const e of effects ?? []) {
|
|
1328
1435
|
const value = this.evalExpr(e.value);
|
|
1329
1436
|
const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
|
|
1330
|
-
this.
|
|
1437
|
+
this.writeProperty(e.target, value, false);
|
|
1331
1438
|
this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
|
|
1332
1439
|
}
|
|
1333
1440
|
}
|
|
@@ -1336,7 +1443,7 @@ var Flow = class {
|
|
|
1336
1443
|
return truthy(this.evalExpr(node.condition));
|
|
1337
1444
|
}
|
|
1338
1445
|
evalExpr(expr) {
|
|
1339
|
-
return evaluate(this.conditionAst(expr), this.
|
|
1446
|
+
return evaluate(this.conditionAst(expr), this.context(), patterDialect);
|
|
1340
1447
|
}
|
|
1341
1448
|
/** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
|
|
1342
1449
|
* evaluator and the Best-match specificity walker so both work off one parse. */
|
|
@@ -1461,18 +1568,51 @@ var Flow = class {
|
|
|
1461
1568
|
const key = castStringKey(character);
|
|
1462
1569
|
return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.castDisplay.get(character);
|
|
1463
1570
|
}
|
|
1464
|
-
/** Split a ref into scope + name. Tokens: `@scene`,
|
|
1571
|
+
/** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
|
|
1465
1572
|
splitRef(ref) {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1573
|
+
return splitHostRef(this.host, ref);
|
|
1574
|
+
}
|
|
1575
|
+
/** A fresh, unregistered bag for the NOT-shared `@patter` globals (the shared ones live on the host). */
|
|
1576
|
+
newLocal() {
|
|
1577
|
+
return new PropertyBag(this.host.patterLocalDecls, { pathPrefix: "@patter." });
|
|
1578
|
+
}
|
|
1579
|
+
/** Register a fresh globals bag under this flow's key; it claims any values waiting there. */
|
|
1580
|
+
mountLocal() {
|
|
1581
|
+
this.local = this.newLocal();
|
|
1582
|
+
const key = keys.flowGlobals(this.id);
|
|
1583
|
+
this.host.registry.mountOwned(key, this.local, { owner: OWNER });
|
|
1584
|
+
this.registered.add(key);
|
|
1585
|
+
}
|
|
1586
|
+
/** @internal Remove every bag this flow registered; with `keep`, their values wait in the registry for
|
|
1587
|
+
* the flow that replaces this one. Engine-driven (close, loadGame, hotSwap). */
|
|
1588
|
+
releaseBags(keep) {
|
|
1589
|
+
for (const key of this.registered) this.host.registry.remove(key, { keep });
|
|
1590
|
+
this.registered.clear();
|
|
1591
|
+
this.sceneBags.clear();
|
|
1472
1592
|
}
|
|
1473
|
-
/** The
|
|
1474
|
-
|
|
1475
|
-
|
|
1593
|
+
/** The bag a `@scene` property of scene `s` lives in (stage or this flow's), made if missing. */
|
|
1594
|
+
sceneBagFor(s, name) {
|
|
1595
|
+
if (!this.host.bundle.scenes[s]) return void 0;
|
|
1596
|
+
this.ensureSceneBags(s);
|
|
1597
|
+
return this.host.sceneSharedNames.get(s)?.has(name.toLowerCase()) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
|
|
1598
|
+
}
|
|
1599
|
+
/** Make (and register) scene `s`'s stage bag and this flow's bag for it, if not made yet. A bag made
|
|
1600
|
+
* here claims whatever values the registry holds for its key: that is how a loaded save reaches it. */
|
|
1601
|
+
ensureSceneBags(s) {
|
|
1602
|
+
const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
|
|
1603
|
+
const props = this.host.bundle.scenes[s]?.sceneProps ?? [];
|
|
1604
|
+
if (!this.sceneBags.has(s)) {
|
|
1605
|
+
const bag = new PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1606
|
+
const key = keys.flowScene(this.id, s);
|
|
1607
|
+
this.host.registry.mountOwned(key, bag, { owner: OWNER });
|
|
1608
|
+
this.registered.add(key);
|
|
1609
|
+
this.sceneBags.set(s, bag);
|
|
1610
|
+
}
|
|
1611
|
+
if (!this.host.stageBags.has(s)) {
|
|
1612
|
+
const bag = new PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
|
|
1613
|
+
this.host.registry.mountOwned(keys.stage(s), bag, { owner: OWNER });
|
|
1614
|
+
this.host.stageBags.set(s, bag);
|
|
1615
|
+
}
|
|
1476
1616
|
}
|
|
1477
1617
|
/**
|
|
1478
1618
|
* Seed a scene's `@scene` props (spec §7). The not-shared props seed THIS flow's
|
|
@@ -1483,13 +1623,7 @@ var Flow = class {
|
|
|
1483
1623
|
*/
|
|
1484
1624
|
seedScene(scene) {
|
|
1485
1625
|
const shared = this.host.sceneSharedNames.get(scene.id) ?? /* @__PURE__ */ new Set();
|
|
1486
|
-
|
|
1487
|
-
if (!this.sceneBags.has(scene.id)) {
|
|
1488
|
-
this.sceneBags.set(scene.id, new PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase()))));
|
|
1489
|
-
}
|
|
1490
|
-
if (!this.host.stageBags.has(scene.id)) {
|
|
1491
|
-
this.host.stageBags.set(scene.id, new PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase()))));
|
|
1492
|
-
}
|
|
1626
|
+
this.ensureSceneBags(scene.id);
|
|
1493
1627
|
for (const decl of scene.sceneProps ?? []) {
|
|
1494
1628
|
if (!decl.temporary) continue;
|
|
1495
1629
|
const name = decl.name.toLowerCase();
|
|
@@ -1535,16 +1669,32 @@ function toDecl(decl) {
|
|
|
1535
1669
|
function toForeignDecl(decl) {
|
|
1536
1670
|
return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
|
|
1537
1671
|
}
|
|
1538
|
-
function
|
|
1539
|
-
const
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
}
|
|
1672
|
+
function selfBackedDecl(decl, scopeWritable) {
|
|
1673
|
+
const d = toForeignDecl(decl);
|
|
1674
|
+
const writable = decl.writable ?? scopeWritable;
|
|
1675
|
+
return writable === void 0 ? d : { ...d, writable };
|
|
1676
|
+
}
|
|
1677
|
+
function splitHostRef(host, ref) {
|
|
1678
|
+
if (host.refSplitRevision !== host.registry.revision) {
|
|
1679
|
+
host.refSplitCache.clear();
|
|
1680
|
+
host.refSplitRevision = host.registry.revision;
|
|
1681
|
+
}
|
|
1682
|
+
let hit = host.refSplitCache.get(ref);
|
|
1683
|
+
if (!hit) {
|
|
1684
|
+
hit = splitRef(ref, (t) => t === "scene" || host.registry.has(t) || (host.bundle.externalScopes?.includes(t) ?? false));
|
|
1685
|
+
host.refSplitCache.set(ref, hit);
|
|
1686
|
+
}
|
|
1687
|
+
return hit;
|
|
1688
|
+
}
|
|
1689
|
+
function sectionsFromV2(save) {
|
|
1690
|
+
const out = {};
|
|
1691
|
+
if (save.shared?.patter) out["patter"] = save.shared.patter;
|
|
1692
|
+
for (const [s, v] of Object.entries(save.stageBags ?? {})) out[keys.stage(s)] = v;
|
|
1693
|
+
for (const [id, f] of Object.entries(save.flows ?? {})) {
|
|
1694
|
+
if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
|
|
1695
|
+
for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
|
|
1696
|
+
}
|
|
1697
|
+
return out;
|
|
1548
1698
|
}
|
|
1549
1699
|
function truthy(v) {
|
|
1550
1700
|
if (typeof v === "boolean") return v;
|