@patterkit/ops 0.7.7 → 0.8.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 +185 -17
- package/dist/index.js +430 -122
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,11 +1,252 @@
|
|
|
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 game = {
|
|
215
|
+
version: 1,
|
|
216
|
+
owner: GAME_SCOPES_OWNER,
|
|
217
|
+
scopes: (project.scopeRegistry?.scopes ?? []).filter((h) => isGameToken(h.token, void 0)).map((h) => ({
|
|
218
|
+
token: h.token,
|
|
219
|
+
...h.writable !== void 0 ? { writable: h.writable } : {},
|
|
220
|
+
...h.declarations ? { declarations: h.declarations.map((d) => ({ ...d })) } : {}
|
|
221
|
+
}))
|
|
222
|
+
};
|
|
223
|
+
const writes = [
|
|
224
|
+
{ path: join(target, PATTER_SCOPES_FILE), content: serialiseScopesFile(patterScopesFile(project)) },
|
|
225
|
+
{ path: join(target, GAME_SCOPES_FILE), content: serialiseScopesFile(game) }
|
|
226
|
+
];
|
|
227
|
+
let found = false;
|
|
228
|
+
for (let at = resolve(root); ; ) {
|
|
229
|
+
if (join(at, GAME_SCOPES_DIR) === target) {
|
|
230
|
+
found = true;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
const up = dirname(at);
|
|
234
|
+
if (existsSync(join(at, ".git")) || up === at) break;
|
|
235
|
+
at = up;
|
|
236
|
+
}
|
|
237
|
+
const { gameScopes: _drop, ...rest } = project;
|
|
238
|
+
const next = found ? rest : { ...rest, gameScopes: relative(resolve(root), target) || "." };
|
|
239
|
+
return { writes, project: next };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/load.ts
|
|
243
|
+
function withGameScopes(root, project) {
|
|
244
|
+
const { gameScopes, missing } = discoverGameScopes(root, project);
|
|
245
|
+
return { ...gameScopes ? { gameScopes } : {}, ...missing ? { gameScopesMissing: missing } : {} };
|
|
246
|
+
}
|
|
6
247
|
function readDirSafe(dir) {
|
|
7
248
|
try {
|
|
8
|
-
return
|
|
249
|
+
return readdirSync2(dir, { withFileTypes: true });
|
|
9
250
|
} catch {
|
|
10
251
|
return [];
|
|
11
252
|
}
|
|
@@ -14,26 +255,26 @@ function walkFiles(dir, ext) {
|
|
|
14
255
|
const out = [];
|
|
15
256
|
for (const e of readDirSafe(dir)) {
|
|
16
257
|
if (e.name.startsWith(".")) continue;
|
|
17
|
-
const p =
|
|
258
|
+
const p = join2(dir, e.name);
|
|
18
259
|
if (e.isDirectory()) out.push(...walkFiles(p, ext));
|
|
19
260
|
else if (e.isFile() && e.name.endsWith(ext)) out.push(p);
|
|
20
261
|
}
|
|
21
262
|
return out.sort();
|
|
22
263
|
}
|
|
23
264
|
function findProjectFile(startPath) {
|
|
24
|
-
let dir =
|
|
265
|
+
let dir = resolve2(startPath);
|
|
25
266
|
try {
|
|
26
267
|
if (statSync(dir).isFile()) {
|
|
27
268
|
if (dir.endsWith(".patterproj")) return dir;
|
|
28
|
-
dir =
|
|
269
|
+
dir = dirname2(dir);
|
|
29
270
|
}
|
|
30
271
|
} catch {
|
|
31
272
|
}
|
|
32
273
|
for (; ; ) {
|
|
33
|
-
const here = readDirSafe(dir).filter((e) => e.isFile() && e.name.endsWith(".patterproj")).map((e) =>
|
|
274
|
+
const here = readDirSafe(dir).filter((e) => e.isFile() && e.name.endsWith(".patterproj")).map((e) => join2(dir, e.name)).sort();
|
|
34
275
|
if (here.length > 1) throw new Error(`multiple project files in ${dir}: ${here.map((p) => p.slice(dir.length + 1)).join(", ")}`);
|
|
35
276
|
if (here.length === 1) return here[0];
|
|
36
|
-
const parent =
|
|
277
|
+
const parent = dirname2(dir);
|
|
37
278
|
if (parent === dir) throw new Error(`no .patterproj found at or above ${startPath}`);
|
|
38
279
|
dir = parent;
|
|
39
280
|
}
|
|
@@ -41,7 +282,7 @@ function findProjectFile(startPath) {
|
|
|
41
282
|
function parseFile(file, expectSchema, shapeKey) {
|
|
42
283
|
let parsed;
|
|
43
284
|
try {
|
|
44
|
-
parsed = parseSource(
|
|
285
|
+
parsed = parseSource(readFileSync2(file, "utf8"));
|
|
45
286
|
} catch (e) {
|
|
46
287
|
throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
47
288
|
}
|
|
@@ -97,16 +338,16 @@ function foldDeclaredNames(parsed, expectSchema) {
|
|
|
97
338
|
}
|
|
98
339
|
}
|
|
99
340
|
function sceneIdForShard(shardPath) {
|
|
100
|
-
const p =
|
|
341
|
+
const p = resolve2(shardPath);
|
|
101
342
|
try {
|
|
102
|
-
if (p.endsWith(".patterflow")) return parseSource(
|
|
103
|
-
if (p.endsWith(".patterloc")) return parseSource(
|
|
343
|
+
if (p.endsWith(".patterflow")) return parseSource(readFileSync2(p, "utf8")).scene?.id;
|
|
344
|
+
if (p.endsWith(".patterloc")) return parseSource(readFileSync2(p, "utf8")).scene;
|
|
104
345
|
if (p.endsWith(".patterx")) {
|
|
105
346
|
const projectFile = findProjectFile(p);
|
|
106
347
|
const project = parseFile(projectFile, "patter/project", "project");
|
|
107
|
-
const flowDir =
|
|
108
|
-
const flowPath =
|
|
109
|
-
return parseSource(
|
|
348
|
+
const flowDir = join2(dirname2(projectFile), project.layout?.flow ?? "scenes/");
|
|
349
|
+
const flowPath = join2(flowDir, basename(p).replace(/\.patterx$/, ".patterflow"));
|
|
350
|
+
return parseSource(readFileSync2(flowPath, "utf8")).scene?.id;
|
|
110
351
|
}
|
|
111
352
|
} catch {
|
|
112
353
|
}
|
|
@@ -114,13 +355,13 @@ function sceneIdForShard(shardPath) {
|
|
|
114
355
|
}
|
|
115
356
|
function loadProjectLanding(startPath, opts) {
|
|
116
357
|
const projectFile = findProjectFile(startPath);
|
|
117
|
-
const root =
|
|
358
|
+
const root = dirname2(projectFile);
|
|
118
359
|
const project = parseFile(projectFile, "patter/project", "project");
|
|
119
360
|
const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...project.layout };
|
|
120
|
-
const flowFiles = walkFiles(
|
|
121
|
-
const launchPath = opts?.launchPath ?
|
|
361
|
+
const flowFiles = walkFiles(join2(root, layout.flow), ".patterflow");
|
|
362
|
+
const launchPath = opts?.launchPath ? resolve2(opts.launchPath) : void 0;
|
|
122
363
|
let landingFile;
|
|
123
|
-
if (launchPath?.endsWith(".patterflow")) landingFile = flowFiles.find((f) =>
|
|
364
|
+
if (launchPath?.endsWith(".patterflow")) landingFile = flowFiles.find((f) => resolve2(f) === launchPath);
|
|
124
365
|
if (!landingFile) {
|
|
125
366
|
const targetId = (opts?.launchPath ? sceneIdForShard(opts.launchPath) : void 0) ?? opts?.preferId;
|
|
126
367
|
if (targetId) {
|
|
@@ -144,10 +385,10 @@ function loadProjectLanding(startPath, opts) {
|
|
|
144
385
|
const localeFiles = [];
|
|
145
386
|
if (landingScene && landingFile) {
|
|
146
387
|
const defaultLocale = project.locales.default;
|
|
147
|
-
const strings =
|
|
148
|
-
const guess =
|
|
388
|
+
const strings = join2(root, layout.strings);
|
|
389
|
+
const guess = join2(strings, defaultLocale, `${basename(landingFile).replace(/\.patterflow$/, "")}.patterloc`);
|
|
149
390
|
let found = false;
|
|
150
|
-
if (
|
|
391
|
+
if (existsSync2(guess)) {
|
|
151
392
|
try {
|
|
152
393
|
const loc = parseFile(guess, "patter/strings", "strings");
|
|
153
394
|
if (loc.scene === landingScene.id) {
|
|
@@ -168,7 +409,7 @@ function loadProjectLanding(startPath, opts) {
|
|
|
168
409
|
}
|
|
169
410
|
}
|
|
170
411
|
}
|
|
171
|
-
return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [] };
|
|
412
|
+
return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [], ...withGameScopes(root, project) };
|
|
172
413
|
}
|
|
173
414
|
function applySceneOrder(scenes, order) {
|
|
174
415
|
if (!order?.length) return;
|
|
@@ -184,12 +425,12 @@ function applySceneOrder(scenes, order) {
|
|
|
184
425
|
}
|
|
185
426
|
function loadProject(startPath) {
|
|
186
427
|
const projectFile = findProjectFile(startPath);
|
|
187
|
-
const root =
|
|
428
|
+
const root = dirname2(projectFile);
|
|
188
429
|
const project = parseFile(projectFile, "patter/project", "project");
|
|
189
430
|
const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...project.layout };
|
|
190
431
|
const scenes = [];
|
|
191
432
|
const sceneFiles = {};
|
|
192
|
-
for (const f of walkFiles(
|
|
433
|
+
for (const f of walkFiles(join2(root, layout.flow), ".patterflow")) {
|
|
193
434
|
const scene = parseFile(f, "patter/flow", "scene").scene;
|
|
194
435
|
const existing = sceneFiles[scene.id];
|
|
195
436
|
if (existing !== void 0) {
|
|
@@ -201,38 +442,38 @@ function loadProject(startPath) {
|
|
|
201
442
|
applySceneOrder(scenes, project.sceneOrder);
|
|
202
443
|
const locales = [];
|
|
203
444
|
const localeFiles = [];
|
|
204
|
-
for (const f of walkFiles(
|
|
445
|
+
for (const f of walkFiles(join2(root, layout.strings), ".patterloc")) {
|
|
205
446
|
locales.push(parseFile(f, "patter/strings", "strings"));
|
|
206
447
|
localeFiles.push(f);
|
|
207
448
|
}
|
|
208
449
|
const authoring = [];
|
|
209
450
|
const authoringFiles = [];
|
|
210
|
-
for (const f of walkFiles(
|
|
451
|
+
for (const f of walkFiles(join2(root, layout.authoring), ".patterx")) {
|
|
211
452
|
authoring.push(parseFile(f, "patter/authoring", "schema"));
|
|
212
453
|
authoringFiles.push(f);
|
|
213
454
|
}
|
|
214
|
-
return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring, authoringFiles };
|
|
455
|
+
return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring, authoringFiles, ...withGameScopes(root, project) };
|
|
215
456
|
}
|
|
216
457
|
|
|
217
458
|
// src/write.ts
|
|
218
459
|
import { writeFileSync, mkdirSync } from "fs";
|
|
219
|
-
import { dirname as
|
|
460
|
+
import { dirname as dirname3 } from "path";
|
|
220
461
|
function applyWrites(writes) {
|
|
221
462
|
for (const w of writes) {
|
|
222
|
-
mkdirSync(
|
|
463
|
+
mkdirSync(dirname3(w.path), { recursive: true });
|
|
223
464
|
writeFileSync(w.path, w.content, "utf8");
|
|
224
465
|
}
|
|
225
466
|
}
|
|
226
467
|
|
|
227
468
|
// src/validate.ts
|
|
228
|
-
import { readFileSync as
|
|
229
|
-
import { basename as basename2, isAbsolute, join as
|
|
469
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
470
|
+
import { basename as basename2, isAbsolute, join as join3, relative as relative2 } from "path";
|
|
230
471
|
|
|
231
472
|
// src/merge.ts
|
|
232
|
-
import { canonicalStringify } from "@patterkit/core";
|
|
473
|
+
import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
|
|
233
474
|
var UnsupportedMergeError = class extends Error {
|
|
234
475
|
};
|
|
235
|
-
var eq = (a, b) =>
|
|
476
|
+
var eq = (a, b) => canonicalStringify2(a) === canonicalStringify2(b);
|
|
236
477
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
237
478
|
var CONFLICT_SIDECAR = ".patterconflict";
|
|
238
479
|
function sidecarIssues(sidecarPaths) {
|
|
@@ -273,7 +514,7 @@ function merge3(base, ours, theirs, id, path, conflicts) {
|
|
|
273
514
|
conflicts.push({ id, path, base, ours, theirs, kind: deletedKind(base, ours, theirs) });
|
|
274
515
|
return ours;
|
|
275
516
|
}
|
|
276
|
-
function mergeMap(base, ours, theirs, prefix, conflicts,
|
|
517
|
+
function mergeMap(base, ours, theirs, prefix, conflicts, resolve6) {
|
|
277
518
|
const out = {};
|
|
278
519
|
for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
|
|
279
520
|
const b = base[k], o = ours[k], t = theirs[k];
|
|
@@ -282,7 +523,7 @@ function mergeMap(base, ours, theirs, prefix, conflicts, resolve5) {
|
|
|
282
523
|
else if (eq(b, o)) v = t;
|
|
283
524
|
else if (eq(b, t)) v = o;
|
|
284
525
|
else {
|
|
285
|
-
const r =
|
|
526
|
+
const r = resolve6?.(k, b, o, t);
|
|
286
527
|
if (r && r.resolved) v = r.value;
|
|
287
528
|
else {
|
|
288
529
|
conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
|
|
@@ -572,7 +813,7 @@ var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
|
572
813
|
|
|
573
814
|
// src/validate.ts
|
|
574
815
|
import { validateProject, parseSource as parseSource2 } from "@patterkit/core";
|
|
575
|
-
import { validateConditions, validateInterpolation, exportBundle as exportBundle2, hostScopesToSpec } from "@patterkit/compiler";
|
|
816
|
+
import { validateConditions, validateInterpolation, exportBundle as exportBundle2, hostScopesToSpec, projectScopes } from "@patterkit/compiler";
|
|
576
817
|
|
|
577
818
|
// src/reachability.ts
|
|
578
819
|
import { exportBundle } from "@patterkit/compiler";
|
|
@@ -682,8 +923,8 @@ var termsOfAst = (ast, sceneId) => terms(ast, sceneId, keyOf);
|
|
|
682
923
|
var requirementsOf = (expr, sceneId) => expr === void 0 ? [] : termsOfAst(expr.ast, sceneId);
|
|
683
924
|
var asFlags = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
684
925
|
function reachabilityIssues(loaded, compiled) {
|
|
685
|
-
const bundle = compiled ?? exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
|
|
686
|
-
const hostTokens =
|
|
926
|
+
const bundle = compiled ?? exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
|
|
927
|
+
const hostTokens = hostScopeTokens(loaded, bundle);
|
|
687
928
|
const preset = /* @__PURE__ */ new Set();
|
|
688
929
|
const notALatch = (decl, key) => {
|
|
689
930
|
if (decl.temporary) {
|
|
@@ -826,14 +1067,17 @@ function reachabilityIssues(loaded, compiled) {
|
|
|
826
1067
|
function runValidate(loaded) {
|
|
827
1068
|
const { project, scenes, locales } = loaded;
|
|
828
1069
|
const structural = validateProject({ project, scenes, authoring: loaded.authoring });
|
|
829
|
-
const
|
|
830
|
-
const
|
|
831
|
-
const
|
|
1070
|
+
const merged = loaded.gameScopes?.merged;
|
|
1071
|
+
const scopes = projectScopes(project, merged);
|
|
1072
|
+
const foreignScopes = hostScopesToSpec(scopes.host);
|
|
1073
|
+
const conditions = validateConditions({ project, scenes }, { foreignScopes, gameScopes: merged });
|
|
1074
|
+
const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes, gameScopes: merged });
|
|
1075
|
+
const gameScopes = gameScopesIssues(loaded, scopes.notes);
|
|
832
1076
|
const hygiene = checkHygiene([loaded.projectFile, ...Object.values(loaded.sceneFiles), ...loaded.localeFiles, ...loaded.authoringFiles]);
|
|
833
1077
|
const staleBundles = checkBundles(loaded);
|
|
834
1078
|
const unresolvedMerges = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
|
|
835
1079
|
const orphans = orphanShards(loaded);
|
|
836
|
-
const reachability = structural.length === 0 && conditions.
|
|
1080
|
+
const reachability = structural.length === 0 && !conditions.some(isError) ? reachabilityIssues(loaded) : [];
|
|
837
1081
|
return {
|
|
838
1082
|
structural,
|
|
839
1083
|
conditions,
|
|
@@ -843,9 +1087,22 @@ function runValidate(loaded) {
|
|
|
843
1087
|
staleBundles,
|
|
844
1088
|
unresolvedMerges,
|
|
845
1089
|
orphans,
|
|
846
|
-
|
|
1090
|
+
gameScopes,
|
|
1091
|
+
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
1092
|
};
|
|
848
1093
|
}
|
|
1094
|
+
var isError = (i) => i.severity === "error";
|
|
1095
|
+
function gameScopesIssues(loaded, notes) {
|
|
1096
|
+
const out = [];
|
|
1097
|
+
if (loaded.gameScopesMissing) out.push({ file: loaded.projectFile, severity: "error", message: loaded.gameScopesMissing });
|
|
1098
|
+
const gs = loaded.gameScopes;
|
|
1099
|
+
if (!gs) return out;
|
|
1100
|
+
for (const i of gs.issues) out.push({ file: i.file, severity: i.severity, message: i.message });
|
|
1101
|
+
for (const n of notes) out.push({ file: join3(gs.dir, n.file), severity: "warning", message: n.message });
|
|
1102
|
+
const stale = patterScopesStale(loaded.project, gs);
|
|
1103
|
+
if (stale) out.push({ ...stale, severity: "warning" });
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
849
1106
|
function orphanShards(loaded) {
|
|
850
1107
|
const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...loaded.project.layout };
|
|
851
1108
|
const kind = {
|
|
@@ -855,14 +1112,14 @@ function orphanShards(loaded) {
|
|
|
855
1112
|
".patterproj": { what: "project", home: null }
|
|
856
1113
|
};
|
|
857
1114
|
const inside = (file, dir) => {
|
|
858
|
-
const r =
|
|
1115
|
+
const r = relative2(dir, file);
|
|
859
1116
|
return r !== "" && !r.startsWith("..") && !isAbsolute(r);
|
|
860
1117
|
};
|
|
861
1118
|
const out = [];
|
|
862
1119
|
for (const [ext, { what, home }] of Object.entries(kind)) {
|
|
863
1120
|
for (const file of walkFiles(loaded.root, ext)) {
|
|
864
|
-
if (home ? inside(file,
|
|
865
|
-
const rel =
|
|
1121
|
+
if (home ? inside(file, join3(loaded.root, home)) : file === loaded.projectFile) continue;
|
|
1122
|
+
const rel = relative2(loaded.root, file);
|
|
866
1123
|
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
1124
|
out.push({ file, message });
|
|
868
1125
|
}
|
|
@@ -875,14 +1132,14 @@ function checkBundles(loaded) {
|
|
|
875
1132
|
if (bundles.length === 0) return issues;
|
|
876
1133
|
let fresh;
|
|
877
1134
|
try {
|
|
878
|
-
fresh = exportBundle2({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales }).content.hash;
|
|
1135
|
+
fresh = exportBundle2({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged }).content.hash;
|
|
879
1136
|
} catch {
|
|
880
1137
|
return issues;
|
|
881
1138
|
}
|
|
882
1139
|
for (const file of bundles) {
|
|
883
1140
|
let hash;
|
|
884
1141
|
try {
|
|
885
|
-
const parsed = parseSource2(
|
|
1142
|
+
const parsed = parseSource2(readFileSync3(file, "utf8"));
|
|
886
1143
|
hash = parsed?.content?.hash;
|
|
887
1144
|
} catch {
|
|
888
1145
|
issues.push({ file, message: "compiled bundle is unparseable - run `patter export`" });
|
|
@@ -911,7 +1168,7 @@ function checkHygiene(files) {
|
|
|
911
1168
|
}
|
|
912
1169
|
const fileIssues = [];
|
|
913
1170
|
try {
|
|
914
|
-
const text =
|
|
1171
|
+
const text = readFileSync3(file, "utf8");
|
|
915
1172
|
if (text.charCodeAt(0) === 65279) {
|
|
916
1173
|
fileIssues.push({ file, message: "file starts with a UTF-8 BOM (canonical form is UTF-8 without BOM - run `patter format`)" });
|
|
917
1174
|
}
|
|
@@ -928,7 +1185,7 @@ function checkHygiene(files) {
|
|
|
928
1185
|
}
|
|
929
1186
|
|
|
930
1187
|
// src/export.ts
|
|
931
|
-
import { basename as basename3, isAbsolute as isAbsolute2, join as
|
|
1188
|
+
import { basename as basename3, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
932
1189
|
import { exportBundle as exportBundle3 } from "@patterkit/compiler";
|
|
933
1190
|
import { isContentlessBeat } from "@patterkit/model";
|
|
934
1191
|
function compileScenes(loaded) {
|
|
@@ -961,7 +1218,7 @@ function refuseUnresolvedMerge(loaded) {
|
|
|
961
1218
|
function runExport(loaded) {
|
|
962
1219
|
refuseUnresolvedMerge(loaded);
|
|
963
1220
|
const { project, locales } = loaded;
|
|
964
|
-
const full = exportBundle3({ project, scenes: compileScenes(loaded), locales });
|
|
1221
|
+
const full = exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
|
|
965
1222
|
const loc = project.export?.localisation;
|
|
966
1223
|
if (!loc || loc.mode === "embedded") return full;
|
|
967
1224
|
const strings = loc.sourceDebug ? { [full.locales.default]: full.strings[full.locales.default] ?? {} } : {};
|
|
@@ -970,17 +1227,17 @@ function runExport(loaded) {
|
|
|
970
1227
|
function runExportFull(loaded) {
|
|
971
1228
|
refuseUnresolvedMerge(loaded);
|
|
972
1229
|
const { project, locales } = loaded;
|
|
973
|
-
return exportBundle3({ project, scenes: compileScenes(loaded), locales });
|
|
1230
|
+
return exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
|
|
974
1231
|
}
|
|
975
1232
|
function bundleOutputPath(loaded) {
|
|
976
1233
|
const override = loaded.project.export?.bundle;
|
|
977
|
-
if (override) return isAbsolute2(override) ? override :
|
|
1234
|
+
if (override) return isAbsolute2(override) ? override : join4(loaded.root, override);
|
|
978
1235
|
const stem = basename3(loaded.projectFile).replace(/\.patterproj$/, "");
|
|
979
|
-
return
|
|
1236
|
+
return join4(loaded.root, "dist", `${stem}.patterc`);
|
|
980
1237
|
}
|
|
981
1238
|
|
|
982
1239
|
// src/export-html.ts
|
|
983
|
-
import { canonicalStringify as
|
|
1240
|
+
import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
|
|
984
1241
|
|
|
985
1242
|
// src/playable-runtime.ts
|
|
986
1243
|
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 +1365,7 @@ function sourceStory(loaded) {
|
|
|
1108
1365
|
const def = full.locales?.default ?? Object.keys(full.strings)[0] ?? "en";
|
|
1109
1366
|
const bundle = { ...full, locales: { default: def, included: [def] }, strings: { [def]: full.strings[def] ?? {} } };
|
|
1110
1367
|
const title = loaded.project.project.name?.trim() || "A Patter story";
|
|
1111
|
-
const bundleJson =
|
|
1368
|
+
const bundleJson = canonicalStringify3(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
|
|
1112
1369
|
return { title, lang: def, bundleJson };
|
|
1113
1370
|
}
|
|
1114
1371
|
function page(title, lang, head, tail) {
|
|
@@ -1180,8 +1437,8 @@ ${PLAYER_JS}`,
|
|
|
1180
1437
|
}
|
|
1181
1438
|
|
|
1182
1439
|
// src/audio-scan.ts
|
|
1183
|
-
import { readdirSync as
|
|
1184
|
-
import { resolve as
|
|
1440
|
+
import { readdirSync as readdirSync3 } from "fs";
|
|
1441
|
+
import { resolve as resolve3, extname, basename as basename4 } from "path";
|
|
1185
1442
|
import { deriveRecordingFolders, DEFAULT_RECORDING_STATUSES } from "@patterkit/model";
|
|
1186
1443
|
var AUDIO_EXT = /* @__PURE__ */ new Set([".wav", ".mp3"]);
|
|
1187
1444
|
function scanAudioStatus(loaded) {
|
|
@@ -1194,7 +1451,7 @@ function scanAudioStatus(loaded) {
|
|
|
1194
1451
|
if (!rung.folder) continue;
|
|
1195
1452
|
let files;
|
|
1196
1453
|
try {
|
|
1197
|
-
files =
|
|
1454
|
+
files = readdirSync3(resolve3(loaded.root, rung.folder));
|
|
1198
1455
|
} catch {
|
|
1199
1456
|
continue;
|
|
1200
1457
|
}
|
|
@@ -1261,9 +1518,10 @@ function mergeAuthoring2(loaded) {
|
|
|
1261
1518
|
|
|
1262
1519
|
// src/play.ts
|
|
1263
1520
|
function runPlay(loaded, opts = {}) {
|
|
1264
|
-
const bundle = exportBundle4({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
|
|
1521
|
+
const bundle = exportBundle4({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
|
|
1265
1522
|
const events = [];
|
|
1266
|
-
const
|
|
1523
|
+
const registry = previewRegistry(loaded.gameScopes, bundle);
|
|
1524
|
+
const engine = new Engine(bundle, { seed: opts.seed, ...registry ? { registry } : {} });
|
|
1267
1525
|
const start = resolveStart(loaded, opts);
|
|
1268
1526
|
const flow = engine.openFlow("main", { scene: start.scene, block: start.block });
|
|
1269
1527
|
const scripted = [...opts.choices ?? []];
|
|
@@ -1474,12 +1732,13 @@ function blockedGates(beatId, analysis, drivenRefs, reachedRuns) {
|
|
|
1474
1732
|
return out;
|
|
1475
1733
|
}
|
|
1476
1734
|
function proposeCoverageDrivers(loaded) {
|
|
1477
|
-
const
|
|
1735
|
+
const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
|
|
1736
|
+
const hostScopes = bundle.scopeRegistry?.scopes ?? [];
|
|
1737
|
+
const hostTokens = new Set(hostScopes.map((s) => s.token));
|
|
1478
1738
|
if (hostTokens.size === 0) return [];
|
|
1479
|
-
const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
|
|
1480
1739
|
const { written, proposals } = analyzeHostScopes(bundle, hostTokens);
|
|
1481
1740
|
const declByRef = /* @__PURE__ */ new Map();
|
|
1482
|
-
for (const s of
|
|
1741
|
+
for (const s of hostScopes) {
|
|
1483
1742
|
for (const d of s.declarations ?? []) declByRef.set(`@${s.token}.${d.name}`, { type: d.type, values: d.values, stages: d.stages });
|
|
1484
1743
|
}
|
|
1485
1744
|
const drivers = [];
|
|
@@ -1537,9 +1796,9 @@ function* sweep(loaded, options = {}, hooks = {}) {
|
|
|
1537
1796
|
const reachedRuns = new Map(order.map((id) => [id, 0]));
|
|
1538
1797
|
const dryRuns = /* @__PURE__ */ new Map();
|
|
1539
1798
|
const termination = { ended: 0, capped: 0, stalled: 0, evalError: 0 };
|
|
1540
|
-
const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
|
|
1799
|
+
const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
|
|
1541
1800
|
const rng = mulberry32(seed);
|
|
1542
|
-
const hostTokens =
|
|
1801
|
+
const hostTokens = hostScopeTokens(loaded, bundle);
|
|
1543
1802
|
const analysis = analyzeHostScopes(bundle, hostTokens);
|
|
1544
1803
|
const drivers = (options.drivers ?? loaded.project.coverageDrivers ?? []).filter(
|
|
1545
1804
|
(d) => d.values.length > 0 && hostTokens.has(d.ref.replace(/^@/, "").split(".")[0] ?? "")
|
|
@@ -1556,9 +1815,11 @@ function* sweep(loaded, options = {}, hooks = {}) {
|
|
|
1556
1815
|
break;
|
|
1557
1816
|
}
|
|
1558
1817
|
const dryThisRun = /* @__PURE__ */ new Set();
|
|
1818
|
+
const registry = previewRegistry(loaded.gameScopes, bundle);
|
|
1559
1819
|
const engine = new Engine2(bundle, {
|
|
1560
1820
|
seed: Math.floor(rng() * 4294967296),
|
|
1561
|
-
onDryChoice: (groupId) => dryThisRun.add(groupId)
|
|
1821
|
+
onDryChoice: (groupId) => dryThisRun.add(groupId),
|
|
1822
|
+
...registry ? { registry } : {}
|
|
1562
1823
|
});
|
|
1563
1824
|
for (const d of initialDrivers) engine.setProperty(d.ref, pick(d.values));
|
|
1564
1825
|
const flow = engine.openFlow("cov", { scene: start.scene, block: start.block });
|
|
@@ -1703,14 +1964,14 @@ function renderCoverageText(report, sceneName = (id) => id) {
|
|
|
1703
1964
|
}
|
|
1704
1965
|
|
|
1705
1966
|
// src/format.ts
|
|
1706
|
-
import { readFileSync as
|
|
1707
|
-
import { canonicalStringify as
|
|
1967
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1968
|
+
import { canonicalStringify as canonicalStringify4, parseSource as parseSource3 } from "@patterkit/core";
|
|
1708
1969
|
function runFormat(files) {
|
|
1709
1970
|
return files.map((file) => {
|
|
1710
|
-
const original =
|
|
1971
|
+
const original = readFileSync4(file, "utf8");
|
|
1711
1972
|
let canonical;
|
|
1712
1973
|
try {
|
|
1713
|
-
canonical =
|
|
1974
|
+
canonical = canonicalStringify4(parseSource3(original));
|
|
1714
1975
|
} catch (e) {
|
|
1715
1976
|
throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1716
1977
|
}
|
|
@@ -2094,8 +2355,8 @@ async function runReportXlsx(data) {
|
|
|
2094
2355
|
}
|
|
2095
2356
|
|
|
2096
2357
|
// src/localisation.ts
|
|
2097
|
-
import { basename as basename5, join as
|
|
2098
|
-
import { canonicalStringify as
|
|
2358
|
+
import { basename as basename5, join as join5, sep } from "path";
|
|
2359
|
+
import { canonicalStringify as canonicalStringify5 } from "@patterkit/core";
|
|
2099
2360
|
import { walkNodes as walkNodes3, castStringKey, PROJECT_LOCALE_SCENE } from "@patterkit/model";
|
|
2100
2361
|
|
|
2101
2362
|
// src/documentation.ts
|
|
@@ -2216,14 +2477,14 @@ function targetLocPath(loaded, scene, locale, defaultLocale) {
|
|
|
2216
2477
|
}
|
|
2217
2478
|
}
|
|
2218
2479
|
const stem = scene === PROJECT_LOCALE_SCENE ? "_project" : scene;
|
|
2219
|
-
return
|
|
2480
|
+
return join5(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
|
|
2220
2481
|
}
|
|
2221
2482
|
function authoringPath(loaded, scene) {
|
|
2222
|
-
const dir =
|
|
2223
|
-
if (scene === PROJECT_LOCALE_SCENE) return
|
|
2483
|
+
const dir = join5(loaded.root, layoutOf(loaded).authoring);
|
|
2484
|
+
if (scene === PROJECT_LOCALE_SCENE) return join5(dir, "_project.patterx");
|
|
2224
2485
|
const flow = loaded.sceneFiles[scene];
|
|
2225
2486
|
const stem = flow ? basename5(flow).replace(/\.patterflow$/, "") : scene;
|
|
2226
|
-
return
|
|
2487
|
+
return join5(dir, `${stem}.patterx`);
|
|
2227
2488
|
}
|
|
2228
2489
|
var findFile = (files, items, path) => {
|
|
2229
2490
|
const i = files.indexOf(path);
|
|
@@ -2253,7 +2514,7 @@ function applyLoc(loaded, catalog, opts = {}) {
|
|
|
2253
2514
|
merged[id] = text;
|
|
2254
2515
|
}
|
|
2255
2516
|
const file = { schema: existing?.schema ?? "patter/strings@0", scene, locale, strings: merged };
|
|
2256
|
-
writes.push({ path, content:
|
|
2517
|
+
writes.push({ path, content: canonicalStringify5(file) });
|
|
2257
2518
|
const fresh = bySceneFresh.get(scene);
|
|
2258
2519
|
if (!fresh?.size) continue;
|
|
2259
2520
|
const aPath = authoringPath(loaded, scene);
|
|
@@ -2262,7 +2523,7 @@ function applyLoc(loaded, catalog, opts = {}) {
|
|
|
2262
2523
|
const editsMap = { ...authoring.edits ?? {} };
|
|
2263
2524
|
for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
|
|
2264
2525
|
authoring.edits = editsMap;
|
|
2265
|
-
writes.push({ path: aPath, content:
|
|
2526
|
+
writes.push({ path: aPath, content: canonicalStringify5(authoring) });
|
|
2266
2527
|
}
|
|
2267
2528
|
return { writes, stats: { updated, files: bySceneText.size } };
|
|
2268
2529
|
}
|
|
@@ -2880,7 +3141,7 @@ var SERIF2 = (bold, italic) => bold && italic ? "Serif-BoldItalic" : bold ? "Ser
|
|
|
2880
3141
|
var isEmoji = (cp) => cp >= 126976 || cp >= 9728 && cp <= 10175;
|
|
2881
3142
|
async function scriptToPdf(doc) {
|
|
2882
3143
|
const { scriptFont } = await import("./script-fonts-AQNQOADI.js");
|
|
2883
|
-
return new Promise((
|
|
3144
|
+
return new Promise((resolve6, reject) => {
|
|
2884
3145
|
const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
|
|
2885
3146
|
pdf.registerFont("Serif", scriptFont("serif"));
|
|
2886
3147
|
pdf.registerFont("Serif-Bold", scriptFont("serifBold"));
|
|
@@ -2921,7 +3182,7 @@ async function scriptToPdf(doc) {
|
|
|
2921
3182
|
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
3183
|
const chunks = [];
|
|
2923
3184
|
pdf.on("data", (c) => chunks.push(c));
|
|
2924
|
-
pdf.on("end", () =>
|
|
3185
|
+
pdf.on("end", () => resolve6(Buffer.concat(chunks)));
|
|
2925
3186
|
pdf.on("error", reject);
|
|
2926
3187
|
const pageRight = pdf.page.width - MARGIN;
|
|
2927
3188
|
const pageBottom = pdf.page.height - MARGIN;
|
|
@@ -3068,16 +3329,16 @@ async function scriptToPdf(doc) {
|
|
|
3068
3329
|
}
|
|
3069
3330
|
|
|
3070
3331
|
// src/init.ts
|
|
3071
|
-
import { join as
|
|
3072
|
-
import { readdirSync as
|
|
3073
|
-
import { newId, slug, canonicalStringify as
|
|
3332
|
+
import { join as join6, basename as basename6, resolve as resolve4 } from "path";
|
|
3333
|
+
import { readdirSync as readdirSync4, existsSync as existsSync3 } from "fs";
|
|
3334
|
+
import { newId, slug, canonicalStringify as canonicalStringify6 } from "@patterkit/core";
|
|
3074
3335
|
function runInit(opts) {
|
|
3075
|
-
const dir =
|
|
3336
|
+
const dir = resolve4(opts.dir);
|
|
3076
3337
|
const name = opts.name?.trim() || basename6(dir).replace(/\.patter$/, "");
|
|
3077
3338
|
const fileSlug = slug(name);
|
|
3078
3339
|
let existing = [];
|
|
3079
3340
|
try {
|
|
3080
|
-
existing =
|
|
3341
|
+
existing = readdirSync4(dir).filter((f) => f.endsWith(".patterproj"));
|
|
3081
3342
|
} catch {
|
|
3082
3343
|
}
|
|
3083
3344
|
if (existing.length > 0) throw new Error(`a project already exists here: ${existing[0]}`);
|
|
@@ -3121,25 +3382,25 @@ function runInit(opts) {
|
|
|
3121
3382
|
strings: { [beatId]: `Welcome to ${name}. This is the first line of your story - replace it with your own.` }
|
|
3122
3383
|
};
|
|
3123
3384
|
const bundle = opts.bundle ?? "commit";
|
|
3124
|
-
const projectFile =
|
|
3385
|
+
const projectFile = join6(dir, `${fileSlug}.patterproj`);
|
|
3125
3386
|
const writes = [
|
|
3126
|
-
{ path: projectFile, content:
|
|
3127
|
-
{ path:
|
|
3128
|
-
{ path:
|
|
3129
|
-
{ path:
|
|
3387
|
+
{ path: projectFile, content: canonicalStringify6(project) },
|
|
3388
|
+
{ path: join6(dir, "scenes", "start.patterflow"), content: canonicalStringify6(flow) },
|
|
3389
|
+
{ path: join6(dir, "loc", "en", "start.patterloc"), content: canonicalStringify6(locale) },
|
|
3390
|
+
{ path: join6(dir, ".editorconfig"), content: EDITORCONFIG },
|
|
3130
3391
|
...vcsConfigWrites(dir, opts.vcs, bundle)
|
|
3131
3392
|
];
|
|
3132
|
-
const collisions = writes.map((w) => w.path).filter((p) =>
|
|
3393
|
+
const collisions = writes.map((w) => w.path).filter((p) => existsSync3(p));
|
|
3133
3394
|
if (collisions.length > 0) {
|
|
3134
3395
|
throw new Error(`refusing to overwrite existing file(s): ${collisions.join(", ")}`);
|
|
3135
3396
|
}
|
|
3136
3397
|
return { writes, projectFile, name };
|
|
3137
3398
|
}
|
|
3138
3399
|
function vcsConfigWrites(dir, vcs, bundle = "commit") {
|
|
3139
|
-
const writes = [{ path:
|
|
3140
|
-
if (vcs === "git") writes.push({ path:
|
|
3400
|
+
const writes = [{ path: join6(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
|
|
3401
|
+
if (vcs === "git") writes.push({ path: join6(dir, ".gitattributes"), content: GITATTRIBUTES });
|
|
3141
3402
|
const ignore = ignoreFileFor(vcs);
|
|
3142
|
-
if (ignore) writes.push({ path:
|
|
3403
|
+
if (ignore) writes.push({ path: join6(dir, ignore), content: ignoreContent(bundle) });
|
|
3143
3404
|
return writes;
|
|
3144
3405
|
}
|
|
3145
3406
|
var EDITORCONFIG = `# Patter projects are UTF-8 + LF, always (spec - patter format enforces this).
|
|
@@ -3561,7 +3822,7 @@ function buildIndex(loaded) {
|
|
|
3561
3822
|
}
|
|
3562
3823
|
|
|
3563
3824
|
// src/replace.ts
|
|
3564
|
-
import { canonicalStringify as
|
|
3825
|
+
import { canonicalStringify as canonicalStringify7 } from "@patterkit/core";
|
|
3565
3826
|
import { walkNodes as walkNodes5, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2 } from "@patterkit/model";
|
|
3566
3827
|
import { findMatcher } from "@wildwinter/toolkit";
|
|
3567
3828
|
function runReplace(loaded, opts) {
|
|
@@ -3603,7 +3864,7 @@ function runReplace(loaded, opts) {
|
|
|
3603
3864
|
}
|
|
3604
3865
|
if (changed) {
|
|
3605
3866
|
const next = { ...shard, strings: merged };
|
|
3606
|
-
plan.writes.push({ path, content:
|
|
3867
|
+
plan.writes.push({ path, content: canonicalStringify7(next) });
|
|
3607
3868
|
plan.shards.push(next);
|
|
3608
3869
|
plan.scenes++;
|
|
3609
3870
|
}
|
|
@@ -3613,16 +3874,16 @@ function runReplace(loaded, opts) {
|
|
|
3613
3874
|
|
|
3614
3875
|
// src/pack.ts
|
|
3615
3876
|
import JSZip from "jszip";
|
|
3616
|
-
import { readFileSync as
|
|
3617
|
-
import { dirname as
|
|
3877
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3878
|
+
import { dirname as dirname5, relative as relative3, sep as sep2 } from "path";
|
|
3618
3879
|
import { parseSource as parseSource4 } from "@patterkit/core";
|
|
3619
3880
|
import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
|
|
3620
3881
|
var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
|
|
3621
3882
|
var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
|
|
3622
3883
|
async function runPack(startPath) {
|
|
3623
3884
|
const projectFile = findProjectFile(startPath);
|
|
3624
|
-
const root =
|
|
3625
|
-
const project = parseSource4(
|
|
3885
|
+
const root = dirname5(projectFile);
|
|
3886
|
+
const project = parseSource4(readFileSync5(projectFile, "utf8"));
|
|
3626
3887
|
const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
|
|
3627
3888
|
if (unresolved.length) {
|
|
3628
3889
|
throw new Error(
|
|
@@ -3630,31 +3891,40 @@ async function runPack(startPath) {
|
|
|
3630
3891
|
` + unresolved.map((i) => ` ${i.file}`).join("\n")
|
|
3631
3892
|
);
|
|
3632
3893
|
}
|
|
3633
|
-
const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel:
|
|
3894
|
+
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));
|
|
3895
|
+
const scopes = gameScopesSnapshot(root, project);
|
|
3634
3896
|
const manifest = {
|
|
3635
3897
|
schema: "patter/document@0",
|
|
3636
3898
|
project: { id: project.project.id, name: project.project.name },
|
|
3637
|
-
files: files.map((f) => f.rel)
|
|
3899
|
+
files: files.map((f) => f.rel),
|
|
3900
|
+
...scopes.length ? { gameScopes: scopes.map((f) => f.fileName) } : {}
|
|
3638
3901
|
};
|
|
3639
3902
|
const zip = new JSZip();
|
|
3640
3903
|
zip.file("patter.manifest.json", JSON.stringify(manifest, null, 2) + "\n", ENTRY_OPTS);
|
|
3641
|
-
for (const f of files) zip.file(f.rel,
|
|
3904
|
+
for (const f of files) zip.file(f.rel, readFileSync5(f.abs, "utf8"), ENTRY_OPTS);
|
|
3905
|
+
for (const f of scopes) zip.file(`${GAME_SCOPES_DIR}/${f.fileName}`, f.text, ENTRY_OPTS);
|
|
3642
3906
|
return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
|
|
3643
3907
|
}
|
|
3644
3908
|
|
|
3645
3909
|
// src/unpack.ts
|
|
3646
3910
|
import JSZip2 from "jszip";
|
|
3647
|
-
import { join as
|
|
3648
|
-
import { existsSync as
|
|
3649
|
-
import { parseSource as parseSource5, canonicalStringify as
|
|
3911
|
+
import { join as join7, resolve as resolve5, sep as sep3 } from "path";
|
|
3912
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
3913
|
+
import { parseSource as parseSource5, canonicalStringify as canonicalStringify8 } from "@patterkit/core";
|
|
3650
3914
|
import { isUnsafeEntry } from "@wildwinter/toolkit/archive";
|
|
3915
|
+
import { SCOPES_FILE_SUFFIX as SCOPES_FILE_SUFFIX2 } from "@wildwinter/scoperegistry/scopes";
|
|
3651
3916
|
import { isUnsafeEntry as isUnsafeEntry2 } from "@wildwinter/toolkit/archive";
|
|
3652
3917
|
var MANIFEST = "patter.manifest.json";
|
|
3918
|
+
var isScopesEntry = (name) => {
|
|
3919
|
+
const rest = name.startsWith(`${GAME_SCOPES_DIR}/`) ? name.slice(GAME_SCOPES_DIR.length + 1) : void 0;
|
|
3920
|
+
return rest !== void 0 && !rest.includes("/") && rest.endsWith(SCOPES_FILE_SUFFIX2);
|
|
3921
|
+
};
|
|
3653
3922
|
var UnsafeEntryError = class extends Error {
|
|
3654
3923
|
};
|
|
3655
3924
|
async function readDoc(bytes) {
|
|
3656
3925
|
const zip = await JSZip2.loadAsync(bytes);
|
|
3657
3926
|
const shards = /* @__PURE__ */ new Map();
|
|
3927
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
3658
3928
|
let manifest;
|
|
3659
3929
|
for (const [name, entry] of Object.entries(zip.files)) {
|
|
3660
3930
|
if (entry.dir) continue;
|
|
@@ -3666,16 +3936,14 @@ async function readDoc(bytes) {
|
|
|
3666
3936
|
continue;
|
|
3667
3937
|
}
|
|
3668
3938
|
if (isUnsafeEntry(name)) throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
|
|
3669
|
-
shards.set(name, await entry.async("string"));
|
|
3939
|
+
(isScopesEntry(name) ? scopes : shards).set(name, await entry.async("string"));
|
|
3670
3940
|
}
|
|
3671
|
-
return { shards, manifest };
|
|
3672
|
-
}
|
|
3673
|
-
async function readDocShards(bytes) {
|
|
3674
|
-
return (await readDoc(bytes)).shards;
|
|
3941
|
+
return { shards, scopes, ...manifest ? { manifest } : {} };
|
|
3675
3942
|
}
|
|
3676
3943
|
async function runUnpack(bytes, targetDir) {
|
|
3677
|
-
const
|
|
3678
|
-
|
|
3944
|
+
const doc = await readDoc(bytes);
|
|
3945
|
+
const plan = (entries) => [...entries.entries()].map(([name, content]) => ({ path: containedWrite(targetDir, name), content })).sort((a, b) => a.path.localeCompare(b.path));
|
|
3946
|
+
return { shards: plan(doc.shards), scopes: plan(doc.scopes) };
|
|
3679
3947
|
}
|
|
3680
3948
|
function checkProvenance(returned, base, target) {
|
|
3681
3949
|
const known = [returned, base, target].filter((id) => typeof id === "string" && id !== "");
|
|
@@ -3688,7 +3956,7 @@ function checkProvenance(returned, base, target) {
|
|
|
3688
3956
|
}
|
|
3689
3957
|
function targetProjectId(projectDir) {
|
|
3690
3958
|
try {
|
|
3691
|
-
const pf = parseSource5(
|
|
3959
|
+
const pf = parseSource5(readFileSync6(findProjectFile(projectDir), "utf8"));
|
|
3692
3960
|
return pf.project?.id;
|
|
3693
3961
|
} catch {
|
|
3694
3962
|
return void 0;
|
|
@@ -3704,9 +3972,11 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
|
|
|
3704
3972
|
const writes = [];
|
|
3705
3973
|
const sidecars = [];
|
|
3706
3974
|
let conflicts = 0, warnings = 0;
|
|
3975
|
+
const projectRel = projectEntry(theirs);
|
|
3976
|
+
let projectSides;
|
|
3707
3977
|
for (const [rel, theirText] of [...theirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
3708
3978
|
const outPath = containedWrite(projectDir, rel);
|
|
3709
|
-
if (!
|
|
3979
|
+
if (!existsSync4(outPath)) {
|
|
3710
3980
|
writes.push({ path: outPath, content: theirText });
|
|
3711
3981
|
shards.push({ path: rel, added: true });
|
|
3712
3982
|
continue;
|
|
@@ -3718,12 +3988,15 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
|
|
|
3718
3988
|
throw new Error(`${rel}: the ${side} copy is not readable Patter source - ${e instanceof Error ? e.message : String(e)}`);
|
|
3719
3989
|
}
|
|
3720
3990
|
};
|
|
3721
|
-
const oursObj = readSide("ours",
|
|
3991
|
+
const oursObj = readSide("ours", readFileSync6(outPath, "utf8"));
|
|
3722
3992
|
const theirsObj = readSide("theirs", theirText);
|
|
3723
3993
|
const baseText = base.get(rel);
|
|
3724
3994
|
const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
|
|
3725
3995
|
const result = runMerge(baseObj, oursObj, theirsObj);
|
|
3726
|
-
writes.push({ path: outPath, content:
|
|
3996
|
+
writes.push({ path: outPath, content: canonicalStringify8(result.merged) });
|
|
3997
|
+
if (rel === projectRel && baseText !== void 0) {
|
|
3998
|
+
projectSides = { ours: oursObj, base: baseObj, theirs: theirsObj, merged: result.merged };
|
|
3999
|
+
}
|
|
3727
4000
|
if (result.conflicts.length > 0) {
|
|
3728
4001
|
sidecars.push({ path: `${outPath}.patterconflict`, content: JSON.stringify({ type: result.type, conflicts: result.conflicts, warnings: result.warnings }, null, 2) + "\n" });
|
|
3729
4002
|
conflicts += result.conflicts.length;
|
|
@@ -3731,11 +4004,24 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
|
|
|
3731
4004
|
warnings += result.warnings.length;
|
|
3732
4005
|
shards.push({ path: rel, result, added: false });
|
|
3733
4006
|
}
|
|
3734
|
-
|
|
4007
|
+
let gameScopes;
|
|
4008
|
+
const local = projectSides && discoverGameScopes(resolve5(projectDir), projectSides.ours).gameScopes;
|
|
4009
|
+
if (projectSides && local) {
|
|
4010
|
+
const world = planReturnedWorld(projectSides.ours, projectSides.base, projectSides.theirs, projectSides.merged, local);
|
|
4011
|
+
if (world.write) {
|
|
4012
|
+
writes.push(world.write);
|
|
4013
|
+
gameScopes = { path: world.write.path };
|
|
4014
|
+
} else if (world.error) gameScopes = { path: join7(local.dir, GAME_SCOPES_FILE), error: world.error };
|
|
4015
|
+
}
|
|
4016
|
+
return { shards, writes, sidecars, conflicts, warnings, provenance, ...gameScopes ? { gameScopes } : {} };
|
|
4017
|
+
}
|
|
4018
|
+
function projectEntry(shards) {
|
|
4019
|
+
const at = [...shards.keys()].filter((n) => !n.includes("/") && n.endsWith(".patterproj"));
|
|
4020
|
+
return at.length === 1 ? at[0] : void 0;
|
|
3735
4021
|
}
|
|
3736
4022
|
function containedWrite(dir, name) {
|
|
3737
|
-
const root =
|
|
3738
|
-
const full =
|
|
4023
|
+
const root = resolve5(dir);
|
|
4024
|
+
const full = resolve5(join7(dir, name));
|
|
3739
4025
|
if (full !== root && !full.startsWith(root + sep3)) {
|
|
3740
4026
|
throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
|
|
3741
4027
|
}
|
|
@@ -3744,6 +4030,11 @@ function containedWrite(dir, name) {
|
|
|
3744
4030
|
export {
|
|
3745
4031
|
AUTHORING_HANDLED,
|
|
3746
4032
|
CONFLICT_SIDECAR,
|
|
4033
|
+
GAME_SCOPES_DIR,
|
|
4034
|
+
GAME_SCOPES_FILE,
|
|
4035
|
+
GAME_SCOPES_OWNER,
|
|
4036
|
+
PATTER_SCOPES_FILE,
|
|
4037
|
+
PATTER_SCOPES_OWNER,
|
|
3747
4038
|
SHARD_EXTENSIONS,
|
|
3748
4039
|
UnsafeEntryError,
|
|
3749
4040
|
UnsupportedMergeError,
|
|
@@ -3755,17 +4046,32 @@ export {
|
|
|
3755
4046
|
catalogToPo,
|
|
3756
4047
|
catalogToXlsx,
|
|
3757
4048
|
classesForChannel,
|
|
4049
|
+
defaultGameScopesDir,
|
|
3758
4050
|
detectMergeType,
|
|
4051
|
+
discoverGameScopes,
|
|
4052
|
+
drivableScopes,
|
|
3759
4053
|
extractLoc,
|
|
3760
4054
|
findProjectFile,
|
|
4055
|
+
gameScopeTokens,
|
|
4056
|
+
gameScopesCatalogue,
|
|
4057
|
+
gameScopesSnapshot,
|
|
4058
|
+
hostScopeTokens,
|
|
3761
4059
|
isUnsafeEntry2 as isUnsafeEntry,
|
|
3762
4060
|
jsonToCatalog,
|
|
3763
4061
|
listProjectTags,
|
|
3764
4062
|
loadProject,
|
|
3765
4063
|
loadProjectLanding,
|
|
4064
|
+
patterScopesFile,
|
|
4065
|
+
patterScopesStale,
|
|
4066
|
+
patterScopesWrite,
|
|
4067
|
+
planReturnedWorld,
|
|
4068
|
+
planShareScopes,
|
|
4069
|
+
planWorldSave,
|
|
3766
4070
|
poToCatalog,
|
|
4071
|
+
previewRegistry,
|
|
3767
4072
|
proposeCoverageDrivers,
|
|
3768
4073
|
reachabilityIssues,
|
|
4074
|
+
readGameScopes,
|
|
3769
4075
|
renderCoverageText,
|
|
3770
4076
|
renderPlay,
|
|
3771
4077
|
renderReportText,
|
|
@@ -3799,8 +4105,10 @@ export {
|
|
|
3799
4105
|
sceneIdForShard,
|
|
3800
4106
|
scriptToDocx,
|
|
3801
4107
|
scriptToPdf,
|
|
4108
|
+
serialiseScopesFile,
|
|
3802
4109
|
sidecarIssues,
|
|
3803
4110
|
vcsConfigWrites,
|
|
3804
4111
|
voiceScriptToXlsx,
|
|
4112
|
+
worldSettingsScopes,
|
|
3805
4113
|
xlsxToCatalog
|
|
3806
4114
|
};
|