@patterkit/ops 0.19.0 → 0.20.1

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +148 -9
  2. package/dist/index.js +624 -122
  3. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, existsSync as existsSync2 } from "fs";
3
3
  import { join as join2, dirname as dirname2, resolve as resolve2, basename } from "path";
4
4
  import { parseSource } from "@patterkit/core";
5
- import { isCaseOnlyPropertyName, projectLayout } from "@patterkit/model";
5
+ import { EXTENSION_SHARD, EXTENSIONS_DIR, MANIFEST_EXTENSION, isCaseOnlyPropertyName, projectLayout } from "@patterkit/model";
6
6
 
7
7
  // src/game-scopes.ts
8
8
  import { existsSync, readdirSync, readFileSync } from "fs";
@@ -251,6 +251,46 @@ function planShareScopes(root, project, dir) {
251
251
  }
252
252
 
253
253
  // src/load.ts
254
+ var NO_EXTENSIONS = () => ({ manifests: [], extensionShards: [], extensionShardFiles: [], extensionIssues: [] });
255
+ function loadExtensions(root, layout) {
256
+ const out = NO_EXTENSIONS();
257
+ const manifestDir = join2(root, EXTENSIONS_DIR);
258
+ const manifestFiles = readDirSafe(manifestDir).filter((e) => e.isFile() && !e.name.startsWith(".") && e.name.endsWith(MANIFEST_EXTENSION)).map((e) => join2(manifestDir, e.name)).sort();
259
+ for (const path of manifestFiles) {
260
+ try {
261
+ out.manifests.push({ path, id: basename(path).slice(0, -MANIFEST_EXTENSION.length), manifest: parseSource(readFileSync2(path, "utf8")) });
262
+ } catch (e) {
263
+ out.extensionIssues.push({ file: path, message: `the extension's manifest will not parse: ${e instanceof Error ? e.message : String(e)}` });
264
+ }
265
+ }
266
+ for (const file of walkFiles(join2(root, layout.extensions), EXTENSION_SHARD)) {
267
+ let parsed;
268
+ try {
269
+ parsed = parseSource(readFileSync2(file, "utf8"));
270
+ } catch (e) {
271
+ out.extensionIssues.push({ file, message: `the extension data will not parse, so it is left out: ${e instanceof Error ? e.message : String(e)}` });
272
+ continue;
273
+ }
274
+ const problem = extensionShardProblem(parsed);
275
+ if (problem) {
276
+ out.extensionIssues.push({ file, message: `${problem}, so it is left out` });
277
+ continue;
278
+ }
279
+ out.extensionShards.push(parsed);
280
+ out.extensionShardFiles.push(file);
281
+ }
282
+ return out;
283
+ }
284
+ function extensionShardProblem(v) {
285
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return "not an extension data file";
286
+ const o = v;
287
+ if (typeof o["schema"] !== "string" || !o["schema"].startsWith("patter/extension@")) return `schema is '${String(o["schema"])}', expected 'patter/extension@...'`;
288
+ if (typeof o["extension"] !== "string" || o["extension"] === "") return "it names no extension";
289
+ if (typeof o["scene"] !== "string" || o["scene"] === "") return "it names no scene";
290
+ const entries = o["entries"];
291
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) return "its entries are not keyed by node kind";
292
+ return void 0;
293
+ }
254
294
  function withGameScopes(root, project) {
255
295
  const { gameScopes, missing } = discoverGameScopes(root, project);
256
296
  return { ...gameScopes ? { gameScopes } : {}, ...missing ? { gameScopesMissing: missing } : {} };
@@ -436,7 +476,7 @@ function loadProjectLanding(startPath, opts) {
436
476
  }
437
477
  }
438
478
  }
439
- return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [], ...withGameScopes(root, project) };
479
+ return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [], ...withGameScopes(root, project), ...NO_EXTENSIONS() };
440
480
  }
441
481
  function applySceneOrder(scenes, order) {
442
482
  if (!order?.length) return;
@@ -479,7 +519,19 @@ function loadProject(startPath) {
479
519
  authoring.push(parseFile(f, "patter/authoring", "schema"));
480
520
  authoringFiles.push(f);
481
521
  }
482
- return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring, authoringFiles, ...withGameScopes(root, project) };
522
+ return {
523
+ root,
524
+ projectFile,
525
+ project,
526
+ scenes,
527
+ locales,
528
+ sceneFiles,
529
+ localeFiles,
530
+ authoring,
531
+ authoringFiles,
532
+ ...withGameScopes(root, project),
533
+ ...loadExtensions(root, layout)
534
+ };
483
535
  }
484
536
 
485
537
  // src/write.ts
@@ -494,11 +546,12 @@ function applyWrites(writes) {
494
546
 
495
547
  // src/validate.ts
496
548
  import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
497
- import { basename as basename4, isAbsolute as isAbsolute2, join as join5, relative as relative3 } from "path";
549
+ import { basename as basename4, dirname as dirname5, isAbsolute as isAbsolute2, join as join5, relative as relative3 } from "path";
498
550
 
499
551
  // src/merge.ts
500
- import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
552
+ import { canonicalStringify as canonicalStringify2, parseSource as parseSource2 } from "@patterkit/core";
501
553
  import { DEFAULT_QUALIFIERS } from "@patterkit/model";
554
+ import { markUnresolved, mergeExtensionData, unresolvedProblems, withoutMark } from "@wildwinter/toolkit/extensions";
502
555
  var UnsupportedMergeError = class extends Error {
503
556
  };
504
557
  var eq = (a, b) => canonicalStringify2(a) === canonicalStringify2(b);
@@ -510,7 +563,13 @@ function sidecarIssues(sidecarPaths) {
510
563
  message: "unresolved merge conflict - resolve it and delete the .patterconflict sidecar before committing"
511
564
  }));
512
565
  }
513
- var MERGE_TYPES = { flow: "flow", strings: "loc", authoring: "authoring", project: "project" };
566
+ var MERGE_TYPES = {
567
+ flow: "flow",
568
+ strings: "loc",
569
+ authoring: "authoring",
570
+ project: "project",
571
+ extension: "extension"
572
+ };
514
573
  function detectMergeType(file) {
515
574
  const s = typeof file.schema === "string" ? file.schema : "";
516
575
  const name = /^patter\/([a-z]+)(?:@|$)/.exec(s)?.[1];
@@ -520,16 +579,32 @@ function detectMergeType(file) {
520
579
  }
521
580
  function runMerge(base, ours, theirs, opts) {
522
581
  const type = opts?.type ?? detectMergeType(ours);
523
- switch (type) {
524
- case "loc":
525
- return mergeLoc(base, ours, theirs);
526
- case "authoring":
527
- return mergeAuthoring(base, ours, theirs);
528
- case "flow":
529
- return mergeFlow(base, ours, theirs);
530
- case "project":
531
- return mergeProject(base, ours, theirs);
582
+ if (type === "extension") {
583
+ const r2 = mergeExtensionData(base, ours, theirs, { host: "patter", ...opts?.manifest !== void 0 ? { manifest: opts.manifest } : {} });
584
+ return { type, merged: r2.merged, conflicts: [...r2.conflicts], warnings: [...r2.warnings] };
585
+ }
586
+ const b = withoutMark(base), o = withoutMark(ours), t = withoutMark(theirs);
587
+ const r = type === "loc" ? mergeLoc(b, o, t) : type === "authoring" ? mergeAuthoring(b, o, t) : type === "flow" ? mergeFlow(b, o, t) : mergeProject(b, o, t);
588
+ return { ...r, merged: markUnresolved(r.merged, r.conflicts, { base, ours, theirs }) };
589
+ }
590
+ function unresolvedMarkIssues(files) {
591
+ const issues = [];
592
+ for (const { file, text } of files) {
593
+ if (!text.includes("unresolved")) continue;
594
+ let shard;
595
+ try {
596
+ shard = parseSource2(text);
597
+ } catch {
598
+ continue;
599
+ }
600
+ for (const p of unresolvedProblems(shard)) {
601
+ issues.push({
602
+ file,
603
+ message: `unresolved merge at ${p.path}: the shard holds our side of a clash - resolve it, then remove the entry from "unresolved"`
604
+ });
605
+ }
532
606
  }
607
+ return issues;
533
608
  }
534
609
  function deletedKind(base, ours, theirs) {
535
610
  const deleted = base !== void 0 && (ours === void 0 || theirs === void 0);
@@ -542,7 +617,7 @@ function merge3(base, ours, theirs, id, path, conflicts) {
542
617
  conflicts.push({ id, path, base, ours, theirs, kind: deletedKind(base, ours, theirs) });
543
618
  return ours;
544
619
  }
545
- function mergeMap(base, ours, theirs, prefix, conflicts, resolve7) {
620
+ function mergeMap(base, ours, theirs, prefix, conflicts, resolve8) {
546
621
  const out = {};
547
622
  for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
548
623
  const b = base[k], o = ours[k], t = theirs[k];
@@ -551,7 +626,7 @@ function mergeMap(base, ours, theirs, prefix, conflicts, resolve7) {
551
626
  else if (eq(b, o)) v = t;
552
627
  else if (eq(b, t)) v = o;
553
628
  else {
554
- const r = resolve7?.(k, b, o, t);
629
+ const r = resolve8?.(k, b, o, t);
555
630
  if (r && r.resolved) v = r.value;
556
631
  else {
557
632
  conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
@@ -697,13 +772,13 @@ function mergeEditRecord(o, t) {
697
772
  if (Object.keys(loc).length > 0) merged.localisedAt = loc;
698
773
  return merged;
699
774
  }
700
- function mergeKeyedByName(base, ours, theirs, path, conflicts, resolve7) {
775
+ function mergeKeyedByName(base, ours, theirs, path, conflicts, resolve8) {
701
776
  const byName = (arr) => {
702
777
  const m = {};
703
778
  for (const p of arr) if (isObj(p) && typeof p.name === "string") m[p.name] = p;
704
779
  return m;
705
780
  };
706
- const merged = mergeMap(byName(base), byName(ours), byName(theirs), path, conflicts, resolve7?.(path, conflicts));
781
+ const merged = mergeMap(byName(base), byName(ours), byName(theirs), path, conflicts, resolve8?.(path, conflicts));
707
782
  return Object.keys(merged).sort().map((name) => merged[name]);
708
783
  }
709
784
  function mergeCastMember(path, conflicts) {
@@ -718,6 +793,7 @@ function mergeCastMember(path, conflicts) {
718
793
  return { resolved: true, value: out };
719
794
  };
720
795
  }
796
+ var PROJECT_HANDLED = /* @__PURE__ */ new Set(["properties", "cast", "gameDataFields", "qualifiers", "locales", "extensions"]);
721
797
  function mergeProject(base, ours, theirs) {
722
798
  const conflicts = [];
723
799
  const merged = {};
@@ -736,6 +812,9 @@ function mergeProject(base, ours, theirs) {
736
812
  if (v !== void 0) merged[k] = v;
737
813
  } else if (k === "locales") {
738
814
  merged.locales = mergeLocales(asMap(base.locales), asMap(ours.locales), asMap(theirs.locales), conflicts);
815
+ } else if (k === "extensions" && [base[k], ours[k], theirs[k]].every((x) => x === void 0 || isObj(x))) {
816
+ const v = mergeProjectExtensions(asMap(base[k]), asMap(ours[k]), asMap(theirs[k]), conflicts);
817
+ if (Object.keys(v).length > 0) merged[k] = v;
739
818
  } else {
740
819
  const v = merge3(base[k], ours[k], theirs[k], "", k, conflicts);
741
820
  if (v !== void 0) merged[k] = v;
@@ -743,6 +822,31 @@ function mergeProject(base, ours, theirs) {
743
822
  }
744
823
  return { type: "project", merged, conflicts, warnings: [] };
745
824
  }
825
+ function mergeProjectExtensions(b, o, t, conflicts) {
826
+ const out = {};
827
+ for (const id of /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(o), ...Object.keys(t)])) {
828
+ const bv = b[id], ov = o[id], tv = t[id];
829
+ const path = `extensions.${id}`;
830
+ if (!isObj(ov) || !isObj(tv) || bv !== void 0 && !isObj(bv)) {
831
+ const v = merge3(bv, ov, tv, id, path, conflicts);
832
+ if (v !== void 0) out[id] = v;
833
+ continue;
834
+ }
835
+ const bo = asMap(bv);
836
+ const entry = {};
837
+ for (const f of /* @__PURE__ */ new Set([...Object.keys(bo), ...Object.keys(ov), ...Object.keys(tv)])) {
838
+ if (f === "settings" && [bo[f], ov[f], tv[f]].every((x) => x === void 0 || isObj(x))) {
839
+ const settings = mergeMap(asMap(bo[f]), asMap(ov[f]), asMap(tv[f]), `${path}.settings`, conflicts);
840
+ if (Object.keys(settings).length > 0) entry[f] = settings;
841
+ } else {
842
+ const v = merge3(bo[f], ov[f], tv[f], id, `${path}.${f}`, conflicts);
843
+ if (v !== void 0) entry[f] = v;
844
+ }
845
+ }
846
+ out[id] = entry;
847
+ }
848
+ return out;
849
+ }
746
850
  function mergeGameDataFields(b, o, t, conflicts) {
747
851
  const out = {};
748
852
  for (const kind of /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(o), ...Object.keys(t)])) {
@@ -916,14 +1020,14 @@ function checkDuplicateIds(root, conflicts) {
916
1020
  n.children.forEach(walk);
917
1021
  };
918
1022
  walk(root);
919
- for (const id of dup) conflicts.push({ id, path: "scene", base: void 0, ours: void 0, theirs: void 0, kind: "structural" });
1023
+ for (const id of dup) conflicts.push({ id, path: `scene[${id}]`, base: void 0, ours: void 0, theirs: void 0, kind: "structural" });
920
1024
  }
921
1025
  var idMap = (nodes) => new Map(nodes.filter((n) => n.id).map((n) => [n.id, n]));
922
1026
  var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
923
1027
 
924
1028
  // src/validate.ts
925
1029
  import { validateProject } from "@patterkit/core";
926
- import { validateConditions, validateInterpolation, hostScopesToSpec, projectScopes } from "@patterkit/compiler";
1030
+ import { validateConditions, validateInterpolation, hostScopesToSpec, projectScopes, extensionPass } from "@patterkit/compiler";
927
1031
 
928
1032
  // src/compile.ts
929
1033
  import { basename as basename2, extname, isAbsolute, resolve as resolve3 } from "path";
@@ -949,7 +1053,19 @@ function compileScenes(loaded) {
949
1053
  }));
950
1054
  }
951
1055
  function compileLoaded(loaded) {
952
- return exportBundle({ project: loaded.project, scenes: compileScenes(loaded), locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
1056
+ return exportBundle({
1057
+ project: loaded.project,
1058
+ scenes: compileScenes(loaded),
1059
+ locales: loaded.locales,
1060
+ gameScopes: loaded.gameScopes?.merged,
1061
+ extensions: loadedExtensions(loaded)
1062
+ });
1063
+ }
1064
+ function loadedExtensions(loaded) {
1065
+ return {
1066
+ manifests: loaded.manifests,
1067
+ shards: loaded.extensionShards.map((shard, i) => ({ path: loaded.extensionShardFiles[i], shard }))
1068
+ };
953
1069
  }
954
1070
  function defaultBundlePath(loaded) {
955
1071
  return `../patter-dist/${basename2(loaded.projectFile).replace(/\.patterproj$/, "")}.patterc`;
@@ -967,7 +1083,7 @@ function besideBundlePath(loaded, ext) {
967
1083
  }
968
1084
 
969
1085
  // src/validate.ts
970
- import { PROJECT_LOCALE_SCENE, projectLayout as projectLayout2 } from "@patterkit/model";
1086
+ import { EXTENSION_SHARD as EXTENSION_SHARD3, PROJECT_LOCALE_SCENE, projectLayout as projectLayout2 } from "@patterkit/model";
971
1087
 
972
1088
  // src/reachability.ts
973
1089
  import { disjuncts, latchOf, scopedRef, terms } from "@wildwinter/expr";
@@ -1130,9 +1246,10 @@ function reachabilityIssues(loaded, compiled) {
1130
1246
 
1131
1247
  // src/pack.ts
1132
1248
  import JSZip from "jszip";
1133
- import { readFileSync as readFileSync4 } from "fs";
1249
+ import { readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
1134
1250
  import { dirname as dirname4, join as join4, relative as relative2, sep } from "path";
1135
- import { parseSource as parseSource2 } from "@patterkit/core";
1251
+ import { parseSource as parseSource3 } from "@patterkit/core";
1252
+ import { EXTENSION_SHARD as EXTENSION_SHARD2, EXTENSIONS_DIR as EXTENSIONS_DIR2, MANIFEST_EXTENSION as MANIFEST_EXTENSION2 } from "@patterkit/model";
1136
1253
  import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
1137
1254
 
1138
1255
  // src/handoff.ts
@@ -1219,12 +1336,12 @@ function readMarker(text) {
1219
1336
  }
1220
1337
 
1221
1338
  // src/pack.ts
1222
- var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
1339
+ var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj", EXTENSION_SHARD2];
1223
1340
  var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
1224
1341
  async function runPack(startPath) {
1225
1342
  const projectFile = findProjectFile(startPath);
1226
1343
  const root = dirname4(projectFile);
1227
- const project = parseSource2(readFileSync4(projectFile, "utf8"));
1344
+ const project = parseSource3(readFileSync4(projectFile, "utf8"));
1228
1345
  const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
1229
1346
  if (unresolved.length) {
1230
1347
  throw new Error(
@@ -1232,7 +1349,15 @@ async function runPack(startPath) {
1232
1349
  ` + unresolved.map((i) => ` ${i.file}`).join("\n")
1233
1350
  );
1234
1351
  }
1235
- const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative2(root, abs).split(sep).join("/") })).sort((a, b) => a.rel.localeCompare(b.rel));
1352
+ const shardFiles = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative2(root, abs).split(sep).join("/") }));
1353
+ const marked = [...new Set(unresolvedMarkIssues(shardFiles.map((f) => ({ file: f.rel, text: readFileSync4(f.abs, "utf8") }))).map((i) => i.file))];
1354
+ if (marked.length) {
1355
+ throw new Error(
1356
+ `${marked.length} shard(s) still marked with an unresolved merge - resolve the clashes each names under "unresolved", then remove the entries, before packing:
1357
+ ` + marked.map((f) => ` ${f}`).join("\n")
1358
+ );
1359
+ }
1360
+ const files = [...shardFiles, ...extensionManifests(root)].sort((a, b) => a.rel.localeCompare(b.rel));
1236
1361
  const scopes = gameScopesSnapshot(root, project);
1237
1362
  const handoffs = walkFiles(join4(root, HANDOFF_DIR), ".json").map((abs) => ({ rel: relative2(root, abs).split(sep).join("/"), text: readFileSync4(abs, "utf8") })).filter((f) => {
1238
1363
  try {
@@ -1255,6 +1380,19 @@ async function runPack(startPath) {
1255
1380
  for (const f of handoffs) zip.file(f.rel, f.text, ENTRY_OPTS);
1256
1381
  return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
1257
1382
  }
1383
+ function extensionManifests(root) {
1384
+ let names;
1385
+ try {
1386
+ names = readdirSync3(join4(root, EXTENSIONS_DIR2), { withFileTypes: true }).filter((e) => e.isFile() && !e.name.startsWith(".") && e.name.endsWith(MANIFEST_EXTENSION2)).map((e) => e.name);
1387
+ } catch {
1388
+ return [];
1389
+ }
1390
+ return names.map((name) => ({ abs: join4(root, EXTENSIONS_DIR2, name), rel: `${EXTENSIONS_DIR2}/${name}` }));
1391
+ }
1392
+ var isManifestEntry = (name) => {
1393
+ const rest = name.startsWith(`${EXTENSIONS_DIR2}/`) ? name.slice(EXTENSIONS_DIR2.length + 1) : void 0;
1394
+ return rest !== void 0 && !rest.includes("/") && rest.endsWith(MANIFEST_EXTENSION2) && rest.length > MANIFEST_EXTENSION2.length;
1395
+ };
1258
1396
 
1259
1397
  // src/validate.ts
1260
1398
  function runValidate(loaded) {
@@ -1266,7 +1404,7 @@ function runValidate(loaded) {
1266
1404
  const conditions = validateConditions({ project, scenes }, { foreignScopes, gameScopes: merged });
1267
1405
  const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes, gameScopes: merged });
1268
1406
  const gameScopes = gameScopesIssues(loaded, scopes.notes);
1269
- const hygiene = checkHygiene([loaded.projectFile, ...Object.values(loaded.sceneFiles), ...loaded.localeFiles, ...loaded.authoringFiles]);
1407
+ const hygiene = checkHygiene([loaded.projectFile, ...Object.values(loaded.sceneFiles), ...loaded.localeFiles, ...loaded.authoringFiles, ...loaded.extensionShardFiles]);
1270
1408
  const tree = walkFilesByExt(loaded.root, [...SHARD_EXTENSIONS, CONFLICT_SIDECAR, ".patterc"]);
1271
1409
  const localisation = locIssues(loaded);
1272
1410
  let compiled;
@@ -1277,8 +1415,9 @@ function runValidate(loaded) {
1277
1415
  }
1278
1416
  const staleBundles = checkBundles(loaded, compiled, tree.get(".patterc"));
1279
1417
  if (compiled) gameScopes.push(...unplayableScopes(loaded, compiled));
1280
- const unresolvedMerges = sidecarIssues(tree.get(CONFLICT_SIDECAR));
1418
+ const unresolvedMerges = [...sidecarIssues(tree.get(CONFLICT_SIDECAR)), ...markedShards(SHARD_EXTENSIONS.flatMap((ext) => tree.get(ext)))];
1281
1419
  const orphans = orphanShards(loaded, tree);
1420
+ const extensions = [...extensionIssues(loaded), ...gitattributesHint(loaded)];
1282
1421
  const structuralErrors = structural.filter((i) => i.severity !== "warning");
1283
1422
  const reachability = compiled && structuralErrors.length === 0 && !conditions.some(isError) ? reachabilityIssues(loaded, compiled) : [];
1284
1423
  return {
@@ -1292,7 +1431,8 @@ function runValidate(loaded) {
1292
1431
  orphans,
1293
1432
  gameScopes,
1294
1433
  localisation,
1295
- ok: !localisation.some(isError) && structuralErrors.length === 0 && !conditions.some(isError) && !interpolation.some(isError) && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0 && !gameScopes.some(isError)
1434
+ extensions,
1435
+ ok: !localisation.some(isError) && !extensions.some(isError) && structuralErrors.length === 0 && !conditions.some(isError) && !interpolation.some(isError) && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0 && !gameScopes.some(isError)
1296
1436
  };
1297
1437
  }
1298
1438
  var isError = (i) => i.severity === "error";
@@ -1306,9 +1446,53 @@ function exportBlockers(loaded) {
1306
1446
  return [
1307
1447
  ...structural.map((i) => `[${i.code}] ${i.message}`),
1308
1448
  ...[...conditions, ...interpolation].map((i) => `[${i.field}] ${i.nodeId}: ${i.message} (${i.src})`),
1309
- ...locIssues(loaded).filter(isError).map((i) => `[localisation] ${i.file}: ${i.message}`)
1449
+ ...locIssues(loaded).filter(isError).map((i) => `[localisation] ${i.file}: ${i.message}`),
1450
+ ...extensionIssues(loaded).filter(isError).map((i) => `[extension] ${i.file}: ${i.message}`)
1310
1451
  ];
1311
1452
  }
1453
+ function extensionIssues(loaded) {
1454
+ const loadIssues = loaded.extensionIssues.map((i) => ({ severity: "error", file: i.file, message: i.message }));
1455
+ if (loaded.project.extensions === void 0 && loaded.manifests.length === 0 && loaded.extensionShards.length === 0) return loadIssues;
1456
+ const { issues } = extensionPass({
1457
+ project: loaded.project,
1458
+ scenes: loaded.scenes,
1459
+ ...loadedExtensions(loaded),
1460
+ projectPath: loaded.projectFile,
1461
+ sceneFiles: loaded.sceneFiles
1462
+ });
1463
+ return [...loadIssues, ...issues];
1464
+ }
1465
+ function gitattributesHint(loaded) {
1466
+ if (loaded.extensionShardFiles.length === 0) return [];
1467
+ const files = [];
1468
+ let dir = loaded.root, repo = false;
1469
+ for (; ; ) {
1470
+ const f = join5(dir, ".gitattributes");
1471
+ if (existsSync4(f)) files.push(f);
1472
+ if (existsSync4(join5(dir, ".git"))) {
1473
+ repo = true;
1474
+ break;
1475
+ }
1476
+ const up = dirname5(dir);
1477
+ if (up === dir) break;
1478
+ dir = up;
1479
+ }
1480
+ const consulted = repo ? files : files.filter((f) => dirname5(f) === loaded.root);
1481
+ const texts = consulted.map((f) => {
1482
+ try {
1483
+ return readFileSync5(f, "utf8");
1484
+ } catch {
1485
+ return "";
1486
+ }
1487
+ });
1488
+ if (!texts.some((t) => /\bmerge=patter\b/.test(t))) return [];
1489
+ if (texts.some((t) => /^\s*\*\.patterextension\b.*\bmerge=patter\b/m.test(t))) return [];
1490
+ return [{
1491
+ severity: "warning",
1492
+ file: consulted[0],
1493
+ message: 'the project has extension data, but .gitattributes has no merge rule for it, so git merges it as plain text: add the line "*.patterextension text eol=lf merge=patter"'
1494
+ }];
1495
+ }
1312
1496
  function gameScopesIssues(loaded, notes) {
1313
1497
  const out = [];
1314
1498
  if (loaded.gameScopesMissing) out.push({ file: loaded.projectFile, severity: "error", message: loaded.gameScopesMissing });
@@ -1334,7 +1518,8 @@ function orphanShards(loaded, tree = walkFilesByExt(loaded.root, SHARD_EXTENSION
1334
1518
  ".patterflow": { what: "scene", home: layout.flow },
1335
1519
  ".patterloc": { what: "strings", home: layout.strings },
1336
1520
  ".patterx": { what: "authoring", home: layout.authoring },
1337
- ".patterproj": { what: "project", home: null }
1521
+ ".patterproj": { what: "project", home: null },
1522
+ [EXTENSION_SHARD3]: { what: "extension data", home: layout.extensions }
1338
1523
  };
1339
1524
  const inside = (file, dir) => {
1340
1525
  const r = relative3(dir, file);
@@ -1398,6 +1583,34 @@ function locIssues(loaded) {
1398
1583
  return out;
1399
1584
  }
1400
1585
  var hygieneCache = /* @__PURE__ */ new Map();
1586
+ var markCache = /* @__PURE__ */ new Map();
1587
+ function markedShards(files) {
1588
+ const wanted = new Set(files);
1589
+ for (const file of markCache.keys()) if (!wanted.has(file)) markCache.delete(file);
1590
+ const issues = [];
1591
+ for (const file of files) {
1592
+ let mtimeMs, text;
1593
+ try {
1594
+ mtimeMs = statSync2(file).mtimeMs;
1595
+ } catch {
1596
+ continue;
1597
+ }
1598
+ const hit = markCache.get(file);
1599
+ if (hit && hit.mtimeMs === mtimeMs) {
1600
+ issues.push(...hit.issues);
1601
+ continue;
1602
+ }
1603
+ try {
1604
+ text = readFileSync5(file, "utf8");
1605
+ } catch {
1606
+ continue;
1607
+ }
1608
+ const found = unresolvedMarkIssues([{ file, text }]);
1609
+ markCache.set(file, { mtimeMs, issues: found });
1610
+ issues.push(...found);
1611
+ }
1612
+ return issues;
1613
+ }
1401
1614
  function checkHygiene(files) {
1402
1615
  const wanted = new Set(files);
1403
1616
  for (const file of hygieneCache.keys()) if (!wanted.has(file)) hygieneCache.delete(file);
@@ -1433,10 +1646,11 @@ function checkHygiene(files) {
1433
1646
  }
1434
1647
 
1435
1648
  // src/export.ts
1649
+ import { readFileSync as readFileSync6 } from "fs";
1436
1650
  import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
1437
1651
 
1438
1652
  // src/audio-index.ts
1439
- import { readdirSync as readdirSync3 } from "fs";
1653
+ import { readdirSync as readdirSync4 } from "fs";
1440
1654
  import { join as join6, relative as relative4, resolve as resolve4, sep as sep2 } from "path";
1441
1655
  import { deriveRecordingFolders, DEFAULT_RECORDING_STATUSES } from "@patterkit/model";
1442
1656
  var AUDIO_MANIFEST_SCHEMA = "patter/audio@0";
@@ -1472,7 +1686,7 @@ function scanAudio(loaded) {
1472
1686
  return pickAudio(rungs.map((r) => {
1473
1687
  let files;
1474
1688
  try {
1475
- files = readdirSync3(r.dir);
1689
+ files = readdirSync4(r.dir);
1476
1690
  } catch {
1477
1691
  files = void 0;
1478
1692
  }
@@ -1501,11 +1715,19 @@ function audioManifestWrite(loaded, snapshot) {
1501
1715
  // src/export.ts
1502
1716
  function refuseUnresolvedMerge(loaded) {
1503
1717
  const issues = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
1504
- if (!issues.length) return;
1505
- throw new Error(
1506
- `${issues.length} unresolved merge conflict(s) - resolve them and delete the .patterconflict sidecar(s) first:
1718
+ if (issues.length) {
1719
+ throw new Error(
1720
+ `${issues.length} unresolved merge conflict(s) - resolve them and delete the .patterconflict sidecar(s) first:
1507
1721
  ` + issues.map((i) => ` ${i.file}`).join("\n")
1508
- );
1722
+ );
1723
+ }
1724
+ const marked = [...new Set(unresolvedMarkIssues(SHARD_EXTENSIONS.flatMap((ext) => walkFiles(loaded.root, ext)).map((file) => ({ file, text: readFileSync6(file, "utf8") }))).map((i) => i.file))];
1725
+ if (marked.length) {
1726
+ throw new Error(
1727
+ `${marked.length} shard(s) still marked with an unresolved merge - resolve the clashes each names under "unresolved", then remove the entries:
1728
+ ` + marked.map((f) => ` ${f}`).join("\n")
1729
+ );
1730
+ }
1509
1731
  }
1510
1732
  function refuseInvalid(loaded, opts) {
1511
1733
  if (opts.allowInvalid) return;
@@ -1548,7 +1770,7 @@ import { canonicalStringify as canonicalStringify4 } from "@patterkit/core";
1548
1770
  import { DEFAULT_PAD_AFTER } from "@patterkit/model";
1549
1771
 
1550
1772
  // src/playable-runtime.ts
1551
- var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var ne=Object.defineProperty;var ct=Object.getOwnPropertyDescriptor;var lt=Object.getOwnPropertyNames;var pt=Object.prototype.hasOwnProperty;var dt=(n,e)=>{for(var t in e)ne(n,t,{get:e[t],enumerable:!0})},ut=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of lt(e))!pt.call(n,s)&&s!==t&&ne(n,s,{get:()=>e[s],enumerable:!(r=ct(e,s))||r.enumerable});return n};var gt=n=>ut(ne({},"__esModule",{value:!0}),n);var Mt={};dt(Mt,{Checkpoint:()=>_,DEFAULT_PAD_AFTER:()=>k,Engine:()=>ee,Flow:()=>R,buildTagIndex:()=>Z,compareStages:()=>O,describeBundle:()=>rt,effectiveGameData:()=>st,gameDataFields:()=>nt,gameDataValue:()=>he,nextStage:()=>B,stageIndex:()=>P});function N(n,e){return n!==void 0&&Object.prototype.hasOwnProperty.call(n,e)?n[e]:void 0}function E(n){switch(n[0]){case"b":return{kind:"bool",value:n[1]};case"n":return{kind:"number",value:n[1]};case"s":return{kind:"string",value:n[1]};case"sv":return{kind:"scopedvar",scope:n[1],name:n[2]};case"u":return{kind:"unary",op:n[1],operand:E(n[2])};case"bin":return{kind:"binary",op:n[1],left:E(n[2]),right:E(n[3])};case"call":{let e=n.slice(2).map(E);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var u=class extends Error{constructor(e){super(e),this.name="EvalError"}};function W(n,e,t){let r=new Map(t.scopes.map(i=>[i.token,i.missing??"false"])),s=i=>{switch(i.kind){case"bool":return i.value;case"number":return i.value;case"string":return i.value;case"scopedvar":{let o=N(e.scopes,i.scope);if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):N(o,i.name);if(a===void 0){if(r.get(i.scope)==="throw")throw new u(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!N(t.functions,i.name)){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new u(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=se(a,e);if(c===void 0)throw new u("advance() needs a quality reference (@scope.name of a quality property)");let l=b(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=N(t.functions,i.name);if(!o)throw new u(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new u("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new u(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new u(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let h=s(i.left);if(typeof h!="boolean")throw new u(`\'and\' requires boolean operands, left is ${typeof h}`);if(!h)return!1;let f=s(i.right);if(typeof f!="boolean")throw new u(`\'and\' requires boolean operands, right is ${typeof f}`);return f}if(i.op==="or"){let h=s(i.left);if(typeof h!="boolean")throw new u(`\'or\' requires boolean operands, left is ${typeof h}`);if(h)return!0;let f=s(i.right);if(typeof f!="boolean")throw new u(`\'or\' requires boolean operands, right is ${typeof f}`);return f}let o=s(i.left),a=s(i.right),c=se(i.left,e),l=se(i.right,e),p=c??l;if(p!==void 0){if(c&&l&&!ft(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new u(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return b(o,p,">")>b(a,p,">");case">=":return b(o,p,">=")>=b(a,p,">=");case"<":return b(o,p,"<")<b(a,p,"<");case"<=":return b(o,p,"<=")<=b(a,p,"<=");case"+":case"-":case"*":case"/":throw new u(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return ke(o,a);case"!=":return!ke(o,a);case">":return C(o,a,">"),o>a;case">=":return C(o,a,">="),o>=a;case"<":return C(o,a,"<"),o<a;case"<=":return C(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new u(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return C(o,a,"-"),o-a;case"*":return C(o,a,"*"),o*a;case"/":if(C(o,a,"/"),a===0)throw new u("division by zero");return o/a}}}};return s(n)}function ke(n,e){if(Array.isArray(n)||Array.isArray(e)){if(!Array.isArray(n)||!Array.isArray(e)||n.length!==e.length)return!1;let t=[...n].sort(),r=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==r[s])return!1;return!0}return n===e}function C(n,e,t){if(typeof n!="number"||typeof e!="number")throw new u(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function se(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function b(n,e,t){if(typeof n!="string")throw new u(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new u(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var ft=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);function F(n){if(Number.isNaN(n)||!Number.isFinite(n))return 0;let e=Math.trunc(n)%4294967296;return e<0?e+4294967296:e}function ie(n){let e=F(n);return{next(){e=e+1831565813>>>0;let t=Math.imul(e^e>>>15,1|e);return t=t+Math.imul(t^t>>>7,61|t)^t,((t^t>>>14)>>>0)/4294967296},state(){return e}}}var ae=n=>Array.isArray(n)?n:n.stages??[],ht=n=>Array.isArray(n)?"flags":n==null?"nothing":typeof n;function P(n,e){return typeof e=="string"?ae(n).indexOf(e):-1}function oe(n,e){if(typeof e!="string")throw new u(`a stage is a name, got ${ht(e)}`);let t=n.indexOf(e);if(t<0)throw new u(`"${e}" is not a stage of this quality (stages: ${n.join(", ")})`);return t}function O(n,e,t){let r=ae(n),s=oe(r,e)-oe(r,t);return s<0?-1:s>0?1:0}function B(n,e){let t=ae(n);return t[Math.min(oe(t,e)+1,t.length-1)]}function ce(n,e,t){switch(n.type){case"boolean":return typeof e=="boolean";case"number":return typeof e=="number";case"string":case"text":case"multiline":return typeof e=="string";case"enum":return typeof e!="string"?!1:e===""&&t?.noneChosen?!0:!Array.isArray(n.values)||n.values.includes(e);case"flags":return Array.isArray(e)&&e.every(r=>typeof r=="string"&&(!Array.isArray(n.values)||n.values.includes(r)));case"quality":return typeof e=="string"&&(!Array.isArray(n.stages)||n.stages.includes(e));default:return!0}}var mt={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},yt=[mt];function Ee(n,e,t){let r=t?.countingCalls??yt;return K(n,t?.want??!0,e,r)}function K(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=K(n.left,e,t,r),i=K(n.right,e,t,r);return n.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(n.kind==="unary"&&n.op==="not")return K(n.operand,!e,t,r);if(n.kind==="call"){let s=r.find(i=>i.name===n.name);if(s){let i=s.count(n),o=t(n);return e?o?i:0:o?0:1}}return t(n)===e?1:0}var x=class n{values=Object.create(null);decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(r=>r.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let r=this.norm(t.name);this.decls.set(r,t),this.values[r]=structuredClone(t.default??A(t))}}get(e){return this.values[this.norm(e)]}normalise(e){return this.norm(e)}writable(e){return this.decls.get(this.norm(e))?.writable!==!1}set(e,t,r){let s=this.norm(e);if(!r?.host&&!this.writable(s))throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:r?.silent??!1,reason:r?.reason};this.values[s]=t;for(let o of[...this.auditors])o(i);if(!i.silent)for(let o of[...this.subscribers])o(i);return i}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}onAudit(e){return this.auditors.add(e),()=>this.auditors.delete(e)}rows(){return[...this.decls.entries()].map(([e,t])=>De(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new n([],{normalise:this.norm,pathPrefix:this.pathPrefix});return e.decls=new Map(this.decls),Object.assign(e.values,structuredClone(this.values)),e}reseed(e){for(let t of Object.keys(this.values))delete this.values[t];this.decls.clear(),this.seed(e)}save(){return structuredClone(this.values)}load(e,t){let r=new Map;for(let[i,o]of Object.entries(e)){let a=this.norm(i);this.values[a]=structuredClone(o),r.set(a,o)}let s=[];if(!t?.refit)return s;for(let[i,o]of this.decls){if(!r.has(i))continue;let a=r.get(i);ce(o,a)||(this.values[i]=structuredClone(A(o)),s.push({name:i,value:a}))}return s}};function De(n,e,t,r,s=""){let i=r??n.name.toLowerCase();return{name:i,path:s+i,type:n.type,value:e,default:n.default??A(n),...n.values!==void 0?{values:n.values}:{},...n.stages!==void 0?{stages:n.stages}:{},writable:t??n.writable??!0}}var St=n=>n.toLowerCase(),le=1,Q=class{scopes=new Map;parked=new Map;parkedRefit=new Set;misfits=[];rev=0;get revision(){return this.rev}defineOwned(e,t,r){let s=typeof r=="string"?{pathPrefix:r}:r??{},i=new x(t,{pathPrefix:s.pathPrefix??`${e}.`,...s.normalise?{normalise:s.normalise}:{}});return this.mountOwned(e,i,s.owner!==void 0?{owner:s.owner}:void 0)}mountOwned(e,t,r){this.assertFree(e,r?.owner),this.scopes.set(e,{kind:"owned",bag:t,...r?.owner!==void 0?{owner:r.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(this.note(e,t.load(s,{refit:this.parkedRefit.has(e)})),this.parked.delete(e),this.parkedRefit.delete(e)),this}remove(e,t){let r=this.scopes.get(e);if(!r)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&r.kind==="owned"&&(this.parked.set(e,r.bag.save()),this.parkedRefit.delete(e)),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear(),this.parkedRefit.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&(this.parked.delete(t),this.parkedRefit.delete(t));return this}takeMisfits(e){if(!e){let r=this.misfits;return this.misfits=[],r}let t=this.misfits.filter(r=>e(r.scope));return this.misfits=this.misfits.filter(r=>!e(r.scope)),t}note(e,t){for(let r of t)this.misfits.push({scope:e,name:r.name,value:r.value})}ownedBag(e){let t=this.scopes.get(e);if(!t||t.kind!=="owned")throw new Error(`\'@${e}\' is not an owned scope`);return t.bag}reseedOwned(e,t){return this.ownedBag(e).reseed(t),this}defineForeign(e,t,r=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??St,a=new Map;for(let c of r)a.set(o(c.name),c);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:a,scopeWritable:i.writable??!0,norm:o,...i.owner!==void 0?{owner:i.owner}:{}}),this.rev++,this}has(e){return this.scopes.has(e)}get(e,t){let r=this.scopes.get(e);if(r)return r.kind==="owned"?r.bag.get(t):r.resolver.get(r.norm(t))}set(e,t,r,s){let i=this.scopes.get(e);if(!i)throw new Error(`unknown scope \'@${e}\'`);if(i.kind==="owned"){if(!s?.host&&!i.bag.writable(t))throw new Error(`\'@${e}.${t}\' is read-only`);i.bag.set(t,r,s?.host?{host:!0}:void 0);return}let o=i.norm(t);if(!i.resolver.set)throw new Error(`\'@${e}.${t}\' is read-only`);if(!s?.host&&!this.foreignWritable(i,o))throw new Error(`\'@${e}.${t}\' is read-only`);i.resolver.set(o,r)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,r]of this.scopes){let s=r.owner!==void 0?{owner:r.owner}:{};if(r.kind==="owned")for(let i of r.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of r.decls)e.push({scope:t,...s,...De(o,r.resolver.get(i),this.foreignWritable(r,i),i,`${t}.`)})}return e}toEvalContext(e,t){let r=this.view(t?.aliases),s={};for(let[o,a]of r)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(r);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=r.get(o);return c?i.get(o)?.get(wt(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[r,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${r}\' names \'${s}\', which is not registered`);t.set(r,i)}return t}qualityLadders(e){let t=new Map;for(let[r,s]of e)for(let[i,o]of Ce(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(r);a||(a=new Map,t.set(r,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[r,s]of this.view(e?.aliases)){let i=Ce(s);if(i.length===0)continue;let o=new Map;for(let[a,c]of i)o.set(a,{type:c.type,enumValues:c.values,...c.stages!==void 0?{stages:c.stages}:{}});t.set(r,o)}return{properties:t}}save(){let e={};for(let[t,r]of this.scopes)r.kind==="owned"&&(e[t]=r.bag.save());for(let[t,r]of this.parked)e[t]=structuredClone(r);return e}load(e,t){t?.keepParked||(this.parked.clear(),this.parkedRefit.clear());let r=t?.refit===!0;for(let[s,i]of Object.entries(e)){let o=this.scopes.get(s);o?.kind==="owned"?this.note(s,o.bag.load(i,{refit:r})):o||(this.parked.set(s,structuredClone(i)),r?this.parkedRefit.add(s):this.parkedRefit.delete(s))}}saveFragment(){return{version:le,scopes:this.save()}}loadFragment(e){if(e.version!==le)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${le})`);this.load(e.scopes)}assertFree(e,t){let r=this.scopes.get(e);if(!r)return;let s=r.owner!==void 0?` by ${r.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function Ce(n){return n.kind==="foreign"?[...n.decls.entries()]:n.bag.declarations().map(e=>[n.bag.normalise(e.name),e])}function wt(n){return n.kind==="foreign"?n.norm:e=>n.bag.normalise(e)}function A(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":case"text":case"multiline":return"";case"enum":return n.values?.[0]??"";case"flags":return[];case"quality":return n.stages?.[0]??"";default:return!1}}var Ie=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function T(n){return n.ctx.host??{}}var pe={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new u("random(a, b) requires exactly 2 arguments");let t=T(e).nextRandom;if(!t)throw new u("random() called without a PRNG in context");let r=e.evaluate(n[0]),s=e.evaluate(n[1]);if(typeof r!="number"||typeof s!="number")throw new u("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new u("random(a, b) arguments must be integers");let i=Math.min(r,s),o=Math.max(r,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:Pe("check_flags"),eval(n,e){let t=Re(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new u("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:Pe("set_flags"),eval(n,e){let t=[...Re(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new u("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:z("visits"),eval:(n,e)=>T(e).visits?.(J(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:z("seen"),eval:(n,e)=>(T(e).visits?.(J(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:z("patter_visits"),eval:(n,e)=>T(e).patterVisits?.(J(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:z("patter_seen"),eval:(n,e)=>(T(e).patterVisits?.(J(n,e,"patter_seen"))??0)>0}}},ur=Ie.map(n=>n.token).filter(n=>n!=="patter"&&n!=="scene");function Oe(n,e){let t=n.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function J(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new u(`${t}(id) requires a string node id`);return r}function z(n){return(e,t)=>{let r=e[0];r&&r.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(id): the argument must be a string id literal (a scene / block / node id)`})}}function Re(n,e,t){if(!n)throw new u(`${t}() requires at least one argument (the flags variable)`);let r=e.evaluate(n);if(Array.isArray(r))return r;if(r===!1||r===null||r===void 0)return[];throw new u(`${t}() first argument must be a flags property`)}function Pe(n){return(e,t)=>{if(e.length===0)return;let r=e[0];if(r.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(r.scope)?.get(r.name);if(s&&s.type!=="flags"){let i=r.scope===t.defaultScope?r.name:`${r.scope}.${r.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): \'@${i}\' is not a flags property (got ${s.type})`});return}for(let i=1;i<e.length;i++){let o=e[i];o.kind!=="flagdelta"?t.report({path:[...t.path,"args",i],kind:"wrong-arg-type",severity:"error",message:`${n}(): argument ${i+1} must be +flagName or -flagName`}):s?.type==="flags"&&s.enumValues&&!s.enumValues.includes(o.name)&&t.report({path:[...t.path,"args",i],kind:"unknown-flag-name",severity:"error",message:`${n}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var bt=/^@[A-Za-z0-9_.]+$/;function*vt(n){let e="",t=0;for(;t<n.length;){let r=n[t];if(r==="{"&&n[t+1]==="{"){e+="{",t+=2;continue}if(r==="}"&&n[t+1]==="}"){e+="}",t+=2;continue}if(r==="{"){let s=n.indexOf("}",t+1);if(s!==-1){let i=n.slice(t,s+1),o=n.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:bt.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=r,t+=1}e&&(yield{kind:"text",value:e})}function xt(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function kt(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function Et(n){let e="",t=!1;for(let r of n){if(kt(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function Be(n,e,t){if(e.length===0||n.indexOf(e)<0)return n;let r="",s=0,i=!1;for(;s<n.length;){if(n.startsWith(e,s)){let o=n.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}r+=n.slice(s);break}r+=n[s],s+=1}return i?Et(r):n}function Ae(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of vt(n)){if(r.kind==="text"){t+=r.value;continue}if(!r.ref){t+=r.raw;continue}let s=e(r.ref);t+=s===void 0?"":xt(s)}return t}function D(n,e){for(let t of n){e(t);let r=t.children;r&&D(r,e)}}function Ct(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function w(n){let e=n.gameId?.trim();return e||Ct(n.name)}var k=.6;function Y(n){return`cast:${n}`}function X(n){return`qualifier:${n}`}function Ne(n,e){return`speaker:${n}:${e}`}var Fe="speaker:pattern",Te="{name} ({qualifier})";function Le(n,e,t){return n.split("{name}").map(r=>r.split("{qualifier}").join(t)).join(e)}var de={open:"[",close:"]"},Ve="SFX";var L=3;function V(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function Z(n){let e=new Map,t=(r,s)=>{let i=V([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group"&&r.prompt&&e.set(r.prompt.id,V([...i,...r.prompt.tags??[]])),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,V([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=V(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=V([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var Me=n=>n.kind==="line"||n.kind==="text",M=n=>typeof n=="number"&&Number.isFinite(n)?n:void 0;function je(n){let e=new Map,t=(i,o,a)=>{let c=i.kind==="gameEvent"?void 0:M(i.padAfter),l=c??o;a&&l<0&&(l=0),e.set(i.id,c!==void 0?{own:c,resolved:l}:{resolved:l})},r=(i,o)=>{let a=M(i.padAfterDefault)??o;if(i.type==="group"){i.prompt&&t(i.prompt,a,!1);for(let p of i.children)r(p,a);return}let c=i.beats??[],l=-1;c.forEach((p,h)=>{Me(p)&&(l=h)}),c.forEach((p,h)=>{Me(p)&&t(p,a,h===l)})},s=M(n.padAfterDefault)??k;for(let i of Object.values(n.scenes)){let o=M(i.padAfterDefault)??s;for(let a of i.blocks){let c=M(a.padAfterDefault)??o;for(let l of a.children)r(l,c)}}return e}var Ge=new WeakMap,qe=new WeakMap;function Dt(n){let e=qe.get(n);return e||(e=Z(n),qe.set(n,e)),e}var $e=new WeakMap;function It(n){let e=$e.get(n);return e||(e=je(n),$e.set(n,e)),e}var _e=new WeakMap;function Rt(n){let e=_e.get(n);if(e)return e;e=new Map;for(let t of Object.values(n.scenes))for(let r of t.blocks)D(r.children,s=>{if(s.type==="group"){s.prompt&&e.set(s.prompt.id,s.prompt);return}for(let i of s.beats??[])e.set(i.id,i)});return _e.set(n,e),e}function I(n,e){return n.tagIndex.get(e)?.slice()}var q="Patter",j=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),Pt=n=>n.replace(/%2F/g,"/").replace(/%25/g,"%"),He=Symbol("ownsRegistry"),S={stage:n=>`patter/scene/${j(n)}`,flow:n=>`patter/flow/${j(n)}/`,flowGlobals:n=>`patter/flow/${j(n)}/patter`,flowScene:(n,e)=>`patter/flow/${j(n)}/scene/${j(e)}`},_=class{constructor(e){this.journal=e}journal},ee=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];engineSeq=0;engineTraceHandlers=new Set;constructor(e,t={}){this.creationOptions=t;let r=t.locale??e.locales.default,s=e.strings;this.allStrings=s,this.currentLocale=r;let i=s[r]??{},o=s[e.locales.default]??{},a=e.localisation,c=a?.mode==="ids"&&!a.sourceDebug;this.sourceDebug=a?.mode==="ids"&&!!a.sourceDebug,this.sourceDebug&&typeof console<"u"&&console.warn("[Patterplay] source-only DEBUG build: strings are the source language for debugging, not a shippable localised build.");let l=new Map;for(let d of e.cast??[])d.displayName&&l.set(d.name,d.displayName);let p=new Map;for(let d of e.qualifiers??[])d?.gameId&&p.set(d.gameId,d.name);let h=new Map;for(let d of e.cast??[])for(let[g,y]of Object.entries(d.qualifierLabels??{}))typeof y=="string"&&h.set(`${d.name}\\0${g}`,y);let f=e.speakerPattern??Te;this.defaultSeed=(t.seed??2654435769)>>>0;let me=new Map,ye=new Map,Se=new Map;for(let[d,g]of Object.entries(e.scenes)){this.sceneGameIdToId.set(w(g),d);let y=new Map;for(let m of g.blocks)ye.set(m.id,{sceneId:d}),Se.set(m.id,m),y.set(w(m),m.id),D(m.children,U=>me.set(U.id,U));this.blockGameIdToId.set(d,y)}let we=e.properties??[],te=we.filter(d=>d.shared??!0).map(Qe),it=we.filter(d=>!(d.shared??!0)).map(Qe),ot=new Set(te.map(d=>d.name.toLowerCase())),v=t.registry??new Q,be=!t.registry||t[He]===!0,ve=new x(te,{pathPrefix:"@patter."}),re=[],H=[];try{v.mountOwned("patter",ve,{owner:q}),H.push("patter");let d=new Set;for(let[g,y]of Object.entries(Je(t))){let m=e.scopeRegistry?.scopes.find(at=>at.token===g),U=(m?.declarations??[]).map(Ze);v.defineForeign(g,y,U,{writable:m?.writable??!0,owner:q}),H.push(g),d.add(g)}for(let g of be?e.scopeRegistry?.scopes??[]:[]){if(d.has(g.token)||v.has(g.token))continue;let y=(g.declarations??[]).map(m=>At(m,g.writable));v.defineOwned(g.token,y,{owner:q}),H.push(g.token),re.push(g.token)}}catch(d){for(let g of H)v.remove(g,{keep:!0});throw d}let xe=new Map;for(let[d,g]of Object.entries(e.scenes)){let y=new Set((g.sceneProps??[]).filter(m=>m.shared??!1).map(m=>m.name.toLowerCase()));xe.set(d,y)}this.host={logEnabled:t.log??!1,tracing:t.log??!1,emitEngine:(d,g,y)=>this.emitEngine(d,g,y),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,qualifierDisplay:p,speakerLabels:h,speakerPattern:f,nodeIndex:me,blockIndex:ye,blockById:Se,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:Dt(e),padIndex:It(e),registry:v,ownsRegistry:be,patterBag:ve,hostScopes:re,patterSharedDecls:te,patterLocalDecls:it,patterSharedNames:ot,sceneSharedNames:xe,sharedVisits:new Map,qualityLadders:new Map,sharedSelectors:new Map,stageBags:new Map,journal:null,customRng:t.rng,onDryChoice:t.onDryChoice,onError:t.onError??(d=>console.warn(`Patterplay: ${d.kind} on ${d.node} in flow \'${d.flow}\' failed, played through: ${d.message}`)),reportRefits:()=>{let d=v.takeMisfits(g=>g==="patter"||g.startsWith("patter/")||re.includes(g));if(d.length!==0)if(t.onRefit)t.onRefit(d);else for(let g of d)console.warn(`Patterplay: the save\'s ${g.scope}.${g.name} was ${JSON.stringify(g.value)}, which no longer fits its declaration; it is back at its default`)},replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??de).open,captionClose:(e.closedCaptions??de).close,captionCharacter:e.closedCaptions?.character||Ve,refSplitCache:new Map,refSplitRevision:v.revision},this.host.reportRefits()}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}get buildId(){return this.host.bundle.content?.hash??""}setLocale(e){this.currentLocale=e,this.host.strings=this.allStrings[e]??{}}replaceStrings(e){this.allStrings=e.strings,this.host.strings=this.allStrings[this.currentLocale]??{},this.host.defaultStrings=this.allStrings[this.host.bundle.locales.default]??{}}hotSwap(e){this.refuseInCheckpoint("hotSwap");let t=this.saveGame(),r=c=>(c.setLocale(this.currentLocale),c.setClosedCaptions(this.host.captionsOn),c),s=()=>new n(e,{...this.creationOptions,registry:this.host.registry,[He]:this.host.ownsRegistry}),i=new Set(["patter",...this.host.hostScopes]),o=this.host.ownsRegistry?void 0:Object.fromEntries(Object.entries(this.host.registry.save()).filter(([c])=>i.has(c)||c.startsWith("patter/")));this.release(!0),o&&this.host.registry.load(o,{keepParked:!0,refit:!0});let a=s();try{return a.loadGame(t),r(a)}catch{a.release(!0);let c=this.host.ownsRegistry?t.registry:o;c&&this.host.registry.load(c,{keepParked:!0,refit:!0});let l=s();for(let[p,h]of Object.entries(t.flows)){let f=h.cursor.currentSceneId;try{l.openFlow(p,f!==null?{scene:f}:{})}catch{}}return r(l)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let{sceneId:r,blockId:s}=this.resolveOpenAddress(t.scene,t.block),i=this.flowsById.get(e),o=this.host.journal;if(o&&i&&!i.isClosed)throw new Error(`openFlow would replace the open flow \'${e}\', which can\'t be undone while a checkpoint is open`);i?.close();let a=new R(e,this.host,t.seed??this.defaultSeed);return o&&(o.flows.add(a),o.opened.add(a),o.undo.push(()=>{a.close(),i?this.flowsById.set(e,i):this.flowsById.delete(e)})),this.flowsById.set(e,a),a.begin(r,s),a}runFlow(e,t,r){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:r}).advanceToStop().played;if(!s.goto(t,r))throw new Error(`runFlow: address not found: ${t}${r===void 0?"":` / ${r}`}`);return s.advanceToStop().played}assertExternalScopes(){for(let e of this.host.bundle.externalScopes??[])if(!this.host.registry.has(e))throw new Error(`this content names @${e}, which no engine on this registry registered: give every engine the game\'s one registry`)}resolveOpenAddress(e,t){if(e!==void 0){let r=ue(this.host,e);if(r===void 0)throw new Error(`unknown scene: ${e}`);if(t===void 0)return{sceneId:r};let s=ge(this.host,r,t);if(s===void 0)throw new Error(`unknown block: ${t}`);return{sceneId:r,blockId:s}}if(t!==void 0){if(!this.host.blockIndex.has(t))throw new Error(`unknown block: ${t}`);return{blockId:t}}if(Object.keys(this.host.bundle.scenes).length===0)throw new Error("no scenes in bundle");return{}}resolveSceneRef(e){if(e!=null)return ue(this.host,e)??e}resolveBlockRef(e,t){if(t!=null)return e==null?t:ge(this.host,e,t)}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?w(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?w(t):void 0}tagsForBeat(e){return I(this.host,e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?I(this.host,t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?I(this.host,s):void 0)??[]}gameDataForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.gameData}}gameDataForBlock(e,t){let r=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(r!=null?this.host.blockById.get(r):void 0)?.gameData}}getCast(){let e=[];for(let t of this.host.bundle.cast??[])t?.name&&e.push(t.name);return e}castForScene(e){let t=this.resolveSceneRef(e),r=t!=null?this.host.bundle.scenes[t]:void 0;if(!r)return[];let s=new Set;for(let i of r.blocks)Ue(i.children,s);return[...s]}castForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return Ue(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...w(e)?{gameId:w(e)}:{},name:e.name,...We(e.gameData),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...w(t)?{gameId:w(t)}:{},name:t.name,...We(t.gameData),...this.tagsField(t.id),children:t.children.map(r=>this.outlineNode(r))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let r of t.blocks)D(r.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:r.id,snippetId:s.id,beat:this.beatInfo(i)})});return e}outlineNode(e){return e.type==="group"?{type:"group",id:e.id,...this.tagsField(e.id),...e.selector?{selector:e.selector}:{},...e.prompt?{prompt:this.beatInfo(e.prompt)}:{},children:e.children.map(t=>this.outlineNode(t))}:{type:"snippet",id:e.id,...this.tagsField(e.id),beats:(e.beats??[]).map(t=>this.beatInfo(t)),...e.jump?{jumpTo:e.jump.to,...e.jump.mode?{jumpMode:e.jump.mode}:{}}:{}}}beatInfo(e){let t=I(this.host,e.id),r={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){r.character=e.character;let i=this.host.defaultStrings[Y(e.character)]??this.host.castDisplay.get(e.character);i!==void 0&&(r.characterName=i)}if(e.direction!==void 0&&(r.direction=e.direction),e.qualifier!==void 0){r.qualifier=e.qualifier;let i=this.host.defaultStrings[X(e.qualifier)]??this.host.qualifierDisplay.get(e.qualifier);i!==void 0&&(r.qualifierName=i)}let s=G(this.host,e.character,e.qualifier,!0);s!==void 0&&(r.speakerLabel=s)}if(e.kind==="line"||e.kind==="text"){let s=this.host.padIndex.get(e.id);r.padAfter=s?.resolved??k,s?.own!==void 0&&(r.ownPadAfter=s.own);let i=this.host.defaultStrings[e.id];i!==void 0&&(r.text=i)}return e.gameData&&Object.keys(e.gameData).length&&(r.gameData=e.gameData),t&&t.length&&(r.tags=t),r}tagsField(e){let t=I(this.host,e);return t&&t.length?{tags:t}:{}}getFlow(e){return this.flowsById.get(e)}flows(){return[...this.flowsById.values()]}listBags(){let e=[{bag:this.host.patterBag}];for(let[t,r]of this.host.stageBags)e.push({bag:r,pathPrefix:`@scene:${t}.`});return e}checkpoint(){if(this.host.journal)throw new Error("a checkpoint is already open: roll it back or commit it first");let e={undo:[],flows:new Set,opened:new Set,selectors:new Set,flowSelectors:new Map};return this.host.journal=e,new _(e)}rollback(e){this.endCheckpoint(e);let t=e.journal.undo;for(let r=t.length-1;r>=0;r--)t[r]();n.forget(e.journal)}commit(e){this.endCheckpoint(e),n.forget(e.journal)}get inCheckpoint(){return this.host.journal!==null}endCheckpoint(e){if(this.host.journal!==e.journal)throw new Error("that checkpoint is not the open one");this.host.journal=null}static forget(e){e.undo=[],e.flows.clear(),e.opened.clear(),e.selectors.clear(),e.flowSelectors.clear()}refuseInCheckpoint(e){if(this.host.journal)throw new Error(`${e} can\'t be undone, so it isn\'t allowed while a checkpoint is open`)}captureFlow(e){let t=this.flowsById.get(e);if(!t)throw new Error(`no open flow \'${e}\' to capture`);return{version:L,...t.snapshot(),...Ft(e,this.host.registry.save())}}restoreFlow(e,t){this.refuseInCheckpoint("restoreFlow");let r=t.version;if(r!==2&&r!==L)throw new Error(`unsupported flow state version: ${String(r)}`);let s=t.cursor;if(typeof s!="object"||s===null)throw new Error("malformed flow state: no cursor");if(this.flowsById.has(e))throw new Error(`a flow \'${e}\' is already open; close it before restoring another under that id`);this.assertExternalScopes();let i=S.flow(e);this.host.registry.discardParked(i),this.host.registry.load(tt(e,t),{keepParked:!0,refit:!0});let o=new R(e,this.host,this.defaultSeed);try{o.restore(t)}catch(a){throw o.close(),this.host.registry.discardParked(i),a}return this.flowsById.set(e,o),this.host.reportRefits(),o}closeFlow(e){this.refuseInCheckpoint("closeFlow"),this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){this.refuseInCheckpoint("reset");for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(S.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let r of this.flowsById.values())r.releaseBags(e),r.close();this.flowsById.clear();let t=this.host.registry;for(let r of this.host.stageBags.keys())t.remove(S.stage(r),{keep:e});this.host.stageBags.clear();for(let r of["patter",...this.host.hostScopes])t.has(r)&&t.remove(r,{keep:e});for(let r of Object.keys(Je(this.creationOptions)))t.has(r)&&t.remove(r)}getProperty(e){let{scope:t,name:r}=this.splitShared(e);return this.host.registry.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitShared(e),i=this.host.journal?this.host.registry.get(r,s):void 0;this.host.registry.set(r,s,t,{host:!0}),this.host.journal&&i!==void 0&&this.host.journal.undo.push(()=>this.host.registry.set(r,s,i,{host:!0}))}stageIndex(e,t){return P(this.ladderOf(e),t)}compareStages(e,t,r){return O(this.ladderOf(e),t,r)}nextStage(e,t){return B(this.ladderOf(e),t)}ladderOf(e){let{scope:t,name:r}=this.splitShared(e),s=Bt(this.host,t,r);if(!s)throw new Error(`\'${e}\' is not a quality`);return s}log(){return this.engineLog}clearLog(){this.engineLog.length=0}onTrace(e){return this.engineTraceHandlers.add(e),this.host.tracing=!0,()=>{this.engineTraceHandlers.delete(e),this.host.tracing=this.host.logEnabled||this.engineTraceHandlers.size>0}}emitEngine(e,t,r){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineSeq++,...r?{scene:r}:{}})}getVisitCounts(){return Object.fromEntries(this.host.sharedVisits)}listProperties(){return this.host.patterSharedDecls.map(e=>({name:e.name,path:`@patter.${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:A(e),writable:e.writable??!0}))}splitShared(e){let t=et(this.host,e);if(t.scope==="scene")throw new Error(`\'${e}\': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);return t}save(){return{patter:this.host.patterBag.save()}}load(e){e.patter&&this.host.patterBag.load(e.patter)}saveGame(){let e={};for(let[t,r]of this.flowsById)e[t]=r.snapshot();return{version:L,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:Ye(this.host.sharedSelectors),flows:e}}loadGame(e){this.refuseInCheckpoint("loadGame");let t=e.version;if(t!==2&&t!==L)throw new Error(`unsupported save version: ${String(t)}`);let r=e.flows;if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("malformed save: no flows");this.assertExternalScopes();let s=this.host.registry;for(let[o,a]of this.flowsById)a.releaseBags(o in e.flows),a.close();this.flowsById.clear();for(let o of this.host.stageBags.keys())s.remove(S.stage(o),{keep:!0});this.host.stageBags.clear();let i=e.version===2?Nt(e):e.registry;i&&(this.host.ownsRegistry?s.load(i,{refit:!0}):s.load(i,{keepParked:!0,refit:!0})),this.host.sharedVisits.clear();for(let[o,a]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(o,a);this.host.sharedSelectors.clear();for(let[o,a]of Xe(e.sharedSelectors))this.host.sharedSelectors.set(o,a);for(let[o,a]of Object.entries(e.flows)){let c=new R(o,this.host,this.defaultSeed);c.restore(a),this.flowsById.set(o,c)}this.host.reportRefits()}},R=class{id;host;local;registered=new Set;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPrompt=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.patterBag.get(e):this.local.get(e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.registry.set("patter",e,t):this.local.set(e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;if(t!==null)return this.sceneBagFor(t,e)?.get(e)},set:(e,t)=>{let r=this.currentSceneId;if(r===null)return;let s=this.sceneBagFor(r,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,r){this.id=e,this.host=t,this.rngState=F(r),this.local=this.newLocal(),this.evalCtx={scopes:{},host:{nextRandom:this.rng,visits:s=>this.visitCounts.get(s)??0,patterVisits:s=>this.host.sharedVisits.get(s)??0},qualities:(s,i)=>this.stagesFor(s,i)}}context(){let e=this.host.registry;if(e.revision!==this.ctxRevision){let t=e.toEvalContext(),r=this.evalCtx.scopes;for(let s of Object.keys(r))delete r[s];Object.assign(r,t.scopes),r.patter=this.patterResolver,r.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let r=t.toLowerCase();if(e==="patter")return $(this.host,"patter",()=>[...this.host.patterSharedDecls,...this.host.patterLocalDecls]).get(r);if(e==="scene"){let s=this.currentSceneId;return s==null?void 0:$(this.host,`scene/${s}`,()=>this.host.bundle.scenes[s]?.sceneProps).get(r)}return this.registryQualities?.(e,t)??$(this.host,`host/${e}`,()=>this.host.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(r)}begin(e,t){if(this.host.journal&&!this.host.journal.opened.has(this))throw new Error("a flow can\'t be started or reset while a checkpoint is open");if(this.releaseBags(!1),this.host.registry.discardParked(S.flow(this.id)),this.mountLocal(),this.selectors.clear(),this.visitCounts.clear(),this.stack=[],this.currentSceneId=null,this.flowEnded=!1,this.activeSnippet=null,this.beatIndex=0,this.clearPending(),this.started=!0,t){let r=this.host.blockIndex.get(t);if(!r)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(r.sceneId),this.stack=[{sceneId:r.sceneId,containerId:t,index:0}],this.enter(t)}else{let r=e??Object.keys(this.host.bundle.scenes)[0],s=r?this.host.bundle.scenes[r]:void 0;if(!s)throw new Error(r?`unknown scene: ${r}`:"no scenes in bundle");this.enterSceneSetup(r);let i=s.blocks[0];i&&(this.stack=[{sceneId:r,containerId:i.id,index:0}],this.enter(i.id))}this.settle()}reset(e,t){this.begin(e,t)}start(e,t){this.reset(e,t)}goto(e,t){if(this.closed)return!1;if(this.touch(),e==="END")return this.started=!0,this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=ue(this.host,e);if(r===void 0)return!1;let s;return t!==void 0&&(s=ge(this.host,r,t),s===void 0)?!1:this.started?(this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??r,"jump"),this.settle(),!0):(this.begin(r,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.clearPending()}clearPending(){this.pendingChoice=null,this.pendingPrompt=null,this.pendingPromptOwnerId=null}touch(){let e=this.host.journal;if(!e||e.flows.has(this))return;e.flows.add(this);let{rngState:t,started:r,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:p}=this,h=this.stack.map(f=>({...f}));e.undo.push(()=>{Object.assign(this,{rngState:t,started:r,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:p,stack:h})})}get isClosed(){return this.closed}get currentScene(){return this.currentSceneId}advance(){if(this.closed)return{type:"end"};if(!this.started)throw new Error("flow has not been started");if(this.touch(),this.pendingPrompt){let e=this.pendingPrompt;return this.pendingPrompt=null,this.pendingPromptOwnerId=null,e.shown?this.promptResult(e.beat,e.shown):this.beatResult(e.beat)}return this.settle(),this.flowEnded?{type:"end"}:this.pendingChoice?{type:"choice",groupId:this.pendingChoice.groupId,options:this.pendingChoice.options}:this.activeSnippet?this.beatResult(this.activeSnippet.beats[this.beatIndex++]):(this.flowEnded=!0,{type:"end"})}advanceToStop(){let e=[];for(;;){let t=this.advance();if(t.type==="choice"||t.type==="end")return{played:e,stop:t};e.push(t)}}settle(){let e=0;for(;;){if(++e>1e4)throw new Error("flow did not settle after 10000 transitions - likely a jump cycle with no deliverable content");if(this.flowEnded||this.pendingChoice)return;if(this.activeSnippet){if(this.beatIndex<(this.activeSnippet.beats?.length??0))return;this.runEffects(this.activeSnippet.onExit,this.activeSnippet.id);let i=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(i);continue}let t=this.stack[this.stack.length-1];if(!t){this.flowEnded=!0;return}t.sceneId!==this.currentSceneId&&(this.currentSceneId=t.sceneId);let r=this.childrenOf(t.containerId);if(!r){this.stack.pop();continue}let s=t.index;for(;t.index<r.length&&!this.eligible(r[t.index]);)t.index++;if(this.host.tracing&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:r.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:r[t.index]?.id??null}),t.index>=r.length){this.stack.pop();continue}this.enterChild(r[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,r]of this.sceneBags)e.push({bag:r,pathPrefix:`${this.id}/@scene:${t}.`});return e}log(){return this.flowLog}clearLog(){this.flowLog.length=0}emit(e){let t=this.currentSceneId??void 0;this.host.emitEngine(this.id,e,t),this.host.logEnabled&&this.flowLog.push({...e,seq:this.flowSeq++,...t?{scene:t}:{}})}getVisitCounts(){return Object.fromEntries(this.visitCounts)}stepFor(e){let t=Rt(this.host.bundle).get(e);return t?this.beatResult(t):void 0}getChoices(){return this.pendingChoice?.options??[]}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let r=t.options.find(o=>o.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);this.touch();let s=t.byId.get(e);this.host.tracing&&this.emit({type:"chose",group:t.groupId,option:e}),this.pendingChoice=null;let i=this.host.replayPromptOnChoose?this.authoredPromptOf(s):void 0;this.pendingPrompt=i&&r.prompt?{beat:i,shown:{...r.prompt}}:null,this.pendingPromptOwnerId=this.pendingPrompt?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:r}=this.splitRef(e);return t==="patter"?this.patterResolver.get(r):t==="scene"?this.sceneResolver.get(r):this.host.registry.get(t,r)}setProperty(e,t){this.writeProperty(e,t,!0)}stageIndex(e,t){return P(this.ladderOf(e),t)}compareStages(e,t,r){return O(this.ladderOf(e),t,r)}nextStage(e,t){return B(this.ladderOf(e),t)}ladderOf(e){let{scope:t,name:r}=this.splitRef(e);this.context();let s=this.stagesFor(t,r);if(!s)throw new Error(`\'${e}\' is not a quality`);return s}writeProperty(e,t,r){let{scope:s,name:i}=this.splitRef(e),o=this.host.journal;if(s==="patter"){if(o){let a=this.host.patterSharedNames.has(i),c=a?this.host.patterBag.get(i):this.local.get(i),l=this.local;c!==void 0&&o.undo.push(a?()=>this.host.registry.set("patter",i,c):()=>l.set(i,c))}this.patterResolver.set(i,t)}else if(s==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);if(o){let a=this.sceneBagFor(this.currentSceneId,i),c=a?.get(i);a&&c!==void 0&&o.undo.push(()=>a.set(i,c))}this.sceneResolver.set(i,t)}else{let a=o?this.host.registry.get(s,i):void 0;this.host.registry.set(s,i,t,r?{host:!0}:void 0),o&&a!==void 0&&o.undo.push(()=>this.host.registry.set(s,i,a,{host:!0}))}}snapshot(){return{rngState:this.rngState,visits:Object.fromEntries(this.visitCounts),cursor:{flowEnded:this.flowEnded,currentSceneId:this.currentSceneId,stack:this.stack.map(e=>{let t=this.childrenOf(e.containerId)?.[e.index];return t?{...e,nextId:t.id}:{...e}}),activeSnippetId:this.activeSnippet?.id??null,beatIndex:this.beatIndex,pendingChoice:this.pendingChoice?{groupId:this.pendingChoice.groupId,options:this.pendingChoice.options.map(e=>({...e}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,...this.pendingPrompt?.shown?{pendingPrompt:Ot(this.pendingPrompt.shown)}:{},selectors:Ye(this.selectors)}}}restore(e){if(this.host.journal)throw new Error("a flow can\'t be restored while a checkpoint is open");this.restoreCursor(e),this.releaseBags(!0),this.mountLocal();for(let t of new Set([this.currentSceneId,...this.stack.map(r=>r.sceneId)]))t!==null&&this.host.bundle.scenes[t]&&this.ensureSceneBags(t)}restoreCursor(e){this.rngState=F(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;if(this.started=!0,this.flowEnded=t.flowEnded,this.beatIndex=t.beatIndex,this.currentSceneId=t.currentSceneId,this.stack=t.stack.map(r=>{let{nextId:s,...i}=r;if(s!==void 0){let o=this.childrenOf(i.containerId)?.findIndex(a=>a.id===s)??-1;if(o>=0)return{...i,index:o}}return{...i}}),this.activeSnippet=null,t.activeSnippetId!==null){let r=this.host.nodeIndex.get(t.activeSnippetId);r&&r.type==="snippet"&&(this.activeSnippet=r)}if(this.selectors=Xe(t.selectors),this.pendingChoice=null,t.pendingChoice!==null){let r=new Map,s=[];for(let i of t.pendingChoice.options){let o=this.host.nodeIndex.get(i.id);if(!o)continue;r.set(i.id,o);let{prompt:a,...c}=i,l=a?.kind==="line"&&a.speakerLabel===void 0?G(this.host,a.character,a.qualifier):void 0;s.push({...c,...a?{prompt:{...a,padAfter:a.padAfter??this.promptPadOf(o),...l!==void 0?{speakerLabel:l}:{}}}:{}})}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:r})}if(this.pendingPrompt=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId),s=r?this.authoredPromptOf(r):void 0,i=t.pendingPrompt,o=i?.kind==="line"&&i.speakerLabel===void 0?G(this.host,i.character,i.qualifier):void 0;s?this.pendingPrompt={beat:s,...i?{shown:{...i,padAfter:i.padAfter??this.padOf(s.id),...o!==void 0?{speakerLabel:o}:{}}}:{}}:this.pendingPromptOwnerId=null}}enterSceneSetup(e){let t=this.host.bundle.scenes[e];if(!t)throw new Error(`unknown scene: ${e}`);this.currentSceneId=e,this.enter(e),this.seedScene(t),this.runEffects(t.onEntry,t.id)}enterChild(e){if(this.enter(e.id),e.type==="snippet"){this.beginSnippet(e);return}let t=e.selector??"run";if(t==="run"){this.stack.push({sceneId:this.currentSceneId,containerId:e.id,index:0});return}if(t==="choice"){this.setupChoice(e);return}let r=this.selectChild(e);r&&this.enterChild(r)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let r=this.host.nodeIndex.get(e);if(r&&r.type==="group")return r.children}beginSnippet(e){this.runEffects(e.onEnter,e.id),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],r=new Map,s=[];for(let o of e.children){if(o.fallback===!0){s.push(o);continue}if(o.sticky!==!0&&(this.visitCounts.get(o.id)??0)>=1)continue;let a=this.eligible(o),c=o.secretUntilEligible===!0;!a&&c||(t.push({id:o.id,prompt:this.promptFor(o),eligible:a,gameData:o.gameData}),r.set(o.id,o))}if(t.some(o=>o.eligible)){this.host.tracing&&this.emit({type:"choice",group:e.id,options:t.map(o=>({id:o.id,eligible:o.eligible}))}),this.pendingChoice={groupId:e.id,options:t,byId:r};return}let i=s.find(o=>this.eligible(o));if(i){this.enterChild(i);return}this.host.tracing&&this.emit({type:"dry",group:e.id}),this.host.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(this.host.tracing&&this.emit({type:"jump",to:e,mode:t}),e==="END"){this.flowEnded=!0,this.stack=[];return}let r,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}r=e,s=a.id}else{let a=this.host.blockIndex.get(e);if(!a)throw new Error(`jump target not found: ${e}`);a.sceneId!==this.currentSceneId&&this.enterSceneSetup(a.sceneId),r=a.sceneId,s=e}this.enter(s);let o={sceneId:r,containerId:s,index:0};t==="call"?this.stack.push(o):this.stack=[o]}selectChild(e){let t=e.children.map(c=>({id:c.id,eligible:this.eligible(c)})),r=e.children.filter((c,l)=>t[l].eligible),s=e.options?.order??"sequential",i=e.options?.exhaust??"once",o=c=>(this.host.tracing&&this.emit({type:"select",group:e.id,selector:e.selector??"default",...e.selector==="sequence"?{order:s,exhaust:i}:{},children:t,picked:c?.id??null}),c);if(r.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(r[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(r,i,a):s==="specificity"?this.pickSpecificity(r,i,a):this.pickSequential(r,i,a));default:return null}}pickSequential(e,t,r){let s=e.length,i=r.seq??0;return r.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,r){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(f=>f.id),a=new Set(e.map(f=>f.id));if(r.bag===void 0&&(r.bag=o()),!r.bag.some(f=>a.has(f))){if(t==="once")return null;if(i){let f=e[s-1];return r.last=f.id,f}if(r.bag=o(),r.bag.length===0)return null}let c=r.bag.filter(f=>a.has(f)),l=r.last!==void 0&&c.length>1?c.indexOf(r.last):-1,p=Math.floor(this.rng()*(l>=0?c.length-1:c.length));l>=0&&p>=l&&p++;let h=c[p];return r.bag.splice(r.bag.indexOf(h),1),r.last=h,e.find(f=>f.id===h)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(p=>p.id));let l=new Set(r.bag);if(s=e.filter(p=>l.has(p.id)),s.length===0)return t==="stick"&&r.last!==void 0?e.find(p=>p.id===r.last)??null:null}let i=-1,a=s.map(l=>{let p=this.specScore(l);return p>i&&(i=p),{c:l,s:p}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=r.last!==void 0?a.findIndex(h=>h.id===r.last):-1,p=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&p>=l&&p++,c=a[p]}return t!=="repeat"&&(r.bag=r.bag.filter(l=>l!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0,e):0}matchedSpec(e,t,r){return Ee(e,i=>{try{return ze(W(i,this.context(),pe))}catch(o){return this.reportError("best-match",r.id,r.condition,o),!1}},{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=this.host.journal,s;if(r&&(s=e.shared?r.selectors:r.flowSelectors.get(this),s||(s=new Set,r.flowSelectors.set(this,s))),r&&s&&!s.has(e.id)){s.add(e.id);let o=t.get(e.id),a=o?{...o,...o.bag?{bag:[...o.bag]}:{}}:void 0;r.undo.push(()=>{a?t.set(e.id,a):t.delete(e.id)})}let i=t.get(e.id);return i||(i={},t.set(e.id,i)),i}runEffects(e,t){for(let r of e??[]){let s;try{s=this.evalExpr(r.value);let i=this.host.tracing?this.getProperty(r.target):void 0;this.writeProperty(r.target,s,!1),this.host.tracing&&this.emit({type:"write",target:r.target,value:s,...i!==void 0?{prev:i}:{}})}catch(i){this.reportError("effect",t,r.value,i)}}}eligible(e){if(!e.condition)return!0;try{return ze(this.evalExpr(e.condition))}catch(t){return this.reportError("condition",e.id,e.condition,t),!1}}reportError(e,t,r,s){let i=s instanceof Error?s.message:String(s),o=r?.src;this.host.onError({flow:this.id,kind:e,node:t,...o?{source:o}:{},message:i}),this.host.tracing&&this.emit({type:"diagnostic",kind:e,node:t,...o?{source:o}:{},message:i})}evalExpr(e){return W(this.conditionAst(e),this.context(),pe)}conditionAst(e){let t=Ge.get(e);return t||(t=E(e.ast),Ge.set(e,t)),t}enter(e){let t=this.visitCounts,r=t.get(e);t.set(e,(r??0)+1);let s=this.host.sharedVisits,i=s.get(e);s.set(e,(i??0)+1),this.host.journal&&this.host.journal.undo.push(()=>{r===void 0?t.delete(e):t.set(e,r),i===void 0?s.delete(e):s.set(e,i)})}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=ie(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=I(this.host,e.id),r=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:e.gameData,...r};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),padAfter:this.padOf(e.id),gameData:e.gameData,...r};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,qualifier:c?void 0:e.qualifier,qualifierName:c?void 0:this.resolveQualifierName(e.qualifier),speakerLabel:c?void 0:G(this.host,e.character,e.qualifier),padAfter:this.padOf(e.id),gameData:e.gameData,...r}}}}interpolate(e){return Ae(e,t=>this.getProperty(t))}stripCaptions(e){return Be(e,this.host.captionOpen,this.host.captionClose)}captionLine(e){return this.host.captionsOn?e:this.stripCaptions(e)}promptFor(e){let t=this.promptBeatOf(e);if(!t)return;let r=this.interpolate(this.resolveString(t.id)),s=this.padOf(t.id);return t.kind==="line"?{kind:"line",text:this.captionLine(r),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction,qualifier:t.qualifier,qualifierName:this.resolveQualifierName(t.qualifier),speakerLabel:G(this.host,t.character,t.qualifier),padAfter:s}:{kind:"text",text:r,padAfter:s}}authoredPromptOf(e){return e.type==="group"?e.prompt:void 0}promptResult(e,t){let r=I(this.host,e.id),s=r&&r.length?{tags:r}:{};return t.kind==="text"?{type:"text",id:e.id,text:t.text,padAfter:this.padOf(e.id),gameData:e.gameData,...s}:{type:"line",id:e.id,text:t.text,character:t.character,characterName:t.characterName,direction:t.direction,qualifier:t.qualifier,qualifierName:t.qualifierName,speakerLabel:t.speakerLabel,padAfter:this.padOf(e.id),gameData:e.gameData,...s}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(r=>r.kind==="line"||r.kind==="text")}firstTextSnippetIn(e){let t;return D(e,r=>{!t&&r.type==="snippet"&&(r.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=r)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let r=this.host.defaultStrings[e];return r!==void 0?`<Untranslated: ${e}> ${r}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=Y(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}promptPadOf(e){let t=this.promptBeatOf(e);return t?this.padOf(t.id):k}padOf(e){return this.host.padIndex.get(e)?.resolved??k}resolveQualifierName(e){if(e===void 0||this.host.emitIds)return;let t=X(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.qualifierDisplay.get(e)}splitRef(e){return et(this.host,e)}newLocal(){return new x(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=S.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:q}),this.registered.add(e),this.host.reportRefits()}releaseBags(e){for(let t of this.registered)this.host.registry.remove(t,{keep:e});this.registered.clear(),this.sceneBags.clear()}sceneBagFor(e,t){if(this.host.bundle.scenes[e])return this.ensureSceneBags(e),this.host.sceneSharedNames.get(e)?.has(t.toLowerCase())?this.host.stageBags.get(e):this.sceneBags.get(e)}ensureSceneBags(e){let t=this.host.sceneSharedNames.get(e)??new Set,r=this.host.bundle.scenes[e]?.sceneProps??[],s=this.host.journal,i=!this.sceneBags.has(e)||!this.host.stageBags.has(e);if(!this.sceneBags.has(e)){let o=new x(r.filter(l=>!t.has(l.name.toLowerCase())),{pathPrefix:"@scene."}),a=S.flowScene(this.id,e),c=Ke(this.host.registry,a,o,s!==null);this.registered.add(a),this.sceneBags.set(e,o),s&&s.undo.push(()=>{this.sceneBags.get(e)===o&&(this.host.registry.remove(a,c?{keep:!0}:void 0),this.registered.delete(a),this.sceneBags.delete(e))})}if(!this.host.stageBags.has(e)){let o=new x(r.filter(c=>t.has(c.name.toLowerCase())),{pathPrefix:"@scene."}),a=Ke(this.host.registry,S.stage(e),o,s!==null);this.host.stageBags.set(e,o),s&&s.undo.push(()=>{this.host.stageBags.get(e)===o&&(this.host.registry.remove(S.stage(e),a?{keep:!0}:void 0),this.host.stageBags.delete(e))})}i&&this.host.reportRefits()}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let r of e.sceneProps??[]){if(!r.temporary)continue;let s=r.name.toLowerCase(),i=t.has(s)?this.host.stageBags.get(e.id):this.sceneBags.get(e.id);if(!i)continue;let o=this.host.journal?i.get(s):void 0;o!==void 0&&this.host.journal.undo.push(()=>i.set(s,o)),i.set(s,A(r))}}};function Ue(n,e){D(n,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let r of t.beats??[])r.kind==="line"&&r.character&&e.add(r.character)})}function G(n,e,t,r=!1){if(!e||n.emitIds)return;let s=c=>(r?void 0:n.strings[c])??n.defaultStrings[c],i=s(Y(e))??n.castDisplay.get(e)??e;if(t===void 0)return i;let o=n.speakerLabels.get(`${e}\\0${t}`);if(o!==void 0)return s(Ne(e,t))??o;let a=s(X(t))??n.qualifierDisplay.get(t)??t;return Le(s(Fe)??n.speakerPattern,i,a)}function We(n){return n&&Object.keys(n).length?{gameData:n}:{}}function Ot(n){let e={kind:n.kind,text:n.text};return n.character!==void 0&&(e.character=n.character),n.characterName!==void 0&&(e.characterName=n.characterName),n.direction!==void 0&&(e.direction=n.direction),n.qualifier!==void 0&&(e.qualifier=n.qualifier),n.qualifierName!==void 0&&(e.qualifierName=n.qualifierName),n.speakerLabel!==void 0&&(e.speakerLabel=n.speakerLabel),typeof n.padAfter=="number"&&(e.padAfter=n.padAfter),e}function Ye(n){let e={};for(let[t,r]of n){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e[t]=s}return e}function Xe(n){let e=new Map;for(let[t,r]of Object.entries(n??{})){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e.set(t,s)}return e}function Ke(n,e,t,r){let s=r?JSON.stringify(t.save()):"";return n.mountOwned(e,t,{owner:q}),r&&JSON.stringify(t.save())!==s}function Qe(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function Je(n){return{...n.world?{world:n.world}:{},...n.hostScopes}}function $(n,e,t){let r=n.qualityLadders.get(e);if(!r){r=new Map;for(let s of t()??[]){let i=s.name.toLowerCase();s.type==="quality"&&s.stages&&!r.has(i)&&r.set(i,s.stages)}n.qualityLadders.set(e,r)}return r}function Bt(n,e,t){let r=t.toLowerCase();return e==="patter"?$(n,"patter",()=>[...n.patterSharedDecls,...n.patterLocalDecls]).get(r):n.registry.toEvalContext().qualities?.(e,t)??$(n,`host/${e}`,()=>n.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(r)}function Ze(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function At(n,e){let t=Ze(n),r=n.writable??e;return r===void 0?t:{...t,writable:r}}function ue(n,e){return n.sceneGameIdToId.get(e)??(n.bundle.scenes[e]?e:void 0)}function ge(n,e,t){return n.blockGameIdToId.get(e)?.get(t)??(n.blockIndex.get(t)?.sceneId===e?t:void 0)}function et(n,e){n.refSplitRevision!==n.registry.revision&&(n.refSplitCache.clear(),n.refSplitRevision=n.registry.revision);let t=n.refSplitCache.get(e);return t||(t=Oe(e,r=>r==="scene"||n.registry.has(r)||(n.bundle.externalScopes?.includes(r)??!1)),n.refSplitCache.set(e,t)),t}function Nt(n){let e={};n.shared?.patter&&(e.patter=n.shared.patter);for(let[t,r]of Object.entries(n.stageBags??{}))e[S.stage(t)]=r;for(let[t,r]of Object.entries(n.flows??{}))Object.assign(e,tt(t,r));return e}function tt(n,e){let t={};e.scopes?.patter&&(t[S.flowGlobals(n)]=e.scopes.patter);for(let[r,s]of Object.entries(e.sceneBags??{}))t[S.flowScene(n,r)]=s;return t}function Ft(n,e){let t={scopes:{},sceneBags:{}},r=S.flow(n);for(let[s,i]of Object.entries(e)){if(!s.startsWith(r))continue;let o=s.slice(r.length);o==="patter"?t.scopes.patter=i:o.startsWith("scene/")&&(t.sceneBags[Pt(o.slice(6))]=i)}return t}function ze(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var Tt=(n,e)=>n.shared??e;function fe(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:Tt(n,e)}}function Lt(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function Vt(n,e){e.blocks++;let t=[...n.children];for(;t.length;){let r=t.pop();if(r.type==="group"){e.groups++,r.prompt&&e.prompts++,t.push(...r.children);continue}e.snippets++;for(let s of r.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function rt(n){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:n.cast?.length??0},t=[],r=[];for(let o of Object.values(n.scenes)){e.scenes++;let a=w(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:w(c),name:c.name}))});for(let c of o.blocks)Vt(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>fe(c,!1))})}let s=(n.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>fe(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(Lt)}));return{identity:{schema:n.schema,project:n.content.project,...n.content.version!==void 0?{version:n.content.version}:{},...n.content.hash!==void 0?{hash:n.content.hash}:{},...n.content.structureHash!==void 0?{structureHash:n.content.structureHash}:{},voiced:n.voiced,defaultLocale:n.locales.default,locales:[...n.locales.included],localisation:n.localisation?.mode??"embedded",sourceDebug:n.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(n.properties??[]).map(o=>fe(o,!0)),scene:r},gameData:i,counts:e}}function nt(n,e){return n.gameDataFields?.[e]??[]}function he(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function st(n,e){let t={};for(let r of n){let s=he(n,e,r.name);s!==void 0&&(t[r.name]=s)}for(let[r,s]of Object.entries(e??{}))r in t||(t[r]=s);return t}return gt(Mt);})();\n';
1773
+ var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var ie=Object.defineProperty;var gt=Object.getOwnPropertyDescriptor;var ft=Object.getOwnPropertyNames;var ht=Object.prototype.hasOwnProperty;var mt=(r,e)=>{for(var t in e)ie(r,t,{get:e[t],enumerable:!0})},yt=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ft(e))!ht.call(r,s)&&s!==t&&ie(r,s,{get:()=>e[s],enumerable:!(n=gt(e,s))||n.enumerable});return r};var St=r=>yt(ie({},"__esModule",{value:!0}),r);var _t={};mt(_t,{Checkpoint:()=>U,DEFAULT_PAD_AFTER:()=>E,Engine:()=>ne,Flow:()=>O,buildTagIndex:()=>te,compareStages:()=>N,describeBundle:()=>st,effectiveGameData:()=>ot,extensionDecls:()=>at,extensionSettings:()=>ct,extensionsOf:()=>lt,gameDataFields:()=>it,gameDataValue:()=>ye,nextStage:()=>B,stageIndex:()=>P});function T(r,e){return r!==void 0&&Object.prototype.hasOwnProperty.call(r,e)?r[e]:void 0}function R(r){switch(r[0]){case"b":return{kind:"bool",value:r[1]};case"n":return{kind:"number",value:r[1]};case"s":return{kind:"string",value:r[1]};case"sv":return{kind:"scopedvar",scope:r[1],name:r[2]};case"u":return{kind:"unary",op:r[1],operand:R(r[2])};case"bin":return{kind:"binary",op:r[1],left:R(r[2]),right:R(r[3])};case"call":{let e=r.slice(2).map(R);return{kind:"call",name:r[1],args:e}}case"fd":return{kind:"flagdelta",sign:r[1],name:r[2]}}}var u=class extends Error{constructor(e){super(e),this.name="EvalError"}};function J(r,e,t){let n=new Map(t.scopes.map(i=>[i.token,i.missing??"false"])),s=i=>{switch(i.kind){case"bool":return i.value;case"number":return i.value;case"string":return i.value;case"scopedvar":{let o=T(e.scopes,i.scope);if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):T(o,i.name);if(a===void 0){if(n.get(i.scope)==="throw")throw new u(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!T(t.functions,i.name)){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new u(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=oe(a,e);if(c===void 0)throw new u("advance() needs a quality reference (@scope.name of a quality property)");let l=v(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=T(t.functions,i.name);if(!o)throw new u(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new u("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new u(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new u(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let h=s(i.left);if(typeof h!="boolean")throw new u(`\'and\' requires boolean operands, left is ${typeof h}`);if(!h)return!1;let f=s(i.right);if(typeof f!="boolean")throw new u(`\'and\' requires boolean operands, right is ${typeof f}`);return f}if(i.op==="or"){let h=s(i.left);if(typeof h!="boolean")throw new u(`\'or\' requires boolean operands, left is ${typeof h}`);if(h)return!0;let f=s(i.right);if(typeof f!="boolean")throw new u(`\'or\' requires boolean operands, right is ${typeof f}`);return f}let o=s(i.left),a=s(i.right),c=oe(i.left,e),l=oe(i.right,e),d=c??l;if(d!==void 0){if(c&&l&&!wt(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new u(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return v(o,d,">")>v(a,d,">");case">=":return v(o,d,">=")>=v(a,d,">=");case"<":return v(o,d,"<")<v(a,d,"<");case"<=":return v(o,d,"<=")<=v(a,d,"<=");case"+":case"-":case"*":case"/":throw new u(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return Re(o,a);case"!=":return!Re(o,a);case">":return C(o,a,">"),o>a;case">=":return C(o,a,">="),o>=a;case"<":return C(o,a,"<"),o<a;case"<=":return C(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new u(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return C(o,a,"-"),o-a;case"*":return C(o,a,"*"),o*a;case"/":if(C(o,a,"/"),a===0)throw new u("division by zero");return o/a}}}};return s(r)}function Re(r,e){if(Array.isArray(r)||Array.isArray(e)){if(!Array.isArray(r)||!Array.isArray(e)||r.length!==e.length)return!1;let t=[...r].sort(),n=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==n[s])return!1;return!0}return r===e}function C(r,e,t){if(typeof r!="number"||typeof e!="number")throw new u(`\'${t}\' requires numeric operands, got ${typeof r} and ${typeof e}`)}function oe(r,e){if(!(r.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(r.scope,r.name)}function v(r,e,t){if(typeof r!="string")throw new u(`\'${t}\' on a quality compares stages, got ${typeof r}`);let n=e.indexOf(r);if(n<0)throw new u(`"${r}" is not a stage of this quality (stages: ${e.join(", ")})`);return n}var wt=(r,e)=>r.length===e.length&&r.every((t,n)=>t===e[n]);function L(r){if(Number.isNaN(r)||!Number.isFinite(r))return 0;let e=Math.trunc(r)%4294967296;return e<0?e+4294967296:e}function ae(r){let e=L(r);return{next(){e=e+1831565813>>>0;let t=Math.imul(e^e>>>15,1|e);return t=t+Math.imul(t^t>>>7,61|t)^t,((t^t>>>14)>>>0)/4294967296},state(){return e}}}var le=r=>Array.isArray(r)?r:r.stages??[],vt=r=>Array.isArray(r)?"flags":r==null?"nothing":typeof r;function P(r,e){return typeof e=="string"?le(r).indexOf(e):-1}function ce(r,e){if(typeof e!="string")throw new u(`a stage is a name, got ${vt(e)}`);let t=r.indexOf(e);if(t<0)throw new u(`"${e}" is not a stage of this quality (stages: ${r.join(", ")})`);return t}function N(r,e,t){let n=le(r),s=ce(n,e)-ce(n,t);return s<0?-1:s>0?1:0}function B(r,e){let t=le(r);return t[Math.min(ce(t,e)+1,t.length-1)]}function de(r,e,t){switch(r.type){case"boolean":return typeof e=="boolean";case"number":return typeof e=="number";case"string":case"text":case"multiline":return typeof e=="string";case"enum":return typeof e!="string"?!1:e===""&&t?.noneChosen?!0:!Array.isArray(r.values)||r.values.includes(e);case"flags":return Array.isArray(e)&&e.every(n=>typeof n=="string"&&(!Array.isArray(r.values)||r.values.includes(n)));case"quality":return typeof e=="string"&&(!Array.isArray(r.stages)||r.stages.includes(e));default:return!0}}var bt={name:"check_flags",count:r=>Math.max(1,r.args.length-1)},xt=[bt];function Ce(r,e,t){let n=t?.countingCalls??xt;return Q(r,t?.want??!0,e,n)}function Q(r,e,t,n){if(r.kind==="binary"&&(r.op==="and"||r.op==="or")){let s=Q(r.left,e,t,n),i=Q(r.right,e,t,n);return r.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(r.kind==="unary"&&r.op==="not")return Q(r.operand,!e,t,n);if(r.kind==="call"){let s=n.find(i=>i.name===r.name);if(s){let i=s.count(r),o=t(r);return e?o?i:0:o?0:1}}return t(r)===e?1:0}var k=class r{values=Object.create(null);decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(n=>n.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let n=this.norm(t.name);this.decls.set(n,t),this.values[n]=structuredClone(t.default??A(t))}}get(e){return this.values[this.norm(e)]}normalise(e){return this.norm(e)}writable(e){return this.decls.get(this.norm(e))?.writable!==!1}set(e,t,n){let s=this.norm(e);if(!n?.host&&!this.writable(s))throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:n?.silent??!1,reason:n?.reason};this.values[s]=t;for(let o of[...this.auditors])o(i);if(!i.silent)for(let o of[...this.subscribers])o(i);return i}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}onAudit(e){return this.auditors.add(e),()=>this.auditors.delete(e)}rows(){return[...this.decls.entries()].map(([e,t])=>Ie(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new r([],{normalise:this.norm,pathPrefix:this.pathPrefix});return e.decls=new Map(this.decls),Object.assign(e.values,structuredClone(this.values)),e}reseed(e){for(let t of Object.keys(this.values))delete this.values[t];this.decls.clear(),this.seed(e)}save(){return structuredClone(this.values)}load(e,t){let n=new Map;for(let[i,o]of Object.entries(e)){let a=this.norm(i);this.values[a]=structuredClone(o),n.set(a,o)}let s=[];if(!t?.refit)return s;for(let[i,o]of this.decls){if(!n.has(i))continue;let a=n.get(i);de(o,a)||(this.values[i]=structuredClone(A(o)),s.push({name:i,value:a}))}return s}};function Ie(r,e,t,n,s=""){let i=n??r.name.toLowerCase();return{name:i,path:s+i,type:r.type,value:e,default:r.default??A(r),...r.values!==void 0?{values:r.values}:{},...r.stages!==void 0?{stages:r.stages}:{},writable:t??r.writable??!0}}var kt=r=>r.toLowerCase(),pe=1,z=class{scopes=new Map;parked=new Map;parkedRefit=new Set;misfits=[];rev=0;get revision(){return this.rev}defineOwned(e,t,n){let s=typeof n=="string"?{pathPrefix:n}:n??{},i=new k(t,{pathPrefix:s.pathPrefix??`${e}.`,...s.normalise?{normalise:s.normalise}:{}});return this.mountOwned(e,i,s.owner!==void 0?{owner:s.owner}:void 0)}mountOwned(e,t,n){this.assertFree(e,n?.owner),this.scopes.set(e,{kind:"owned",bag:t,...n?.owner!==void 0?{owner:n.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(this.note(e,t.load(s,{refit:this.parkedRefit.has(e)})),this.parked.delete(e),this.parkedRefit.delete(e)),this}remove(e,t){let n=this.scopes.get(e);if(!n)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&n.kind==="owned"&&(this.parked.set(e,n.bag.save()),this.parkedRefit.delete(e)),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear(),this.parkedRefit.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&(this.parked.delete(t),this.parkedRefit.delete(t));return this}takeMisfits(e){if(!e){let n=this.misfits;return this.misfits=[],n}let t=this.misfits.filter(n=>e(n.scope));return this.misfits=this.misfits.filter(n=>!e(n.scope)),t}note(e,t){for(let n of t)this.misfits.push({scope:e,name:n.name,value:n.value})}ownedBag(e){let t=this.scopes.get(e);if(!t||t.kind!=="owned")throw new Error(`\'@${e}\' is not an owned scope`);return t.bag}reseedOwned(e,t){return this.ownedBag(e).reseed(t),this}defineForeign(e,t,n=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??kt,a=new Map;for(let c of n)a.set(o(c.name),c);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:a,scopeWritable:i.writable??!0,norm:o,...i.owner!==void 0?{owner:i.owner}:{}}),this.rev++,this}has(e){return this.scopes.has(e)}get(e,t){let n=this.scopes.get(e);if(n)return n.kind==="owned"?n.bag.get(t):n.resolver.get(n.norm(t))}set(e,t,n,s){let i=this.scopes.get(e);if(!i)throw new Error(`unknown scope \'@${e}\'`);if(i.kind==="owned"){if(!s?.host&&!i.bag.writable(t))throw new Error(`\'@${e}.${t}\' is read-only`);i.bag.set(t,n,s?.host?{host:!0}:void 0);return}let o=i.norm(t);if(!i.resolver.set)throw new Error(`\'@${e}.${t}\' is read-only`);if(!s?.host&&!this.foreignWritable(i,o))throw new Error(`\'@${e}.${t}\' is read-only`);i.resolver.set(o,n)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,n]of this.scopes){let s=n.owner!==void 0?{owner:n.owner}:{};if(n.kind==="owned")for(let i of n.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of n.decls)e.push({scope:t,...s,...Ie(o,n.resolver.get(i),this.foreignWritable(n,i),i,`${t}.`)})}return e}toEvalContext(e,t){let n=this.view(t?.aliases),s={};for(let[o,a]of n)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(n);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=n.get(o);return c?i.get(o)?.get(Et(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[n,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${n}\' names \'${s}\', which is not registered`);t.set(n,i)}return t}qualityLadders(e){let t=new Map;for(let[n,s]of e)for(let[i,o]of De(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(n);a||(a=new Map,t.set(n,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[n,s]of this.view(e?.aliases)){let i=De(s);if(i.length===0)continue;let o=new Map;for(let[a,c]of i)o.set(a,{type:c.type,enumValues:c.values,...c.stages!==void 0?{stages:c.stages}:{}});t.set(n,o)}return{properties:t}}save(){let e={};for(let[t,n]of this.scopes)n.kind==="owned"&&(e[t]=n.bag.save());for(let[t,n]of this.parked)e[t]=structuredClone(n);return e}load(e,t){t?.keepParked||(this.parked.clear(),this.parkedRefit.clear());let n=t?.refit===!0;for(let[s,i]of Object.entries(e)){let o=this.scopes.get(s);o?.kind==="owned"?this.note(s,o.bag.load(i,{refit:n})):o||(this.parked.set(s,structuredClone(i)),n?this.parkedRefit.add(s):this.parkedRefit.delete(s))}}saveFragment(){return{version:pe,scopes:this.save()}}loadFragment(e){if(e.version!==pe)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${pe})`);this.load(e.scopes)}assertFree(e,t){let n=this.scopes.get(e);if(!n)return;let s=n.owner!==void 0?` by ${n.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function De(r){return r.kind==="foreign"?[...r.decls.entries()]:r.bag.declarations().map(e=>[r.bag.normalise(e.name),e])}function Et(r){return r.kind==="foreign"?r.norm:e=>r.bag.normalise(e)}function A(r){if(r.default!==void 0)return r.default;switch(r.type){case"boolean":return!1;case"number":return 0;case"string":case"text":case"multiline":return"";case"enum":return r.values?.[0]??"";case"flags":return[];case"quality":return r.stages?.[0]??"";default:return!1}}var Oe=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function j(r){return r.ctx.host??{}}var ue={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(r,e){if(r.length!==2)throw new u("random(a, b) requires exactly 2 arguments");let t=j(e).nextRandom;if(!t)throw new u("random() called without a PRNG in context");let n=e.evaluate(r[0]),s=e.evaluate(r[1]);if(typeof n!="number"||typeof s!="number")throw new u("random(a, b) arguments must be numbers");if(!Number.isInteger(n)||!Number.isInteger(s))throw new u("random(a, b) arguments must be integers");let i=Math.min(n,s),o=Math.max(n,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:Ne("check_flags"),eval(r,e){let t=Pe(r[0],e,"check_flags");for(let n=1;n<r.length;n++){let s=r[n];if(s.kind!=="flagdelta")throw new u("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:Ne("set_flags"),eval(r,e){let t=[...Pe(r[0],e,"set_flags")];for(let n=1;n<r.length;n++){let s=r[n];if(s.kind!=="flagdelta")throw new u("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:X("visits"),eval:(r,e)=>j(e).visits?.(Y(r,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:X("seen"),eval:(r,e)=>(j(e).visits?.(Y(r,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:X("patter_visits"),eval:(r,e)=>j(e).patterVisits?.(Y(r,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:X("patter_seen"),eval:(r,e)=>(j(e).patterVisits?.(Y(r,e,"patter_seen"))??0)>0}}},Sn=Oe.map(r=>r.token).filter(r=>r!=="patter"&&r!=="scene");function Be(r,e){let t=r.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function Y(r,e,t){let n=e.evaluate(r[0]);if(typeof n!="string")throw new u(`${t}(id) requires a string node id`);return n}function X(r){return(e,t)=>{let n=e[0];n&&n.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${r}(id): the argument must be a string id literal (a scene / block / node id)`})}}function Pe(r,e,t){if(!r)throw new u(`${t}() requires at least one argument (the flags variable)`);let n=e.evaluate(r);if(Array.isArray(n))return n;if(n===!1||n===null||n===void 0)return[];throw new u(`${t}() first argument must be a flags property`)}function Ne(r){return(e,t)=>{if(e.length===0)return;let n=e[0];if(n.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${r}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(n.scope)?.get(n.name);if(s&&s.type!=="flags"){let i=n.scope===t.defaultScope?n.name:`${n.scope}.${n.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${r}(): \'@${i}\' is not a flags property (got ${s.type})`});return}for(let i=1;i<e.length;i++){let o=e[i];o.kind!=="flagdelta"?t.report({path:[...t.path,"args",i],kind:"wrong-arg-type",severity:"error",message:`${r}(): argument ${i+1} must be +flagName or -flagName`}):s?.type==="flags"&&s.enumValues&&!s.enumValues.includes(o.name)&&t.report({path:[...t.path,"args",i],kind:"unknown-flag-name",severity:"error",message:`${r}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var Rt=/^@[A-Za-z0-9_.]+$/;function*Ct(r){let e="",t=0;for(;t<r.length;){let n=r[t];if(n==="{"&&r[t+1]==="{"){e+="{",t+=2;continue}if(n==="}"&&r[t+1]==="}"){e+="}",t+=2;continue}if(n==="{"){let s=r.indexOf("}",t+1);if(s!==-1){let i=r.slice(t,s+1),o=r.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:Rt.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=n,t+=1}e&&(yield{kind:"text",value:e})}function Dt(r){return Array.isArray(r)?r.join(", "):typeof r=="boolean"?r?"true":"false":String(r)}function It(r){return r===" "||r===" "||r===`\n`||r==="\\r"||r==="\\f"||r==="\\v"}function Ot(r){let e="",t=!1;for(let n of r){if(It(n)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=n}return e}function Ae(r,e,t){if(e.length===0||r.indexOf(e)<0)return r;let n="",s=0,i=!1;for(;s<r.length;){if(r.startsWith(e,s)){let o=r.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}n+=r.slice(s);break}n+=r[s],s+=1}return i?Ot(n):r}function Fe(r,e){if(r.indexOf("{")<0)return r;let t="";for(let n of Ct(r)){if(n.kind==="text"){t+=n.value;continue}if(!n.ref){t+=n.raw;continue}let s=e(n.ref);t+=s===void 0?"":Dt(s)}return t}function D(r,e){for(let t of r){e(t);let n=t.children;n&&D(n,e)}}function Pt(r){return r.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function y(r){let e=r.gameId?.trim();return e||Pt(r.name)}var E=.6;function Z(r){return`cast:${r}`}function ee(r){return`qualifier:${r}`}function Te(r,e){return`speaker:${r}:${e}`}var Le="speaker:pattern",je="{name} ({qualifier})";function Me(r,e,t){return r.split("{name}").map(n=>n.split("{qualifier}").join(t)).join(e)}var ge={open:"[",close:"]"},Ve="SFX";var M=3;function V(r){let e=new Set,t=[];for(let n of r)e.has(n)||(e.add(n),t.push(n));return t}function te(r){let e=new Map,t=(n,s)=>{let i=V([...s,...n.tags??[]]);if(e.set(n.id,i),n.type==="group"&&n.prompt&&e.set(n.prompt.id,V([...i,...n.prompt.tags??[]])),n.type==="group")for(let o of n.children)t(o,i);else for(let o of n.beats??[])e.set(o.id,V([...i,...o.tags??[]]))};for(let n of Object.values(r.scenes)){let s=V(n.tags??[]);e.set(n.id,s);for(let i of n.blocks){let o=V([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var Ge=r=>r.kind==="line"||r.kind==="text",G=r=>typeof r=="number"&&Number.isFinite(r)?r:void 0;function qe(r){let e=new Map,t=(i,o,a)=>{let c=i.kind==="gameEvent"?void 0:G(i.padAfter),l=c??o;a&&l<0&&(l=0),e.set(i.id,c!==void 0?{own:c,resolved:l}:{resolved:l})},n=(i,o)=>{let a=G(i.padAfterDefault)??o;if(i.type==="group"){i.prompt&&t(i.prompt,a,!1);for(let d of i.children)n(d,a);return}let c=i.beats??[],l=-1;c.forEach((d,h)=>{Ge(d)&&(l=h)}),c.forEach((d,h)=>{Ge(d)&&t(d,a,h===l)})},s=G(r.padAfterDefault)??E;for(let i of Object.values(r.scenes)){let o=G(i.padAfterDefault)??s;for(let a of i.blocks){let c=G(a.padAfterDefault)??o;for(let l of a.children)n(l,c)}}return e}var $e=new WeakMap,_e=new WeakMap;function Nt(r){let e=_e.get(r);return e||(e=te(r),_e.set(r,e)),e}var He=new WeakMap;function Bt(r){let e=He.get(r);return e||(e=qe(r),He.set(r,e)),e}var Ue=new WeakMap;function At(r){let e=Ue.get(r);if(e)return e;e=new Map;for(let t of Object.values(r.scenes))for(let n of t.blocks)D(n.children,s=>{if(s.type==="group"){s.prompt&&e.set(s.prompt.id,s.prompt);return}for(let i of s.beats??[])e.set(i.id,i)});return Ue.set(r,e),e}function I(r,e){return r.tagIndex.get(e)?.slice()}var _="Patter",q=r=>r.replace(/%/g,"%25").replace(/\\//g,"%2F"),Ft=r=>r.replace(/%2F/g,"/").replace(/%25/g,"%"),We=Symbol("ownsRegistry"),w={stage:r=>`patter/scene/${q(r)}`,flow:r=>`patter/flow/${q(r)}/`,flowGlobals:r=>`patter/flow/${q(r)}/patter`,flowScene:(r,e)=>`patter/flow/${q(r)}/scene/${q(e)}`},U=class{constructor(e){this.journal=e}journal},ne=class r{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];engineSeq=0;engineTraceHandlers=new Set;constructor(e,t={}){this.creationOptions=t;let n=t.locale??e.locales.default,s=e.strings;this.allStrings=s,this.currentLocale=n;let i=s[n]??{},o=s[e.locales.default]??{},a=e.localisation,c=a?.mode==="ids"&&!a.sourceDebug;this.sourceDebug=a?.mode==="ids"&&!!a.sourceDebug,this.sourceDebug&&typeof console<"u"&&console.warn("[Patterplay] source-only DEBUG build: strings are the source language for debugging, not a shippable localised build.");let l=new Map;for(let p of e.cast??[])p.displayName&&l.set(p.name,p.displayName);let d=new Map;for(let p of e.qualifiers??[])p?.gameId&&d.set(p.gameId,p.name);let h=new Map;for(let p of e.cast??[])for(let[g,S]of Object.entries(p.qualifierLabels??{}))typeof S=="string"&&h.set(`${p.name}\\0${g}`,S);let f=e.speakerPattern??je;this.defaultSeed=(t.seed??2654435769)>>>0;let Se=new Map,we=new Map,ve=new Map;for(let[p,g]of Object.entries(e.scenes)){this.sceneGameIdToId.set(y(g),p);let S=new Map;for(let m of g.blocks)we.set(m.id,{sceneId:p}),ve.set(m.id,m),S.set(y(m),m.id),D(m.children,K=>Se.set(K.id,K));this.blockGameIdToId.set(p,S)}let be=e.properties??[],re=be.filter(p=>p.shared??!0).map(ze),dt=be.filter(p=>!(p.shared??!0)).map(ze),pt=new Set(re.map(p=>p.name.toLowerCase())),x=t.registry??new z,xe=!t.registry||t[We]===!0,ke=new k(re,{pathPrefix:"@patter."}),se=[],W=[];try{x.mountOwned("patter",ke,{owner:_}),W.push("patter");let p=new Set;for(let[g,S]of Object.entries(Ye(t))){let m=e.scopeRegistry?.scopes.find(ut=>ut.token===g),K=(m?.declarations??[]).map(tt);x.defineForeign(g,S,K,{writable:m?.writable??!0,owner:_}),W.push(g),p.add(g)}for(let g of xe?e.scopeRegistry?.scopes??[]:[]){if(p.has(g.token)||x.has(g.token))continue;let S=(g.declarations??[]).map(m=>jt(m,g.writable));x.defineOwned(g.token,S,{owner:_}),W.push(g.token),se.push(g.token)}}catch(p){for(let g of W)x.remove(g,{keep:!0});throw p}let Ee=new Map;for(let[p,g]of Object.entries(e.scenes)){let S=new Set((g.sceneProps??[]).filter(m=>m.shared??!1).map(m=>m.name.toLowerCase()));Ee.set(p,S)}this.host={logEnabled:t.log??!1,tracing:t.log??!1,emitEngine:(p,g,S)=>this.emitEngine(p,g,S),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,qualifierDisplay:d,speakerLabels:h,speakerPattern:f,nodeIndex:Se,blockIndex:we,blockById:ve,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:Nt(e),padIndex:Bt(e),registry:x,ownsRegistry:xe,patterBag:ke,hostScopes:se,patterSharedDecls:re,patterLocalDecls:dt,patterSharedNames:pt,sceneSharedNames:Ee,sharedVisits:new Map,qualityLadders:new Map,sharedSelectors:new Map,stageBags:new Map,journal:null,customRng:t.rng,onDryChoice:t.onDryChoice,onError:t.onError??(p=>console.warn(`Patterplay: ${p.kind} on ${p.node} in flow \'${p.flow}\' failed, played through: ${p.message}`)),reportRefits:()=>{let p=x.takeMisfits(g=>g==="patter"||g.startsWith("patter/")||se.includes(g));if(p.length!==0)if(t.onRefit)t.onRefit(p);else for(let g of p)console.warn(`Patterplay: the save\'s ${g.scope}.${g.name} was ${JSON.stringify(g.value)}, which no longer fits its declaration; it is back at its default`)},replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??ge).open,captionClose:(e.closedCaptions??ge).close,captionCharacter:e.closedCaptions?.character||Ve,refSplitCache:new Map,refSplitRevision:x.revision},this.host.reportRefits()}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}get buildId(){return this.host.bundle.content?.hash??""}setLocale(e){this.currentLocale=e,this.host.strings=this.allStrings[e]??{}}replaceStrings(e){this.allStrings=e.strings,this.host.strings=this.allStrings[this.currentLocale]??{},this.host.defaultStrings=this.allStrings[this.host.bundle.locales.default]??{}}hotSwap(e){this.refuseInCheckpoint("hotSwap");let t=this.saveGame(),n=c=>(c.setLocale(this.currentLocale),c.setClosedCaptions(this.host.captionsOn),c),s=()=>new r(e,{...this.creationOptions,registry:this.host.registry,[We]:this.host.ownsRegistry}),i=new Set(["patter",...this.host.hostScopes]),o=this.host.ownsRegistry?void 0:Object.fromEntries(Object.entries(this.host.registry.save()).filter(([c])=>i.has(c)||c.startsWith("patter/")));this.release(!0),o&&this.host.registry.load(o,{keepParked:!0,refit:!0});let a=s();try{return a.loadGame(t),n(a)}catch{a.release(!0);let c=this.host.ownsRegistry?t.registry:o;c&&this.host.registry.load(c,{keepParked:!0,refit:!0});let l=s();for(let[d,h]of Object.entries(t.flows)){let f=h.cursor.currentSceneId;try{l.openFlow(d,f!==null?{scene:f}:{})}catch{}}return n(l)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let{sceneId:n,blockId:s}=this.resolveOpenAddress(t.scene,t.block),i=this.flowsById.get(e),o=this.host.journal;if(o&&i&&!i.isClosed)throw new Error(`openFlow would replace the open flow \'${e}\', which can\'t be undone while a checkpoint is open`);i?.close();let a=new O(e,this.host,t.seed??this.defaultSeed);return o&&(o.flows.add(a),o.opened.add(a),o.undo.push(()=>{a.close(),i?this.flowsById.set(e,i):this.flowsById.delete(e)})),this.flowsById.set(e,a),a.begin(n,s),a}runFlow(e,t,n){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:n}).advanceToStop().played;if(!s.goto(t,n))throw new Error(`runFlow: address not found: ${t}${n===void 0?"":` / ${n}`}`);return s.advanceToStop().played}assertExternalScopes(){for(let e of this.host.bundle.externalScopes??[])if(!this.host.registry.has(e))throw new Error(`this content names @${e}, which no engine on this registry registered: give every engine the game\'s one registry`)}resolveOpenAddress(e,t){if(e!==void 0){let n=fe(this.host,e);if(n===void 0)throw new Error(`unknown scene: ${e}`);if(t===void 0)return{sceneId:n};let s=he(this.host,n,t);if(s===void 0)throw new Error(`unknown block: ${t}`);return{sceneId:n,blockId:s}}if(t!==void 0){if(!this.host.blockIndex.has(t))throw new Error(`unknown block: ${t}`);return{blockId:t}}if(Object.keys(this.host.bundle.scenes).length===0)throw new Error("no scenes in bundle");return{}}resolveSceneRef(e){if(e!=null)return fe(this.host,e)??e}resolveBlockRef(e,t){if(t!=null)return e==null?t:he(this.host,e,t)}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?y(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?y(t):void 0}tagsForBeat(e){return I(this.host,e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?I(this.host,t):void 0)??[]}tagsForBlock(e,t){let n=this.resolveSceneRef(e),s=this.resolveBlockRef(n,t);return(s!=null?I(this.host,s):void 0)??[]}gameDataForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.gameData}}gameDataForBlock(e,t){let n=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(n!=null?this.host.blockById.get(n):void 0)?.gameData}}extensionsForScene(e){let t=this.resolveSceneRef(e);return{...(t!=null?this.host.bundle.scenes[t]:void 0)?.extensions}}extensionsForBlock(e,t){let n=this.resolveBlockRef(this.resolveSceneRef(e),t);return{...(n!=null?this.host.blockById.get(n):void 0)?.extensions}}getCast(){let e=[];for(let t of this.host.bundle.cast??[])t?.name&&e.push(t.name);return e}castForScene(e){let t=this.resolveSceneRef(e),n=t!=null?this.host.bundle.scenes[t]:void 0;if(!n)return[];let s=new Set;for(let i of n.blocks)Ke(i.children,s);return[...s]}castForBlock(e,t){let n=this.resolveSceneRef(e),s=this.resolveBlockRef(n,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return Ke(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...y(e)?{gameId:y(e)}:{},name:e.name,...Je(e.gameData),...b(e.extensions),...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...y(t)?{gameId:y(t)}:{},name:t.name,...Je(t.gameData),...b(t.extensions),...this.tagsField(t.id),children:t.children.map(n=>this.outlineNode(n))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let n of t.blocks)D(n.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:n.id,snippetId:s.id,beat:this.beatInfo(i)})});return e}outlineNode(e){return e.type==="group"?{type:"group",id:e.id,...this.tagsField(e.id),...e.selector?{selector:e.selector}:{},...e.prompt?{prompt:this.beatInfo(e.prompt)}:{},children:e.children.map(t=>this.outlineNode(t))}:{type:"snippet",id:e.id,...this.tagsField(e.id),beats:(e.beats??[]).map(t=>this.beatInfo(t)),...e.jump?{jumpTo:e.jump.to,...e.jump.mode?{jumpMode:e.jump.mode}:{}}:{}}}beatInfo(e){let t=I(this.host,e.id),n={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){n.character=e.character;let i=this.host.defaultStrings[Z(e.character)]??this.host.castDisplay.get(e.character);i!==void 0&&(n.characterName=i)}if(e.direction!==void 0&&(n.direction=e.direction),e.qualifier!==void 0){n.qualifier=e.qualifier;let i=this.host.defaultStrings[ee(e.qualifier)]??this.host.qualifierDisplay.get(e.qualifier);i!==void 0&&(n.qualifierName=i)}let s=$(this.host,e.character,e.qualifier,!0);s!==void 0&&(n.speakerLabel=s)}if(e.kind==="line"||e.kind==="text"){let s=this.host.padIndex.get(e.id);n.padAfter=s?.resolved??E,s?.own!==void 0&&(n.ownPadAfter=s.own);let i=this.host.defaultStrings[e.id];i!==void 0&&(n.text=i)}return e.gameData&&Object.keys(e.gameData).length&&(n.gameData=e.gameData),e.extensions&&Object.keys(e.extensions).length&&(n.extensions=e.extensions),t&&t.length&&(n.tags=t),n}tagsField(e){let t=I(this.host,e);return t&&t.length?{tags:t}:{}}getFlow(e){return this.flowsById.get(e)}flows(){return[...this.flowsById.values()]}listBags(){let e=[{bag:this.host.patterBag}];for(let[t,n]of this.host.stageBags)e.push({bag:n,pathPrefix:`@scene:${t}.`});return e}checkpoint(){if(this.host.journal)throw new Error("a checkpoint is already open: roll it back or commit it first");let e={undo:[],flows:new Set,opened:new Set,selectors:new Set,flowSelectors:new Map};return this.host.journal=e,new U(e)}rollback(e){this.endCheckpoint(e);let t=e.journal.undo;for(let n=t.length-1;n>=0;n--)t[n]();r.forget(e.journal)}commit(e){this.endCheckpoint(e),r.forget(e.journal)}get inCheckpoint(){return this.host.journal!==null}endCheckpoint(e){if(this.host.journal!==e.journal)throw new Error("that checkpoint is not the open one");this.host.journal=null}static forget(e){e.undo=[],e.flows.clear(),e.opened.clear(),e.selectors.clear(),e.flowSelectors.clear()}refuseInCheckpoint(e){if(this.host.journal)throw new Error(`${e} can\'t be undone, so it isn\'t allowed while a checkpoint is open`)}captureFlow(e){let t=this.flowsById.get(e);if(!t)throw new Error(`no open flow \'${e}\' to capture`);return{version:M,...t.snapshot(),...Vt(e,this.host.registry.save())}}restoreFlow(e,t){this.refuseInCheckpoint("restoreFlow");let n=t.version;if(n!==2&&n!==M)throw new Error(`unsupported flow state version: ${String(n)}`);let s=t.cursor;if(typeof s!="object"||s===null)throw new Error("malformed flow state: no cursor");if(this.flowsById.has(e))throw new Error(`a flow \'${e}\' is already open; close it before restoring another under that id`);this.assertExternalScopes();let i=w.flow(e);this.host.registry.discardParked(i),this.host.registry.load(rt(e,t),{keepParked:!0,refit:!0});let o=new O(e,this.host,this.defaultSeed);try{o.restore(t)}catch(a){throw o.close(),this.host.registry.discardParked(i),a}return this.flowsById.set(e,o),this.host.reportRefits(),o}closeFlow(e){this.refuseInCheckpoint("closeFlow"),this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){this.refuseInCheckpoint("reset");for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(w.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let n of this.flowsById.values())n.releaseBags(e),n.close();this.flowsById.clear();let t=this.host.registry;for(let n of this.host.stageBags.keys())t.remove(w.stage(n),{keep:e});this.host.stageBags.clear();for(let n of["patter",...this.host.hostScopes])t.has(n)&&t.remove(n,{keep:e});for(let n of Object.keys(Ye(this.creationOptions)))t.has(n)&&t.remove(n)}getProperty(e){let{scope:t,name:n}=this.splitShared(e);return this.host.registry.get(t,n)}setProperty(e,t){let{scope:n,name:s}=this.splitShared(e),i=this.host.journal?this.host.registry.get(n,s):void 0;this.host.registry.set(n,s,t,{host:!0}),this.host.journal&&i!==void 0&&this.host.journal.undo.push(()=>this.host.registry.set(n,s,i,{host:!0}))}stageIndex(e,t){return P(this.ladderOf(e),t)}compareStages(e,t,n){return N(this.ladderOf(e),t,n)}nextStage(e,t){return B(this.ladderOf(e),t)}ladderOf(e){let{scope:t,name:n}=this.splitShared(e),s=Lt(this.host,t,n);if(!s)throw new Error(`\'${e}\' is not a quality`);return s}log(){return this.engineLog}clearLog(){this.engineLog.length=0}onTrace(e){return this.engineTraceHandlers.add(e),this.host.tracing=!0,()=>{this.engineTraceHandlers.delete(e),this.host.tracing=this.host.logEnabled||this.engineTraceHandlers.size>0}}emitEngine(e,t,n){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineSeq++,...n?{scene:n}:{}})}getVisitCounts(){return Object.fromEntries(this.host.sharedVisits)}listProperties(){return this.host.patterSharedDecls.map(e=>({name:e.name,path:`@patter.${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:A(e),writable:e.writable??!0}))}splitShared(e){let t=nt(this.host,e);if(t.scope==="scene")throw new Error(`\'${e}\': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);return t}save(){return{patter:this.host.patterBag.save()}}load(e){e.patter&&this.host.patterBag.load(e.patter)}saveGame(){let e={};for(let[t,n]of this.flowsById)e[t]=n.snapshot();return{version:M,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:Ze(this.host.sharedSelectors),flows:e}}loadGame(e){this.refuseInCheckpoint("loadGame");let t=e.version;if(t!==2&&t!==M)throw new Error(`unsupported save version: ${String(t)}`);let n=e.flows;if(typeof n!="object"||n===null||Array.isArray(n))throw new Error("malformed save: no flows");this.assertExternalScopes();let s=this.host.registry;for(let[o,a]of this.flowsById)a.releaseBags(o in e.flows),a.close();this.flowsById.clear();for(let o of this.host.stageBags.keys())s.remove(w.stage(o),{keep:!0});this.host.stageBags.clear();let i=e.version===2?Mt(e):e.registry;i&&(this.host.ownsRegistry?s.load(i,{refit:!0}):s.load(i,{keepParked:!0,refit:!0})),this.host.sharedVisits.clear();for(let[o,a]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(o,a);this.host.sharedSelectors.clear();for(let[o,a]of et(e.sharedSelectors))this.host.sharedSelectors.set(o,a);for(let[o,a]of Object.entries(e.flows)){let c=new O(o,this.host,this.defaultSeed);c.restore(a),this.flowsById.set(o,c)}this.host.reportRefits()}},O=class{id;host;local;registered=new Set;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPrompt=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.patterBag.get(e):this.local.get(e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.registry.set("patter",e,t):this.local.set(e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;if(t!==null)return this.sceneBagFor(t,e)?.get(e)},set:(e,t)=>{let n=this.currentSceneId;if(n===null)return;let s=this.sceneBagFor(n,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,n){this.id=e,this.host=t,this.rngState=L(n),this.local=this.newLocal(),this.evalCtx={scopes:{},host:{nextRandom:this.rng,visits:s=>this.visitCounts.get(s)??0,patterVisits:s=>this.host.sharedVisits.get(s)??0},qualities:(s,i)=>this.stagesFor(s,i)}}context(){let e=this.host.registry;if(e.revision!==this.ctxRevision){let t=e.toEvalContext(),n=this.evalCtx.scopes;for(let s of Object.keys(n))delete n[s];Object.assign(n,t.scopes),n.patter=this.patterResolver,n.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let n=t.toLowerCase();if(e==="patter")return H(this.host,"patter",()=>[...this.host.patterSharedDecls,...this.host.patterLocalDecls]).get(n);if(e==="scene"){let s=this.currentSceneId;return s==null?void 0:H(this.host,`scene/${s}`,()=>this.host.bundle.scenes[s]?.sceneProps).get(n)}return this.registryQualities?.(e,t)??H(this.host,`host/${e}`,()=>this.host.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(n)}begin(e,t){if(this.host.journal&&!this.host.journal.opened.has(this))throw new Error("a flow can\'t be started or reset while a checkpoint is open");if(this.releaseBags(!1),this.host.registry.discardParked(w.flow(this.id)),this.mountLocal(),this.selectors.clear(),this.visitCounts.clear(),this.stack=[],this.currentSceneId=null,this.flowEnded=!1,this.activeSnippet=null,this.beatIndex=0,this.clearPending(),this.started=!0,t){let n=this.host.blockIndex.get(t);if(!n)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(n.sceneId),this.stack=[{sceneId:n.sceneId,containerId:t,index:0}],this.enter(t)}else{let n=e??Object.keys(this.host.bundle.scenes)[0],s=n?this.host.bundle.scenes[n]:void 0;if(!s)throw new Error(n?`unknown scene: ${n}`:"no scenes in bundle");this.enterSceneSetup(n);let i=s.blocks[0];i&&(this.stack=[{sceneId:n,containerId:i.id,index:0}],this.enter(i.id))}this.settle()}reset(e,t){this.begin(e,t)}start(e,t){this.reset(e,t)}goto(e,t){if(this.closed)return!1;if(this.touch(),e==="END")return this.started=!0,this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let n=fe(this.host,e);if(n===void 0)return!1;let s;return t!==void 0&&(s=he(this.host,n,t),s===void 0)?!1:this.started?(this.clearPending(),this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??n,"jump"),this.settle(),!0):(this.begin(n,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.clearPending()}clearPending(){this.pendingChoice=null,this.pendingPrompt=null,this.pendingPromptOwnerId=null}touch(){let e=this.host.journal;if(!e||e.flows.has(this))return;e.flows.add(this);let{rngState:t,started:n,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:d}=this,h=this.stack.map(f=>({...f}));e.undo.push(()=>{Object.assign(this,{rngState:t,started:n,flowEnded:s,currentSceneId:i,activeSnippet:o,beatIndex:a,pendingChoice:c,pendingPrompt:l,pendingPromptOwnerId:d,stack:h})})}get isClosed(){return this.closed}get currentScene(){return this.currentSceneId}advance(){if(this.closed)return{type:"end"};if(!this.started)throw new Error("flow has not been started");if(this.touch(),this.pendingPrompt){let e=this.pendingPrompt;return this.pendingPrompt=null,this.pendingPromptOwnerId=null,e.shown?this.promptResult(e.beat,e.shown):this.beatResult(e.beat)}return this.settle(),this.flowEnded?{type:"end"}:this.pendingChoice?{type:"choice",groupId:this.pendingChoice.groupId,options:this.pendingChoice.options}:this.activeSnippet?this.beatResult(this.activeSnippet.beats[this.beatIndex++]):(this.flowEnded=!0,{type:"end"})}advanceToStop(){let e=[];for(;;){let t=this.advance();if(t.type==="choice"||t.type==="end")return{played:e,stop:t};e.push(t)}}settle(){let e=0;for(;;){if(++e>1e4)throw new Error("flow did not settle after 10000 transitions - likely a jump cycle with no deliverable content");if(this.flowEnded||this.pendingChoice)return;if(this.activeSnippet){if(this.beatIndex<(this.activeSnippet.beats?.length??0))return;this.runEffects(this.activeSnippet.onExit,this.activeSnippet.id);let i=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(i);continue}let t=this.stack[this.stack.length-1];if(!t){this.flowEnded=!0;return}t.sceneId!==this.currentSceneId&&(this.currentSceneId=t.sceneId);let n=this.childrenOf(t.containerId);if(!n){this.stack.pop();continue}let s=t.index;for(;t.index<n.length&&!this.eligible(n[t.index]);)t.index++;if(this.host.tracing&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:n.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:n[t.index]?.id??null}),t.index>=n.length){this.stack.pop();continue}this.enterChild(n[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,n]of this.sceneBags)e.push({bag:n,pathPrefix:`${this.id}/@scene:${t}.`});return e}log(){return this.flowLog}clearLog(){this.flowLog.length=0}emit(e){let t=this.currentSceneId??void 0;this.host.emitEngine(this.id,e,t),this.host.logEnabled&&this.flowLog.push({...e,seq:this.flowSeq++,...t?{scene:t}:{}})}getVisitCounts(){return Object.fromEntries(this.visitCounts)}stepFor(e){let t=At(this.host.bundle).get(e);return t?this.beatResult(t):void 0}getChoices(){return this.pendingChoice?.options??[]}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let n=t.options.find(o=>o.id===e);if(!n)throw new Error(`unknown choice option: ${e}`);if(!n.eligible)throw new Error(`choice option is not eligible: ${e}`);this.touch();let s=t.byId.get(e);this.host.tracing&&this.emit({type:"chose",group:t.groupId,option:e}),this.pendingChoice=null;let i=this.host.replayPromptOnChoose?this.authoredPromptOf(s):void 0;this.pendingPrompt=i&&n.prompt?{beat:i,shown:{...n.prompt}}:null,this.pendingPromptOwnerId=this.pendingPrompt?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:n}=this.splitRef(e);return t==="patter"?this.patterResolver.get(n):t==="scene"?this.sceneResolver.get(n):this.host.registry.get(t,n)}setProperty(e,t){this.writeProperty(e,t,!0)}stageIndex(e,t){return P(this.ladderOf(e),t)}compareStages(e,t,n){return N(this.ladderOf(e),t,n)}nextStage(e,t){return B(this.ladderOf(e),t)}ladderOf(e){let{scope:t,name:n}=this.splitRef(e);this.context();let s=this.stagesFor(t,n);if(!s)throw new Error(`\'${e}\' is not a quality`);return s}writeProperty(e,t,n){let{scope:s,name:i}=this.splitRef(e),o=this.host.journal;if(s==="patter"){if(o){let a=this.host.patterSharedNames.has(i),c=a?this.host.patterBag.get(i):this.local.get(i),l=this.local;c!==void 0&&o.undo.push(a?()=>this.host.registry.set("patter",i,c):()=>l.set(i,c))}this.patterResolver.set(i,t)}else if(s==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);if(o){let a=this.sceneBagFor(this.currentSceneId,i),c=a?.get(i);a&&c!==void 0&&o.undo.push(()=>a.set(i,c))}this.sceneResolver.set(i,t)}else{let a=o?this.host.registry.get(s,i):void 0;this.host.registry.set(s,i,t,n?{host:!0}:void 0),o&&a!==void 0&&o.undo.push(()=>this.host.registry.set(s,i,a,{host:!0}))}}snapshot(){return{rngState:this.rngState,visits:Object.fromEntries(this.visitCounts),cursor:{flowEnded:this.flowEnded,currentSceneId:this.currentSceneId,stack:this.stack.map(e=>{let t=this.childrenOf(e.containerId)?.[e.index];return t?{...e,nextId:t.id}:{...e}}),activeSnippetId:this.activeSnippet?.id??null,beatIndex:this.beatIndex,pendingChoice:this.pendingChoice?{groupId:this.pendingChoice.groupId,options:this.pendingChoice.options.map(({extensions:e,...t})=>({...t}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,...this.pendingPrompt?.shown?{pendingPrompt:Tt(this.pendingPrompt.shown)}:{},selectors:Ze(this.selectors)}}}restore(e){if(this.host.journal)throw new Error("a flow can\'t be restored while a checkpoint is open");this.restoreCursor(e),this.releaseBags(!0),this.mountLocal();for(let t of new Set([this.currentSceneId,...this.stack.map(n=>n.sceneId)]))t!==null&&this.host.bundle.scenes[t]&&this.ensureSceneBags(t)}restoreCursor(e){this.rngState=L(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;if(this.started=!0,this.flowEnded=t.flowEnded,this.beatIndex=t.beatIndex,this.currentSceneId=t.currentSceneId,this.stack=t.stack.map(n=>{let{nextId:s,...i}=n;if(s!==void 0){let o=this.childrenOf(i.containerId)?.findIndex(a=>a.id===s)??-1;if(o>=0)return{...i,index:o}}return{...i}}),this.activeSnippet=null,t.activeSnippetId!==null){let n=this.host.nodeIndex.get(t.activeSnippetId);n&&n.type==="snippet"&&(this.activeSnippet=n)}if(this.selectors=et(t.selectors),this.pendingChoice=null,t.pendingChoice!==null){let n=new Map,s=[];for(let i of t.pendingChoice.options){let o=this.host.nodeIndex.get(i.id);if(!o)continue;n.set(i.id,o);let{prompt:a,extensions:c,...l}=i,d=a?.kind==="line"&&a.speakerLabel===void 0?$(this.host,a.character,a.qualifier):void 0;s.push({...l,...a?{prompt:{...a,padAfter:a.padAfter??this.promptPadOf(o),...d!==void 0?{speakerLabel:d}:{}}}:{},...b(o.extensions)})}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:n})}if(this.pendingPrompt=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let n=this.host.nodeIndex.get(this.pendingPromptOwnerId),s=n?this.authoredPromptOf(n):void 0,i=t.pendingPrompt,o=i?.kind==="line"&&i.speakerLabel===void 0?$(this.host,i.character,i.qualifier):void 0;s?this.pendingPrompt={beat:s,...i?{shown:{...i,padAfter:i.padAfter??this.padOf(s.id),...o!==void 0?{speakerLabel:o}:{}}}:{}}:this.pendingPromptOwnerId=null}}enterSceneSetup(e){let t=this.host.bundle.scenes[e];if(!t)throw new Error(`unknown scene: ${e}`);this.currentSceneId=e,this.enter(e),this.seedScene(t),this.runEffects(t.onEntry,t.id)}enterChild(e){if(this.enter(e.id),e.type==="snippet"){this.beginSnippet(e);return}let t=e.selector??"run";if(t==="run"){this.stack.push({sceneId:this.currentSceneId,containerId:e.id,index:0});return}if(t==="choice"){this.setupChoice(e);return}let n=this.selectChild(e);n&&this.enterChild(n)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let n=this.host.nodeIndex.get(e);if(n&&n.type==="group")return n.children}beginSnippet(e){this.runEffects(e.onEnter,e.id),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],n=new Map,s=[];for(let o of e.children){if(o.fallback===!0){s.push(o);continue}if(o.sticky!==!0&&(this.visitCounts.get(o.id)??0)>=1)continue;let a=this.eligible(o),c=o.secretUntilEligible===!0;!a&&c||(t.push({id:o.id,prompt:this.promptFor(o),eligible:a,gameData:o.gameData,...b(o.extensions)}),n.set(o.id,o))}if(t.some(o=>o.eligible)){this.host.tracing&&this.emit({type:"choice",group:e.id,options:t.map(o=>({id:o.id,eligible:o.eligible}))}),this.pendingChoice={groupId:e.id,options:t,byId:n};return}let i=s.find(o=>this.eligible(o));if(i){this.enterChild(i);return}this.host.tracing&&this.emit({type:"dry",group:e.id}),this.host.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(this.host.tracing&&this.emit({type:"jump",to:e,mode:t}),e==="END"){this.flowEnded=!0,this.stack=[];return}let n,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}n=e,s=a.id}else{let a=this.host.blockIndex.get(e);if(!a)throw new Error(`jump target not found: ${e}`);a.sceneId!==this.currentSceneId&&this.enterSceneSetup(a.sceneId),n=a.sceneId,s=e}this.enter(s);let o={sceneId:n,containerId:s,index:0};t==="call"?this.stack.push(o):this.stack=[o]}selectChild(e){let t=e.children.map(c=>({id:c.id,eligible:this.eligible(c)})),n=e.children.filter((c,l)=>t[l].eligible),s=e.options?.order??"sequential",i=e.options?.exhaust??"once",o=c=>(this.host.tracing&&this.emit({type:"select",group:e.id,selector:e.selector??"default",...e.selector==="sequence"?{order:s,exhaust:i}:{},children:t,picked:c?.id??null}),c);if(n.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(n[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(n,i,a):s==="specificity"?this.pickSpecificity(n,i,a):this.pickSequential(n,i,a));default:return null}}pickSequential(e,t,n){let s=e.length,i=n.seq??0;return n.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,n){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(f=>f.id),a=new Set(e.map(f=>f.id));if(n.bag===void 0&&(n.bag=o()),!n.bag.some(f=>a.has(f))){if(t==="once")return null;if(i){let f=e[s-1];return n.last=f.id,f}if(n.bag=o(),n.bag.length===0)return null}let c=n.bag.filter(f=>a.has(f)),l=n.last!==void 0&&c.length>1?c.indexOf(n.last):-1,d=Math.floor(this.rng()*(l>=0?c.length-1:c.length));l>=0&&d>=l&&d++;let h=c[d];return n.bag.splice(n.bag.indexOf(h),1),n.last=h,e.find(f=>f.id===h)}pickSpecificity(e,t,n){let s=e;if(t!=="repeat"){n.bag===void 0&&(n.bag=e.map(d=>d.id));let l=new Set(n.bag);if(s=e.filter(d=>l.has(d.id)),s.length===0)return t==="stick"&&n.last!==void 0?e.find(d=>d.id===n.last)??null:null}let i=-1,a=s.map(l=>{let d=this.specScore(l);return d>i&&(i=d),{c:l,s:d}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=n.last!==void 0?a.findIndex(h=>h.id===n.last):-1,d=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&d>=l&&d++,c=a[d]}return t!=="repeat"&&(n.bag=n.bag.filter(l=>l!==c.id)),n.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0,e):0}matchedSpec(e,t,n){return Ce(e,i=>{try{return Xe(J(i,this.context(),ue))}catch(o){return this.reportError("best-match",n.id,n.condition,o),!1}},{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,n=this.host.journal,s;if(n&&(s=e.shared?n.selectors:n.flowSelectors.get(this),s||(s=new Set,n.flowSelectors.set(this,s))),n&&s&&!s.has(e.id)){s.add(e.id);let o=t.get(e.id),a=o?{...o,...o.bag?{bag:[...o.bag]}:{}}:void 0;n.undo.push(()=>{a?t.set(e.id,a):t.delete(e.id)})}let i=t.get(e.id);return i||(i={},t.set(e.id,i)),i}runEffects(e,t){for(let n of e??[]){let s;try{s=this.evalExpr(n.value);let i=this.host.tracing?this.getProperty(n.target):void 0;this.writeProperty(n.target,s,!1),this.host.tracing&&this.emit({type:"write",target:n.target,value:s,...i!==void 0?{prev:i}:{}})}catch(i){this.reportError("effect",t,n.value,i)}}}eligible(e){if(!e.condition)return!0;try{return Xe(this.evalExpr(e.condition))}catch(t){return this.reportError("condition",e.id,e.condition,t),!1}}reportError(e,t,n,s){let i=s instanceof Error?s.message:String(s),o=n?.src;this.host.onError({flow:this.id,kind:e,node:t,...o?{source:o}:{},message:i}),this.host.tracing&&this.emit({type:"diagnostic",kind:e,node:t,...o?{source:o}:{},message:i})}evalExpr(e){return J(this.conditionAst(e),this.context(),ue)}conditionAst(e){let t=$e.get(e);return t||(t=R(e.ast),$e.set(e,t)),t}enter(e){let t=this.visitCounts,n=t.get(e);t.set(e,(n??0)+1);let s=this.host.sharedVisits,i=s.get(e);s.set(e,(i??0)+1),this.host.journal&&this.host.journal.undo.push(()=>{n===void 0?t.delete(e):t.set(e,n),i===void 0?s.delete(e):s.set(e,i)})}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=ae(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=I(this.host,e.id),n=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:e.gameData,...b(e.extensions),...n};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),padAfter:this.padOf(e.id),gameData:e.gameData,...b(e.extensions),...n};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,qualifier:c?void 0:e.qualifier,qualifierName:c?void 0:this.resolveQualifierName(e.qualifier),speakerLabel:c?void 0:$(this.host,e.character,e.qualifier),padAfter:this.padOf(e.id),gameData:e.gameData,...b(e.extensions),...n}}}}interpolate(e){return Fe(e,t=>this.getProperty(t))}stripCaptions(e){return Ae(e,this.host.captionOpen,this.host.captionClose)}captionLine(e){return this.host.captionsOn?e:this.stripCaptions(e)}promptFor(e){let t=this.promptBeatOf(e);if(!t)return;let n=this.interpolate(this.resolveString(t.id)),s=this.padOf(t.id);return t.kind==="line"?{kind:"line",text:this.captionLine(n),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction,qualifier:t.qualifier,qualifierName:this.resolveQualifierName(t.qualifier),speakerLabel:$(this.host,t.character,t.qualifier),padAfter:s}:{kind:"text",text:n,padAfter:s}}authoredPromptOf(e){return e.type==="group"?e.prompt:void 0}promptResult(e,t){let n=I(this.host,e.id),s=n&&n.length?{tags:n}:{};return t.kind==="text"?{type:"text",id:e.id,text:t.text,padAfter:this.padOf(e.id),gameData:e.gameData,...b(e.extensions),...s}:{type:"line",id:e.id,text:t.text,character:t.character,characterName:t.characterName,direction:t.direction,qualifier:t.qualifier,qualifierName:t.qualifierName,speakerLabel:t.speakerLabel,padAfter:this.padOf(e.id),gameData:e.gameData,...b(e.extensions),...s}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(n=>n.kind==="line"||n.kind==="text")}firstTextSnippetIn(e){let t;return D(e,n=>{!t&&n.type==="snippet"&&(n.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=n)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let n=this.host.defaultStrings[e];return n!==void 0?`<Untranslated: ${e}> ${n}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=Z(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}promptPadOf(e){let t=this.promptBeatOf(e);return t?this.padOf(t.id):E}padOf(e){return this.host.padIndex.get(e)?.resolved??E}resolveQualifierName(e){if(e===void 0||this.host.emitIds)return;let t=ee(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.qualifierDisplay.get(e)}splitRef(e){return nt(this.host,e)}newLocal(){return new k(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=w.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:_}),this.registered.add(e),this.host.reportRefits()}releaseBags(e){for(let t of this.registered)this.host.registry.remove(t,{keep:e});this.registered.clear(),this.sceneBags.clear()}sceneBagFor(e,t){if(this.host.bundle.scenes[e])return this.ensureSceneBags(e),this.host.sceneSharedNames.get(e)?.has(t.toLowerCase())?this.host.stageBags.get(e):this.sceneBags.get(e)}ensureSceneBags(e){let t=this.host.sceneSharedNames.get(e)??new Set,n=this.host.bundle.scenes[e]?.sceneProps??[],s=this.host.journal,i=!this.sceneBags.has(e)||!this.host.stageBags.has(e);if(!this.sceneBags.has(e)){let o=new k(n.filter(l=>!t.has(l.name.toLowerCase())),{pathPrefix:"@scene."}),a=w.flowScene(this.id,e),c=Qe(this.host.registry,a,o,s!==null);this.registered.add(a),this.sceneBags.set(e,o),s&&s.undo.push(()=>{this.sceneBags.get(e)===o&&(this.host.registry.remove(a,c?{keep:!0}:void 0),this.registered.delete(a),this.sceneBags.delete(e))})}if(!this.host.stageBags.has(e)){let o=new k(n.filter(c=>t.has(c.name.toLowerCase())),{pathPrefix:"@scene."}),a=Qe(this.host.registry,w.stage(e),o,s!==null);this.host.stageBags.set(e,o),s&&s.undo.push(()=>{this.host.stageBags.get(e)===o&&(this.host.registry.remove(w.stage(e),a?{keep:!0}:void 0),this.host.stageBags.delete(e))})}i&&this.host.reportRefits()}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let n of e.sceneProps??[]){if(!n.temporary)continue;let s=n.name.toLowerCase(),i=t.has(s)?this.host.stageBags.get(e.id):this.sceneBags.get(e.id);if(!i)continue;let o=this.host.journal?i.get(s):void 0;o!==void 0&&this.host.journal.undo.push(()=>i.set(s,o)),i.set(s,A(n))}}};function Ke(r,e){D(r,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let n of t.beats??[])n.kind==="line"&&n.character&&e.add(n.character)})}function $(r,e,t,n=!1){if(!e||r.emitIds)return;let s=c=>(n?void 0:r.strings[c])??r.defaultStrings[c],i=s(Z(e))??r.castDisplay.get(e)??e;if(t===void 0)return i;let o=r.speakerLabels.get(`${e}\\0${t}`);if(o!==void 0)return s(Te(e,t))??o;let a=s(ee(t))??r.qualifierDisplay.get(t)??t;return Me(s(Le)??r.speakerPattern,i,a)}function Je(r){return r&&Object.keys(r).length?{gameData:r}:{}}function b(r){return r&&Object.keys(r).length?{extensions:r}:{}}function Tt(r){let e={kind:r.kind,text:r.text};return r.character!==void 0&&(e.character=r.character),r.characterName!==void 0&&(e.characterName=r.characterName),r.direction!==void 0&&(e.direction=r.direction),r.qualifier!==void 0&&(e.qualifier=r.qualifier),r.qualifierName!==void 0&&(e.qualifierName=r.qualifierName),r.speakerLabel!==void 0&&(e.speakerLabel=r.speakerLabel),typeof r.padAfter=="number"&&(e.padAfter=r.padAfter),e}function Ze(r){let e={};for(let[t,n]of r){let s={};n.seq!==void 0&&(s.seq=n.seq),n.bag&&(s.bag=[...n.bag]),n.last!==void 0&&(s.last=n.last),e[t]=s}return e}function et(r){let e=new Map;for(let[t,n]of Object.entries(r??{})){let s={};n.seq!==void 0&&(s.seq=n.seq),n.bag&&(s.bag=[...n.bag]),n.last!==void 0&&(s.last=n.last),e.set(t,s)}return e}function Qe(r,e,t,n){let s=n?JSON.stringify(t.save()):"";return r.mountOwned(e,t,{owner:_}),n&&JSON.stringify(t.save())!==s}function ze(r){return{name:r.name,type:r.type,values:r.values,stages:r.stages,default:r.default}}function Ye(r){return{...r.world?{world:r.world}:{},...r.hostScopes}}function H(r,e,t){let n=r.qualityLadders.get(e);if(!n){n=new Map;for(let s of t()??[]){let i=s.name.toLowerCase();s.type==="quality"&&s.stages&&!n.has(i)&&n.set(i,s.stages)}r.qualityLadders.set(e,n)}return n}function Lt(r,e,t){let n=t.toLowerCase();return e==="patter"?H(r,"patter",()=>[...r.patterSharedDecls,...r.patterLocalDecls]).get(n):r.registry.toEvalContext().qualities?.(e,t)??H(r,`host/${e}`,()=>r.bundle.scopeRegistry?.scopes.find(s=>s.token===e)?.declarations).get(n)}function tt(r){return{name:r.name,type:r.type,values:r.values,stages:r.stages,default:r.default,writable:r.writable}}function jt(r,e){let t=tt(r),n=r.writable??e;return n===void 0?t:{...t,writable:n}}function fe(r,e){return r.sceneGameIdToId.get(e)??(r.bundle.scenes[e]?e:void 0)}function he(r,e,t){return r.blockGameIdToId.get(e)?.get(t)??(r.blockIndex.get(t)?.sceneId===e?t:void 0)}function nt(r,e){r.refSplitRevision!==r.registry.revision&&(r.refSplitCache.clear(),r.refSplitRevision=r.registry.revision);let t=r.refSplitCache.get(e);return t||(t=Be(e,n=>n==="scene"||r.registry.has(n)||(r.bundle.externalScopes?.includes(n)??!1)),r.refSplitCache.set(e,t)),t}function Mt(r){let e={};r.shared?.patter&&(e.patter=r.shared.patter);for(let[t,n]of Object.entries(r.stageBags??{}))e[w.stage(t)]=n;for(let[t,n]of Object.entries(r.flows??{}))Object.assign(e,rt(t,n));return e}function rt(r,e){let t={};e.scopes?.patter&&(t[w.flowGlobals(r)]=e.scopes.patter);for(let[n,s]of Object.entries(e.sceneBags??{}))t[w.flowScene(r,n)]=s;return t}function Vt(r,e){let t={scopes:{},sceneBags:{}},n=w.flow(r);for(let[s,i]of Object.entries(e)){if(!s.startsWith(n))continue;let o=s.slice(n.length);o==="patter"?t.scopes.patter=i:o.startsWith("scene/")&&(t.sceneBags[Ft(o.slice(6))]=i)}return t}function Xe(r){return typeof r=="boolean"?r:typeof r=="number"?r!==0:typeof r=="string"?r!=="":r.length>0}var Gt=(r,e)=>r.shared??e;function me(r,e){return{name:r.name,type:r.type,hasDefault:r.default!==void 0,...r.default!==void 0?{default:r.default}:{},shared:Gt(r,e)}}function qt(r){return{name:r.name,type:r.type,hasDefault:r.default!==void 0,...r.values?{values:[...r.values]}:{},...r.purpose?{purpose:r.purpose}:{}}}function $t(r,e){e.blocks++;let t=[...r.children];for(;t.length;){let n=t.pop();if(n.type==="group"){e.groups++,n.prompt&&e.prompts++,t.push(...n.children);continue}e.snippets++;for(let s of n.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function st(r){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:r.cast?.length??0},t=[],n=[];for(let o of Object.values(r.scenes)){e.scenes++;let a=y(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:y(c),name:c.name}))});for(let c of o.blocks)$t(c,e);o.sceneProps?.length&&n.push({gameId:a,properties:o.sceneProps.map(c=>me(c,!1))})}let s=(r.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>me(a,!0))})),i=Object.entries(r.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(qt)}));return{identity:{schema:r.schema,project:r.content.project,...r.content.version!==void 0?{version:r.content.version}:{},...r.content.hash!==void 0?{hash:r.content.hash}:{},...r.content.structureHash!==void 0?{structureHash:r.content.structureHash}:{},voiced:r.voiced,defaultLocale:r.locales.default,locales:[...r.locales.included],localisation:r.localisation?.mode??"embedded",sourceDebug:r.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(r.properties??[]).map(o=>me(o,!0)),scene:n},gameData:i,counts:e}}var F=(r,e)=>Object.prototype.hasOwnProperty.call(r,e);function it(r,e){return r.gameDataFields?.[e]??[]}function ye(r,e,t){return e&&F(e,t)?e[t]:r.find(n=>n.name===t)?.default}function ot(r,e){let t={};for(let n of r){let s=ye(r,e,n.name);s!==void 0&&(t[n.name]=s)}for(let[n,s]of Object.entries(e??{}))F(t,n)||(t[n]=s);return t}function at(r,e,t){let n=r.extensions!==void 0&&F(r.extensions,e)?r.extensions[e]:void 0;return n!==void 0&&F(n.fields,t)?n.fields[t]??[]:[]}function ct(r,e){return{...(r.extensions!==void 0&&F(r.extensions,e)?r.extensions[e]:void 0)?.settings??{}}}function lt(r,e,t=""){let n=o=>({...o?.extensions??{}}),s=Object.values(r.scenes??{}),i=o=>(F(r.scenes??{},o)?r.scenes[o]:void 0)??s.find(a=>y(a)===o);switch(e){case"project":{let o={};for(let[a,c]of Object.entries(r.extensions??{}))c.project!==void 0&&(o[a]=c.project);return o}case"scene":return n(i(t));case"block":{let o=t.indexOf("/");if(o<0)return{};let a=i(t.slice(0,o))?.blocks??[],c=t.slice(o+1);return n(a.find(l=>l.id===c)??a.find(l=>y(l)===c))}case"snippet":case"option":case"line":case"text":case"gameEvent":{let o,a=(c,l)=>{for(let d of c??[]){if(o!==void 0)return;if(d.type==="group"){if(l&&d.id===t&&e==="option"){o=d;return}if(l&&d.prompt?.id===t&&d.prompt.kind===e){o=d.prompt;return}a(d.children??[],d.selector==="choice");continue}if(d.id===t&&e==="snippet"){o=d;return}let h=(d.beats??[]).find(f=>f.id===t);if(h!==void 0){h.kind===e&&(o=h);return}}};for(let c of s)for(let l of c.blocks??[])o===void 0&&a(l.children??[],!1);return n(o)}default:return{}}}return St(_t);})();\n';
1552
1774
  var PLAYABLE_TIMING_JS = 'var GameSubtitles=(()=>{var l=Object.defineProperty;var a=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var u=(t,e)=>{for(var s in e)l(t,s,{get:e[s],enumerable:!0})},f=(t,e,s,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of c(e))!_.call(t,i)&&i!==s&&l(t,i,{get:()=>e[i],enumerable:!(n=a(e,i))||n.enumerable});return t};var p=t=>f(l({},"__esModule",{value:!0}),t);var d={};u(d,{estimateDuration:()=>h});function h(t,{charsPerSecond:e=14,minSeconds:s=1.5,maxSeconds:n=18}={}){let i=0;for(let o of t??"")o!=="\\xAD"&&i++;let r=e>0?e:14;return Math.min(Math.max(i/r,s),n)}return p(d);})();\n';
1553
1775
 
1554
1776
  // src/play-timeline.ts
@@ -2497,9 +2719,160 @@ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
2497
2719
  }
2498
2720
 
2499
2721
  // src/format.ts
2500
- import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
2501
- import { dirname as dirname5 } from "path";
2502
- import { canonicalStringify as canonicalStringify5, parseSource as parseSource4 } from "@patterkit/core";
2722
+ import { readFileSync as readFileSync8, statSync as statSync3 } from "fs";
2723
+ import { dirname as dirname6, resolve as resolve5 } from "path";
2724
+ import { canonicalStringify as canonicalStringify6, parseSource as parseSource6 } from "@patterkit/core";
2725
+
2726
+ // src/extensions.ts
2727
+ import { readFileSync as readFileSync7 } from "fs";
2728
+ import { join as join7, relative as relative5, sep as sep3 } from "path";
2729
+ import { canonicalStringify as canonicalStringify5, parseSource as parseSource5 } from "@patterkit/core";
2730
+ import { EXTENSIONS_DIR as EXTENSIONS_DIR3, MANIFEST_EXTENSION as MANIFEST_EXTENSION3 } from "@patterkit/model";
2731
+ import { manifestProblems, parseManifest, renameStages, targetOf } from "@wildwinter/toolkit/extensions";
2732
+ var CONFLICT_MARKERS = /^(<{7}|={7}|>{7})( |$)/m;
2733
+ var manifestPath = (projectDir, id) => join7(projectDir, EXTENSIONS_DIR3, `${id}${MANIFEST_EXTENSION3}`);
2734
+ function readProjectManifest(projectDir, id) {
2735
+ if (!/^[a-z][a-z0-9-]*$/.test(id)) return void 0;
2736
+ let text;
2737
+ try {
2738
+ text = readFileSync7(manifestPath(projectDir, id), "utf8");
2739
+ } catch {
2740
+ return void 0;
2741
+ }
2742
+ return soundManifest(text);
2743
+ }
2744
+ function soundManifest(text) {
2745
+ if (CONFLICT_MARKERS.test(text)) return void 0;
2746
+ try {
2747
+ const manifest = parseManifest(text);
2748
+ return manifestProblems(manifest).some((p) => p.severity === "error") ? void 0 : manifest;
2749
+ } catch {
2750
+ return void 0;
2751
+ }
2752
+ }
2753
+ var projectManifests = (projectDir) => (id) => readProjectManifest(projectDir, id);
2754
+ var soundOf = (manifest) => manifestProblems(manifest).some((p) => p.severity === "error") ? void 0 : manifest;
2755
+ function planExtensionUpgrades(loaded) {
2756
+ const writes = /* @__PURE__ */ new Map();
2757
+ const report = [];
2758
+ const notes = /* @__PURE__ */ new Map();
2759
+ const note = (path, line) => {
2760
+ notes.set(path, [...notes.get(path) ?? [], line]);
2761
+ };
2762
+ const taken = loaded.project.extensions ?? {};
2763
+ const moved = {};
2764
+ const rel = (path) => relative5(loaded.root, path).split(sep3).join("/");
2765
+ for (const [id, pin] of Object.entries(taken)) {
2766
+ const file = loaded.manifests.find((m) => m.id === id);
2767
+ const manifest = file !== void 0 ? soundOf(file.manifest) : void 0;
2768
+ if (manifest === void 0 || pin?.version === manifest.version) continue;
2769
+ moved[id] = { ...pin, version: manifest.version };
2770
+ const took = `took the Patter extension "${id}" at version ${manifest.version}${pin?.version ? ` (was ${pin.version})` : ""}`;
2771
+ report.push(took);
2772
+ note(loaded.projectFile, took);
2773
+ loaded.extensionShards.forEach((shard, i) => {
2774
+ if (shard.extension !== id) return;
2775
+ let renamed = 0;
2776
+ const entries = {};
2777
+ for (const [kind, byNode] of Object.entries(shard.entries ?? {})) {
2778
+ const target = targetOf(manifest, "patter", kind);
2779
+ const out = {};
2780
+ for (const [node, entry] of Object.entries(byNode ?? {})) {
2781
+ if (target === void 0 || typeof entry !== "object" || entry === null || Array.isArray(entry)) {
2782
+ out[node] = entry;
2783
+ continue;
2784
+ }
2785
+ const r = renameStages(target, entry);
2786
+ renamed += r.renames.length;
2787
+ out[node] = r.entry;
2788
+ }
2789
+ entries[kind] = out;
2790
+ }
2791
+ if (renamed > 0) {
2792
+ const path = loaded.extensionShardFiles[i];
2793
+ writes.set(path, canonicalStringify5({ ...shard, entries }));
2794
+ const what = `renamed ${renamed} stage value${renamed === 1 ? "" : "s"} for "${id}"`;
2795
+ report.push(` ${what} in ${rel(path)}`);
2796
+ note(path, what);
2797
+ }
2798
+ });
2799
+ }
2800
+ if (Object.keys(moved).length > 0) {
2801
+ const project = parseSource5(readFileSync7(loaded.projectFile, "utf8"));
2802
+ writes.set(loaded.projectFile, canonicalStringify5({ ...project, extensions: { ...taken, ...moved } }));
2803
+ }
2804
+ return { writes, report, notes };
2805
+ }
2806
+ function renameCastKeys(shards, manifests, renames) {
2807
+ const out = [];
2808
+ if (renames.size === 0) return out;
2809
+ const sound = /* @__PURE__ */ new Map();
2810
+ for (const m of manifests) {
2811
+ const s = soundOf(m.manifest);
2812
+ if (s !== void 0) sound.set(m.id, s);
2813
+ }
2814
+ for (const { path, shard } of shards) {
2815
+ const manifest = sound.get(shard.extension);
2816
+ if (manifest === void 0) continue;
2817
+ const count = { n: 0 };
2818
+ const entries = {};
2819
+ for (const [kind, byNode] of Object.entries(shard.entries ?? {})) {
2820
+ const fields = targetOf(manifest, "patter", kind)?.fields ?? [];
2821
+ const nodes = {};
2822
+ for (const [node, entry] of Object.entries(byNode ?? {})) {
2823
+ nodes[node] = typeof entry === "object" && entry !== null && !Array.isArray(entry) ? renameFields(fields, entry, renames, count) : entry;
2824
+ }
2825
+ entries[kind] = nodes;
2826
+ }
2827
+ if (count.n > 0) out.push({ path, shard: { ...shard, entries }, renamed: count.n });
2828
+ }
2829
+ return out;
2830
+ }
2831
+ function renameFields(fields, record, renames, count) {
2832
+ const decls = new Map(fields.map((f) => [f.name, f]));
2833
+ const out = {};
2834
+ for (const [k, v] of Object.entries(record)) {
2835
+ const decl = decls.get(k);
2836
+ out[k] = decl !== void 0 ? renameValue(decl, v, renames, count) : v;
2837
+ }
2838
+ return out;
2839
+ }
2840
+ function renameValue(decl, value, renames, count) {
2841
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2842
+ switch (decl.type) {
2843
+ case "reference": {
2844
+ const to = decl.to === "cast" && typeof value === "string" ? renames.get(value) : void 0;
2845
+ if (to === void 0) return value;
2846
+ count.n++;
2847
+ return to;
2848
+ }
2849
+ case "record":
2850
+ return isRecord(value) ? renameFields(decl.fields ?? [], value, renames, count) : value;
2851
+ case "list":
2852
+ return Array.isArray(value) && decl.of !== void 0 ? value.map((v) => renameValue(decl.of, v, renames, count)) : value;
2853
+ case "map": {
2854
+ if (!isRecord(value)) return value;
2855
+ const castKeys = decl.key?.type === "reference" && decl.key.to === "cast";
2856
+ const finals = Object.keys(value).map((k) => castKeys ? renames.get(k) ?? k : k);
2857
+ const taken = /* @__PURE__ */ new Map();
2858
+ for (const f of finals) taken.set(f, (taken.get(f) ?? 0) + 1);
2859
+ const out = {};
2860
+ Object.entries(value).forEach(([k, v], i) => {
2861
+ const renamed = decl.of !== void 0 ? renameValue(decl.of, v, renames, count) : v;
2862
+ const to = finals[i];
2863
+ if (to !== k && taken.get(to) === 1) {
2864
+ out[to] = renamed;
2865
+ count.n++;
2866
+ } else out[k] = renamed;
2867
+ });
2868
+ return out;
2869
+ }
2870
+ default:
2871
+ return value;
2872
+ }
2873
+ }
2874
+
2875
+ // src/format.ts
2503
2876
  var isShard = (file) => SHARD_EXTENSIONS.some((ext) => file.endsWith(ext));
2504
2877
  function expand(path) {
2505
2878
  let dir = false;
@@ -2508,21 +2881,43 @@ function expand(path) {
2508
2881
  } catch {
2509
2882
  }
2510
2883
  if (!dir) return [path];
2511
- const root = dirname5(findProjectFile(path));
2884
+ const root = dirname6(findProjectFile(path));
2512
2885
  return SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).sort();
2513
2886
  }
2887
+ function upgradesFor(paths) {
2888
+ const content = /* @__PURE__ */ new Map(), notes = /* @__PURE__ */ new Map();
2889
+ for (const path of paths) {
2890
+ try {
2891
+ if (!statSync3(path).isDirectory()) continue;
2892
+ } catch {
2893
+ continue;
2894
+ }
2895
+ let loaded;
2896
+ try {
2897
+ loaded = loadProject(path);
2898
+ } catch {
2899
+ continue;
2900
+ }
2901
+ const plan = planExtensionUpgrades(loaded);
2902
+ for (const [file, text] of plan.writes) content.set(resolve5(file), text);
2903
+ for (const [file, lines] of plan.notes) notes.set(resolve5(file), [...notes.get(resolve5(file)) ?? [], ...lines]);
2904
+ }
2905
+ return { content, notes };
2906
+ }
2514
2907
  function runFormat(paths) {
2908
+ const upgrades = upgradesFor(paths);
2515
2909
  return paths.flatMap(expand).map((file) => {
2516
2910
  if (!isShard(file)) return { file, changed: false, skipped: true };
2517
- const original = readFileSync6(file, "utf8");
2911
+ const original = readFileSync8(file, "utf8");
2912
+ const notes = upgrades.notes.get(resolve5(file));
2518
2913
  let canonical;
2519
2914
  try {
2520
- canonical = canonicalStringify5(parseSource4(original));
2915
+ canonical = upgrades.content.get(resolve5(file)) ?? canonicalStringify6(parseSource6(original));
2521
2916
  } catch (e) {
2522
2917
  throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
2523
2918
  }
2524
2919
  const changed = canonical !== original;
2525
- return changed ? { file, changed, write: { path: file, content: canonical } } : { file, changed };
2920
+ return { file, changed, ...changed ? { write: { path: file, content: canonical } } : {}, ...notes ? { notes } : {} };
2526
2921
  });
2527
2922
  }
2528
2923
 
@@ -2917,8 +3312,8 @@ async function runReportXlsx(data) {
2917
3312
  }
2918
3313
 
2919
3314
  // src/localisation.ts
2920
- import { basename as basename5, join as join7, sep as sep3 } from "path";
2921
- import { canonicalStringify as canonicalStringify6 } from "@patterkit/core";
3315
+ import { basename as basename5, join as join8, sep as sep4 } from "path";
3316
+ import { canonicalStringify as canonicalStringify7 } from "@patterkit/core";
2922
3317
  import {
2923
3318
  walkNodes as walkNodes2,
2924
3319
  castStringKey,
@@ -3084,23 +3479,23 @@ function targetLocPath(loaded, scene, locale, defaultLocale) {
3084
3479
  if (existing) return existing;
3085
3480
  const fromDefault = existingLocPath(loaded, scene, defaultLocale);
3086
3481
  if (fromDefault) {
3087
- const parts = fromDefault.split(sep3);
3482
+ const parts = fromDefault.split(sep4);
3088
3483
  for (let i = parts.length - 2; i >= 0; i--) {
3089
3484
  if (parts[i] === defaultLocale) {
3090
3485
  parts[i] = locale;
3091
- return parts.join(sep3);
3486
+ return parts.join(sep4);
3092
3487
  }
3093
3488
  }
3094
3489
  }
3095
3490
  const stem = scene === PROJECT_LOCALE_SCENE2 ? "_project" : scene;
3096
- return join7(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
3491
+ return join8(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
3097
3492
  }
3098
3493
  function authoringPath(loaded, scene) {
3099
- const dir = join7(loaded.root, layoutOf(loaded).authoring);
3100
- if (scene === PROJECT_LOCALE_SCENE2) return join7(dir, "_project.patterx");
3494
+ const dir = join8(loaded.root, layoutOf(loaded).authoring);
3495
+ if (scene === PROJECT_LOCALE_SCENE2) return join8(dir, "_project.patterx");
3101
3496
  const flow = loaded.sceneFiles[scene];
3102
3497
  const stem = flow ? basename5(flow).replace(/\.patterflow$/, "") : scene;
3103
- return join7(dir, `${stem}.patterx`);
3498
+ return join8(dir, `${stem}.patterx`);
3104
3499
  }
3105
3500
  var findFile = (files, items, path) => {
3106
3501
  const i = files.indexOf(path);
@@ -3138,7 +3533,7 @@ function applyLoc(loaded, catalog, opts = {}) {
3138
3533
  merged[id] = text;
3139
3534
  }
3140
3535
  const file = { schema: existing?.schema ?? STRINGS_SCHEMA, scene, locale, strings: merged };
3141
- writes.push({ path, content: canonicalStringify6(file) });
3536
+ writes.push({ path, content: canonicalStringify7(file) });
3142
3537
  const fresh = bySceneFresh.get(scene);
3143
3538
  if (!fresh?.size) continue;
3144
3539
  const aPath = authoringPath(loaded, scene);
@@ -3147,7 +3542,7 @@ function applyLoc(loaded, catalog, opts = {}) {
3147
3542
  const editsMap = { ...authoring.edits ?? {} };
3148
3543
  for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
3149
3544
  authoring.edits = editsMap;
3150
- writes.push({ path: aPath, content: canonicalStringify6(authoring) });
3545
+ writes.push({ path: aPath, content: canonicalStringify7(authoring) });
3151
3546
  }
3152
3547
  return { writes, stats: { updated, files: bySceneText.size } };
3153
3548
  }
@@ -3833,9 +4228,9 @@ async function scriptToDocx(doc) {
3833
4228
  import PDFDocument from "pdfkit";
3834
4229
 
3835
4230
  // src/script-fonts.ts
3836
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
4231
+ import { existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
3837
4232
  import { createRequire } from "module";
3838
- import { join as join8 } from "path";
4233
+ import { join as join9 } from "path";
3839
4234
 
3840
4235
  // src/script-fonts.json
3841
4236
  var script_fonts_default = {
@@ -3922,18 +4317,18 @@ function useScriptFontFiles(paths, licencePaths = {}) {
3922
4317
  cache.clear();
3923
4318
  }
3924
4319
  function scriptFontLicencePath(pkg) {
3925
- const dir = (require2.resolve.paths(pkg) ?? []).map((p) => join8(p, pkg)).find((d) => existsSync5(d));
3926
- const file = dir && readdirSync4(dir).find((f) => /^(licen[cs]e|ofl)/i.test(f));
4320
+ const dir = (require2.resolve.paths(pkg) ?? []).map((p) => join9(p, pkg)).find((d) => existsSync5(d));
4321
+ const file = dir && readdirSync5(dir).find((f) => /^(licen[cs]e|ofl)/i.test(f));
3927
4322
  if (!dir || !file) throw new Error(`no licence file found for ${pkg}`);
3928
- return join8(dir, file);
4323
+ return join9(dir, file);
3929
4324
  }
3930
4325
  function scriptFontLicences() {
3931
- return SCRIPT_FONT_PACKAGES.map((pkg) => ({ package: pkg, text: readFileSync7(licences[pkg] ?? scriptFontLicencePath(pkg), "utf8") }));
4326
+ return SCRIPT_FONT_PACKAGES.map((pkg) => ({ package: pkg, text: readFileSync9(licences[pkg] ?? scriptFontLicencePath(pkg), "utf8") }));
3932
4327
  }
3933
4328
  function scriptFontFiles(face) {
3934
4329
  return script_fonts_default[face].map((spec) => {
3935
4330
  let bytes = cache.get(spec);
3936
- if (!bytes) cache.set(spec, bytes = readFileSync7(located[spec] ?? require2.resolve(spec)));
4331
+ if (!bytes) cache.set(spec, bytes = readFileSync9(located[spec] ?? require2.resolve(spec)));
3937
4332
  return bytes;
3938
4333
  });
3939
4334
  }
@@ -3973,7 +4368,7 @@ var ROLES = {
3973
4368
  "Mono": "mono"
3974
4369
  };
3975
4370
  async function scriptToPdf(doc) {
3976
- return new Promise((resolve7, reject) => {
4371
+ return new Promise((resolve8, reject) => {
3977
4372
  const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
3978
4373
  const register = (prefix, face, firstIsPrefix) => scriptFontFiles(face).map((bytes, i) => {
3979
4374
  const name = firstIsPrefix && i === 0 ? prefix : `${prefix}~${i}`;
@@ -4017,7 +4412,7 @@ async function scriptToPdf(doc) {
4017
4412
  const bodyPieces = (runs, color, base = {}) => runs.flatMap((r) => r.code ? split(r.text, "Mono", ACCENT) : split(r.text, SERIF2(base.bold || r.bold, base.italic || r.italic), color));
4018
4413
  const chunks = [];
4019
4414
  pdf.on("data", (c) => chunks.push(c));
4020
- pdf.on("end", () => resolve7(Buffer.concat(chunks)));
4415
+ pdf.on("end", () => resolve8(Buffer.concat(chunks)));
4021
4416
  pdf.on("error", reject);
4022
4417
  const pageRight = pdf.page.width - MARGIN;
4023
4418
  const pageBottom = pdf.page.height - MARGIN;
@@ -4169,17 +4564,17 @@ async function scriptToPdf(doc) {
4169
4564
  }
4170
4565
 
4171
4566
  // src/init.ts
4172
- import { join as join9, basename as basename6, resolve as resolve5 } from "path";
4173
- import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
4174
- import { newId, slug, canonicalStringify as canonicalStringify7 } from "@patterkit/core";
4567
+ import { join as join10, basename as basename6, resolve as resolve6 } from "path";
4568
+ import { readdirSync as readdirSync6, existsSync as existsSync6, readFileSync as readFileSync10 } from "fs";
4569
+ import { newId, slug, canonicalStringify as canonicalStringify8 } from "@patterkit/core";
4175
4570
  import { FLOW_SCHEMA, PROJECT_SCHEMA, STRINGS_SCHEMA as STRINGS_SCHEMA2 } from "@patterkit/model";
4176
4571
  function runInit(opts) {
4177
- const dir = resolve5(opts.dir);
4572
+ const dir = resolve6(opts.dir);
4178
4573
  const name = opts.name?.trim() || basename6(dir).replace(/\.patter$/, "");
4179
4574
  const fileSlug = slug(name);
4180
4575
  let existing = [];
4181
4576
  try {
4182
- existing = readdirSync5(dir).filter((f) => f.endsWith(".patterproj"));
4577
+ existing = readdirSync6(dir).filter((f) => f.endsWith(".patterproj"));
4183
4578
  } catch {
4184
4579
  }
4185
4580
  if (existing.length > 0) throw new Error(`a project already exists here: ${existing[0]}`);
@@ -4223,13 +4618,13 @@ function runInit(opts) {
4223
4618
  strings: { [beatId]: `Welcome to ${name}. This is the first line of your story - replace it with your own.` }
4224
4619
  };
4225
4620
  const bundle = opts.bundle ?? "commit";
4226
- const projectFile = join9(dir, `${fileSlug}.patterproj`);
4621
+ const projectFile = join10(dir, `${fileSlug}.patterproj`);
4227
4622
  const writes = [
4228
- { path: join9(dir, "scenes", "start.patterflow"), content: canonicalStringify7(flow) },
4229
- { path: join9(dir, "loc", "en", "start.patterloc"), content: canonicalStringify7(locale) },
4230
- { path: join9(dir, ".editorconfig"), content: EDITORCONFIG },
4623
+ { path: join10(dir, "scenes", "start.patterflow"), content: canonicalStringify8(flow) },
4624
+ { path: join10(dir, "loc", "en", "start.patterloc"), content: canonicalStringify8(locale) },
4625
+ { path: join10(dir, ".editorconfig"), content: EDITORCONFIG },
4231
4626
  ...vcsConfigWrites(dir, opts.vcs, bundle),
4232
- { path: projectFile, content: canonicalStringify7(project) }
4627
+ { path: projectFile, content: canonicalStringify8(project) }
4233
4628
  ];
4234
4629
  const collisions = writes.map((w) => w.path).filter((p) => existsSync6(p));
4235
4630
  if (collisions.length > 0) {
@@ -4238,16 +4633,16 @@ function runInit(opts) {
4238
4633
  return { writes, projectFile, name };
4239
4634
  }
4240
4635
  function vcsConfigWrites(dir, vcs, bundle = "commit") {
4241
- const writes = [{ path: join9(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
4242
- if (vcs === "git") writes.push({ path: join9(dir, ".gitattributes"), content: GITATTRIBUTES });
4636
+ const writes = [{ path: join10(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
4637
+ if (vcs === "git") writes.push({ path: join10(dir, ".gitattributes"), content: GITATTRIBUTES });
4243
4638
  const ignore = ignoreFileFor(vcs);
4244
- if (ignore) writes.push({ path: join9(dir, ignore), content: ignoreContent(bundle) });
4639
+ if (ignore) writes.push({ path: join10(dir, ignore), content: ignoreContent(bundle) });
4245
4640
  return writes;
4246
4641
  }
4247
4642
  function currentBundlePosture(dir, vcs) {
4248
4643
  const read = (name) => {
4249
4644
  try {
4250
- return readFileSync8(join9(dir, name), "utf8");
4645
+ return readFileSync10(join10(dir, name), "utf8");
4251
4646
  } catch {
4252
4647
  return "";
4253
4648
  }
@@ -4264,7 +4659,7 @@ charset = utf-8
4264
4659
  end_of_line = lf
4265
4660
  insert_final_newline = true
4266
4661
 
4267
- [*.{patterflow,patterloc,patterx,patterproj}]
4662
+ [*.{patterflow,patterloc,patterx,patterproj,patterextension}]
4268
4663
  indent_style = space
4269
4664
  indent_size = 2
4270
4665
  `;
@@ -4273,6 +4668,7 @@ var GITATTRIBUTES = `# Patter source is UTF-8 + LF text (pinned; never let autoc
4273
4668
  *.patterloc text eol=lf
4274
4669
  *.patterx text eol=lf
4275
4670
  *.patterproj text eol=lf
4671
+ *.patterextension text eol=lf
4276
4672
 
4277
4673
  # Id-keyed structured merge for Patter source (the 'patter merge' driver).
4278
4674
  # Register it once per clone (see vcs-setup.md); until then git falls back to a
@@ -4281,6 +4677,11 @@ var GITATTRIBUTES = `# Patter source is UTF-8 + LF text (pinned; never let autoc
4281
4677
  *.patterloc merge=patter
4282
4678
  *.patterx merge=patter
4283
4679
  *.patterproj merge=patter
4680
+ *.patterextension merge=patter
4681
+
4682
+ # An extension's manifest is the extension's own JSON, kept in the project so
4683
+ # every machine reads the same one, and never edited here: text, merged as text.
4684
+ extensions/*.extension.json text eol=lf
4284
4685
 
4285
4686
  # Generated artifacts (spec \xA711). The compiled bundle is committed but
4286
4687
  # REGENERATED, never hand-merged - keep ours on conflict and rebuild (the
@@ -4307,7 +4708,9 @@ function ignoreContent(bundle) {
4307
4708
  "# Patter generated artifacts - not source (see vcs-setup.md).",
4308
4709
  "# Packed document (the send-and-return envelope):",
4309
4710
  "*.patterpack",
4310
- "# Unresolved merge sidecar:",
4711
+ "# Unresolved merge sidecar (a lingering one is a validate error). The merge also names its clashes",
4712
+ '# in the shard itself, under "unresolved", which IS committed with the file, so CI refuses an',
4713
+ "# unresolved merge wherever it goes:",
4311
4714
  "*.patterconflict"
4312
4715
  ];
4313
4716
  if (bundle === "ignore") {
@@ -4351,6 +4754,8 @@ compiled bundle are TEXT with LF; the packed document is BINARY:
4351
4754
  text //....patterloc
4352
4755
  text //....patterx
4353
4756
  text //....patterproj
4757
+ text //....patterextension
4758
+ text //....extension.json
4354
4759
  text //....patterc
4355
4760
  binary //....patterpack
4356
4761
 
@@ -4389,6 +4794,8 @@ Set auto-props so Patter source keeps LF (\`~/.subversion/config\` or repo confi
4389
4794
  *.patterloc = svn:eol-style=LF
4390
4795
  *.patterx = svn:eol-style=LF
4391
4796
  *.patterproj = svn:eol-style=LF
4797
+ *.patterextension = svn:eol-style=LF
4798
+ *.extension.json = svn:eol-style=LF
4392
4799
  *.patterc = svn:eol-style=LF
4393
4800
  *.patterpack = svn:mime-type=application/octet-stream
4394
4801
 
@@ -4676,7 +5083,7 @@ function buildIndex(loaded) {
4676
5083
  }
4677
5084
 
4678
5085
  // src/replace.ts
4679
- import { canonicalStringify as canonicalStringify8 } from "@patterkit/core";
5086
+ import { canonicalStringify as canonicalStringify9 } from "@patterkit/core";
4680
5087
  import { walkNodes as walkNodes4, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE3, AUTHORING_SCHEMA as AUTHORING_SCHEMA2 } from "@patterkit/model";
4681
5088
  import { findMatcher } from "@wildwinter/toolkit";
4682
5089
  function runReplace(loaded, opts) {
@@ -4720,7 +5127,7 @@ function runReplace(loaded, opts) {
4720
5127
  }
4721
5128
  if (changedIds.length) {
4722
5129
  const next = { ...shard, strings: merged };
4723
- plan.writes.push({ path, content: canonicalStringify8(next) });
5130
+ plan.writes.push({ path, content: canonicalStringify9(next) });
4724
5131
  plan.shards.push(next);
4725
5132
  plan.scenes++;
4726
5133
  const aPath = authoringPath(loaded, shard.scene);
@@ -4737,7 +5144,7 @@ function runReplace(loaded, opts) {
4737
5144
  }
4738
5145
  for (const [path, file] of stamped) {
4739
5146
  plan.authoring.push({ path, file });
4740
- plan.writes.push({ path, content: canonicalStringify8(file) });
5147
+ plan.writes.push({ path, content: canonicalStringify9(file) });
4741
5148
  }
4742
5149
  return plan;
4743
5150
  }
@@ -4831,7 +5238,12 @@ function renameGameDataStage(scenes, kind, field, from, to) {
4831
5238
  for (const block of scene.blocks) {
4832
5239
  if (kind === "block") visit(block);
4833
5240
  walkNodes5(block.children, (node) => {
4834
- if (node.type !== "snippet") return;
5241
+ if (node.type !== "snippet") {
5242
+ if (kind === "option" && node.selector === "choice") {
5243
+ for (const option of node.children) if (option.type === "group") visit(option);
5244
+ }
5245
+ return;
5246
+ }
4835
5247
  if (kind === "snippet") visit(node);
4836
5248
  for (const beat of node.beats ?? []) if (beat.kind === kind) visit(beat);
4837
5249
  });
@@ -4842,8 +5254,8 @@ function renameGameDataStage(scenes, kind, field, from, to) {
4842
5254
  }
4843
5255
 
4844
5256
  // src/pin.ts
4845
- import { readFileSync as readFileSync9 } from "fs";
4846
- import { canonicalStringify as canonicalStringify9, parseSource as parseSource5 } from "@patterkit/core";
5257
+ import { readFileSync as readFileSync11 } from "fs";
5258
+ import { canonicalStringify as canonicalStringify10, parseSource as parseSource7 } from "@patterkit/core";
4847
5259
  import { effectiveGameId as effectiveGameId3, gameIdify } from "@patterkit/model";
4848
5260
  function toPin(node) {
4849
5261
  if (node.gameId?.trim()) return void 0;
@@ -4870,8 +5282,8 @@ function planPins(loaded) {
4870
5282
  });
4871
5283
  if (!changed) continue;
4872
5284
  const next = { ...scene, ...sceneGameId !== void 0 ? { gameId: sceneGameId } : {}, blocks };
4873
- const file = parseSource5(readFileSync9(path, "utf8"));
4874
- plan.writes.push({ path, content: canonicalStringify9({ ...file, scene: next }) });
5285
+ const file = parseSource7(readFileSync11(path, "utf8"));
5286
+ plan.writes.push({ path, content: canonicalStringify10({ ...file, scene: next }) });
4875
5287
  plan.scenes.push(next);
4876
5288
  }
4877
5289
  return plan;
@@ -4879,9 +5291,9 @@ function planPins(loaded) {
4879
5291
 
4880
5292
  // src/unpack.ts
4881
5293
  import JSZip2 from "jszip";
4882
- import { join as join10, resolve as resolve6, sep as sep4 } from "path";
4883
- import { existsSync as existsSync7, readFileSync as readFileSync10, readdirSync as readdirSync6 } from "fs";
4884
- import { parseSource as parseSource6, canonicalStringify as canonicalStringify10 } from "@patterkit/core";
5294
+ import { join as join11, resolve as resolve7, sep as sep5 } from "path";
5295
+ import { existsSync as existsSync7, readFileSync as readFileSync12, readdirSync as readdirSync7 } from "fs";
5296
+ import { parseSource as parseSource8, canonicalStringify as canonicalStringify11 } from "@patterkit/core";
4885
5297
  import { isUnsafeEntry } from "@wildwinter/toolkit/archive";
4886
5298
  import { SCOPES_FILE_SUFFIX as SCOPES_FILE_SUFFIX2 } from "@wildwinter/scoperegistry/scopes";
4887
5299
  import { isUnsafeEntry as isUnsafeEntry2 } from "@wildwinter/toolkit/archive";
@@ -4898,7 +5310,7 @@ var UnsafeEntryError = class extends Error {
4898
5310
  };
4899
5311
  var NotAPackError = class extends Error {
4900
5312
  };
4901
- var isShardEntry = (name) => SHARD_EXTENSIONS.some((ext) => name.endsWith(ext));
5313
+ var isShardEntry = (name) => SHARD_EXTENSIONS.some((ext) => name.endsWith(ext)) || isManifestEntry(name);
4902
5314
  function refuseUnsafe(name) {
4903
5315
  if (isUnsafeEntry(name)) throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
4904
5316
  if (name.split("/").some((segment) => segment.startsWith("."))) {
@@ -4948,13 +5360,13 @@ async function runUnpack(bytes, targetDir) {
4948
5360
  function projectIdIn(dir) {
4949
5361
  let names;
4950
5362
  try {
4951
- names = readdirSync6(dir).filter((n) => n.endsWith(".patterproj"));
5363
+ names = readdirSync7(dir).filter((n) => n.endsWith(".patterproj"));
4952
5364
  } catch {
4953
5365
  return void 0;
4954
5366
  }
4955
5367
  for (const n of names) {
4956
5368
  try {
4957
- const id = parseSource6(readFileSync10(join10(dir, n), "utf8")).project?.id;
5369
+ const id = parseSource8(readFileSync12(join11(dir, n), "utf8")).project?.id;
4958
5370
  if (id) return id;
4959
5371
  } catch {
4960
5372
  }
@@ -4965,7 +5377,7 @@ function projectIdOf(shards) {
4965
5377
  const rel = projectEntry(shards);
4966
5378
  if (rel === void 0) return void 0;
4967
5379
  try {
4968
- return parseSource6(shards.get(rel)).project?.id;
5380
+ return parseSource8(shards.get(rel)).project?.id;
4969
5381
  } catch {
4970
5382
  return void 0;
4971
5383
  }
@@ -4981,7 +5393,7 @@ function checkProvenance(returned, base, target) {
4981
5393
  }
4982
5394
  function targetProjectId(projectDir) {
4983
5395
  try {
4984
- const pf = parseSource6(readFileSync10(findProjectFile(projectDir), "utf8"));
5396
+ const pf = parseSource8(readFileSync12(findProjectFile(projectDir), "utf8"));
4985
5397
  return pf.project?.id;
4986
5398
  } catch {
4987
5399
  return void 0;
@@ -4999,6 +5411,8 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
4999
5411
  let conflicts = 0, warnings = 0;
5000
5412
  const projectRel = projectEntry(theirs);
5001
5413
  let projectSides;
5414
+ const manifests = projectManifests(projectDir);
5415
+ const manifestsKept = [];
5002
5416
  for (const [rel, theirText] of [...theirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
5003
5417
  const outPath = containedWrite(projectDir, rel);
5004
5418
  const baseText = base.get(rel);
@@ -5008,10 +5422,15 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
5008
5422
  shards.push({ path: rel, added: true, changed: true });
5009
5423
  continue;
5010
5424
  }
5011
- const oursText = readFileSync10(outPath, "utf8");
5425
+ const oursText = readFileSync12(outPath, "utf8");
5426
+ if (isManifestEntry(rel)) {
5427
+ if (theirText !== oursText) manifestsKept.push(rel);
5428
+ shards.push({ path: rel, added: false, changed: false });
5429
+ continue;
5430
+ }
5012
5431
  if (theirText === baseText || theirText === oursText) {
5013
5432
  if (rel === projectRel && baseText !== void 0) {
5014
- const read = (t) => parseSource6(t);
5433
+ const read = (t) => parseSource8(t);
5015
5434
  const ours = read(oursText);
5016
5435
  projectSides = { ours, base: read(baseText), theirs: read(theirText), merged: ours };
5017
5436
  }
@@ -5020,7 +5439,7 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
5020
5439
  }
5021
5440
  const readSide = (side, text) => {
5022
5441
  try {
5023
- return parseSource6(text);
5442
+ return parseSource8(text);
5024
5443
  } catch (e) {
5025
5444
  throw new Error(`${rel}: the ${side} copy is not readable Patter source - ${e instanceof Error ? e.message : String(e)}`);
5026
5445
  }
@@ -5028,8 +5447,9 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
5028
5447
  const oursObj = readSide("ours", oursText);
5029
5448
  const theirsObj = readSide("theirs", theirText);
5030
5449
  const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
5031
- const result = runMerge(baseObj, oursObj, theirsObj);
5032
- const mergedText = canonicalStringify10(result.merged);
5450
+ const manifest = detectMergeType(oursObj) === "extension" && typeof oursObj["extension"] === "string" ? manifests(oursObj["extension"]) : void 0;
5451
+ const result = runMerge(baseObj, oursObj, theirsObj, manifest !== void 0 ? { manifest } : void 0);
5452
+ const mergedText = canonicalStringify11(result.merged);
5033
5453
  const changed = mergedText !== oursText;
5034
5454
  if (changed) writes.push({ path: outPath, content: mergedText });
5035
5455
  if (rel === projectRel && baseText !== void 0) {
@@ -5043,13 +5463,13 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
5043
5463
  shards.push({ path: rel, result, added: false, changed });
5044
5464
  }
5045
5465
  let gameScopes;
5046
- const local2 = projectSides && discoverGameScopes(resolve6(projectDir), projectSides.ours).gameScopes;
5466
+ const local2 = projectSides && discoverGameScopes(resolve7(projectDir), projectSides.ours).gameScopes;
5047
5467
  if (projectSides && local2) {
5048
5468
  const world = planReturnedWorld(projectSides.ours, projectSides.base, projectSides.theirs, projectSides.merged, local2);
5049
5469
  if (world.write) {
5050
5470
  writes.push(world.write);
5051
5471
  gameScopes = { path: world.write.path };
5052
- } else if (world.error) gameScopes = { path: join10(local2.dir, GAME_SCOPES_FILE), error: world.error };
5472
+ } else if (world.error) gameScopes = { path: join11(local2.dir, GAME_SCOPES_FILE), error: world.error };
5053
5473
  }
5054
5474
  const handoffs = [];
5055
5475
  for (const [rel, theirText] of [...returnedDoc.handoffs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
@@ -5059,13 +5479,24 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
5059
5479
  handoffs.push(rel);
5060
5480
  continue;
5061
5481
  }
5062
- const merged = mergeHandoff(parseHandoff(readFileSync10(outPath, "utf8")), parseHandoff(theirText));
5482
+ const merged = mergeHandoff(parseHandoff(readFileSync12(outPath, "utf8")), parseHandoff(theirText));
5063
5483
  if (merged) {
5064
5484
  writes.push({ path: outPath, content: serialiseHandoff(merged) });
5065
5485
  handoffs.push(rel);
5066
5486
  }
5067
5487
  }
5068
- return { shards, writes, sidecars, other: returnedDoc.other, conflicts, warnings, provenance, ...gameScopes ? { gameScopes } : {}, ...handoffs.length ? { handoffs } : {} };
5488
+ return {
5489
+ shards,
5490
+ writes,
5491
+ sidecars,
5492
+ other: returnedDoc.other,
5493
+ conflicts,
5494
+ warnings,
5495
+ provenance,
5496
+ ...gameScopes ? { gameScopes } : {},
5497
+ ...handoffs.length ? { handoffs } : {},
5498
+ ...manifestsKept.length ? { manifestsKept } : {}
5499
+ };
5069
5500
  }
5070
5501
  function mergeHandoff(ours, theirs) {
5071
5502
  const key = (i) => `${i.at}|${i.fileHash}`;
@@ -5081,18 +5512,76 @@ function projectEntry(shards) {
5081
5512
  return at.length === 1 ? at[0] : void 0;
5082
5513
  }
5083
5514
  function containedWrite(dir, name) {
5084
- const root = resolve6(dir);
5085
- const full = resolve6(join10(dir, name));
5086
- if (full !== root && !full.startsWith(root + sep4)) {
5515
+ const root = resolve7(dir);
5516
+ const full = resolve7(join11(dir, name));
5517
+ if (full !== root && !full.startsWith(root + sep5)) {
5087
5518
  throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
5088
5519
  }
5089
5520
  return full;
5090
5521
  }
5091
5522
 
5523
+ // src/cast-rename.ts
5524
+ import { castStringKey as castStringKey2, speakerLabelKey as speakerLabelKey2, walkNodes as walkNodes6 } from "@patterkit/model";
5525
+ function cleanCastRenames(renames) {
5526
+ const out = /* @__PURE__ */ new Map();
5527
+ for (const [from, to] of Object.entries(renames ?? {})) if (from && to && from !== to) out.set(from, to);
5528
+ return out.size ? out : null;
5529
+ }
5530
+ function renameCastInScene(scene, renames) {
5531
+ let n = 0;
5532
+ const visit = (beat) => {
5533
+ if (beat?.kind !== "line" || beat.character === void 0) return;
5534
+ const to = renames.get(beat.character);
5535
+ if (to !== void 0) {
5536
+ beat.character = to;
5537
+ n++;
5538
+ }
5539
+ };
5540
+ for (const block of scene.blocks ?? []) {
5541
+ walkNodes6(block.children, (node) => {
5542
+ if (node.type === "group") {
5543
+ visit(node.prompt);
5544
+ return;
5545
+ }
5546
+ for (const beat of node.beats ?? []) visit(beat);
5547
+ });
5548
+ }
5549
+ return n;
5550
+ }
5551
+ var CAST_PREFIX = castStringKey2("");
5552
+ var SPEAKER_PREFIX = speakerLabelKey2("", "").slice(0, -1);
5553
+ function renamedKey(key, renames) {
5554
+ if (key.startsWith(CAST_PREFIX)) {
5555
+ const to = renames.get(key.slice(CAST_PREFIX.length));
5556
+ return to !== void 0 ? castStringKey2(to) : void 0;
5557
+ }
5558
+ if (key.startsWith(SPEAKER_PREFIX)) {
5559
+ const at = key.lastIndexOf(":");
5560
+ if (at <= SPEAKER_PREFIX.length) return void 0;
5561
+ const to = renames.get(key.slice(SPEAKER_PREFIX.length, at));
5562
+ return to !== void 0 ? speakerLabelKey2(to, key.slice(at + 1)) : void 0;
5563
+ }
5564
+ return void 0;
5565
+ }
5566
+ function moveCastKeys(record, renames) {
5567
+ const keys = Object.keys(record);
5568
+ const finals = keys.map((k) => renamedKey(k, renames) ?? k);
5569
+ const count = /* @__PURE__ */ new Map();
5570
+ for (const f of finals) count.set(f, (count.get(f) ?? 0) + 1);
5571
+ if (finals.every((f, i) => f === keys[i] || count.get(f) > 1)) return false;
5572
+ const entries = keys.map((k, i) => [count.get(finals[i]) === 1 ? finals[i] : k, record[k]]);
5573
+ for (const k of keys) delete record[k];
5574
+ for (const [k, v] of entries) record[k] = v;
5575
+ return true;
5576
+ }
5577
+ function renameCastStrings(loc, renames) {
5578
+ return loc.strings ? moveCastKeys(loc.strings, renames) : false;
5579
+ }
5580
+
5092
5581
  // src/suggestions.ts
5093
- import { readFileSync as readFileSync11 } from "fs";
5094
- import { canonicalStringify as canonicalStringify11, parseSource as parseSource7 } from "@patterkit/core";
5095
- import { walkNodes as walkNodes6, AUTHORING_SCHEMA as AUTHORING_SCHEMA3 } from "@patterkit/model";
5582
+ import { readFileSync as readFileSync13 } from "fs";
5583
+ import { canonicalStringify as canonicalStringify12, parseSource as parseSource9 } from "@patterkit/core";
5584
+ import { walkNodes as walkNodes7, AUTHORING_SCHEMA as AUTHORING_SCHEMA3 } from "@patterkit/model";
5096
5585
  function staleParts(s, liveText, line) {
5097
5586
  const stale = [];
5098
5587
  if (liveText !== s.baseline) stale.push("the text");
@@ -5123,7 +5612,7 @@ function indexPlaces(loaded) {
5123
5612
  places.set(scene.id, { sceneId: scene.id });
5124
5613
  for (const block of scene.blocks) {
5125
5614
  places.set(block.id, { sceneId: scene.id });
5126
- walkNodes6(block.children, (node) => {
5615
+ walkNodes7(block.children, (node) => {
5127
5616
  places.set(node.id, { sceneId: scene.id });
5128
5617
  if (node.type === "group") {
5129
5618
  if (node.prompt) places.set(node.prompt.id, { sceneId: scene.id });
@@ -5177,7 +5666,7 @@ var Working = class {
5177
5666
  if (!path) return void 0;
5178
5667
  let file = this.flows.get(path);
5179
5668
  if (!file) {
5180
- const onDisk = parseSource7(readFileSync11(path, "utf8"));
5669
+ const onDisk = parseSource9(readFileSync13(path, "utf8"));
5181
5670
  const scene = this.loaded.scenes.find((s) => s.id === sceneId);
5182
5671
  file = { ...onDisk, scene: structuredClone(scene ?? onDisk.scene) };
5183
5672
  this.flows.set(path, file);
@@ -5185,13 +5674,13 @@ var Working = class {
5185
5674
  return { path, file };
5186
5675
  }
5187
5676
  writes() {
5188
- return [...this.locs, ...this.flows, ...this.authoring].filter(([path]) => this.touched.has(path)).map(([path, file]) => ({ path, content: canonicalStringify11(file) })).sort((a, b) => a.path.localeCompare(b.path));
5677
+ return [...this.locs, ...this.flows, ...this.authoring].filter(([path]) => this.touched.has(path)).map(([path, file]) => ({ path, content: canonicalStringify12(file) })).sort((a, b) => a.path.localeCompare(b.path));
5189
5678
  }
5190
5679
  };
5191
5680
  function beatIn(scene, id) {
5192
5681
  let found;
5193
5682
  for (const block of scene.blocks) {
5194
- walkNodes6(block.children, (node) => {
5683
+ walkNodes7(block.children, (node) => {
5195
5684
  if (found || node.type !== "snippet") return;
5196
5685
  found = (node.beats ?? []).find((b) => b.id === id);
5197
5686
  });
@@ -5654,11 +6143,11 @@ function readParagraphs(ps, joiner) {
5654
6143
  c.walk(p, "both");
5655
6144
  return c;
5656
6145
  });
5657
- const join11 = (view) => parts.map((c) => c.text(view)).filter((t) => t !== "").join(joiner);
6146
+ const join12 = (view) => parts.map((c) => c.text(view)).filter((t) => t !== "").join(joiner);
5658
6147
  return {
5659
6148
  text: {
5660
- proposed: join11("proposed"),
5661
- original: join11("original"),
6149
+ proposed: join12("proposed"),
6150
+ original: join12("original"),
5662
6151
  changes: parts.flatMap((c) => c.changes),
5663
6152
  comments: [...new Set(parts.flatMap((c) => c.comments))],
5664
6153
  droppedFormatting: parts.reduce((n, c) => n + c.dropped, 0)
@@ -5791,7 +6280,7 @@ async function readEditableDocx(bytes) {
5791
6280
  }
5792
6281
 
5793
6282
  // src/editable-import.ts
5794
- import { canonicalStringify as canonicalStringify12 } from "@patterkit/core";
6283
+ import { canonicalStringify as canonicalStringify13 } from "@patterkit/core";
5795
6284
  import { AUTHORING_SCHEMA as AUTHORING_SCHEMA4, projectQualifiers as projectQualifiers4 } from "@patterkit/model";
5796
6285
  var REFUSE_MISSING_SHARE = 0.25;
5797
6286
  var tidy = (s) => s.replace(/[\s   ]+/g, " ").trim();
@@ -6059,7 +6548,7 @@ function planEditableImport(loaded, returned, opts) {
6059
6548
  const af = shardFor(c.anchor);
6060
6549
  af.comments = [...af.comments ?? [], c];
6061
6550
  }
6062
- let writes = [...shards].map(([path, af]) => ({ path, content: canonicalStringify12(af) }));
6551
+ let writes = [...shards].map(([path, af]) => ({ path, content: canonicalStringify13(af) }));
6063
6552
  if (opts.direct && suggestions.length) {
6064
6553
  const warned = new Set(problems.filter((p) => p.severity === "warning" && p.anchor).map((p) => p.anchor));
6065
6554
  const clean = suggestions.filter((s) => (live[s.anchor] ?? "") === s.baseline && !warned.has(s.anchor));
@@ -6227,6 +6716,7 @@ export {
6227
6716
  NotAPackError,
6228
6717
  PATTER_SCOPES_FILE,
6229
6718
  PATTER_SCOPES_OWNER,
6719
+ PROJECT_HANDLED,
6230
6720
  RARE_REACH_PCT,
6231
6721
  REFUSE_MISSING_SHARE,
6232
6722
  SCRIPT_FONT_FILES,
@@ -6249,6 +6739,7 @@ export {
6249
6739
  catalogToPo,
6250
6740
  catalogToXlsx,
6251
6741
  classesForChannel,
6742
+ cleanCastRenames,
6252
6743
  compileLoaded,
6253
6744
  cueLabel,
6254
6745
  currentBundlePosture,
@@ -6276,7 +6767,10 @@ export {
6276
6767
  listProjectTags,
6277
6768
  loadProject,
6278
6769
  loadProjectLanding,
6770
+ loadedExtensions,
6771
+ manifestPath,
6279
6772
  markerLength,
6773
+ moveCastKeys,
6280
6774
  newHandoffId,
6281
6775
  parseHandoff,
6282
6776
  patterScopesFile,
@@ -6285,12 +6779,14 @@ export {
6285
6779
  pickAudio,
6286
6780
  planBuild,
6287
6781
  planEditableImport,
6782
+ planExtensionUpgrades,
6288
6783
  planPins,
6289
6784
  planReturnedWorld,
6290
6785
  planShareScopes,
6291
6786
  planWorldSave,
6292
6787
  poToCatalog,
6293
6788
  previewRegistry,
6789
+ projectManifests,
6294
6790
  projectStringSources,
6295
6791
  proposeCoverageDrivers,
6296
6792
  qualifierNamer,
@@ -6300,6 +6796,10 @@ export {
6300
6796
  readHandoff,
6301
6797
  readHandoffs,
6302
6798
  readMarker,
6799
+ readProjectManifest,
6800
+ renameCastInScene,
6801
+ renameCastKeys,
6802
+ renameCastStrings,
6303
6803
  renameGameDataStage,
6304
6804
  renameStage,
6305
6805
  renameStageInExpression,
@@ -6343,9 +6843,11 @@ export {
6343
6843
  serialiseScopesFile,
6344
6844
  setCut,
6345
6845
  sidecarIssues,
6846
+ soundManifest,
6346
6847
  stageDialect,
6347
6848
  stageGateCount,
6348
6849
  staleParts,
6850
+ unresolvedMarkIssues,
6349
6851
  useScriptFontFiles,
6350
6852
  vcsConfigWrites,
6351
6853
  voiceScriptToXlsx,