@patterkit/ops 0.7.7 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +192 -17
- package/dist/index.js +435 -122
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,11 +1,257 @@
|
|
|
1
1
|
// src/load.ts
|
|
2
|
-
import { readdirSync, readFileSync, statSync, existsSync } from "fs";
|
|
3
|
-
import { join, dirname, resolve, basename } from "path";
|
|
2
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, existsSync as existsSync2 } from "fs";
|
|
3
|
+
import { join as join2, dirname as dirname2, resolve as resolve2, basename } from "path";
|
|
4
4
|
import { parseSource } from "@patterkit/core";
|
|
5
5
|
import { isCaseOnlyPropertyName } from "@patterkit/model";
|
|
6
|
+
|
|
7
|
+
// src/game-scopes.ts
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
9
|
+
import { dirname, join, relative, resolve } from "path";
|
|
10
|
+
import {
|
|
11
|
+
findGameScopes,
|
|
12
|
+
mergeScopes,
|
|
13
|
+
parseScopesFile,
|
|
14
|
+
scopesCatalogue,
|
|
15
|
+
serialiseScopesFile,
|
|
16
|
+
standInRegistry,
|
|
17
|
+
GAME_SCOPES_DIR,
|
|
18
|
+
GAME_SCOPES_FILE,
|
|
19
|
+
SCOPES_FILE_SUFFIX
|
|
20
|
+
} from "@wildwinter/scoperegistry/scopes";
|
|
21
|
+
import { canonicalStringify } from "@patterkit/core";
|
|
22
|
+
import { PATTER_SCOPE, EXTERNAL_SCOPES } from "@patterkit/compiler";
|
|
23
|
+
var PATTER_SCOPES_FILE = `${PATTER_SCOPE}${SCOPES_FILE_SUFFIX}`;
|
|
24
|
+
var PATTER_SCOPES_OWNER = "Patter";
|
|
25
|
+
var GAME_SCOPES_OWNER = "Game";
|
|
26
|
+
var nodeFs = {
|
|
27
|
+
exists: (p) => existsSync(p),
|
|
28
|
+
parent: (p) => dirname(p),
|
|
29
|
+
join: (dir, name) => resolve(dir, name)
|
|
30
|
+
};
|
|
31
|
+
function readGameScopes(dir) {
|
|
32
|
+
const files = [];
|
|
33
|
+
const issues = [];
|
|
34
|
+
let patterText;
|
|
35
|
+
let names = [];
|
|
36
|
+
try {
|
|
37
|
+
names = readdirSync(dir).filter((n) => n.endsWith(SCOPES_FILE_SUFFIX)).sort();
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
const path = join(dir, name);
|
|
42
|
+
let text;
|
|
43
|
+
try {
|
|
44
|
+
text = readFileSync(path, "utf8");
|
|
45
|
+
} catch (e) {
|
|
46
|
+
issues.push({ severity: "error", file: path, message: `cannot read it: ${e instanceof Error ? e.message : String(e)}` });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (name === PATTER_SCOPES_FILE) patterText = text;
|
|
50
|
+
const parsed = parseScopesFile(text, name);
|
|
51
|
+
for (const i of parsed.issues) issues.push({ ...i, file: path });
|
|
52
|
+
if (parsed.file) files.push({ fileName: name, file: parsed.file });
|
|
53
|
+
}
|
|
54
|
+
const merged = mergeScopes(files);
|
|
55
|
+
for (const i of merged.issues) issues.push({ ...i, file: join(dir, i.file) });
|
|
56
|
+
return { dir, files, merged, issues, ...patterText !== void 0 ? { patterText } : {} };
|
|
57
|
+
}
|
|
58
|
+
function discoverGameScopes(root, project) {
|
|
59
|
+
const found = findGameScopes(root, nodeFs, project.gameScopes !== void 0 ? { override: project.gameScopes } : {});
|
|
60
|
+
if (found.issue) return { missing: found.issue };
|
|
61
|
+
return found.dir ? { gameScopes: readGameScopes(found.dir) } : {};
|
|
62
|
+
}
|
|
63
|
+
function gameScopesSnapshot(root, project) {
|
|
64
|
+
const found = findGameScopes(root, nodeFs, project.gameScopes !== void 0 ? { override: project.gameScopes } : {});
|
|
65
|
+
if (!found.dir) return [];
|
|
66
|
+
let names = [];
|
|
67
|
+
try {
|
|
68
|
+
names = readdirSync(found.dir).filter((n) => n.endsWith(SCOPES_FILE_SUFFIX)).sort();
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
return names.map((fileName) => ({ fileName, text: readFileSync(join(found.dir, fileName), "utf8") }));
|
|
72
|
+
}
|
|
73
|
+
function patterScopesFile(project) {
|
|
74
|
+
const declarations = (project.properties ?? []).filter((p) => p.shared ?? true).map((p) => ({
|
|
75
|
+
name: p.name,
|
|
76
|
+
type: p.type,
|
|
77
|
+
...p.values !== void 0 ? { values: p.values } : {},
|
|
78
|
+
...p.stages !== void 0 ? { stages: p.stages } : {},
|
|
79
|
+
...p.default !== void 0 ? { default: p.default } : {},
|
|
80
|
+
...p.purpose !== void 0 ? { purpose: p.purpose } : {}
|
|
81
|
+
}));
|
|
82
|
+
return { version: 1, owner: PATTER_SCOPES_OWNER, scopes: [{ token: PATTER_SCOPE, declarations }] };
|
|
83
|
+
}
|
|
84
|
+
function patterScopesWrite(project, gameScopes) {
|
|
85
|
+
if (!gameScopes || !existsSync(gameScopes.dir)) return void 0;
|
|
86
|
+
const content = serialiseScopesFile(patterScopesFile(project));
|
|
87
|
+
if (content === gameScopes.patterText) return void 0;
|
|
88
|
+
return { path: join(gameScopes.dir, PATTER_SCOPES_FILE), content };
|
|
89
|
+
}
|
|
90
|
+
function patterScopesStale(project, gameScopes) {
|
|
91
|
+
if (!gameScopes) return void 0;
|
|
92
|
+
if (serialiseScopesFile(patterScopesFile(project)) === gameScopes.patterText) return void 0;
|
|
93
|
+
return {
|
|
94
|
+
file: join(gameScopes.dir, PATTER_SCOPES_FILE),
|
|
95
|
+
message: `${GAME_SCOPES_DIR}/${PATTER_SCOPES_FILE} is ${gameScopes.patterText === void 0 ? "missing" : "out of date"}: save the project in Patterpad or run export`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function previewRegistry(gameScopes, bundle, into) {
|
|
99
|
+
if (!gameScopes || !bundle.externalScopes?.length) return void 0;
|
|
100
|
+
const registry = into ?? standInRegistry(gameScopes.merged, { except: [PATTER_SCOPE] });
|
|
101
|
+
if (into) {
|
|
102
|
+
for (const s of gameScopes.merged.spec.scopes) {
|
|
103
|
+
if (s.token === PATTER_SCOPE || registry.has(s.token)) continue;
|
|
104
|
+
registry.defineOwned(s.token, ownedDecls(s), { owner: gameScopes.merged.owners.get(s.token)?.owner ?? GAME_SCOPES_OWNER });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const spec of bundle.scopeRegistry?.scopes ?? []) {
|
|
108
|
+
if (!registry.has(spec.token)) registry.defineOwned(spec.token, ownedDecls(spec), { owner: PATTER_SCOPES_OWNER });
|
|
109
|
+
}
|
|
110
|
+
return registry;
|
|
111
|
+
}
|
|
112
|
+
function ownedDecls(s) {
|
|
113
|
+
return (s.declarations ?? []).map((d) => {
|
|
114
|
+
const writable = d.writable ?? s.writable;
|
|
115
|
+
return {
|
|
116
|
+
name: d.name,
|
|
117
|
+
type: d.type,
|
|
118
|
+
values: d.values,
|
|
119
|
+
stages: d.stages,
|
|
120
|
+
default: d.default,
|
|
121
|
+
...writable !== void 0 ? { writable } : {}
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function drivableScopes(loaded, bundle) {
|
|
126
|
+
const own = bundle.scopeRegistry?.scopes ?? [];
|
|
127
|
+
if (!loaded.gameScopes || !bundle.externalScopes?.length) return own;
|
|
128
|
+
const have = new Set(own.map((s) => s.token));
|
|
129
|
+
return [...own, ...loaded.gameScopes.merged.spec.scopes.filter((s) => s.token !== PATTER_SCOPE && !have.has(s.token))];
|
|
130
|
+
}
|
|
131
|
+
var hostScopeTokens = (loaded, bundle) => new Set(drivableScopes(loaded, bundle).map((s) => s.token));
|
|
132
|
+
var gameScopesCatalogue = (gameScopes) => gameScopes ? scopesCatalogue(gameScopes.merged, { except: [PATTER_SCOPE] }) : [];
|
|
133
|
+
var gameScopeTokens = (gameScopes) => (gameScopes?.merged.spec.scopes ?? []).map((s) => s.token).filter((t) => t !== PATTER_SCOPE);
|
|
134
|
+
var asHostScope = (s) => ({
|
|
135
|
+
token: s.token,
|
|
136
|
+
...s.writable !== void 0 ? { writable: s.writable } : {},
|
|
137
|
+
...s.declarations ? { declarations: s.declarations.map((d) => ({ ...d })) } : {}
|
|
138
|
+
});
|
|
139
|
+
function isGameToken(token, merged) {
|
|
140
|
+
if (token === PATTER_SCOPE || EXTERNAL_SCOPES.includes(token)) return false;
|
|
141
|
+
const who = merged?.owners.get(token);
|
|
142
|
+
return !who || who.fileName === GAME_SCOPES_FILE;
|
|
143
|
+
}
|
|
144
|
+
function worldSettingsScopes(project, gameScopes) {
|
|
145
|
+
const own = project.scopeRegistry;
|
|
146
|
+
if (!gameScopes) return own;
|
|
147
|
+
const merged = gameScopes.merged;
|
|
148
|
+
const fromGame = (token) => merged.owners.get(token)?.fileName === GAME_SCOPES_FILE ? merged.spec.scopes.find((s) => s.token === token) : void 0;
|
|
149
|
+
const scopes = (own?.scopes ?? []).map((h) => {
|
|
150
|
+
const g = fromGame(h.token);
|
|
151
|
+
return g ? asHostScope(g) : h;
|
|
152
|
+
});
|
|
153
|
+
const world = fromGame("world");
|
|
154
|
+
if (world && !scopes.some((s) => s.token === "world")) scopes.push(asHostScope(world));
|
|
155
|
+
return scopes.length ? { version: own?.version ?? 1, scopes } : void 0;
|
|
156
|
+
}
|
|
157
|
+
function planWorldSave(project, gameScopes, next) {
|
|
158
|
+
const path = join(gameScopes.dir, GAME_SCOPES_FILE);
|
|
159
|
+
let current;
|
|
160
|
+
let text;
|
|
161
|
+
if (existsSync(path)) {
|
|
162
|
+
text = readFileSync(path, "utf8");
|
|
163
|
+
const parsed = parseScopesFile(text, GAME_SCOPES_FILE);
|
|
164
|
+
if (!parsed.file) return { error: `${GAME_SCOPES_DIR}/${GAME_SCOPES_FILE} won't parse, so it was left alone: ${parsed.issues.map((i) => i.message).join("; ")}` };
|
|
165
|
+
current = parsed.file;
|
|
166
|
+
}
|
|
167
|
+
const merged = readGameScopes(gameScopes.dir).merged;
|
|
168
|
+
const ui = next?.scopes ?? [];
|
|
169
|
+
const scopes = [...current?.scopes ?? []];
|
|
170
|
+
for (const h of ui) {
|
|
171
|
+
if (!isGameToken(h.token, merged)) continue;
|
|
172
|
+
const at = scopes.findIndex((s) => s.token === h.token);
|
|
173
|
+
const spec = {
|
|
174
|
+
token: h.token,
|
|
175
|
+
...h.writable !== void 0 ? { writable: h.writable } : {},
|
|
176
|
+
...h.declarations ? { declarations: h.declarations.map((d) => ({ ...d })) } : {}
|
|
177
|
+
};
|
|
178
|
+
if (at >= 0) scopes[at] = spec;
|
|
179
|
+
else scopes.push(spec);
|
|
180
|
+
}
|
|
181
|
+
const shown2 = /* @__PURE__ */ new Set(["world", ...(project.scopeRegistry?.scopes ?? []).map((s) => s.token)]);
|
|
182
|
+
const kept = scopes.filter((s) => !(shown2.has(s.token) && isGameToken(s.token, merged) && !ui.some((h) => h.token === s.token)));
|
|
183
|
+
const file = { version: current?.version ?? 1, owner: current?.owner ?? GAME_SCOPES_OWNER, scopes: kept };
|
|
184
|
+
const content = serialiseScopesFile(file);
|
|
185
|
+
const write = content === text || !current && kept.length === 0 ? void 0 : { path, content };
|
|
186
|
+
return { ...write ? { write } : {}, ...next && next.scopes.length ? { scopeRegistry: next } : {} };
|
|
187
|
+
}
|
|
188
|
+
function planReturnedWorld(ours, base, theirs, merged, gameScopes) {
|
|
189
|
+
const now = readGameScopes(gameScopes.dir);
|
|
190
|
+
const game = (p) => new Map((p.scopeRegistry?.scopes ?? []).filter((s) => isGameToken(s.token, now.merged)).map((s) => [s.token, canonicalStringify(s)]));
|
|
191
|
+
const was = game(base), came = game(theirs);
|
|
192
|
+
const changed = new Set([...was.keys(), ...came.keys()].filter((t) => was.get(t) !== came.get(t)));
|
|
193
|
+
if (!changed.size) return {};
|
|
194
|
+
const file = now.files.find((f) => f.fileName === GAME_SCOPES_FILE)?.file;
|
|
195
|
+
const next = { version: 1, scopes: [
|
|
196
|
+
...(file?.scopes ?? []).filter((s) => !changed.has(s.token)).map(asHostScope),
|
|
197
|
+
...(merged.scopeRegistry?.scopes ?? []).filter((s) => changed.has(s.token))
|
|
198
|
+
] };
|
|
199
|
+
const shown2 = { ...ours, scopeRegistry: { version: 1, scopes: [...ours.scopeRegistry?.scopes ?? [], ...base.scopeRegistry?.scopes ?? []] } };
|
|
200
|
+
const plan = planWorldSave(shown2, gameScopes, next);
|
|
201
|
+
return { ...plan.write ? { write: plan.write } : {}, ...plan.error ? { error: plan.error } : {} };
|
|
202
|
+
}
|
|
203
|
+
function defaultGameScopesDir(root) {
|
|
204
|
+
for (let dir = resolve(root); ; ) {
|
|
205
|
+
if (existsSync(join(dir, ".git"))) return join(dir, GAME_SCOPES_DIR);
|
|
206
|
+
const up = dirname(dir);
|
|
207
|
+
if (up === dir) break;
|
|
208
|
+
dir = up;
|
|
209
|
+
}
|
|
210
|
+
return join(dirname(resolve(root)), GAME_SCOPES_DIR);
|
|
211
|
+
}
|
|
212
|
+
function planShareScopes(root, project, dir) {
|
|
213
|
+
const target = resolve(dir);
|
|
214
|
+
const gamePath = join(target, GAME_SCOPES_FILE);
|
|
215
|
+
let existing = { version: 1, owner: GAME_SCOPES_OWNER, scopes: [] };
|
|
216
|
+
if (existsSync(gamePath)) {
|
|
217
|
+
const parsed = parseScopesFile(readFileSync(gamePath, "utf8"), GAME_SCOPES_FILE);
|
|
218
|
+
if (!parsed.file) return { error: `${GAME_SCOPES_FILE} in ${target} won't parse, so it was left alone: ${parsed.issues.map((i) => i.message).join("; ")}` };
|
|
219
|
+
existing = parsed.file;
|
|
220
|
+
}
|
|
221
|
+
const held = new Set(existing.scopes.map((s) => s.token));
|
|
222
|
+
const added = (project.scopeRegistry?.scopes ?? []).filter((h) => isGameToken(h.token, void 0) && !held.has(h.token)).map((h) => ({
|
|
223
|
+
token: h.token,
|
|
224
|
+
...h.writable !== void 0 ? { writable: h.writable } : {},
|
|
225
|
+
...h.declarations ? { declarations: h.declarations.map((d) => ({ ...d })) } : {}
|
|
226
|
+
}));
|
|
227
|
+
const game = { ...existing, scopes: [...existing.scopes, ...added] };
|
|
228
|
+
const writes = [
|
|
229
|
+
{ path: join(target, PATTER_SCOPES_FILE), content: serialiseScopesFile(patterScopesFile(project)) },
|
|
230
|
+
{ path: gamePath, content: serialiseScopesFile(game) }
|
|
231
|
+
];
|
|
232
|
+
let found = false;
|
|
233
|
+
for (let at = resolve(root); ; ) {
|
|
234
|
+
if (join(at, GAME_SCOPES_DIR) === target) {
|
|
235
|
+
found = true;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
const up = dirname(at);
|
|
239
|
+
if (existsSync(join(at, ".git")) || up === at) break;
|
|
240
|
+
at = up;
|
|
241
|
+
}
|
|
242
|
+
const { gameScopes: _drop, ...rest } = project;
|
|
243
|
+
const next = found ? rest : { ...rest, gameScopes: relative(resolve(root), target) || "." };
|
|
244
|
+
return { writes, project: next };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/load.ts
|
|
248
|
+
function withGameScopes(root, project) {
|
|
249
|
+
const { gameScopes, missing } = discoverGameScopes(root, project);
|
|
250
|
+
return { ...gameScopes ? { gameScopes } : {}, ...missing ? { gameScopesMissing: missing } : {} };
|
|
251
|
+
}
|
|
6
252
|
function readDirSafe(dir) {
|
|
7
253
|
try {
|
|
8
|
-
return
|
|
254
|
+
return readdirSync2(dir, { withFileTypes: true });
|
|
9
255
|
} catch {
|
|
10
256
|
return [];
|
|
11
257
|
}
|
|
@@ -14,26 +260,26 @@ function walkFiles(dir, ext) {
|
|
|
14
260
|
const out = [];
|
|
15
261
|
for (const e of readDirSafe(dir)) {
|
|
16
262
|
if (e.name.startsWith(".")) continue;
|
|
17
|
-
const p =
|
|
263
|
+
const p = join2(dir, e.name);
|
|
18
264
|
if (e.isDirectory()) out.push(...walkFiles(p, ext));
|
|
19
265
|
else if (e.isFile() && e.name.endsWith(ext)) out.push(p);
|
|
20
266
|
}
|
|
21
267
|
return out.sort();
|
|
22
268
|
}
|
|
23
269
|
function findProjectFile(startPath) {
|
|
24
|
-
let dir =
|
|
270
|
+
let dir = resolve2(startPath);
|
|
25
271
|
try {
|
|
26
272
|
if (statSync(dir).isFile()) {
|
|
27
273
|
if (dir.endsWith(".patterproj")) return dir;
|
|
28
|
-
dir =
|
|
274
|
+
dir = dirname2(dir);
|
|
29
275
|
}
|
|
30
276
|
} catch {
|
|
31
277
|
}
|
|
32
278
|
for (; ; ) {
|
|
33
|
-
const here = readDirSafe(dir).filter((e) => e.isFile() && e.name.endsWith(".patterproj")).map((e) =>
|
|
279
|
+
const here = readDirSafe(dir).filter((e) => e.isFile() && e.name.endsWith(".patterproj")).map((e) => join2(dir, e.name)).sort();
|
|
34
280
|
if (here.length > 1) throw new Error(`multiple project files in ${dir}: ${here.map((p) => p.slice(dir.length + 1)).join(", ")}`);
|
|
35
281
|
if (here.length === 1) return here[0];
|
|
36
|
-
const parent =
|
|
282
|
+
const parent = dirname2(dir);
|
|
37
283
|
if (parent === dir) throw new Error(`no .patterproj found at or above ${startPath}`);
|
|
38
284
|
dir = parent;
|
|
39
285
|
}
|
|
@@ -41,7 +287,7 @@ function findProjectFile(startPath) {
|
|
|
41
287
|
function parseFile(file, expectSchema, shapeKey) {
|
|
42
288
|
let parsed;
|
|
43
289
|
try {
|
|
44
|
-
parsed = parseSource(
|
|
290
|
+
parsed = parseSource(readFileSync2(file, "utf8"));
|
|
45
291
|
} catch (e) {
|
|
46
292
|
throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
47
293
|
}
|
|
@@ -97,16 +343,16 @@ function foldDeclaredNames(parsed, expectSchema) {
|
|
|
97
343
|
}
|
|
98
344
|
}
|
|
99
345
|
function sceneIdForShard(shardPath) {
|
|
100
|
-
const p =
|
|
346
|
+
const p = resolve2(shardPath);
|
|
101
347
|
try {
|
|
102
|
-
if (p.endsWith(".patterflow")) return parseSource(
|
|
103
|
-
if (p.endsWith(".patterloc")) return parseSource(
|
|
348
|
+
if (p.endsWith(".patterflow")) return parseSource(readFileSync2(p, "utf8")).scene?.id;
|
|
349
|
+
if (p.endsWith(".patterloc")) return parseSource(readFileSync2(p, "utf8")).scene;
|
|
104
350
|
if (p.endsWith(".patterx")) {
|
|
105
351
|
const projectFile = findProjectFile(p);
|
|
106
352
|
const project = parseFile(projectFile, "patter/project", "project");
|
|
107
|
-
const flowDir =
|
|
108
|
-
const flowPath =
|
|
109
|
-
return parseSource(
|
|
353
|
+
const flowDir = join2(dirname2(projectFile), project.layout?.flow ?? "scenes/");
|
|
354
|
+
const flowPath = join2(flowDir, basename(p).replace(/\.patterx$/, ".patterflow"));
|
|
355
|
+
return parseSource(readFileSync2(flowPath, "utf8")).scene?.id;
|
|
110
356
|
}
|
|
111
357
|
} catch {
|
|
112
358
|
}
|
|
@@ -114,13 +360,13 @@ function sceneIdForShard(shardPath) {
|
|
|
114
360
|
}
|
|
115
361
|
function loadProjectLanding(startPath, opts) {
|
|
116
362
|
const projectFile = findProjectFile(startPath);
|
|
117
|
-
const root =
|
|
363
|
+
const root = dirname2(projectFile);
|
|
118
364
|
const project = parseFile(projectFile, "patter/project", "project");
|
|
119
365
|
const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...project.layout };
|
|
120
|
-
const flowFiles = walkFiles(
|
|
121
|
-
const launchPath = opts?.launchPath ?
|
|
366
|
+
const flowFiles = walkFiles(join2(root, layout.flow), ".patterflow");
|
|
367
|
+
const launchPath = opts?.launchPath ? resolve2(opts.launchPath) : void 0;
|
|
122
368
|
let landingFile;
|
|
123
|
-
if (launchPath?.endsWith(".patterflow")) landingFile = flowFiles.find((f) =>
|
|
369
|
+
if (launchPath?.endsWith(".patterflow")) landingFile = flowFiles.find((f) => resolve2(f) === launchPath);
|
|
124
370
|
if (!landingFile) {
|
|
125
371
|
const targetId = (opts?.launchPath ? sceneIdForShard(opts.launchPath) : void 0) ?? opts?.preferId;
|
|
126
372
|
if (targetId) {
|
|
@@ -144,10 +390,10 @@ function loadProjectLanding(startPath, opts) {
|
|
|
144
390
|
const localeFiles = [];
|
|
145
391
|
if (landingScene && landingFile) {
|
|
146
392
|
const defaultLocale = project.locales.default;
|
|
147
|
-
const strings =
|
|
148
|
-
const guess =
|
|
393
|
+
const strings = join2(root, layout.strings);
|
|
394
|
+
const guess = join2(strings, defaultLocale, `${basename(landingFile).replace(/\.patterflow$/, "")}.patterloc`);
|
|
149
395
|
let found = false;
|
|
150
|
-
if (
|
|
396
|
+
if (existsSync2(guess)) {
|
|
151
397
|
try {
|
|
152
398
|
const loc = parseFile(guess, "patter/strings", "strings");
|
|
153
399
|
if (loc.scene === landingScene.id) {
|
|
@@ -168,7 +414,7 @@ function loadProjectLanding(startPath, opts) {
|
|
|
168
414
|
}
|
|
169
415
|
}
|
|
170
416
|
}
|
|
171
|
-
return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [] };
|
|
417
|
+
return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [], ...withGameScopes(root, project) };
|
|
172
418
|
}
|
|
173
419
|
function applySceneOrder(scenes, order) {
|
|
174
420
|
if (!order?.length) return;
|
|
@@ -184,12 +430,12 @@ function applySceneOrder(scenes, order) {
|
|
|
184
430
|
}
|
|
185
431
|
function loadProject(startPath) {
|
|
186
432
|
const projectFile = findProjectFile(startPath);
|
|
187
|
-
const root =
|
|
433
|
+
const root = dirname2(projectFile);
|
|
188
434
|
const project = parseFile(projectFile, "patter/project", "project");
|
|
189
435
|
const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...project.layout };
|
|
190
436
|
const scenes = [];
|
|
191
437
|
const sceneFiles = {};
|
|
192
|
-
for (const f of walkFiles(
|
|
438
|
+
for (const f of walkFiles(join2(root, layout.flow), ".patterflow")) {
|
|
193
439
|
const scene = parseFile(f, "patter/flow", "scene").scene;
|
|
194
440
|
const existing = sceneFiles[scene.id];
|
|
195
441
|
if (existing !== void 0) {
|
|
@@ -201,38 +447,38 @@ function loadProject(startPath) {
|
|
|
201
447
|
applySceneOrder(scenes, project.sceneOrder);
|
|
202
448
|
const locales = [];
|
|
203
449
|
const localeFiles = [];
|
|
204
|
-
for (const f of walkFiles(
|
|
450
|
+
for (const f of walkFiles(join2(root, layout.strings), ".patterloc")) {
|
|
205
451
|
locales.push(parseFile(f, "patter/strings", "strings"));
|
|
206
452
|
localeFiles.push(f);
|
|
207
453
|
}
|
|
208
454
|
const authoring = [];
|
|
209
455
|
const authoringFiles = [];
|
|
210
|
-
for (const f of walkFiles(
|
|
456
|
+
for (const f of walkFiles(join2(root, layout.authoring), ".patterx")) {
|
|
211
457
|
authoring.push(parseFile(f, "patter/authoring", "schema"));
|
|
212
458
|
authoringFiles.push(f);
|
|
213
459
|
}
|
|
214
|
-
return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring, authoringFiles };
|
|
460
|
+
return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring, authoringFiles, ...withGameScopes(root, project) };
|
|
215
461
|
}
|
|
216
462
|
|
|
217
463
|
// src/write.ts
|
|
218
464
|
import { writeFileSync, mkdirSync } from "fs";
|
|
219
|
-
import { dirname as
|
|
465
|
+
import { dirname as dirname3 } from "path";
|
|
220
466
|
function applyWrites(writes) {
|
|
221
467
|
for (const w of writes) {
|
|
222
|
-
mkdirSync(
|
|
468
|
+
mkdirSync(dirname3(w.path), { recursive: true });
|
|
223
469
|
writeFileSync(w.path, w.content, "utf8");
|
|
224
470
|
}
|
|
225
471
|
}
|
|
226
472
|
|
|
227
473
|
// src/validate.ts
|
|
228
|
-
import { readFileSync as
|
|
229
|
-
import { basename as basename2, isAbsolute, join as
|
|
474
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
475
|
+
import { basename as basename2, isAbsolute, join as join3, relative as relative2 } from "path";
|
|
230
476
|
|
|
231
477
|
// src/merge.ts
|
|
232
|
-
import { canonicalStringify } from "@patterkit/core";
|
|
478
|
+
import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
|
|
233
479
|
var UnsupportedMergeError = class extends Error {
|
|
234
480
|
};
|
|
235
|
-
var eq = (a, b) =>
|
|
481
|
+
var eq = (a, b) => canonicalStringify2(a) === canonicalStringify2(b);
|
|
236
482
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
237
483
|
var CONFLICT_SIDECAR = ".patterconflict";
|
|
238
484
|
function sidecarIssues(sidecarPaths) {
|
|
@@ -273,7 +519,7 @@ function merge3(base, ours, theirs, id, path, conflicts) {
|
|
|
273
519
|
conflicts.push({ id, path, base, ours, theirs, kind: deletedKind(base, ours, theirs) });
|
|
274
520
|
return ours;
|
|
275
521
|
}
|
|
276
|
-
function mergeMap(base, ours, theirs, prefix, conflicts,
|
|
522
|
+
function mergeMap(base, ours, theirs, prefix, conflicts, resolve6) {
|
|
277
523
|
const out = {};
|
|
278
524
|
for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
|
|
279
525
|
const b = base[k], o = ours[k], t = theirs[k];
|
|
@@ -282,7 +528,7 @@ function mergeMap(base, ours, theirs, prefix, conflicts, resolve5) {
|
|
|
282
528
|
else if (eq(b, o)) v = t;
|
|
283
529
|
else if (eq(b, t)) v = o;
|
|
284
530
|
else {
|
|
285
|
-
const r =
|
|
531
|
+
const r = resolve6?.(k, b, o, t);
|
|
286
532
|
if (r && r.resolved) v = r.value;
|
|
287
533
|
else {
|
|
288
534
|
conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
|
|
@@ -572,7 +818,7 @@ var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
|
572
818
|
|
|
573
819
|
// src/validate.ts
|
|
574
820
|
import { validateProject, parseSource as parseSource2 } from "@patterkit/core";
|
|
575
|
-
import { validateConditions, validateInterpolation, exportBundle as exportBundle2, hostScopesToSpec } from "@patterkit/compiler";
|
|
821
|
+
import { validateConditions, validateInterpolation, exportBundle as exportBundle2, hostScopesToSpec, projectScopes } from "@patterkit/compiler";
|
|
576
822
|
|
|
577
823
|
// src/reachability.ts
|
|
578
824
|
import { exportBundle } from "@patterkit/compiler";
|
|
@@ -682,8 +928,8 @@ var termsOfAst = (ast, sceneId) => terms(ast, sceneId, keyOf);
|
|
|
682
928
|
var requirementsOf = (expr, sceneId) => expr === void 0 ? [] : termsOfAst(expr.ast, sceneId);
|
|
683
929
|
var asFlags = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
684
930
|
function reachabilityIssues(loaded, compiled) {
|
|
685
|
-
const bundle = compiled ?? exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
|
|
686
|
-
const hostTokens =
|
|
931
|
+
const bundle = compiled ?? exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
|
|
932
|
+
const hostTokens = hostScopeTokens(loaded, bundle);
|
|
687
933
|
const preset = /* @__PURE__ */ new Set();
|
|
688
934
|
const notALatch = (decl, key) => {
|
|
689
935
|
if (decl.temporary) {
|
|
@@ -826,14 +1072,17 @@ function reachabilityIssues(loaded, compiled) {
|
|
|
826
1072
|
function runValidate(loaded) {
|
|
827
1073
|
const { project, scenes, locales } = loaded;
|
|
828
1074
|
const structural = validateProject({ project, scenes, authoring: loaded.authoring });
|
|
829
|
-
const
|
|
830
|
-
const
|
|
831
|
-
const
|
|
1075
|
+
const merged = loaded.gameScopes?.merged;
|
|
1076
|
+
const scopes = projectScopes(project, merged);
|
|
1077
|
+
const foreignScopes = hostScopesToSpec(scopes.host);
|
|
1078
|
+
const conditions = validateConditions({ project, scenes }, { foreignScopes, gameScopes: merged });
|
|
1079
|
+
const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes, gameScopes: merged });
|
|
1080
|
+
const gameScopes = gameScopesIssues(loaded, scopes.notes);
|
|
832
1081
|
const hygiene = checkHygiene([loaded.projectFile, ...Object.values(loaded.sceneFiles), ...loaded.localeFiles, ...loaded.authoringFiles]);
|
|
833
1082
|
const staleBundles = checkBundles(loaded);
|
|
834
1083
|
const unresolvedMerges = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
|
|
835
1084
|
const orphans = orphanShards(loaded);
|
|
836
|
-
const reachability = structural.length === 0 && conditions.
|
|
1085
|
+
const reachability = structural.length === 0 && !conditions.some(isError) ? reachabilityIssues(loaded) : [];
|
|
837
1086
|
return {
|
|
838
1087
|
structural,
|
|
839
1088
|
conditions,
|
|
@@ -843,9 +1092,22 @@ function runValidate(loaded) {
|
|
|
843
1092
|
staleBundles,
|
|
844
1093
|
unresolvedMerges,
|
|
845
1094
|
orphans,
|
|
846
|
-
|
|
1095
|
+
gameScopes,
|
|
1096
|
+
ok: structural.length === 0 && !conditions.some(isError) && !interpolation.some(isError) && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0 && !gameScopes.some(isError)
|
|
847
1097
|
};
|
|
848
1098
|
}
|
|
1099
|
+
var isError = (i) => i.severity === "error";
|
|
1100
|
+
function gameScopesIssues(loaded, notes) {
|
|
1101
|
+
const out = [];
|
|
1102
|
+
if (loaded.gameScopesMissing) out.push({ file: loaded.projectFile, severity: "error", message: loaded.gameScopesMissing });
|
|
1103
|
+
const gs = loaded.gameScopes;
|
|
1104
|
+
if (!gs) return out;
|
|
1105
|
+
for (const i of gs.issues) out.push({ file: i.file, severity: i.severity, message: i.message });
|
|
1106
|
+
for (const n of notes) out.push({ file: join3(gs.dir, n.file), severity: "warning", message: n.message });
|
|
1107
|
+
const stale = patterScopesStale(loaded.project, gs);
|
|
1108
|
+
if (stale) out.push({ ...stale, severity: "warning" });
|
|
1109
|
+
return out;
|
|
1110
|
+
}
|
|
849
1111
|
function orphanShards(loaded) {
|
|
850
1112
|
const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...loaded.project.layout };
|
|
851
1113
|
const kind = {
|
|
@@ -855,14 +1117,14 @@ function orphanShards(loaded) {
|
|
|
855
1117
|
".patterproj": { what: "project", home: null }
|
|
856
1118
|
};
|
|
857
1119
|
const inside = (file, dir) => {
|
|
858
|
-
const r =
|
|
1120
|
+
const r = relative2(dir, file);
|
|
859
1121
|
return r !== "" && !r.startsWith("..") && !isAbsolute(r);
|
|
860
1122
|
};
|
|
861
1123
|
const out = [];
|
|
862
1124
|
for (const [ext, { what, home }] of Object.entries(kind)) {
|
|
863
1125
|
for (const file of walkFiles(loaded.root, ext)) {
|
|
864
|
-
if (home ? inside(file,
|
|
865
|
-
const rel =
|
|
1126
|
+
if (home ? inside(file, join3(loaded.root, home)) : file === loaded.projectFile) continue;
|
|
1127
|
+
const rel = relative2(loaded.root, file);
|
|
866
1128
|
const message = home ? `${rel} is a ${what} file outside the project's folders: nothing loads it, so none of it is in the project. Move it under ${home}, or delete it.` : `${rel} is a second project file: only ${basename2(loaded.projectFile)} is read. Delete it, or move it out of the project.`;
|
|
867
1129
|
out.push({ file, message });
|
|
868
1130
|
}
|
|
@@ -875,14 +1137,14 @@ function checkBundles(loaded) {
|
|
|
875
1137
|
if (bundles.length === 0) return issues;
|
|
876
1138
|
let fresh;
|
|
877
1139
|
try {
|
|
878
|
-
fresh = exportBundle2({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales }).content.hash;
|
|
1140
|
+
fresh = exportBundle2({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged }).content.hash;
|
|
879
1141
|
} catch {
|
|
880
1142
|
return issues;
|
|
881
1143
|
}
|
|
882
1144
|
for (const file of bundles) {
|
|
883
1145
|
let hash;
|
|
884
1146
|
try {
|
|
885
|
-
const parsed = parseSource2(
|
|
1147
|
+
const parsed = parseSource2(readFileSync3(file, "utf8"));
|
|
886
1148
|
hash = parsed?.content?.hash;
|
|
887
1149
|
} catch {
|
|
888
1150
|
issues.push({ file, message: "compiled bundle is unparseable - run `patter export`" });
|
|
@@ -911,7 +1173,7 @@ function checkHygiene(files) {
|
|
|
911
1173
|
}
|
|
912
1174
|
const fileIssues = [];
|
|
913
1175
|
try {
|
|
914
|
-
const text =
|
|
1176
|
+
const text = readFileSync3(file, "utf8");
|
|
915
1177
|
if (text.charCodeAt(0) === 65279) {
|
|
916
1178
|
fileIssues.push({ file, message: "file starts with a UTF-8 BOM (canonical form is UTF-8 without BOM - run `patter format`)" });
|
|
917
1179
|
}
|
|
@@ -928,7 +1190,7 @@ function checkHygiene(files) {
|
|
|
928
1190
|
}
|
|
929
1191
|
|
|
930
1192
|
// src/export.ts
|
|
931
|
-
import { basename as basename3, isAbsolute as isAbsolute2, join as
|
|
1193
|
+
import { basename as basename3, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
932
1194
|
import { exportBundle as exportBundle3 } from "@patterkit/compiler";
|
|
933
1195
|
import { isContentlessBeat } from "@patterkit/model";
|
|
934
1196
|
function compileScenes(loaded) {
|
|
@@ -961,7 +1223,7 @@ function refuseUnresolvedMerge(loaded) {
|
|
|
961
1223
|
function runExport(loaded) {
|
|
962
1224
|
refuseUnresolvedMerge(loaded);
|
|
963
1225
|
const { project, locales } = loaded;
|
|
964
|
-
const full = exportBundle3({ project, scenes: compileScenes(loaded), locales });
|
|
1226
|
+
const full = exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
|
|
965
1227
|
const loc = project.export?.localisation;
|
|
966
1228
|
if (!loc || loc.mode === "embedded") return full;
|
|
967
1229
|
const strings = loc.sourceDebug ? { [full.locales.default]: full.strings[full.locales.default] ?? {} } : {};
|
|
@@ -970,17 +1232,17 @@ function runExport(loaded) {
|
|
|
970
1232
|
function runExportFull(loaded) {
|
|
971
1233
|
refuseUnresolvedMerge(loaded);
|
|
972
1234
|
const { project, locales } = loaded;
|
|
973
|
-
return exportBundle3({ project, scenes: compileScenes(loaded), locales });
|
|
1235
|
+
return exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
|
|
974
1236
|
}
|
|
975
1237
|
function bundleOutputPath(loaded) {
|
|
976
1238
|
const override = loaded.project.export?.bundle;
|
|
977
|
-
if (override) return isAbsolute2(override) ? override :
|
|
1239
|
+
if (override) return isAbsolute2(override) ? override : join4(loaded.root, override);
|
|
978
1240
|
const stem = basename3(loaded.projectFile).replace(/\.patterproj$/, "");
|
|
979
|
-
return
|
|
1241
|
+
return join4(loaded.root, "dist", `${stem}.patterc`);
|
|
980
1242
|
}
|
|
981
1243
|
|
|
982
1244
|
// src/export-html.ts
|
|
983
|
-
import { canonicalStringify as
|
|
1245
|
+
import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
|
|
984
1246
|
|
|
985
1247
|
// src/playable-runtime.ts
|
|
986
1248
|
var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var q=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var Be=Object.getOwnPropertyNames;var Pe=Object.prototype.hasOwnProperty;var Ne=(n,e)=>{for(var t in e)q(n,t,{get:e[t],enumerable:!0})},Fe=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Be(e))!Pe.call(n,s)&&s!==t&&q(n,s,{get:()=>e[s],enumerable:!(r=Oe(e,s))||r.enumerable});return n};var Te=n=>Fe(q({},"__esModule",{value:!0}),n);var Qe={};Ne(Qe,{Engine:()=>$,Flow:()=>D,buildTagIndex:()=>M,describeBundle:()=>ke,effectiveGameData:()=>Ie,gameDataFields:()=>Ce,gameDataValue:()=>Q});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 p=class extends Error{constructor(e){super(e),this.name="EvalError"}};function F(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=e.scopes[i.scope];if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):o[i.name];if(a===void 0){if(r.get(i.scope)==="throw")throw new p(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!t.functions[i.name]){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new p(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=_(a,e);if(c===void 0)throw new p("advance() needs a quality reference (@scope.name of a quality property)");let l=w(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=t.functions[i.name];if(!o)throw new p(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new p("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 p(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new p(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let u=s(i.left);if(typeof u!="boolean")throw new p(`\'and\' requires boolean operands, left is ${typeof u}`);if(!u)return!1;let S=s(i.right);if(typeof S!="boolean")throw new p(`\'and\' requires boolean operands, right is ${typeof S}`);return S}if(i.op==="or"){let u=s(i.left);if(typeof u!="boolean")throw new p(`\'or\' requires boolean operands, left is ${typeof u}`);if(u)return!0;let S=s(i.right);if(typeof S!="boolean")throw new p(`\'or\' requires boolean operands, right is ${typeof S}`);return S}let o=s(i.left),a=s(i.right),c=_(i.left,e),l=_(i.right,e),d=c??l;if(d!==void 0){if(c&&l&&!Ae(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new p(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return w(o,d,">")>w(a,d,">");case">=":return w(o,d,">=")>=w(a,d,">=");case"<":return w(o,d,"<")<w(a,d,"<");case"<=":return w(o,d,"<=")<=w(a,d,"<=");case"+":case"-":case"*":case"/":throw new p(`\'${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 se(o,a);case"!=":return!se(o,a);case">":return k(o,a,">"),o>a;case">=":return k(o,a,">="),o>=a;case"<":return k(o,a,"<"),o<a;case"<=":return k(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new p(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return k(o,a,"-"),o-a;case"*":return k(o,a,"*"),o*a;case"/":if(k(o,a,"/"),a===0)throw new p("division by zero");return o/a}}}};return s(n)}function se(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 k(n,e,t){if(typeof n!="number"||typeof e!="number")throw new p(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function _(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function w(n,e,t){if(typeof n!="string")throw new p(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new p(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var Ae=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);function R(n){if(Number.isNaN(n)||!Number.isFinite(n))return 0;let e=Math.trunc(n)%4294967296;return e<0?e+4294967296:e}function H(n){let e=R(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 Ge={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},Ve=[Ge];function ie(n,e,t){let r=t?.countingCalls??Ve;return T(n,t?.want??!0,e,r)}function T(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=T(n.left,e,t,r),i=T(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 T(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 b=class n{values={};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??O(t))}}get(e){return this.values[this.norm(e)]}normalise(e){return this.norm(e)}set(e,t,r){let s=this.norm(e);if(!r?.host&&this.decls.get(s)?.writable===!1)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])=>ae(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){for(let[t,r]of Object.entries(e))this.values[this.norm(t)]=r}};function ae(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??O(n),...n.values!==void 0?{values:n.values}:{},...n.stages!==void 0?{stages:n.stages}:{},writable:t??n.writable??!0}}var Le=n=>n.toLowerCase(),U=1,A=class{scopes=new Map;parked=new Map;rev=0;get revision(){return this.rev}defineOwned(e,t,r){let s=typeof r=="string"?{pathPrefix:r}:r??{},i=new b(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&&(t.load(s),this.parked.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.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&this.parked.delete(t);return this}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??Le,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"){try{i.bag.set(t,r,s?.host?{host:!0}:void 0)}catch{throw new Error(`\'@${e}.${t}\' is read-only`)}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,...ae(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(Me(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 oe(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=oe(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();for(let[r,s]of Object.entries(e)){let i=this.scopes.get(r);i?.kind==="owned"?i.bag.load(s):i||this.parked.set(r,structuredClone(s))}}saveFragment(){return{version:U,scopes:this.save()}}loadFragment(e){if(e.version!==U)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${U})`);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 oe(n){return n.kind==="foreign"?[...n.decls.entries()]:n.bag.declarations().map(e=>[n.bag.normalise(e.name),e])}function Me(n){return n.kind==="foreign"?n.norm:e=>n.bag.normalise(e)}function O(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"enum":return n.values?.[0]??"";case"flags":return[];case"quality":return n.stages?.[0]??"";default:return!1}}var ce=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function B(n){return n.ctx.host??{}}var W={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new p("random(a, b) requires exactly 2 arguments");let t=B(e).nextRandom;if(!t)throw new p("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 p("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new p("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=le(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("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=[...le(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("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:V("visits"),eval:(n,e)=>B(e).visits?.(G(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:V("seen"),eval:(n,e)=>(B(e).visits?.(G(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:V("patter_visits"),eval:(n,e)=>B(e).patterVisits?.(G(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:V("patter_seen"),eval:(n,e)=>(B(e).patterVisits?.(G(n,e,"patter_seen"))??0)>0}}},ut=ce.map(n=>n.token).filter(n=>n!=="patter"&&n!=="scene");function de(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 G(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new p(`${t}(id) requires a string node id`);return r}function V(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 le(n,e,t){if(!n)throw new p(`${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 p(`${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 $e=/^@[A-Za-z0-9_.]+$/;function*je(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:$e.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 qe(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function _e(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function He(n){let e="",t=!1;for(let r of n){if(_e(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function ge(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?He(r):n}function ue(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of je(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?"":qe(s)}return t}function C(n,e){for(let t of n){e(t);let r=t.children;r&&C(r,e)}}function Ue(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function m(n){let e=n.gameId?.trim();return e||Ue(n.name)}function K(n){return`cast:${n}`}var J={open:"[",close:"]"},he="SFX";var z=3;function L(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function M(n){let e=new Map,t=(r,s)=>{let i=L([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,L([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=L(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=L([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var fe=new WeakMap,I="Patter",P=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),me=Symbol("ownsRegistry"),v={stage:n=>`patter/scene/${P(n)}`,flow:n=>`patter/flow/${P(n)}/`,flowGlobals:n=>`patter/flow/${P(n)}/patter`,flowScene:(n,e)=>`patter/flow/${P(n)}/scene/${P(e)}`},$=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];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 g of e.cast??[])g.displayName&&l.set(g.name,g.displayName);this.defaultSeed=(t.seed??2654435769)>>>0;let d=new Map,u=new Map,S=new Map;for(let[g,h]of Object.entries(e.scenes)){this.sceneGameIdToId.set(m(h),g);let y=new Map;for(let f of h.blocks)u.set(f.id,{sceneId:g}),S.set(f.id,f),y.set(m(f),f.id),C(f.children,ne=>d.set(ne.id,ne));this.blockGameIdToId.set(g,y)}let X=e.properties??[],j=X.filter(g=>g.shared??!0).map(ye),De=X.filter(g=>!(g.shared??!0)).map(ye),Re=new Set(j.map(g=>g.name.toLowerCase())),x=t.registry??new A,Z=!t.registry||t[me]===!0,ee=new b(j,{pathPrefix:"@patter."}),te=[],N=[];try{x.mountOwned("patter",ee,{owner:I}),N.push("patter");let g=new Set;if(t.world){let h=e.scopeRegistry?.scopes.find(f=>f.token==="world"),y=(h?.declarations??[]).map(xe);x.defineForeign("world",t.world,y,{writable:h?.writable??!0,owner:I}),N.push("world"),g.add("world")}for(let h of Z?e.scopeRegistry?.scopes??[]:[]){if(g.has(h.token)||x.has(h.token))continue;let y=(h.declarations??[]).map(f=>We(f,h.writable));x.defineOwned(h.token,y,{owner:I}),N.push(h.token),te.push(h.token)}}catch(g){for(let h of N)x.remove(h,{keep:!0});throw g}let re=new Map;for(let[g,h]of Object.entries(e.scenes)){let y=new Set((h.sceneProps??[]).filter(f=>f.shared??!1).map(f=>f.name.toLowerCase()));re.set(g,y)}this.host={logEnabled:t.log??!1,emitEngine:(g,h,y)=>this.emitEngine(g,h,y),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,nodeIndex:d,blockIndex:u,blockById:S,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:M(e),registry:x,ownsRegistry:Z,patterBag:ee,hostScopes:te,patterSharedDecls:j,patterLocalDecls:De,patterSharedNames:Re,sceneSharedNames:re,sharedVisits:new Map,sharedSelectors:new Map,stageBags:new Map,customRng:t.rng,onDryChoice:t.onDryChoice,replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??J).open,captionClose:(e.closedCaptions??J).close,captionCharacter:e.closedCaptions?.character||he,refSplitCache:new Map,refSplitRevision:x.revision}}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}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){let t=this.saveGame(),r=o=>(o.setLocale(this.currentLocale),o.setClosedCaptions(this.host.captionsOn),o),s=()=>new n(e,{...this.creationOptions,registry:this.host.registry,[me]:this.host.ownsRegistry});this.release(!0);let i=s();try{return i.loadGame(t),r(i)}catch{i.release(!0);let o=s();for(let[a,c]of Object.entries(t.flows)){let l=c.cursor.currentSceneId;try{o.openFlow(a,l!==null?{scene:l}:{})}catch{}}return r(o)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let r=this.resolveSceneRef(t.scene),s=this.resolveBlockRef(r,t.block);this.flowsById.get(e)?.close();let i=new D(e,this.host,t.seed??this.defaultSeed);return this.flowsById.set(e,i),i.start(r,s),i}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`)}resolveSceneRef(e){if(e!=null)return this.host.bundle.scenes[e]?e:this.sceneGameIdToId.get(e)??e}resolveBlockRef(e,t){if(t!=null){if(this.host.blockById.has(t))return t;if(e!=null){let r=this.blockGameIdToId.get(e)?.get(t);if(r)return r}return t}}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?m(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?m(t):void 0}tagsForBeat(e){return this.host.tagIndex.get(e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?this.host.tagIndex.get(t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?this.host.tagIndex.get(s):void 0)??[]}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)Se(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 Se(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...m(e)?{gameId:m(e)}:{},name:e.name,...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...m(t)?{gameId:m(t)}:{},name:t.name,...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)C(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=this.host.tagIndex.get(e.id),r={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){r.character=e.character;let s=this.host.defaultStrings[K(e.character)]??this.host.castDisplay.get(e.character);s!==void 0&&(r.characterName=s)}e.direction!==void 0&&(r.direction=e.direction)}if(e.kind==="line"||e.kind==="text"){let s=this.host.defaultStrings[e.id];s!==void 0&&(r.text=s)}return e.gameData&&Object.keys(e.gameData).length&&(r.gameData=e.gameData),t&&t.length&&(r.tags=t),r}tagsField(e){let t=this.host.tagIndex.get(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}closeFlow(e){this.flowsById.get(e)?.close(),this.flowsById.delete(e)}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(v.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(v.stage(r),{keep:e});this.host.stageBags.clear();for(let r of["patter",...this.host.hostScopes])t.has(r)&&t.remove(r,{keep:e});this.creationOptions.world&&t.has("world")&&t.remove("world")}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);this.host.registry.set(r,s,t,{host:!0})}log(){return this.engineLog}clearLog(){this.engineLog.length=0}onTrace(e){return this.engineTraceHandlers.add(e),()=>this.engineTraceHandlers.delete(e)}emitEngine(e,t,r){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineLog.length,...r?{scene:r}:{}})}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:O(e),writable:e.writable??!0}))}splitShared(e){let t=Ee(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:z,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:we(this.host.sharedSelectors),flows:e}}loadGame(e){let t=e.version;if(t!==2&&t!==z)throw new Error(`unsupported save version: ${String(t)}`);this.assertExternalScopes();let r=this.host.registry;for(let[i,o]of this.flowsById)o.releaseBags(i in e.flows),o.close();this.flowsById.clear();for(let i of this.host.stageBags.keys())r.remove(v.stage(i),{keep:!0});this.host.stageBags.clear();let s=e.version===2?Ke(e):e.registry;s&&(this.host.ownsRegistry?r.load(s):r.load(s,{keepParked:!0})),this.host.sharedVisits.clear();for(let[i,o]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(i,o);this.host.sharedSelectors.clear();for(let[i,o]of be(e.sharedSelectors))this.host.sharedSelectors.set(i,o);for(let[i,o]of Object.entries(e.flows)){let a=new D(i,this.host,this.defaultSeed);a.restore(o),this.flowsById.set(i,a)}}},D=class{id;host;local;registered=new Set;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPromptBeat=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=R(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(),s=i=>i?.find(o=>o.name.toLowerCase()===r&&o.type==="quality")?.stages;if(e==="patter")return s(this.host.patterSharedDecls)??s(this.host.patterLocalDecls);if(e==="scene"){let i=this.currentSceneId!=null?this.host.bundle.scenes[this.currentSceneId]:void 0;return s(i?.sceneProps)}return this.registryQualities?.(e,t)??s(this.host.bundle.scopeRegistry?.scopes.find(i=>i.token===e)?.declarations)}start(e,t){if(this.releaseBags(!1),this.host.registry.discardParked(v.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.pendingChoice=null,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.start(e,t)}goto(e,t){if(this.closed)return!1;if(e==="END")return this.started=!0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=this.host.sceneGameIdToId.get(e)??(this.host.bundle.scenes[e]?e:void 0);if(r===void 0)return!1;let s;return t!==void 0&&(s=this.host.blockGameIdToId.get(r)?.get(t)??(this.host.blockIndex.get(t)?.sceneId===r?t:void 0),s===void 0)?!1:this.started?(this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??r,"jump"),this.settle(),!0):(this.start(r,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null}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.pendingPromptBeat){let e=this.pendingPromptBeat;return this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.beatResult(e)}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);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.logEnabled&&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}:{}})}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(i=>i.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);let s=t.byId.get(e);this.emit({type:"chose",group:t.groupId,option:e}),this.pendingChoice=null,this.pendingPromptBeat=this.host.replayPromptOnChoose?this.promptBeatOf(s)??null:null,this.pendingPromptOwnerId=this.pendingPromptBeat?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)}writeProperty(e,t,r){let{scope:s,name:i}=this.splitRef(e);if(s==="patter")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`);this.sceneResolver.set(i,t)}else this.host.registry.set(s,i,t,r?{host:!0}:void 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,selectors:we(this.selectors)}}}restore(e){this.rngState=R(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;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.releaseBags(!1),this.mountLocal();for(let r of new Set([t.currentSceneId,...this.stack.map(s=>s.sceneId)]))r!==null&&this.host.bundle.scenes[r]&&this.ensureSceneBags(r);if(this.activeSnippet=null,t.activeSnippetId!==null){let r=this.host.nodeIndex.get(t.activeSnippetId);r&&r.type==="snippet"&&(this.activeSnippet=r)}if(this.selectors=be(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);o&&(r.set(i.id,o),s.push({...i}))}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:r})}if(this.pendingPromptBeat=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId);this.pendingPromptBeat=r?this.promptBeatOf(r)??null:null,this.pendingPromptBeat||(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)}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),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.length>0){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.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.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=>this.eligible(c)),s=e.options?.order??"sequential",i=e.options?.exhaust??"once",o=c=>(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(u=>u.id);if(r.bag===void 0&&(r.bag=o()),r.bag.length===0){if(t==="once")return null;if(i){let u=e[s-1];return r.last=u.id,u}r.bag=o()}let a=r.bag,c=r.last!==void 0&&a.length>1?a.indexOf(r.last):-1,l=Math.floor(this.rng()*(c>=0?a.length-1:a.length));c>=0&&l>=c&&l++;let d=a[l];return a.splice(l,1),r.last=d,e.find(u=>u.id===d)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(d=>d.id));let l=new Set(r.bag);if(s=e.filter(d=>l.has(d.id)),s.length===0)return t==="stick"&&r.last!==void 0?e.find(d=>d.id===r.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=r.last!==void 0?a.findIndex(u=>u.id===r.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"&&(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):0}matchedSpec(e,t){return ie(e,s=>ve(F(s,this.context(),W)),{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=t.get(e.id);return r||(r={},t.set(e.id,r)),r}runEffects(e){for(let t of e??[]){let r=this.evalExpr(t.value),s=this.host.logEnabled?this.getProperty(t.target):void 0;this.writeProperty(t.target,r,!1),this.emit({type:"write",target:t.target,value:r,...s!==void 0?{prev:s}:{}})}}eligible(e){return e.condition?ve(this.evalExpr(e.condition)):!0}evalExpr(e){return F(this.conditionAst(e),this.context(),W)}conditionAst(e){let t=fe.get(e);return t||(t=E(e.ast),fe.set(e,t)),t}enter(e){this.visitCounts.set(e,(this.visitCounts.get(e)??0)+1),this.host.sharedVisits.set(e,(this.host.sharedVisits.get(e)??0)+1)}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=H(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=this.host.tagIndex.get(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)),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,gameData:e.gameData,...r}}}}interpolate(e){return ue(e,t=>this.getProperty(t))}stripCaptions(e){return ge(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));return t.kind==="line"?{kind:"line",text:this.captionLine(r),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction}:{kind:"text",text:r}}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 C(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=K(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}splitRef(e){return Ee(this.host,e)}newLocal(){return new b(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=v.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:I}),this.registered.add(e)}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??[];if(!this.sceneBags.has(e)){let s=new b(r.filter(o=>!t.has(o.name.toLowerCase())),{pathPrefix:"@scene."}),i=v.flowScene(this.id,e);this.host.registry.mountOwned(i,s,{owner:I}),this.registered.add(i),this.sceneBags.set(e,s)}if(!this.host.stageBags.has(e)){let s=new b(r.filter(i=>t.has(i.name.toLowerCase())),{pathPrefix:"@scene."});this.host.registry.mountOwned(v.stage(e),s,{owner:I}),this.host.stageBags.set(e,s)}}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);i&&i.set(s,O(r))}}};function Se(n,e){C(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 we(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 be(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 ye(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function xe(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function We(n,e){let t=xe(n),r=n.writable??e;return r===void 0?t:{...t,writable:r}}function Ee(n,e){n.refSplitRevision!==n.registry.revision&&(n.refSplitCache.clear(),n.refSplitRevision=n.registry.revision);let t=n.refSplitCache.get(e);return t||(t=de(e,r=>r==="scene"||n.registry.has(r)||(n.bundle.externalScopes?.includes(r)??!1)),n.refSplitCache.set(e,t)),t}function Ke(n){let e={};n.shared?.patter&&(e.patter=n.shared.patter);for(let[t,r]of Object.entries(n.stageBags??{}))e[v.stage(t)]=r;for(let[t,r]of Object.entries(n.flows??{})){r.scopes?.patter&&(e[v.flowGlobals(t)]=r.scopes.patter);for(let[s,i]of Object.entries(r.sceneBags??{}))e[v.flowScene(t,s)]=i}return e}function ve(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var Je=(n,e)=>n.shared??e;function Y(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:Je(n,e)}}function ze(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function Ye(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 ke(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=m(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:m(c),name:c.name}))});for(let c of o.blocks)Ye(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>Y(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=>Y(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(ze)}));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=>Y(o,!0)),scene:r},gameData:i,counts:e}}function Ce(n,e){return n.gameDataFields?.[e]??[]}function Q(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function Ie(n,e){let t={};for(let r of n){let s=Q(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 Te(Qe);})();\n';
|
|
@@ -1108,7 +1370,7 @@ function sourceStory(loaded) {
|
|
|
1108
1370
|
const def = full.locales?.default ?? Object.keys(full.strings)[0] ?? "en";
|
|
1109
1371
|
const bundle = { ...full, locales: { default: def, included: [def] }, strings: { [def]: full.strings[def] ?? {} } };
|
|
1110
1372
|
const title = loaded.project.project.name?.trim() || "A Patter story";
|
|
1111
|
-
const bundleJson =
|
|
1373
|
+
const bundleJson = canonicalStringify3(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
|
|
1112
1374
|
return { title, lang: def, bundleJson };
|
|
1113
1375
|
}
|
|
1114
1376
|
function page(title, lang, head, tail) {
|
|
@@ -1180,8 +1442,8 @@ ${PLAYER_JS}`,
|
|
|
1180
1442
|
}
|
|
1181
1443
|
|
|
1182
1444
|
// src/audio-scan.ts
|
|
1183
|
-
import { readdirSync as
|
|
1184
|
-
import { resolve as
|
|
1445
|
+
import { readdirSync as readdirSync3 } from "fs";
|
|
1446
|
+
import { resolve as resolve3, extname, basename as basename4 } from "path";
|
|
1185
1447
|
import { deriveRecordingFolders, DEFAULT_RECORDING_STATUSES } from "@patterkit/model";
|
|
1186
1448
|
var AUDIO_EXT = /* @__PURE__ */ new Set([".wav", ".mp3"]);
|
|
1187
1449
|
function scanAudioStatus(loaded) {
|
|
@@ -1194,7 +1456,7 @@ function scanAudioStatus(loaded) {
|
|
|
1194
1456
|
if (!rung.folder) continue;
|
|
1195
1457
|
let files;
|
|
1196
1458
|
try {
|
|
1197
|
-
files =
|
|
1459
|
+
files = readdirSync3(resolve3(loaded.root, rung.folder));
|
|
1198
1460
|
} catch {
|
|
1199
1461
|
continue;
|
|
1200
1462
|
}
|
|
@@ -1261,9 +1523,10 @@ function mergeAuthoring2(loaded) {
|
|
|
1261
1523
|
|
|
1262
1524
|
// src/play.ts
|
|
1263
1525
|
function runPlay(loaded, opts = {}) {
|
|
1264
|
-
const bundle = exportBundle4({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
|
|
1526
|
+
const bundle = exportBundle4({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
|
|
1265
1527
|
const events = [];
|
|
1266
|
-
const
|
|
1528
|
+
const registry = previewRegistry(loaded.gameScopes, bundle);
|
|
1529
|
+
const engine = new Engine(bundle, { seed: opts.seed, ...registry ? { registry } : {} });
|
|
1267
1530
|
const start = resolveStart(loaded, opts);
|
|
1268
1531
|
const flow = engine.openFlow("main", { scene: start.scene, block: start.block });
|
|
1269
1532
|
const scripted = [...opts.choices ?? []];
|
|
@@ -1474,12 +1737,13 @@ function blockedGates(beatId, analysis, drivenRefs, reachedRuns) {
|
|
|
1474
1737
|
return out;
|
|
1475
1738
|
}
|
|
1476
1739
|
function proposeCoverageDrivers(loaded) {
|
|
1477
|
-
const
|
|
1740
|
+
const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
|
|
1741
|
+
const hostScopes = bundle.scopeRegistry?.scopes ?? [];
|
|
1742
|
+
const hostTokens = new Set(hostScopes.map((s) => s.token));
|
|
1478
1743
|
if (hostTokens.size === 0) return [];
|
|
1479
|
-
const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
|
|
1480
1744
|
const { written, proposals } = analyzeHostScopes(bundle, hostTokens);
|
|
1481
1745
|
const declByRef = /* @__PURE__ */ new Map();
|
|
1482
|
-
for (const s of
|
|
1746
|
+
for (const s of hostScopes) {
|
|
1483
1747
|
for (const d of s.declarations ?? []) declByRef.set(`@${s.token}.${d.name}`, { type: d.type, values: d.values, stages: d.stages });
|
|
1484
1748
|
}
|
|
1485
1749
|
const drivers = [];
|
|
@@ -1537,9 +1801,9 @@ function* sweep(loaded, options = {}, hooks = {}) {
|
|
|
1537
1801
|
const reachedRuns = new Map(order.map((id) => [id, 0]));
|
|
1538
1802
|
const dryRuns = /* @__PURE__ */ new Map();
|
|
1539
1803
|
const termination = { ended: 0, capped: 0, stalled: 0, evalError: 0 };
|
|
1540
|
-
const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
|
|
1804
|
+
const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
|
|
1541
1805
|
const rng = mulberry32(seed);
|
|
1542
|
-
const hostTokens =
|
|
1806
|
+
const hostTokens = hostScopeTokens(loaded, bundle);
|
|
1543
1807
|
const analysis = analyzeHostScopes(bundle, hostTokens);
|
|
1544
1808
|
const drivers = (options.drivers ?? loaded.project.coverageDrivers ?? []).filter(
|
|
1545
1809
|
(d) => d.values.length > 0 && hostTokens.has(d.ref.replace(/^@/, "").split(".")[0] ?? "")
|
|
@@ -1556,9 +1820,11 @@ function* sweep(loaded, options = {}, hooks = {}) {
|
|
|
1556
1820
|
break;
|
|
1557
1821
|
}
|
|
1558
1822
|
const dryThisRun = /* @__PURE__ */ new Set();
|
|
1823
|
+
const registry = previewRegistry(loaded.gameScopes, bundle);
|
|
1559
1824
|
const engine = new Engine2(bundle, {
|
|
1560
1825
|
seed: Math.floor(rng() * 4294967296),
|
|
1561
|
-
onDryChoice: (groupId) => dryThisRun.add(groupId)
|
|
1826
|
+
onDryChoice: (groupId) => dryThisRun.add(groupId),
|
|
1827
|
+
...registry ? { registry } : {}
|
|
1562
1828
|
});
|
|
1563
1829
|
for (const d of initialDrivers) engine.setProperty(d.ref, pick(d.values));
|
|
1564
1830
|
const flow = engine.openFlow("cov", { scene: start.scene, block: start.block });
|
|
@@ -1703,14 +1969,14 @@ function renderCoverageText(report, sceneName = (id) => id) {
|
|
|
1703
1969
|
}
|
|
1704
1970
|
|
|
1705
1971
|
// src/format.ts
|
|
1706
|
-
import { readFileSync as
|
|
1707
|
-
import { canonicalStringify as
|
|
1972
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1973
|
+
import { canonicalStringify as canonicalStringify4, parseSource as parseSource3 } from "@patterkit/core";
|
|
1708
1974
|
function runFormat(files) {
|
|
1709
1975
|
return files.map((file) => {
|
|
1710
|
-
const original =
|
|
1976
|
+
const original = readFileSync4(file, "utf8");
|
|
1711
1977
|
let canonical;
|
|
1712
1978
|
try {
|
|
1713
|
-
canonical =
|
|
1979
|
+
canonical = canonicalStringify4(parseSource3(original));
|
|
1714
1980
|
} catch (e) {
|
|
1715
1981
|
throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1716
1982
|
}
|
|
@@ -2094,8 +2360,8 @@ async function runReportXlsx(data) {
|
|
|
2094
2360
|
}
|
|
2095
2361
|
|
|
2096
2362
|
// src/localisation.ts
|
|
2097
|
-
import { basename as basename5, join as
|
|
2098
|
-
import { canonicalStringify as
|
|
2363
|
+
import { basename as basename5, join as join5, sep } from "path";
|
|
2364
|
+
import { canonicalStringify as canonicalStringify5 } from "@patterkit/core";
|
|
2099
2365
|
import { walkNodes as walkNodes3, castStringKey, PROJECT_LOCALE_SCENE } from "@patterkit/model";
|
|
2100
2366
|
|
|
2101
2367
|
// src/documentation.ts
|
|
@@ -2216,14 +2482,14 @@ function targetLocPath(loaded, scene, locale, defaultLocale) {
|
|
|
2216
2482
|
}
|
|
2217
2483
|
}
|
|
2218
2484
|
const stem = scene === PROJECT_LOCALE_SCENE ? "_project" : scene;
|
|
2219
|
-
return
|
|
2485
|
+
return join5(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
|
|
2220
2486
|
}
|
|
2221
2487
|
function authoringPath(loaded, scene) {
|
|
2222
|
-
const dir =
|
|
2223
|
-
if (scene === PROJECT_LOCALE_SCENE) return
|
|
2488
|
+
const dir = join5(loaded.root, layoutOf(loaded).authoring);
|
|
2489
|
+
if (scene === PROJECT_LOCALE_SCENE) return join5(dir, "_project.patterx");
|
|
2224
2490
|
const flow = loaded.sceneFiles[scene];
|
|
2225
2491
|
const stem = flow ? basename5(flow).replace(/\.patterflow$/, "") : scene;
|
|
2226
|
-
return
|
|
2492
|
+
return join5(dir, `${stem}.patterx`);
|
|
2227
2493
|
}
|
|
2228
2494
|
var findFile = (files, items, path) => {
|
|
2229
2495
|
const i = files.indexOf(path);
|
|
@@ -2253,7 +2519,7 @@ function applyLoc(loaded, catalog, opts = {}) {
|
|
|
2253
2519
|
merged[id] = text;
|
|
2254
2520
|
}
|
|
2255
2521
|
const file = { schema: existing?.schema ?? "patter/strings@0", scene, locale, strings: merged };
|
|
2256
|
-
writes.push({ path, content:
|
|
2522
|
+
writes.push({ path, content: canonicalStringify5(file) });
|
|
2257
2523
|
const fresh = bySceneFresh.get(scene);
|
|
2258
2524
|
if (!fresh?.size) continue;
|
|
2259
2525
|
const aPath = authoringPath(loaded, scene);
|
|
@@ -2262,7 +2528,7 @@ function applyLoc(loaded, catalog, opts = {}) {
|
|
|
2262
2528
|
const editsMap = { ...authoring.edits ?? {} };
|
|
2263
2529
|
for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
|
|
2264
2530
|
authoring.edits = editsMap;
|
|
2265
|
-
writes.push({ path: aPath, content:
|
|
2531
|
+
writes.push({ path: aPath, content: canonicalStringify5(authoring) });
|
|
2266
2532
|
}
|
|
2267
2533
|
return { writes, stats: { updated, files: bySceneText.size } };
|
|
2268
2534
|
}
|
|
@@ -2880,7 +3146,7 @@ var SERIF2 = (bold, italic) => bold && italic ? "Serif-BoldItalic" : bold ? "Ser
|
|
|
2880
3146
|
var isEmoji = (cp) => cp >= 126976 || cp >= 9728 && cp <= 10175;
|
|
2881
3147
|
async function scriptToPdf(doc) {
|
|
2882
3148
|
const { scriptFont } = await import("./script-fonts-AQNQOADI.js");
|
|
2883
|
-
return new Promise((
|
|
3149
|
+
return new Promise((resolve6, reject) => {
|
|
2884
3150
|
const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
|
|
2885
3151
|
pdf.registerFont("Serif", scriptFont("serif"));
|
|
2886
3152
|
pdf.registerFont("Serif-Bold", scriptFont("serifBold"));
|
|
@@ -2921,7 +3187,7 @@ async function scriptToPdf(doc) {
|
|
|
2921
3187
|
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));
|
|
2922
3188
|
const chunks = [];
|
|
2923
3189
|
pdf.on("data", (c) => chunks.push(c));
|
|
2924
|
-
pdf.on("end", () =>
|
|
3190
|
+
pdf.on("end", () => resolve6(Buffer.concat(chunks)));
|
|
2925
3191
|
pdf.on("error", reject);
|
|
2926
3192
|
const pageRight = pdf.page.width - MARGIN;
|
|
2927
3193
|
const pageBottom = pdf.page.height - MARGIN;
|
|
@@ -3068,16 +3334,16 @@ async function scriptToPdf(doc) {
|
|
|
3068
3334
|
}
|
|
3069
3335
|
|
|
3070
3336
|
// src/init.ts
|
|
3071
|
-
import { join as
|
|
3072
|
-
import { readdirSync as
|
|
3073
|
-
import { newId, slug, canonicalStringify as
|
|
3337
|
+
import { join as join6, basename as basename6, resolve as resolve4 } from "path";
|
|
3338
|
+
import { readdirSync as readdirSync4, existsSync as existsSync3 } from "fs";
|
|
3339
|
+
import { newId, slug, canonicalStringify as canonicalStringify6 } from "@patterkit/core";
|
|
3074
3340
|
function runInit(opts) {
|
|
3075
|
-
const dir =
|
|
3341
|
+
const dir = resolve4(opts.dir);
|
|
3076
3342
|
const name = opts.name?.trim() || basename6(dir).replace(/\.patter$/, "");
|
|
3077
3343
|
const fileSlug = slug(name);
|
|
3078
3344
|
let existing = [];
|
|
3079
3345
|
try {
|
|
3080
|
-
existing =
|
|
3346
|
+
existing = readdirSync4(dir).filter((f) => f.endsWith(".patterproj"));
|
|
3081
3347
|
} catch {
|
|
3082
3348
|
}
|
|
3083
3349
|
if (existing.length > 0) throw new Error(`a project already exists here: ${existing[0]}`);
|
|
@@ -3121,25 +3387,25 @@ function runInit(opts) {
|
|
|
3121
3387
|
strings: { [beatId]: `Welcome to ${name}. This is the first line of your story - replace it with your own.` }
|
|
3122
3388
|
};
|
|
3123
3389
|
const bundle = opts.bundle ?? "commit";
|
|
3124
|
-
const projectFile =
|
|
3390
|
+
const projectFile = join6(dir, `${fileSlug}.patterproj`);
|
|
3125
3391
|
const writes = [
|
|
3126
|
-
{ path: projectFile, content:
|
|
3127
|
-
{ path:
|
|
3128
|
-
{ path:
|
|
3129
|
-
{ path:
|
|
3392
|
+
{ path: projectFile, content: canonicalStringify6(project) },
|
|
3393
|
+
{ path: join6(dir, "scenes", "start.patterflow"), content: canonicalStringify6(flow) },
|
|
3394
|
+
{ path: join6(dir, "loc", "en", "start.patterloc"), content: canonicalStringify6(locale) },
|
|
3395
|
+
{ path: join6(dir, ".editorconfig"), content: EDITORCONFIG },
|
|
3130
3396
|
...vcsConfigWrites(dir, opts.vcs, bundle)
|
|
3131
3397
|
];
|
|
3132
|
-
const collisions = writes.map((w) => w.path).filter((p) =>
|
|
3398
|
+
const collisions = writes.map((w) => w.path).filter((p) => existsSync3(p));
|
|
3133
3399
|
if (collisions.length > 0) {
|
|
3134
3400
|
throw new Error(`refusing to overwrite existing file(s): ${collisions.join(", ")}`);
|
|
3135
3401
|
}
|
|
3136
3402
|
return { writes, projectFile, name };
|
|
3137
3403
|
}
|
|
3138
3404
|
function vcsConfigWrites(dir, vcs, bundle = "commit") {
|
|
3139
|
-
const writes = [{ path:
|
|
3140
|
-
if (vcs === "git") writes.push({ path:
|
|
3405
|
+
const writes = [{ path: join6(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
|
|
3406
|
+
if (vcs === "git") writes.push({ path: join6(dir, ".gitattributes"), content: GITATTRIBUTES });
|
|
3141
3407
|
const ignore = ignoreFileFor(vcs);
|
|
3142
|
-
if (ignore) writes.push({ path:
|
|
3408
|
+
if (ignore) writes.push({ path: join6(dir, ignore), content: ignoreContent(bundle) });
|
|
3143
3409
|
return writes;
|
|
3144
3410
|
}
|
|
3145
3411
|
var EDITORCONFIG = `# Patter projects are UTF-8 + LF, always (spec - patter format enforces this).
|
|
@@ -3561,7 +3827,7 @@ function buildIndex(loaded) {
|
|
|
3561
3827
|
}
|
|
3562
3828
|
|
|
3563
3829
|
// src/replace.ts
|
|
3564
|
-
import { canonicalStringify as
|
|
3830
|
+
import { canonicalStringify as canonicalStringify7 } from "@patterkit/core";
|
|
3565
3831
|
import { walkNodes as walkNodes5, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2 } from "@patterkit/model";
|
|
3566
3832
|
import { findMatcher } from "@wildwinter/toolkit";
|
|
3567
3833
|
function runReplace(loaded, opts) {
|
|
@@ -3603,7 +3869,7 @@ function runReplace(loaded, opts) {
|
|
|
3603
3869
|
}
|
|
3604
3870
|
if (changed) {
|
|
3605
3871
|
const next = { ...shard, strings: merged };
|
|
3606
|
-
plan.writes.push({ path, content:
|
|
3872
|
+
plan.writes.push({ path, content: canonicalStringify7(next) });
|
|
3607
3873
|
plan.shards.push(next);
|
|
3608
3874
|
plan.scenes++;
|
|
3609
3875
|
}
|
|
@@ -3613,16 +3879,16 @@ function runReplace(loaded, opts) {
|
|
|
3613
3879
|
|
|
3614
3880
|
// src/pack.ts
|
|
3615
3881
|
import JSZip from "jszip";
|
|
3616
|
-
import { readFileSync as
|
|
3617
|
-
import { dirname as
|
|
3882
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3883
|
+
import { dirname as dirname5, relative as relative3, sep as sep2 } from "path";
|
|
3618
3884
|
import { parseSource as parseSource4 } from "@patterkit/core";
|
|
3619
3885
|
import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
|
|
3620
3886
|
var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
|
|
3621
3887
|
var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
|
|
3622
3888
|
async function runPack(startPath) {
|
|
3623
3889
|
const projectFile = findProjectFile(startPath);
|
|
3624
|
-
const root =
|
|
3625
|
-
const project = parseSource4(
|
|
3890
|
+
const root = dirname5(projectFile);
|
|
3891
|
+
const project = parseSource4(readFileSync5(projectFile, "utf8"));
|
|
3626
3892
|
const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
|
|
3627
3893
|
if (unresolved.length) {
|
|
3628
3894
|
throw new Error(
|
|
@@ -3630,31 +3896,40 @@ async function runPack(startPath) {
|
|
|
3630
3896
|
` + unresolved.map((i) => ` ${i.file}`).join("\n")
|
|
3631
3897
|
);
|
|
3632
3898
|
}
|
|
3633
|
-
const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel:
|
|
3899
|
+
const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative3(root, abs).split(sep2).join("/") })).sort((a, b) => a.rel.localeCompare(b.rel));
|
|
3900
|
+
const scopes = gameScopesSnapshot(root, project);
|
|
3634
3901
|
const manifest = {
|
|
3635
3902
|
schema: "patter/document@0",
|
|
3636
3903
|
project: { id: project.project.id, name: project.project.name },
|
|
3637
|
-
files: files.map((f) => f.rel)
|
|
3904
|
+
files: files.map((f) => f.rel),
|
|
3905
|
+
...scopes.length ? { gameScopes: scopes.map((f) => f.fileName) } : {}
|
|
3638
3906
|
};
|
|
3639
3907
|
const zip = new JSZip();
|
|
3640
3908
|
zip.file("patter.manifest.json", JSON.stringify(manifest, null, 2) + "\n", ENTRY_OPTS);
|
|
3641
|
-
for (const f of files) zip.file(f.rel,
|
|
3909
|
+
for (const f of files) zip.file(f.rel, readFileSync5(f.abs, "utf8"), ENTRY_OPTS);
|
|
3910
|
+
for (const f of scopes) zip.file(`${GAME_SCOPES_DIR}/${f.fileName}`, f.text, ENTRY_OPTS);
|
|
3642
3911
|
return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
|
|
3643
3912
|
}
|
|
3644
3913
|
|
|
3645
3914
|
// src/unpack.ts
|
|
3646
3915
|
import JSZip2 from "jszip";
|
|
3647
|
-
import { join as
|
|
3648
|
-
import { existsSync as
|
|
3649
|
-
import { parseSource as parseSource5, canonicalStringify as
|
|
3916
|
+
import { join as join7, resolve as resolve5, sep as sep3 } from "path";
|
|
3917
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
3918
|
+
import { parseSource as parseSource5, canonicalStringify as canonicalStringify8 } from "@patterkit/core";
|
|
3650
3919
|
import { isUnsafeEntry } from "@wildwinter/toolkit/archive";
|
|
3920
|
+
import { SCOPES_FILE_SUFFIX as SCOPES_FILE_SUFFIX2 } from "@wildwinter/scoperegistry/scopes";
|
|
3651
3921
|
import { isUnsafeEntry as isUnsafeEntry2 } from "@wildwinter/toolkit/archive";
|
|
3652
3922
|
var MANIFEST = "patter.manifest.json";
|
|
3923
|
+
var isScopesEntry = (name) => {
|
|
3924
|
+
const rest = name.startsWith(`${GAME_SCOPES_DIR}/`) ? name.slice(GAME_SCOPES_DIR.length + 1) : void 0;
|
|
3925
|
+
return rest !== void 0 && !rest.includes("/") && rest.endsWith(SCOPES_FILE_SUFFIX2);
|
|
3926
|
+
};
|
|
3653
3927
|
var UnsafeEntryError = class extends Error {
|
|
3654
3928
|
};
|
|
3655
3929
|
async function readDoc(bytes) {
|
|
3656
3930
|
const zip = await JSZip2.loadAsync(bytes);
|
|
3657
3931
|
const shards = /* @__PURE__ */ new Map();
|
|
3932
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
3658
3933
|
let manifest;
|
|
3659
3934
|
for (const [name, entry] of Object.entries(zip.files)) {
|
|
3660
3935
|
if (entry.dir) continue;
|
|
@@ -3666,16 +3941,14 @@ async function readDoc(bytes) {
|
|
|
3666
3941
|
continue;
|
|
3667
3942
|
}
|
|
3668
3943
|
if (isUnsafeEntry(name)) throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
|
|
3669
|
-
shards.set(name, await entry.async("string"));
|
|
3944
|
+
(isScopesEntry(name) ? scopes : shards).set(name, await entry.async("string"));
|
|
3670
3945
|
}
|
|
3671
|
-
return { shards, manifest };
|
|
3672
|
-
}
|
|
3673
|
-
async function readDocShards(bytes) {
|
|
3674
|
-
return (await readDoc(bytes)).shards;
|
|
3946
|
+
return { shards, scopes, ...manifest ? { manifest } : {} };
|
|
3675
3947
|
}
|
|
3676
3948
|
async function runUnpack(bytes, targetDir) {
|
|
3677
|
-
const
|
|
3678
|
-
|
|
3949
|
+
const doc = await readDoc(bytes);
|
|
3950
|
+
const plan = (entries) => [...entries.entries()].map(([name, content]) => ({ path: containedWrite(targetDir, name), content })).sort((a, b) => a.path.localeCompare(b.path));
|
|
3951
|
+
return { shards: plan(doc.shards), scopes: plan(doc.scopes) };
|
|
3679
3952
|
}
|
|
3680
3953
|
function checkProvenance(returned, base, target) {
|
|
3681
3954
|
const known = [returned, base, target].filter((id) => typeof id === "string" && id !== "");
|
|
@@ -3688,7 +3961,7 @@ function checkProvenance(returned, base, target) {
|
|
|
3688
3961
|
}
|
|
3689
3962
|
function targetProjectId(projectDir) {
|
|
3690
3963
|
try {
|
|
3691
|
-
const pf = parseSource5(
|
|
3964
|
+
const pf = parseSource5(readFileSync6(findProjectFile(projectDir), "utf8"));
|
|
3692
3965
|
return pf.project?.id;
|
|
3693
3966
|
} catch {
|
|
3694
3967
|
return void 0;
|
|
@@ -3704,9 +3977,11 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
|
|
|
3704
3977
|
const writes = [];
|
|
3705
3978
|
const sidecars = [];
|
|
3706
3979
|
let conflicts = 0, warnings = 0;
|
|
3980
|
+
const projectRel = projectEntry(theirs);
|
|
3981
|
+
let projectSides;
|
|
3707
3982
|
for (const [rel, theirText] of [...theirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
3708
3983
|
const outPath = containedWrite(projectDir, rel);
|
|
3709
|
-
if (!
|
|
3984
|
+
if (!existsSync4(outPath)) {
|
|
3710
3985
|
writes.push({ path: outPath, content: theirText });
|
|
3711
3986
|
shards.push({ path: rel, added: true });
|
|
3712
3987
|
continue;
|
|
@@ -3718,12 +3993,15 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
|
|
|
3718
3993
|
throw new Error(`${rel}: the ${side} copy is not readable Patter source - ${e instanceof Error ? e.message : String(e)}`);
|
|
3719
3994
|
}
|
|
3720
3995
|
};
|
|
3721
|
-
const oursObj = readSide("ours",
|
|
3996
|
+
const oursObj = readSide("ours", readFileSync6(outPath, "utf8"));
|
|
3722
3997
|
const theirsObj = readSide("theirs", theirText);
|
|
3723
3998
|
const baseText = base.get(rel);
|
|
3724
3999
|
const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
|
|
3725
4000
|
const result = runMerge(baseObj, oursObj, theirsObj);
|
|
3726
|
-
writes.push({ path: outPath, content:
|
|
4001
|
+
writes.push({ path: outPath, content: canonicalStringify8(result.merged) });
|
|
4002
|
+
if (rel === projectRel && baseText !== void 0) {
|
|
4003
|
+
projectSides = { ours: oursObj, base: baseObj, theirs: theirsObj, merged: result.merged };
|
|
4004
|
+
}
|
|
3727
4005
|
if (result.conflicts.length > 0) {
|
|
3728
4006
|
sidecars.push({ path: `${outPath}.patterconflict`, content: JSON.stringify({ type: result.type, conflicts: result.conflicts, warnings: result.warnings }, null, 2) + "\n" });
|
|
3729
4007
|
conflicts += result.conflicts.length;
|
|
@@ -3731,11 +4009,24 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
|
|
|
3731
4009
|
warnings += result.warnings.length;
|
|
3732
4010
|
shards.push({ path: rel, result, added: false });
|
|
3733
4011
|
}
|
|
3734
|
-
|
|
4012
|
+
let gameScopes;
|
|
4013
|
+
const local = projectSides && discoverGameScopes(resolve5(projectDir), projectSides.ours).gameScopes;
|
|
4014
|
+
if (projectSides && local) {
|
|
4015
|
+
const world = planReturnedWorld(projectSides.ours, projectSides.base, projectSides.theirs, projectSides.merged, local);
|
|
4016
|
+
if (world.write) {
|
|
4017
|
+
writes.push(world.write);
|
|
4018
|
+
gameScopes = { path: world.write.path };
|
|
4019
|
+
} else if (world.error) gameScopes = { path: join7(local.dir, GAME_SCOPES_FILE), error: world.error };
|
|
4020
|
+
}
|
|
4021
|
+
return { shards, writes, sidecars, conflicts, warnings, provenance, ...gameScopes ? { gameScopes } : {} };
|
|
4022
|
+
}
|
|
4023
|
+
function projectEntry(shards) {
|
|
4024
|
+
const at = [...shards.keys()].filter((n) => !n.includes("/") && n.endsWith(".patterproj"));
|
|
4025
|
+
return at.length === 1 ? at[0] : void 0;
|
|
3735
4026
|
}
|
|
3736
4027
|
function containedWrite(dir, name) {
|
|
3737
|
-
const root =
|
|
3738
|
-
const full =
|
|
4028
|
+
const root = resolve5(dir);
|
|
4029
|
+
const full = resolve5(join7(dir, name));
|
|
3739
4030
|
if (full !== root && !full.startsWith(root + sep3)) {
|
|
3740
4031
|
throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
|
|
3741
4032
|
}
|
|
@@ -3744,6 +4035,11 @@ function containedWrite(dir, name) {
|
|
|
3744
4035
|
export {
|
|
3745
4036
|
AUTHORING_HANDLED,
|
|
3746
4037
|
CONFLICT_SIDECAR,
|
|
4038
|
+
GAME_SCOPES_DIR,
|
|
4039
|
+
GAME_SCOPES_FILE,
|
|
4040
|
+
GAME_SCOPES_OWNER,
|
|
4041
|
+
PATTER_SCOPES_FILE,
|
|
4042
|
+
PATTER_SCOPES_OWNER,
|
|
3747
4043
|
SHARD_EXTENSIONS,
|
|
3748
4044
|
UnsafeEntryError,
|
|
3749
4045
|
UnsupportedMergeError,
|
|
@@ -3755,17 +4051,32 @@ export {
|
|
|
3755
4051
|
catalogToPo,
|
|
3756
4052
|
catalogToXlsx,
|
|
3757
4053
|
classesForChannel,
|
|
4054
|
+
defaultGameScopesDir,
|
|
3758
4055
|
detectMergeType,
|
|
4056
|
+
discoverGameScopes,
|
|
4057
|
+
drivableScopes,
|
|
3759
4058
|
extractLoc,
|
|
3760
4059
|
findProjectFile,
|
|
4060
|
+
gameScopeTokens,
|
|
4061
|
+
gameScopesCatalogue,
|
|
4062
|
+
gameScopesSnapshot,
|
|
4063
|
+
hostScopeTokens,
|
|
3761
4064
|
isUnsafeEntry2 as isUnsafeEntry,
|
|
3762
4065
|
jsonToCatalog,
|
|
3763
4066
|
listProjectTags,
|
|
3764
4067
|
loadProject,
|
|
3765
4068
|
loadProjectLanding,
|
|
4069
|
+
patterScopesFile,
|
|
4070
|
+
patterScopesStale,
|
|
4071
|
+
patterScopesWrite,
|
|
4072
|
+
planReturnedWorld,
|
|
4073
|
+
planShareScopes,
|
|
4074
|
+
planWorldSave,
|
|
3766
4075
|
poToCatalog,
|
|
4076
|
+
previewRegistry,
|
|
3767
4077
|
proposeCoverageDrivers,
|
|
3768
4078
|
reachabilityIssues,
|
|
4079
|
+
readGameScopes,
|
|
3769
4080
|
renderCoverageText,
|
|
3770
4081
|
renderPlay,
|
|
3771
4082
|
renderReportText,
|
|
@@ -3799,8 +4110,10 @@ export {
|
|
|
3799
4110
|
sceneIdForShard,
|
|
3800
4111
|
scriptToDocx,
|
|
3801
4112
|
scriptToPdf,
|
|
4113
|
+
serialiseScopesFile,
|
|
3802
4114
|
sidecarIssues,
|
|
3803
4115
|
vcsConfigWrites,
|
|
3804
4116
|
voiceScriptToXlsx,
|
|
4117
|
+
worldSettingsScopes,
|
|
3805
4118
|
xlsxToCatalog
|
|
3806
4119
|
};
|