@patterkit/runtime 0.8.0 → 0.10.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/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { evaluate, deserialiseAst, makePrng, toUint32 } from "@wildwinter/expr";
3
3
  import { matchedSpecificity as scoreSpecificity } from "@wildwinter/expr-specificity";
4
4
  import { ScopeRegistry } from "@wildwinter/scoperegistry";
5
+ import { defaultFor, PropertyBag } from "@wildwinter/scoperegistry";
5
6
  import { patterDialect, interpolate, splitRef, stripCaptions } from "@patterkit/dialect";
6
7
  import { walkNodes, effectiveGameId, castStringKey, DEFAULT_CAPTION_DELIMITERS, DEFAULT_CAPTION_CHARACTER } from "@patterkit/model";
7
8
 
@@ -60,6 +61,10 @@ var Engine = class _Engine {
60
61
  /** The options this engine was built with - reused verbatim by `hotSwap` so the replacement
61
62
  * engine keeps the same world resolver, custom RNG, and diagnostic hooks. */
62
63
  creationOptions;
64
+ /** The run's ordered stream: every flow's events, each naming its flow. Empty and
65
+ * unwritten unless `options.log` asked for it. */
66
+ engineLog = [];
67
+ engineTraceHandlers = /* @__PURE__ */ new Set();
63
68
  constructor(bundle, options = {}) {
64
69
  this.creationOptions = options;
65
70
  const locale = options.locale ?? bundle.locales.default;
@@ -95,7 +100,7 @@ var Engine = class _Engine {
95
100
  const patterSharedDecls = props.filter((p) => p.shared ?? true).map(toDecl);
96
101
  const patterLocalDecls = props.filter((p) => !(p.shared ?? true)).map(toDecl);
97
102
  const patterSharedNames = new Set(patterSharedDecls.map((d) => d.name.toLowerCase()));
98
- const shared = new ScopeRegistry().defineOwned("patter", patterSharedDecls);
103
+ const shared = new ScopeRegistry().defineOwned("patter", patterSharedDecls, "@patter.");
99
104
  const hostBound = /* @__PURE__ */ new Set();
100
105
  if (options.world) {
101
106
  const worldSpec = bundle.scopeRegistry?.scopes.find((s) => s.token === "world");
@@ -114,6 +119,8 @@ var Engine = class _Engine {
114
119
  sceneSharedNames.set(sceneId, names);
115
120
  }
116
121
  this.host = {
122
+ logEnabled: options.log ?? false,
123
+ emitEngine: (flow, event, scene) => this.emitEngine(flow, event, scene),
117
124
  bundle,
118
125
  emitIds,
119
126
  strings,
@@ -456,6 +463,22 @@ var Engine = class _Engine {
456
463
  flows() {
457
464
  return [...this.flowsById.values()];
458
465
  }
466
+ /**
467
+ * The SHARED kernel bags with the path each answers to in a log: the `@patter` globals,
468
+ * and one per scene for the shared `@scene` props. Parity with the Storylet Engine's
469
+ * listBags of the same name - it is what a state logger mounts.
470
+ *
471
+ * A stage bag's log path is `@scene:<sceneId>.`, not the bag's own `@scene.`: a property
472
+ * is ADDRESSED relative to a flow's current scene, but a log spans scenes and has to say
473
+ * which one. That is why a mount may override the bag's prefix.
474
+ *
475
+ * loadGame() replaces every bag, so re-enumerate after a load.
476
+ */
477
+ listBags() {
478
+ const mounts = [{ bag: this.host.shared.ownedBag("patter") }];
479
+ for (const [sceneId, bag] of this.host.stageBags) mounts.push({ bag, pathPrefix: `@scene:${sceneId}.` });
480
+ return mounts;
481
+ }
459
482
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
460
483
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
461
484
  closeFlow(id) {
@@ -488,14 +511,44 @@ var Engine = class _Engine {
488
511
  }
489
512
  /** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
490
513
  * value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
514
+ /** The run's decisions, in order, each naming the flow it happened in. Empty unless the
515
+ * run was opened with `log: true`. A flow's own log stays flow-local; this is the only
516
+ * place a story spanning several flows reads as one sequence. */
517
+ log() {
518
+ return this.engineLog;
519
+ }
520
+ /** Drop the retained entries. `seq` does NOT restart: two reads of a log either side of a
521
+ * clear still agree about what came first. */
522
+ clearLog() {
523
+ this.engineLog.length = 0;
524
+ }
525
+ /** Live tap on the run's decisions, for tooling that wants them as they happen rather than
526
+ * retained. Returns its own unsubscribe. */
527
+ onTrace(handler) {
528
+ this.engineTraceHandlers.add(handler);
529
+ return () => this.engineTraceHandlers.delete(handler);
530
+ }
531
+ emitEngine(flow, event, scene) {
532
+ for (const h of this.engineTraceHandlers) h(flow, event);
533
+ if (!this.host.logEnabled) return;
534
+ this.engineLog.push({ ...event, flow, seq: this.engineLog.length, ...scene ? { scene } : {} });
535
+ }
491
536
  listProperties() {
492
537
  return this.host.patterSharedDecls.map((d) => ({
493
- ref: `@${d.name}`,
538
+ name: d.name,
539
+ // The qualified address, matching what the bag composes for every other scope.
540
+ // `@gold` still resolves on input; it is the shorthand, not the address.
541
+ path: `@patter.${d.name}`,
494
542
  type: d.type,
495
543
  values: d.values,
496
544
  stages: d.stages,
497
545
  value: this.getProperty(`@${d.name}`),
498
- default: declDefault(d)
546
+ default: defaultFor(d),
547
+ // Part of the shared row. Always true here today: `toDecl` never sets it, because
548
+ // Patter has no read-only shared property. The Storylet Engine does declare them
549
+ // and its panels disable the editor accordingly, so the field is carried rather
550
+ // than dropped - and the day a read-only @patter property exists, the row says so.
551
+ writable: d.writable ?? true
499
552
  }));
500
553
  }
501
554
  // @scene is scene-namespaced and needs a flow's current scene - silently
@@ -528,7 +581,7 @@ var Engine = class _Engine {
528
581
  shared: this.host.shared.save(),
529
582
  sharedVisits: Object.fromEntries(this.host.sharedVisits),
530
583
  sharedSelectors: serialiseSelectors(this.host.sharedSelectors),
531
- stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, { ...bag }])),
584
+ stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, bag.save()])),
532
585
  flows
533
586
  };
534
587
  }
@@ -541,7 +594,13 @@ var Engine = class _Engine {
541
594
  this.host.sharedSelectors.clear();
542
595
  for (const [id, st] of deserialiseSelectors(save.sharedSelectors)) this.host.sharedSelectors.set(id, st);
543
596
  this.host.stageBags.clear();
544
- for (const [s, bag] of Object.entries(save.stageBags ?? {})) this.host.stageBags.set(s, { ...bag });
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
+ }
545
604
  this.flowsById.clear();
546
605
  for (const [id, snap] of Object.entries(save.flows)) {
547
606
  const flow = new Flow(id, this.host, this.defaultSeed);
@@ -584,6 +643,7 @@ var Flow = class {
584
643
  // The SHARED halves live on the host (`host.shared` / `host.stageBags`). Each
585
644
  // resolver presents one merged scope, routing each property to its half by the
586
645
  // declared `shared` flag.
646
+ /** This flow's per-scene LOCAL scene props; see FlowHost.stageBags. */
587
647
  sceneBags = /* @__PURE__ */ new Map();
588
648
  patterResolver = {
589
649
  get: (n) => this.host.patterSharedNames.has(n) ? this.host.shared.get("patter", n) : this.local.get("patter", n),
@@ -597,13 +657,13 @@ var Flow = class {
597
657
  const s = this.currentSceneId;
598
658
  if (s === null) return void 0;
599
659
  const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
600
- return bag?.[n];
660
+ return bag?.get(n);
601
661
  },
602
662
  set: (n, v) => {
603
663
  const s = this.currentSceneId;
604
664
  if (s === null) return;
605
665
  const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
606
- if (bag) bag[n] = v;
666
+ if (bag) bag.set(n, v);
607
667
  }
608
668
  };
609
669
  // The eval context is built ONCE: every constituent resolves live state at
@@ -612,6 +672,9 @@ var Flow = class {
612
672
  // `local`/`sceneBags`/`currentSceneId`; the host callbacks read current flow
613
673
  // fields). Rebuilding it per evaluation was the engine's hottest allocation.
614
674
  evalCtx;
675
+ flowLog = [];
676
+ /** Monotonic across the flow's life; survives clearLog so two reads agree on order. */
677
+ flowSeq = 0;
615
678
  constructor(id, host, seed) {
616
679
  this.id = id;
617
680
  this.host = host;
@@ -837,7 +900,17 @@ var Flow = class {
837
900
  this.stack.pop();
838
901
  continue;
839
902
  }
903
+ const from = frame.index;
840
904
  while (frame.index < children.length && !this.eligible(children[frame.index])) frame.index++;
905
+ if (this.host.logEnabled && frame.index !== from) {
906
+ this.emit({
907
+ type: "select",
908
+ group: frame.containerId,
909
+ selector: "run",
910
+ children: children.slice(from, frame.index + 1).map((c, i) => ({ id: c.id, eligible: from + i === frame.index })),
911
+ picked: children[frame.index]?.id ?? null
912
+ });
913
+ }
841
914
  if (frame.index >= children.length) {
842
915
  this.stack.pop();
843
916
  continue;
@@ -845,6 +918,34 @@ var Flow = class {
845
918
  this.enterChild(children[frame.index++]);
846
919
  }
847
920
  }
921
+ /**
922
+ * THIS flow's own kernel bags: its not-shared `@patter` half and its per-scene `@scene`
923
+ * props, each prefixed with the flow id so one path space holds every flow. The shared
924
+ * halves are the Engine's listBags.
925
+ */
926
+ listBags() {
927
+ const mounts = [{ bag: this.local.ownedBag("patter"), pathPrefix: `${this.id}/@patter.` }];
928
+ for (const [sceneId, bag] of this.sceneBags) mounts.push({ bag, pathPrefix: `${this.id}/@scene:${sceneId}.` });
929
+ return mounts;
930
+ }
931
+ /** This flow's decisions, in order. Empty unless the run was opened with `log: true`.
932
+ * The engine's log carries the same events tagged with the flow; this one is what a
933
+ * single conversation reads as. */
934
+ log() {
935
+ return this.flowLog;
936
+ }
937
+ /** Drop the retained entries. `seq` keeps counting, so order survives a clear. */
938
+ clearLog() {
939
+ this.flowLog.length = 0;
940
+ }
941
+ /** Record one decision, on this flow's log and the engine's. Cheap to call with logging
942
+ * off: the entry is never built. */
943
+ emit(event) {
944
+ const scene = this.currentSceneId ?? void 0;
945
+ this.host.emitEngine(this.id, event, scene);
946
+ if (!this.host.logEnabled) return;
947
+ this.flowLog.push({ ...event, seq: this.flowSeq++, ...scene ? { scene } : {} });
948
+ }
848
949
  /** The options of a pending choice (empty when not at a choice point). */
849
950
  getChoices() {
850
951
  return this.pendingChoice?.options ?? [];
@@ -857,6 +958,7 @@ var Flow = class {
857
958
  if (!option) throw new Error(`unknown choice option: ${id}`);
858
959
  if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
859
960
  const node = choice.byId.get(id);
961
+ this.emit({ type: "chose", group: choice.groupId, option: id });
860
962
  this.pendingChoice = null;
861
963
  this.pendingPromptBeat = this.host.replayPromptOnChoose ? this.promptBeatOf(node) ?? null : null;
862
964
  this.pendingPromptOwnerId = this.pendingPromptBeat ? node.id : null;
@@ -890,7 +992,7 @@ var Flow = class {
890
992
  return {
891
993
  scopes: this.local.save(),
892
994
  // owned scope "patter" = the NOT-shared globals (@scene saved separately)
893
- sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, { ...bag }])),
995
+ sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, bag.save()])),
894
996
  rngState: this.rngState,
895
997
  visits: Object.fromEntries(this.visitCounts),
896
998
  cursor: {
@@ -928,7 +1030,13 @@ var Flow = class {
928
1030
  }
929
1031
  return { ...frame };
930
1032
  });
931
- this.sceneBags = new Map(Object.entries(snap.sceneBags ?? {}).map(([s, bag]) => [s, { ...bag }]));
1033
+ this.sceneBags = new Map(Object.entries(snap.sceneBags ?? {}).map(([s, values]) => {
1034
+ const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
1035
+ const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => !shared.has(d.name.toLowerCase()));
1036
+ const bag = new PropertyBag(decls);
1037
+ bag.load(values);
1038
+ return [s, bag];
1039
+ }));
932
1040
  this.local = this.freshLocal();
933
1041
  this.local.load(snap.scopes);
934
1042
  this.activeSnippet = null;
@@ -1022,6 +1130,7 @@ var Flow = class {
1022
1130
  byId.set(child.id, child);
1023
1131
  }
1024
1132
  if (options.length > 0) {
1133
+ this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
1025
1134
  this.pendingChoice = { groupId: group.id, options, byId };
1026
1135
  return;
1027
1136
  }
@@ -1030,6 +1139,7 @@ var Flow = class {
1030
1139
  this.enterChild(fallback);
1031
1140
  return;
1032
1141
  }
1142
+ this.emit({ type: "dry", group: group.id });
1033
1143
  this.host.onDryChoice?.(group.id);
1034
1144
  }
1035
1145
  // -- Jumps (jump / call-return) ----------------------------------------
@@ -1044,6 +1154,7 @@ var Flow = class {
1044
1154
  * hard-ends the flow regardless of the callstack.
1045
1155
  */
1046
1156
  enterTarget(to, mode) {
1157
+ this.emit({ type: "jump", to, mode });
1047
1158
  if (to === "END") {
1048
1159
  this.flowEnded = true;
1049
1160
  this.stack = [];
@@ -1075,17 +1186,28 @@ var Flow = class {
1075
1186
  }
1076
1187
  // -- Selectors ------------------------------------------------------------
1077
1188
  selectChild(group) {
1189
+ const verdicts = group.children.map((c) => ({ id: c.id, eligible: this.eligible(c) }));
1078
1190
  const eligible = group.children.filter((c) => this.eligible(c));
1079
- if (eligible.length === 0) return null;
1191
+ const order = group.options?.order ?? "sequential";
1192
+ const exhaust = group.options?.exhaust ?? "once";
1193
+ const trace = (picked) => {
1194
+ this.emit({
1195
+ type: "select",
1196
+ group: group.id,
1197
+ selector: group.selector ?? "default",
1198
+ ...group.selector === "sequence" ? { order, exhaust } : {},
1199
+ children: verdicts,
1200
+ picked: picked?.id ?? null
1201
+ });
1202
+ return picked;
1203
+ };
1204
+ if (eligible.length === 0) return trace(null);
1080
1205
  const st = this.selectorState(group);
1081
1206
  switch (group.selector) {
1082
1207
  case "branch":
1083
- return eligible[0];
1084
- case "sequence": {
1085
- const order = group.options?.order ?? "sequential";
1086
- const exhaust = group.options?.exhaust ?? "once";
1087
- return order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st);
1088
- }
1208
+ return trace(eligible[0]);
1209
+ case "sequence":
1210
+ return trace(order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st));
1089
1211
  case "run":
1090
1212
  case "choice":
1091
1213
  default:
@@ -1203,7 +1325,10 @@ var Flow = class {
1203
1325
  // -- Effects + expressions ------------------------------------------------
1204
1326
  runEffects(effects) {
1205
1327
  for (const e of effects ?? []) {
1206
- this.setProperty(e.target, this.evalExpr(e.value));
1328
+ const value = this.evalExpr(e.value);
1329
+ const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
1330
+ this.setProperty(e.target, value);
1331
+ this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
1207
1332
  }
1208
1333
  }
1209
1334
  eligible(node) {
@@ -1347,7 +1472,7 @@ var Flow = class {
1347
1472
  }
1348
1473
  /** The per-flow registry: the NOT-shared `@patter` globals (the shared ones live on the host). */
1349
1474
  freshLocal() {
1350
- return new ScopeRegistry().defineOwned("patter", this.host.patterLocalDecls);
1475
+ return new ScopeRegistry().defineOwned("patter", this.host.patterLocalDecls, "@patter.");
1351
1476
  }
1352
1477
  /**
1353
1478
  * Seed a scene's `@scene` props (spec §7). The not-shared props seed THIS flow's
@@ -1358,27 +1483,18 @@ var Flow = class {
1358
1483
  */
1359
1484
  seedScene(scene) {
1360
1485
  const shared = this.host.sceneSharedNames.get(scene.id) ?? /* @__PURE__ */ new Set();
1486
+ const props = scene.sceneProps ?? [];
1361
1487
  if (!this.sceneBags.has(scene.id)) {
1362
- const bag = {};
1363
- for (const decl of scene.sceneProps ?? []) {
1364
- const name = decl.name.toLowerCase();
1365
- if (!shared.has(name)) bag[name] = sceneDefault(decl);
1366
- }
1367
- this.sceneBags.set(scene.id, bag);
1488
+ this.sceneBags.set(scene.id, new PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase()))));
1368
1489
  }
1369
1490
  if (!this.host.stageBags.has(scene.id)) {
1370
- const bag = {};
1371
- for (const decl of scene.sceneProps ?? []) {
1372
- const name = decl.name.toLowerCase();
1373
- if (shared.has(name)) bag[name] = sceneDefault(decl);
1374
- }
1375
- this.host.stageBags.set(scene.id, bag);
1491
+ this.host.stageBags.set(scene.id, new PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase()))));
1376
1492
  }
1377
1493
  for (const decl of scene.sceneProps ?? []) {
1378
1494
  if (!decl.temporary) continue;
1379
1495
  const name = decl.name.toLowerCase();
1380
1496
  const bag = shared.has(name) ? this.host.stageBags.get(scene.id) : this.sceneBags.get(scene.id);
1381
- if (bag) bag[name] = sceneDefault(decl);
1497
+ if (bag) bag.set(name, defaultFor(decl));
1382
1498
  }
1383
1499
  }
1384
1500
  };
@@ -1416,47 +1532,13 @@ function deserialiseSelectors(rec) {
1416
1532
  function toDecl(decl) {
1417
1533
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
1418
1534
  }
1419
- function declDefault(d) {
1420
- if (d.default !== void 0) return d.default;
1421
- switch (d.type) {
1422
- case "number":
1423
- return 0;
1424
- case "string":
1425
- return "";
1426
- case "flags":
1427
- return [];
1428
- case "enum":
1429
- return d.values?.[0] ?? "";
1430
- case "quality":
1431
- return d.stages?.[0] ?? "";
1432
- default:
1433
- return false;
1434
- }
1435
- }
1436
1535
  function toForeignDecl(decl) {
1437
1536
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
1438
1537
  }
1439
- function hostScopeDefault(decl) {
1440
- if (decl.default !== void 0) return decl.default;
1441
- switch (decl.type) {
1442
- case "boolean":
1443
- return false;
1444
- case "number":
1445
- return 0;
1446
- case "string":
1447
- return "";
1448
- case "flags":
1449
- return [];
1450
- case "enum":
1451
- return decl.values?.[0] ?? "";
1452
- case "quality":
1453
- return decl.stages?.[0] ?? "";
1454
- }
1455
- }
1456
1538
  function selfBackedResolver(decls) {
1457
1539
  const key = (name) => name.toLowerCase();
1458
1540
  const bag = /* @__PURE__ */ new Map();
1459
- for (const d of decls) bag.set(key(d.name), hostScopeDefault(d));
1541
+ for (const d of decls) bag.set(key(d.name), defaultFor(d));
1460
1542
  return {
1461
1543
  get: (name) => bag.get(key(name)),
1462
1544
  set: (name, value) => {
@@ -1464,23 +1546,6 @@ function selfBackedResolver(decls) {
1464
1546
  }
1465
1547
  };
1466
1548
  }
1467
- function sceneDefault(decl) {
1468
- if (decl.default !== void 0) return decl.default;
1469
- switch (decl.type) {
1470
- case "boolean":
1471
- return false;
1472
- case "number":
1473
- return 0;
1474
- case "string":
1475
- return "";
1476
- case "flags":
1477
- return [];
1478
- case "enum":
1479
- return decl.values?.[0] ?? "";
1480
- case "quality":
1481
- return decl.stages?.[0] ?? "";
1482
- }
1483
- }
1484
1549
  function truthy(v) {
1485
1550
  if (typeof v === "boolean") return v;
1486
1551
  if (typeof v === "number") return v !== 0;